Production checklist
Secrets, storage, backups, reverse proxy and updates for a production instance.
Self-hosted only
Everything on this page is handled for you on senlo.io.
curl -I https://mail.example.com # HTTPS through the proxy
curl -I http://127.0.0.1:3000 # the app itself, from the server onlySecrets
-
AUTH_SECRETandSECRETS_KEYare random (openssl rand -base64 32), different from each other, and not the values fromenv.example. - A copy of
SECRETS_KEYis stored wherever you back up the database. Provider credentials encrypted with it cannot be read with any other key. - Consider
LINK_SIGNING_SECRET, soAUTH_SECRETcan be rotated later without breaking unsubscribe links in sent mail. - The Postgres password in
docker-compose.ymlandDATABASE_URLis changed fromsenlo_password.
If SECRETS_KEY changes anyway, the Providers page marks affected providers "Credentials can't be decrypted". Re-enter them, or re-encrypt with the old key: docker compose exec -e OLD_SECRETS_KEY="<old>" app pnpm db:reseal-secrets.
Reverse proxy and TLS
- Caddy or nginx in front, serving HTTPS for
APP_URL. - The proxy sets
X-Real-IPto the real client address. - Port 3000 is not open to the internet (
APP_BINDstays127.0.0.1:3000).
Rate limits on sign-in, registration and the API are per client IP, taken from the proxy's headers. Without a proxy anyone can fake those headers, and a client that sends none is not limited at all.
server {
server_name mail.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Then sudo certbot --nginx -d mail.example.com. Caddy gets certificates on its own; its config is in Self-hosting.
-
APP_URLis the final public HTTPS address. It is in every tracking and unsubscribe link you send; changing it later breaks links in mail already delivered. -
DEFAULT_FROM_EMAILis an address on a domain verified with your provider. - Provider webhooks are connected, so bounces and complaints suppress addresses.
Storage
- Uploaded images live in the
uploads_datavolume by default. Back it up, or switch to S3/R2 withSTORAGE_DRIVER=s3(Configuration).
Backups
- Postgres is backed up regularly. With the bundled container:
docker compose exec -T db pg_dump -U senlo_user senlo_db | gzip > senlo-$(date +%F).sql.gz- Restores are tested at least once.
-
SECRETS_KEYis stored with the backups.
Redis
- Redis holds the send queue and scheduled automation steps (delays). The bundled container keeps its data in the
redis_datavolume; for durability across crashes, enable append-only persistence.
Access
-
ALLOW_REGISTRATION=falseonce your accounts exist, unless you want open sign-up.
Updates
- You have a way to hear about new releases (watch the GitHub repository) and follow Upgrading.