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 Dockerfile and docker-compose-live.yaml in 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 from example/Dockerfile and example/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:

  • nodejs only matters if your project uses Tailwind (see the example's package.json and tailwind.config.js); drop this service entirely if you don't build frontend assets.
  • mongodb and mysql are independent — enable only the databases your app actually uses by setting enable: false on the corresponding FinchDBConfig/FinchMysqlConfig/FinchSqliteConfig in your configs.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 by localhost — that's why MONGODB_CONNECTION and MYSQL_HOST point at the service names, not localhost, even though the same .env file used for local (non-Docker) development would say localhost.
  • 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, while 27018 only exists so you can connect a local MongoDB client to localhost:27018 from 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=false and LOCAL_DEBUG=false in 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.conf here is HTTP-only and meant as a starting point.
  • Use named volumes (or a managed database) for mongodb/mysql data in production rather than bind-mounting a host directory, so backups and permissions are handled consistently across hosts.
  • Increase maxConnections on FinchDBConfig/FinchMysqlConfig if you scale beyond a handful of concurrent users — see MongoDB and MySQL.