Production Checklist
Essential steps before going live.
Security
1. Generate secure secrets
Session secret, required when using HTTPS:
openssl rand -hex 32Database password (do not reuse this one as the session secret):
openssl rand -base64 242. Use HTTPS
Put Rustrak behind a reverse proxy that terminates SSL. See Reverse proxy below for the routing.
3. Enable SSL_PROXY
When running behind HTTPS, enable secure cookies:
# In your .env or environment
SSL_PROXY=true
SESSION_SECRET_KEY=$(openssl rand -hex 32)This ensures:
- Cookies are only sent over HTTPS
- Sessions persist across server restarts
4. Database security
With PostgreSQL:
# Enable SSL
DATABASE_URL=postgres://user:pass@host:5432/rustrak?sslmode=require- Use strong passwords
- Restrict network access
- Enable automated backups
Reverse proxy
The container needs no published host port. Put it on the proxy’s network,
delete the ports: block from docker-compose.yml, and let the proxy reach it
on 8080.
One container answers both the dashboard and the API, so there is only one upstream and only one address to get right:
PUBLIC_URLis public. It is the host Rustrak writes into every DSN, so it has to be an address your SDKs can reach from outside.
There is no internal dashboard-to-server address to configure. The dashboard
is static files served from /, and the browser calls the API on the origin it
was loaded from: the public host, through the proxy, like every other request.
One hostname
The simple case, and now the default one. Everything lives behind one name:
# docker-compose.yml
services:
server:
image: rustrak/rustrak-server:latest
environment:
- SESSION_SECRET_KEY=${SESSION_SECRET_KEY:?generate one with openssl rand -hex 32}
- SSL_PROXY=true
- PUBLIC_URL=https://rustrak.example.com
volumes:
- rustrak_data:/data
networks: [proxy]
restart: unless-stopped
volumes:
rustrak_data:
networks:
proxy:
external: trueWith PostgreSQL it is the same service on the postgres image, plus a
DATABASE_URL; see Installation.
server {
listen 443 ssl http2;
server_name rustrak.example.com;
location / {
proxy_pass http://server:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Caddy says the same thing in four lines, and Traefik in two labels
(traefik.http.routers.<name>.rule=Host(...) plus the service port). No path
rules, because there is nothing to split: the server decides for itself which
paths are the API’s and which are the dashboard’s, and the two lists cannot
disagree because there is only one of them.
Two hostnames
Worth it if you want ingestion on a name of its own: an
SDK’s POST /api/<project>/envelope/ is unauthenticated traffic from the open
internet, and separating it lets the proxy rate-limit or firewall that name
without touching the one your team signs in on.
Point both names at the same container. Set PUBLIC_URL to the ingestion
one, since that is the address the DSN has to carry, and DASHBOARD_URL to the
one your team opens, so alert links land on the dashboard:
PUBLIC_URL=https://ingest.example.com
DASHBOARD_URL=https://rustrak.example.comserver {
listen 443 ssl http2;
server_name rustrak.example.com; # the team
location / { proxy_pass http://server:8080; }
}
server {
listen 443 ssl http2;
server_name ingest.example.com; # SDKs and the DSN
location /api/ { proxy_pass http://server:8080; }
}Dashboard on its own host
Most installations never need this. It is for keeping the machine that holds
your data from serving any frontend, with the dashboard on another host.
rustrak/rustrak-ui is the same dashboard bundle behind nginx, which proxies
/api, /auth, /health, /docs and /api-docs to the server named by
RUSTRAK_API_URL.
On the server, turn its own copy of the dashboard off and point alert links at the dashboard host:
RUSTRAK_DASHBOARD=off
PUBLIC_URL=https://ingest.example.com
DASHBOARD_URL=https://rustrak.example.comOn the dashboard host:
services:
ui:
image: rustrak/rustrak-ui:latest
ports: ["3000:3000"]
environment:
- RUSTRAK_API_URL=https://ingest.example.comThe browser still sees a single origin, the one the dashboard was loaded from,
so the session cookie stays first-party and no CORS applies. RUSTRAK_API_URL
is read when the container starts, not when the image is built, and its host
is resolved per request, so the server can move without restarting the
dashboard. The image is about 30MB and idles under 15MB of memory.
RUSTRAK_API_URL is an origin with no path (https://ingest.example.com,
not https://ingest.example.com/; a trailing slash is dropped, a path refuses
to start). Across hosts make it https://: the session cookie travels on that
hop, and nginx verifies the server’s certificate against the system bundle.
Plain http://server:8080 works only when the two containers share a Docker
network.
Performance
Resource allocation
| Component | CPU | Memory |
|---|---|---|
| Server (API + dashboard) | 1 vCPU | 256MB |
| Database | 1 vCPU | 512MB |
The dashboard costs nothing at runtime: it is static files the server reads once at startup and hands out from memory. The whole process idles under 100MB of memory.
Rate limiting
The defaults stop a runaway loop without cutting off ordinary spikes, so most installs never touch them. Lower them when the hardware is small:
# Constrained resources
MAX_EVENTS_PER_MINUTE=1000
MAX_EVENTS_PER_HOUR=20000See Rate Limiting for how the windows work.
Reliability
Health checks
GET /health # Liveness
GET /health/ready # ReadinessRestart policy
# docker-compose.yml
services:
server:
restart: unless-stoppedBackups
PostgreSQL:
# Daily backup
pg_dump -h localhost -U rustrak rustrak > backup_$(date +%Y%m%d).sql
# Restore
psql -h localhost -U rustrak rustrak < backup.sqlSQLite: everything lives in the /data volume: the database at
/data/rustrak.db and uploaded source maps under /data/sourcemaps. The
simplest backup is the whole volume, with the server stopped:
docker stop rustrak
docker run --rm -v rustrak_data:/data -v "$PWD":/backup alpine \
tar czf /backup/rustrak-$(date +%Y%m%d).tgz -C /data .
docker start rustrakWith Docker Compose, the service is server and Compose prefixes the volume
with the project name (docker volume ls shows it, usually
<directory>_rustrak_data):
docker compose stop server
docker run --rm -v <directory>_rustrak_data:/data -v "$PWD":/backup alpine \
tar czf /backup/rustrak-$(date +%Y%m%d).tgz -C /data .
docker compose start serverTo back up only the database while the server runs, use SQLite’s online
backup rather than cp, which can capture a write half done. It writes the
copy to the current directory, off the volume, and does not include source
maps:
docker run --rm -v rustrak_data:/data -v "$PWD":/backup alpine sh -c \
'apk add -q sqlite && sqlite3 /data/rustrak.db ".backup /backup/rustrak-$(date +%Y%m%d).db"'Source maps with PostgreSQL live in the server’s /data volume, not in the
database. Back up that volume too.
Pre-launch checklist
- Secure secrets generated (
SESSION_SECRET_KEY) - HTTPS configured (reverse proxy)
-
SSL_PROXY=trueset -
PUBLIC_URLset to your public domain (e.g.https://rustrak.example.com) - A named volume mounted at
/data - Telemetry left on or turned off (
RUSTRAK_TELEMETRY=off), deliberately - Dashboard served (the default) or
RUSTRAK_DASHBOARD=offif this server should answer the API only - Database SSL enabled
- Rate limits set
- Health checks monitored
- Backup strategy in place
- Restart policies configured
- Admin user created
- Test event sent and received
- DSN shown in dashboard contains correct domain (not
0.0.0.0)