Skip to Content
ConfigurationProduction Checklist

Production Checklist

Essential steps before going live.

Security

1. Generate secure secrets

Session secret, required when using HTTPS:

openssl rand -hex 32

Database password (do not reuse this one as the session secret):

openssl rand -base64 24

2. 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_URL is 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: true

With 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.com
server { 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.com

On the dashboard host:

services: ui: image: rustrak/rustrak-ui:latest ports: ["3000:3000"] environment: - RUSTRAK_API_URL=https://ingest.example.com

The 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

ComponentCPUMemory
Server (API + dashboard)1 vCPU256MB
Database1 vCPU512MB

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=20000

See Rate Limiting for how the windows work.

Reliability

Health checks

GET /health # Liveness GET /health/ready # Readiness

Restart policy

# docker-compose.yml services: server: restart: unless-stopped

Backups

PostgreSQL:

# Daily backup pg_dump -h localhost -U rustrak rustrak > backup_$(date +%Y%m%d).sql # Restore psql -h localhost -U rustrak rustrak < backup.sql

SQLite: 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 rustrak

With 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 server

To 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=true set
  • PUBLIC_URL set 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=off if 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)
Last updated on