Deployment
Never run production with dart run
dart run (and dox serve) uses the JIT compiler. It is made for
development. It uses about 25x more memory at start (around 390 MB
instead of 16 MB) and starts slower. Always build an AOT binary for
production.
Build
Compile the app to a native binary (AOT).
This makes a build/bundle/ folder:
| File | What it is |
|---|---|
build/bundle/bin/server |
the native binary. No Dart SDK needed to run it |
build/bundle/lib/ |
native libraries from build hooks (eg. SQLite) |
build/bundle/db/ |
your migrations, run at start |
build/bundle/.env.example |
copy it to build/bundle/.env, or use env variables |
dox build runs build_runner first (when your app uses it). Then it runs
dart build cli --target bin/server.dart --output build. It exits with code 1
when a step fails, so CI can stop.
Not dart compile exe
dox_orm depends on sqlite3 v3. It bundles SQLite with a build hook.
dart compile exe does not run build hooks, so it stops with an error.
dart build cli is the same AOT compiler and runs the hooks.
Keep bin/ and lib/ together: the binary loads the libraries from ../lib.
Start the application.
The binary reads .env and db/ from the folder you start it in.
A real environment variable wins over the .env file.
Docker
The Dockerfile in a new Dox app is a multi-stage build:
- The
dart:stableimage builds the AOT bundle withdart build cli --target bin/server.dart --output build. - The final image is
FROM scratch. It holds only the Dart AOT runtime files (/runtime/from the dart image), the bundle (/app/bin/serverand/app/lib/),db/and an empty/tmp.
FROM dart:stable AS build
WORKDIR /app
COPY pubspec.* ./
RUN dart pub get
COPY . .
RUN dart pub get --offline
RUN dart run build_runner build
RUN dart build cli --target bin/server.dart --output build
RUN mkdir -p /out/app/storage /out/tmp \
&& chmod 1777 /out/tmp \
&& cp -r build/bundle/. /out/app/ \
&& cp -r db /out/app/db
FROM scratch
COPY --from=build /runtime/ /
COPY --from=build /out/ /
WORKDIR /app
EXPOSE 3000
CMD ["/app/bin/server"]
The image has no .env file, so secrets stay out of it. Pass config as
environment variables.
docker build -t my-app .
docker run -p 3000:3000 -e APP_PORT=3000 -e APP_KEY=... -e DB_HOST=... my-app
Or use env_file: .env in docker-compose.yml.
HTTPS: put a proxy in front
Do not serve HTTPS from Dart. TLS in Dart's HttpServer is slow (a known
Dart SDK issue). Dox has no bindSecure.
Run Dox on plain HTTP on a private port. Put Caddy or nginx in front of it. The proxy does TLS, HTTP/2 and gzip.
Then tell Dox to trust the proxy, so req.ip() and req.scheme() show the
real client:
AppConfig(
/// the proxy's IP, or a range like '10.0.0.0/8', or '172.16.0.0/12' for docker
trustedProxies: <String>['127.0.0.1', '::1'],
);
req.ip()readsX-Forwarded-For. Dox walks the list from the right and skips your proxies, so a fake value from the client is ignored.req.scheme()readsX-Forwarded-Proto(httporhttps).req.isSecure()is true forhttps.trustedProxiesis empty by default. Then Dox ignores those headers, because any client can send them.['*']trusts everyone. Only use it when the app port is not reachable from outside.
Caddy
Caddy gets a certificate for you and sets X-Forwarded-For and
X-Forwarded-Proto. Websockets work with no extra config.
Nginx
upstream dox {
server 127.0.0.1:3000;
keepalive 64;
}
map $http_upgrade $connection_upgrade {
default upgrade;
'' '';
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# match maxMultipartSize in AppConfig
client_max_body_size 100m;
gzip on;
gzip_types application/json text/plain text/css application/javascript;
location / {
proxy_pass http://dox;
proxy_http_version 1.1;
# websocket upgrade
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
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;
# stream uploads to Dox, do not buffer them in nginx first
proxy_request_buffering off;
}
}
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
Keep it running
Dox uses all CPU cores by default (see Isolate).
- When a worker isolate dies, Dox prints the error and starts it again.
- When the main isolate dies, the process exits with a non-zero code.
Let a supervisor restart it:
restart: unless-stoppedin Docker compose, orRestart=alwaysin systemd.
# /etc/systemd/system/dox.service
[Service]
WorkingDirectory=/srv/app/build/bundle
ExecStart=/srv/app/build/bundle/bin/server
EnvironmentFile=/srv/app/build/bundle/.env
Restart=always
[Install]
WantedBy=multi-user.target
Many processes or machines
One Dox process already uses every core. You only need more processes to run on more machines, or to restart one process without downtime. Run each one on its own port (or machine) and list them in the proxy:
Websocket rooms then need a shared adapter (like Redis), because each process has its own memory.