Skip to content

Production checklist

Check each item before you deploy. Each item says what to check, how to do it, and where to read more.

Items marked Dox limit are things Dox does not do for you. You must handle them outside Dox.

Secrets and config

Check How Read more
APP_KEY is set, long and secret dox key:generate, or a 32 character random value in the real env. It signs JWT tokens and encrypts cookies. Never use the value from .env.example. Encryption
APP_ENV=production With it, an empty APP_KEY stops the app at start (StateError). Without it, Dox only logs a warning and runs with an empty key. Env reference
The env is complete Run dox env:check in CI or before the deploy. It exits with code 1 when a key is missing or bad. CLI
No secrets in the image or in git .env in .dockerignore and .gitignore. Pass values as env vars (docker run -e, env_file:, systemd EnvironmentFile=). dox build copies .env.example only. Docker
No comments after values in .env KEY=abc # note gives the value abc # note. Put comments on their own line. Troubleshooting

Auth and passwords

Check How Read more
JWT tokens expire JwtAuthDriver.expiresIn is 1 hour by default. Pick a time that fits your app, like JwtAuthDriver(secret: SecretKey(env.appKey), expiresIn: Duration(hours: 12)). Authentication
You can not revoke one token Dox limit. There is no token list on the server. To log out everyone, change APP_KEY. Authentication
Passwords are hashed user.password = Hash.make(input) (bcrypt). Never store or encrypt a plain password. Hashing
The hash is never sent List<String> get hidden => <String>['password']; in the user model. The JWT payload is user.toJson(), and anyone can read it. Authentication
Passwords have a max length bcrypt throws on input over 72 bytes. Add @MaxLength(72) to the password field of the DTO. It counts characters, so non-ASCII text can still pass 72 bytes. Troubleshooting

HTTP and network

Check How Read more
HTTPS at the proxy Dox has no TLS server. Run Caddy or nginx in front. Keep the app port private. Deployment
trustedProxies is set behind a proxy AppConfig(trustedProxies: <String>['127.0.0.1', '::1']), or the proxy network, like '172.16.0.0/12'. Without it, req.ip() is the proxy and req.scheme() is http. Do not use ['*'] when the app port is public. App config
Body limits are on Keep maxBodySize (10 MB) and maxMultipartSize (100 MB), or lower them. Set the same limit in the proxy: client_max_body_size 100m; Request
CORS has real origins CORSConfig(origin: 'https://my.app'), or a list like <String>['https://my.app', 'https://admin.my.app']. With a list, Dox sends back only the request origin when it is in the list. CORS
No credentials: true with origin: '*' The new project's cors.dart has origin: '*' and credentials: true with enabled: false. When you turn CORS on, set a real origin first. CORS
Rate limiting Dox limit. Dox has no rate limiter. Do it at the proxy. See below. Deployment
gzip at the proxy Keep compress: false. Turn on gzip in Caddy (encode gzip) or nginx (gzip on;). Performance
OpenAPI docs Off in production by default (on only for APP_ENV development or local). Do not set enabled: true for a private API. OpenAPI

Build and run

Check How Read more
AOT binary, not dart run dox build, then run build/bundle/bin/server. Keep bin/ and lib/ together. Not dart compile exe. Deployment
Native server is on The start line says (native). When it says (dart:io), look for the [dox] warning: dox_http_native did not load line. Native server
Native database driver is on No [dox] warning: dox_postgres_native did not load (or mysql) line at start. Do not set DOX_DB_DRIVER=dart. Postgres with TLS uses the Dart driver. Postgres native
Isolates = cores Keep the default totalIsolate (Platform.numberOfProcessors), or set APP_TOTAL_ISOLATE to the cores the container really gets. Use fewer when the database runs on the same machine. Isolate
Shared state across isolates Cache: redis, not memory. WebSocket: WebsocketRedisAdapter. Socket.IO: a Redis adapter and transports: ['websocket']. Isolate
A supervisor restarts the process Docker restart: unless-stopped, or systemd Restart=always. Dox restarts a dead worker isolate, but not the main isolate. Deployment
Writable temp folder Uploads over 1 MB go to a temp file. A FROM scratch image needs /tmp with mode 1777. Docker
Uploads live on a volume or S3 The local disk writes to storage/. Mount it as a volume, or use the s3 disk. File storage

Database

Check How Read more
Postgres or MySQL for a busy app SQLite has one connection per isolate, and a query blocks the isolate. Use it for small apps only. With SQLite use an absolute file path, never :memory: with many isolates. Database
Migrations run once per deploy The new project runs Migrator(migrations: migrations).migrate() in bin/server.dart before startServer(). Or run dart run bin/migrate.dart run (or dox migrate) in CI. Do one, not both at the same time on many machines. Migration
Migrations use the right database dox_migration reads .env when the file exists, and ignores the real env then. Do not ship a .env next to the binary, or give Migrator(connection: ...) your own pool. Troubleshooting
Small MySQL migrations MySQL saves CREATE, ALTER and DROP at once. A failed migration can leave half the changes. One table change per migration. Migration
Pool size fits the database extra: {'maxConnectionCount': 10} is per isolate. Total = isolates x pool size. Keep it under the database max_connections. Database
Query logging is off The new project's database.dart has debug: true on every connection, which prints every query. Set debug: false in production. Database
Backups Dox limit. Dox has no backup tool. Use pg_dump, mysqldump, managed database snapshots, or a copy of the SQLite file (with the -wal file, or sqlite3 app.db ".backup x.db"). Test a restore.

Logs and errors

Check How Read more
JSON logs LoggerConfig(enabled: true, prettyPrint: false). The new project sets prettyPrint only when APP_ENV=development. App config
Log level Set LOG_LEVEL=warn to print only warnings and errors. App config
Access logs in one place LogMiddleware writes one line per request, every 20 ms. A hard kill can lose the last lines. Turn it off when the proxy already logs requests. Performance
Errors go to your tracker Set AppConfig.errorHandler. Any non-HTTP error is passed to it. The client gets 500 with the error text, so do not put secrets in exception messages. HTTP exceptions

Health and shutdown

Check How Read more
Health route Dox limit. Dox has no built-in health route. Add one that asks the database: Route.get('/health', healthController.check) with await QueryBuilder.query('select 1');. Docker
Graceful shutdown On SIGTERM or SIGINT, Dox stops taking connections and lets open requests finish, in every isolate. Set shutdownTimeout below the kill timeout of your platform. Close connections in a DoxShutdown service. Deployment
Zero downtime deploys Run two processes (or machines) behind the proxy. Restart one at a time. Deployment

Rate limit at the proxy

Dox has no rate limiter. Add one in the proxy.

nginx:

# in http { }
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

# in server { location / { } }
limit_req zone=api burst=20 nodelay;
limit_req_status 429;

Caddy needs the caddy-ratelimit plugin (build Caddy with xcaddy build --with github.com/mholt/caddy-ratelimit):

example.com {
  rate_limit {
    zone api {
      key    {remote_host}
      events 100
      window 1m
    }
  }
  reverse_proxy 127.0.0.1:3000
}

Note

The proxy must see the real client IP. When there is a load balancer in front of the proxy, set the real IP module of the proxy first.