Deployment
Compiling
Maat applications build to a native CLI bundle:
dart build cli --target=bin/server.dart --output=buildThe result is under build/bundle: bin/server is the executable and lib/
contains native assets such as SQLite. Copy the whole bundle to a compatible
machine and run it without the Dart SDK.
This works because the framework uses no reflection anywhere — no dart:mirrors, no code generation, no build step. Route handler arity, middleware resolution and validation rules are all dispatched with type checks and string keys rather than runtime introspection.
Running
The binary reads its host and port from the environment:
APP_HOST=0.0.0.0 APP_PORT=8000 ./build/bundle/bin/serverThe generated server waits for SIGINT or SIGTERM. It stops accepting new
requests, waits for active requests, then calls every service provider’s
shutdown() in reverse order.
APP_BASE_PATH sets the base path when the working directory is not the application root.
Environment and Secrets
Before you ship:
APP_ENV=productionAPP_DEBUG=falseAPP_KEY=base64:...Disable debug mode. With it enabled, a 500 response includes the exception class, its message and a full stack trace. That is exactly what you want locally and exactly what you must not expose publicly.
Set an application key. Generate one with maat key:generate, and set it as an environment variable in production rather than shipping a .env file.
Real environment variables take precedence over the .env file, so a container or an orchestrator can supply configuration without the file existing at all.
Compiling CSS
public/css/app.css is not committed by default — only a .gitkeep is. Build it as part of every deploy, before the native bundle:
maat tailwind --minifyThe compiled binary reads public/css/app.css off disk at request time, the same way it reads resources/views/*.khnum.html; public/ has to ship with the application, not just the executable. See Frontend for the pipeline that produces it.
Container Image
The skeleton ships a Dockerfile that compiles the CSS, compiles the application, and copies both into a scratch image:
FROM dart:stable AS buildWORKDIR /appCOPY pubspec.* ./RUN dart pub getCOPY . .RUN dart run bin/maat.dart tailwind --minifyRUN dart pub get --offline && dart build cli --target=bin/server.dart --output=build
FROM scratchCOPY --from=build /runtime/ /COPY --from=build /app/build/bundle /appCOPY --from=build /app/public /app/publicWORKDIR /appENV APP_ENV=production APP_DEBUG=false APP_HOST=0.0.0.0 APP_PORT=8000EXPOSE 8000CMD ["/app/bin/server"]dart run bin/maat.dart tailwind boots the full application, so this stage has to succeed with no .env and no reachable database — sqlite needs neither. If that stops holding, build the CSS in CI instead and COPY public/ straight in.
The complete bundle is copied so native build-hook assets stay beside the
executable. /runtime/ supplies the minimal Dart runtime files expected by the
official image.
docker build -t blog .docker run -p 8080:8080 -e APP_KEY="$APP_KEY" -e APP_DEBUG=false blogLogging in Production
Leave app.log_path unset in a container and let the runtime collect standard output — that is what every log aggregator expects. Set it only when you genuinely want a file on disk, and make sure the directory is writable by the process.
Health Checks
The skeleton’s /api/health route is there for your load balancer or orchestrator:
livenessProbe: httpGet: path: /api/health port: 8080Behind a Reverse Proxy
Bind the application to 127.0.0.1 and let the proxy hold the public port and terminate TLS:
APP_HOST=127.0.0.1 APP_PORT=8000 ./build/bundle/bin/serverrequest.ip reports the socket’s peer address. Behind a proxy, that is the proxy — read the forwarded header if you need the original client:
final clientIp = request.header('x-forwarded-for')?.split(',').first.trim() ?? request.ip;Only trust that header when your proxy sets it, since a client can send it too.
Worker Processes
Use multiple CPU cores by setting the worker count:
APP_WORKERS=4 APP_HOST=0.0.0.0 APP_PORT=8000 ./build/bundle/bin/serverEach worker boots an isolated application and shares the same port. Size
APP_WORKERS to available CPU cores. Database pools are also per worker, so
the possible PostgreSQL connection count is APP_WORKERS × DB_POOL_MAX.
The framework’s global helpers — app(), config(), env(), and route() —
resolve inside that worker’s application. In custom process layouts, create one
application per process; do not host unrelated applications in one isolate.
A Deployment Checklist
-
APP_DEBUG=false -
APP_KEYset from the environment, not committed -
.envexcluded from the image -
dart build cli --target=bin/server.dartsucceeds -
maat tailwind --minifyrun, andpublic/shipped with the image -
dart testpasses - Health check wired to
/api/health - Log destination decided — stdout, or a writable path