Skip to content

Step 7: Build and deploy

This step compiles the app to one native binary, runs it, and shows the Dockerfile of the project.

Never run production with dox s or dart run

They use the JIT compiler. It needs about 25 times more memory and starts slower. Build a native (AOT) binary for production.

1. Build the binary

dox build
Built with build_runner/aot in 1s; wrote 0 outputs.
Running build hooks...Running link hooks...
Generated: /.../blog/build/bundle/bin/server
[Dox] Built build/bundle. Run it with: cd build/bundle && ./bin/server

dox build runs the code generator, then dart build cli --target bin/server.dart --output build. The result is in build/bundle:

Path What it is
bin/server the binary, about 10 MB. It needs no Dart SDK.
lib/ native libraries from build hooks, like libsqlite3. Keep it next to bin/.
db/ your migrations

2. Run the binary

The binary reads .env from the folder you start it in. Copy yours there, and make the db folder for the SQLite file:

cd build/bundle
cp ../../.env .env
mkdir -p db
./bin/server
Migrating: 2023_07_16_131937700_create_users_table
Migrated: 2023_07_16_131937700_create_users_table
Migrating: 2026_10_22_205345_create_posts_table
Migrated: 2026_10_22_205345_create_posts_table
Server started at http://[::]:3000 with 8 isolate (native)

The migrations run again, because DB_NAME=db/dox.sqlite is a new file in build/bundle. The registry in db/migration/migrations.dart is compiled into the binary, so this works without the source code.

A real environment variable wins over .env. So on a server you can skip the .env file and set the values in the environment.

Check it

In a second terminal:

curl http://localhost:3000/api/ping
curl http://localhost:3000/api/posts
pong
[]

The list is empty, because this is a new database file. Stop the server with Ctrl+C.

3. Docker

The project has a Dockerfile. It builds the same bundle in a dart:stable image and copies it to an empty (scratch) image. The final image holds only the binary, the native libraries and the migrations:

Dockerfile
FROM dart:stable AS build

# Resolve dependencies first, so this layer is cached.
WORKDIR /app
COPY pubspec.* ./
RUN dart pub get

COPY . .
RUN dart pub get --offline
RUN dart run build_runner build --delete-conflicting-outputs

# `dart build cli` is AOT like `dart compile exe`, and it also runs
# build hooks. Native libraries go to build/bundle/lib.
RUN dart build cli --target bin/server.dart --output build

# Lay out the final file tree:
# /app/bin/server, /app/lib (native libraries), /app/db/migration,
# /app/storage and a writable /tmp (uploads bigger than 1 MB are
# streamed to a temp file).
RUN mkdir -p /out/app/storage /out/tmp \
  && chmod 1777 /out/tmp \
  && cp -r build/bundle/. /out/app/ \
  && cp -r db /out/app/db \
  # the native HTTP server (Rust) needs libgcc_s, which /runtime/ does not have
  && LIBGCC=$(find / -name libgcc_s.so.1 -path '*lib*' | head -n 1) \
  && mkdir -p /out$(dirname $LIBGCC) && cp $LIBGCC /out$LIBGCC

# Minimal serving image: the AOT runtime files from the dart image
# (`/runtime/`) plus our bundle. No Dart SDK, no source code.
FROM scratch
COPY --from=build /runtime/ /
COPY --from=build /out/ /
WORKDIR /app

# Config comes from env variables. The image has no .env file,
# so secrets stay out of it.
EXPOSE 3000
CMD ["/app/bin/server"]

Build and run it. The image has no .env file, so pass the config as environment variables:

docker build -t blog .
docker run -p 3000:3000 \
  -e APP_KEY=change-me \
  -e DB_CONNECTION=sqlite \
  -e DB_NAME=storage/blog.sqlite \
  blog

SQLite in Docker

The database file lives inside the container. It is lost when the container is removed. Mount a volume for it: -v blog-data:/app/storage. For a busy app, use Postgres or MySQL. See Database.

Where to go next

  • Deployment: HTTPS with a proxy, systemd, many machines.
  • Model: relationships, events, serializers.
  • Authentication: all options of dox_auth.
  • OpenAPI: API docs from your routes and DTOs.