Docker for Finch
Finch is designed to run well inside Docker. The official uproid/finch image includes the Dart SDK, the finch CLI, and everything needed to build and serve a Finch application. The example/ project in this repository ships a complete docker-compose.yaml you can copy as a starting point for MongoDB, MySQL, SQLite, an Nginx reverse proxy, and Tailwind asset building — all in one stack.
Project Structure
A typical Docker-based Finch project uses these files:
my-app/
Dockerfile # Builds the Finch container image
docker-compose.yaml # Orchestrates all services
docker/
nginx.conf # Nginx reverse proxy configuration
lib/ # Application Dart code
public/ # Static files served by Nginx
sqlite/ # SQLite database files (volume-mounted)
mongodb/ # MongoDB data files (volume-mounted)
mysql/ # MySQL data files (volume-mounted)
Dockerfile
The example Dockerfile uses the official Finch base image and installs dependencies at build time:
FROM uproid/finch:latest AS build
WORKDIR /www
# Path/behavior defaults — override per environment in docker-compose.yaml
ENV WIDGETS_PATH=./lib/widgets
ENV WIDGETS_TYPE=j2.html
ENV LANGUAGE_PATH=./lib/languages
ENV PUBLIC_DIR=./public
ENV LOCAL_DEBUG=true
ENV ENABLE_DATABASE=true
COPY pubspec.yaml ./
RUN dart pub get --no-offline
COPY lib/ ./lib/
# Build: generates the finch app binary and pre-compiles routes/widgets
RUN finch -u
# App port (8085) and the Dart VM service/DevTools port (8181)
EXPOSE 8085 8181
# On start: run the app and apply pending MySQL and SQLite migrations
CMD ["finch", "serve", "-p", "/www/lib/watcher.dart", "--args=\"migrate --init --and migrate_sqlite --init\""]
finch -u (see Finch CLI) re-activates the globally installed finch CLI itself — equivalent to running dart pub global activate finch again — so the image's build/serve tooling matches the latest published Finch release rather than whatever happened to be baked into the uproid/finch:latest base tag. The CMD runs the app through finch serve, watching lib/watcher.dart (see Run App) and passing migrate --init / migrate_sqlite --init so database migrations (see Database Migration) are applied automatically on every container start — safe to run repeatedly, since already-applied migrations are skipped.
If you turn on the local debugger (ENABLE_LOCAL_DEBUGGER=true) inside a container, also EXPOSE 8282 and publish it in docker-compose.yaml — that port carries the debugger's interactive terminal over a WebSocket and is not exposed by default.
Note: the root
Dockerfileanddocker-compose-live.yamlin this repository build the Finch framework itself for its own live example and are not meant to be copied into an application project — always start fromexample/Dockerfileandexample/docker-compose.yaml.
docker-compose.yaml
The full example stack has five services: finch (your app), nginx (reverse proxy), mongodb, mysql, and nodejs (for building Tailwind CSS assets):
services:
finch:
hostname: finch
container_name: finch
image: uproid/finch:latest
restart: always
ports:
- "8085:8085" # App port
- "8181:8181" # Dart VM service / DevTools port
environment:
SQLITE_PATH: /www/sqlite/example_database.sqlite
ENABLE_DATABASE: true
MONGODB_CONNECTION: mongodb # Use the service hostname, not localhost
MONGODB_PORT: 27017 # Internal container port (not the host-mapped 27018)
MONGODB_NAME: my_database
MONGODB_AUTH: admin
MONGODB_PASSWORD: change-me
MONGODB_USER: root
MYSQL_HOST: mysql
MYSQL_PORT: 3306
MYSQL_DATABASE: my_database
MYSQL_USER: app_user
MYSQL_PASSWORD: change-me
build:
context: .
volumes:
- ./lib/:/www/lib/ # Hot-reload source files without rebuilding the image
- ./public/:/www/public/
- ./sqlite/:/www/sqlite/ # Persist SQLite data across container restarts
nodejs:
image: node:20-alpine
container_name: nodejs
working_dir: /app
tty: true
stdin_open: true
volumes:
- ./:/app
command: sh -c "npm install && npm run tailwind:watch"
restart: always
nginx:
image: nginx:latest
container_name: nginx
restart: always
ports:
- "8080:8080" # Public-facing port
volumes:
- ./public/:/var/www/html/public
- ./docker/nginx.conf:/etc/nginx/conf.d/default.conf
mongodb:
image: mongo:latest
container_name: mongodb
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: change-me
ports:
- "27018:27017" # Host port 27018 to avoid clashing with a local MongoDB install
volumes:
- ./mongodb:/data/db
mysql:
image: mysql:latest
container_name: mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: change-me-root
MYSQL_DATABASE: my_database
MYSQL_USER: app_user
MYSQL_PASSWORD: change-me
ports:
- "3306:3306"
volumes:
- ./mysql:/var/lib/mysql
Key points:
nodejsonly matters if your project uses Tailwind (see the example'spackage.jsonandtailwind.config.js); drop this service entirely if you don't build frontend assets.mongodbandmysqlare independent — enable only the databases your app actually uses by settingenable: falseon the correspondingFinchDBConfig/FinchMysqlConfig/FinchSqliteConfigin yourconfigs.dart(see Configuration), and simply remove the unused service block here.- Inside the Docker network, containers reach each other by service name (
mongodb,mysql,nginx), never bylocalhost— that's whyMONGODB_CONNECTIONandMYSQL_HOSTpoint at the service names, notlocalhost, even though the same.envfile used for local (non-Docker) development would saylocalhost. - The host-mapped MongoDB port (
27018) is different from the port Finch connects to internally (27017) — the app talks to Mongo over the Docker network on its native port, while27018only exists so you can connect a local MongoDB client tolocalhost:27018from your host machine for debugging.
Nginx Reverse Proxy
docker/nginx.conf proxies both regular HTTP requests and the WebSocket-based /ws and /debugger endpoints (used by WebSockets and the local debugger) to the finch container:
upstream finch {
server finch:8085;
keepalive 32;
}
server {
listen 8080;
server_name localhost:8080;
client_max_body_size 20M;
location / {
root /var/www/html;
try_files /public$uri @dart;
}
# WebSocket support for /ws and /debugger paths
location ~ ^/(ws|debugger) {
proxy_pass http://finch;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_set_header Host $host;
proxy_buffering off;
proxy_read_timeout 3600s;
}
location @dart {
proxy_pass http://finch;
proxy_pass_request_body on;
proxy_pass_request_headers on;
proxy_set_header Host $http_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;
}
}
try_files /public$uri @dart means Nginx serves static files straight from the mounted public/ directory when they exist, and only forwards the request to Finch (@dart) otherwise — this is the same Nginx for Finch configuration used outside Docker, just pointed at the finch container instead of a local process. proxy_buffering off and the long timeouts on the WebSocket location block are required — without them Nginx will buffer or prematurely close long-lived WebSocket connections.
Starting the Stack
# Build and start all services
docker-compose up --build
# Run in the background (detached)
docker-compose up -d --build
# Follow logs for one service
docker-compose logs -f finch
# Rebuild only the finch service after a Dockerfile change
docker-compose up -d --build finch
# Stop everything (containers only — volumes are kept)
docker-compose down
# Stop everything AND delete volumes (drops all database data)
docker-compose down -v
Once running, the app is reachable at http://localhost:8080 (through Nginx) or directly at http://localhost:8085 (bypassing Nginx, useful when debugging whether an issue is in Finch or in the proxy config).
Because ./lib/ and ./public/ are bind-mounted into the container, editing those files on the host is picked up by Finch's file watcher immediately — you do not need to rebuild the image for day-to-day Dart code changes, only when pubspec.yaml dependencies change.
Environment Variables
Every FinchConfigs value that reads from env.get(...) (or env.getInt(...) / env.getBool(...)) can be set as a Docker environment variable or through a .env file loaded by the app. A minimal .env matching the compose file above:
MONGODB_CONNECTION=mongodb
MONGODB_PORT=27017
MONGODB_NAME=my_database
MONGODB_AUTH=admin
MONGODB_USER=root
MONGODB_PASSWORD=change-me
MYSQL_HOST=mysql
MYSQL_PORT=3306
MYSQL_DATABASE=my_database
MYSQL_USER=app_user
MYSQL_PASSWORD=change-me
SQLITE_PATH=/www/sqlite/example_database.sqlite
ENABLE_LOCAL_DEBUGGER=false
LOCAL_DEBUG=false
See Configuration for the full list of supported environment variables, including server, template, and mail settings not shown here.
Never commit real passwords. In docker-compose.yaml itself, prefer ${VAR_NAME} interpolation from a .env file that is excluded via .gitignore, rather than hardcoding secrets directly in the YAML as shown above for brevity.
Production Considerations
- Set
ENABLE_LOCAL_DEBUGGER=falseandLOCAL_DEBUG=falsein production — the local debugger exposes a WebSocket terminal into the running container and is meant for development only. See Debugging. - Don't publish the Dart VM service port (
8181) outside your own network; it grants full read/write access to the running isolate. - Put a real TLS-terminating reverse proxy (Nginx with a certificate, or a managed load balancer) in front of the stack — the example
nginx.confhere is HTTP-only and meant as a starting point. - Use named volumes (or a managed database) for
mongodb/mysqldata in production rather than bind-mounting a host directory, so backups and permissions are handled consistently across hosts. - Increase
maxConnectionsonFinchDBConfig/FinchMysqlConfigif you scale beyond a handful of concurrent users — see MongoDB and MySQL.