Docker 部署 Finch

Finch 的设计使其可以很好地在 Docker 中运行。官方镜像 uproid/finch 包含了 Dart SDK、finch CLI,以及构建和运行 Finch 应用所需的一切。本仓库中的 example/ 项目提供了一份完整的 docker-compose.yaml,你可以将其作为起点直接复制使用,其中一站式集成了 MongoDB、MySQL、SQLite、Nginx 反向代理以及 Tailwind 资源构建。

项目结构

一个典型的基于 Docker 的 Finch 项目会使用以下文件:

my-app/
  Dockerfile             # 构建 Finch 容器镜像
  docker-compose.yaml    # 编排所有服务
  docker/
    nginx.conf           # Nginx 反向代理配置
  lib/                   # 应用的 Dart 代码
  public/                # 由 Nginx 提供的静态文件
  sqlite/                # SQLite 数据库文件(挂载为卷)
  mongodb/               # MongoDB 数据文件(挂载为卷)
  mysql/                 # MySQL 数据文件(挂载为卷)

Dockerfile

示例中的 Dockerfile 使用官方 Finch 基础镜像,并在构建时安装依赖:

FROM uproid/finch:latest AS build
WORKDIR /www

# 路径/行为的默认值——在 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/

# 构建:生成 finch 应用二进制文件,并预编译路由/模板
RUN finch -u

# 应用端口(8085)和 Dart VM service/DevTools 端口(8181)
EXPOSE 8085 8181

# 启动时:运行应用,并应用待处理的 MySQL 和 SQLite 迁移
CMD ["finch", "serve", "-p", "/www/lib/watcher.dart", "--args=\"migrate --init --and migrate_sqlite --init\""]

finch -u(参见 Finch CLI)会重新激活全局安装的 finch CLI 本身——相当于再次运行 dart pub global activate finch——从而使镜像中的构建/运行工具与最新发布的 Finch 版本保持一致,而不是停留在 uproid/finch:latest 基础镜像当初打包时的版本。CMD 通过 finch serve 运行应用,监听 lib/watcher.dart(参见 Run App),并传入 migrate --init / migrate_sqlite --init,使数据库迁移(参见 Database Migration)在每次容器启动时自动执行——由于已经应用过的迁移会被跳过,因此可以放心地重复运行。

如果你在容器内启用了本地调试器(ENABLE_LOCAL_DEBUGGER=true),还需要 EXPOSE 8282 并在 docker-compose.yaml 中发布该端口——该端口通过 WebSocket 承载调试器的交互式终端,默认情况下不会被暴露。

注意: 本仓库根目录下的 Dockerfile 和 docker-compose-live.yaml 用于构建 Finch 框架 自身,以运行其自带的在线示例,并不适合直接复制到你的应用项目中——请始终以 example/Dockerfile 和 example/docker-compose.yaml 作为起点。

docker-compose.yaml

完整的示例技术栈包含五个服务:finch(你的应用)、nginx(反向代理)、mongodb、mysql,以及用于构建 Tailwind CSS 资源的 nodejs:

services:
  finch:
    hostname: finch
    container_name: finch
    image: uproid/finch:latest
    restart: always
    ports:
      - "8085:8085"  # 应用端口
      - "8181:8181"  # Dart VM service / DevTools 端口
    environment:
      SQLITE_PATH: /www/sqlite/example_database.sqlite
      ENABLE_DATABASE: true
      MONGODB_CONNECTION: mongodb   # 使用服务主机名,而不是 localhost
      MONGODB_PORT: 27017           # 容器内部端口(不是映射到主机的 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/           # 热重载源文件,无需重新构建镜像
      - ./public/:/www/public/
      - ./sqlite/:/www/sqlite/     # 在容器重启之间持久化 SQLite 数据

  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"  # 对外暴露的端口
    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"   # 主机端口使用 27018,以避免与本地安装的 MongoDB 冲突
    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

要点:

  • nodejs 只有在你的项目使用 Tailwind 时才需要(参见示例中的 package.json 和 tailwind.config.js);如果你不需要构建前端资源,可以完全删除该服务。
  • mongodb 和 mysql 是相互独立的——只需在 configs.dart 中对相应的 FinchDBConfig/FinchMysqlConfig/FinchSqliteConfig 设置 enable: false(参见 Configuration),即可只启用你的应用实际使用的数据库,并直接删除这里对应的未使用服务块。
  • 在 Docker 网络内部,容器之间是通过服务名(mongodb、mysql、nginx)互相访问的,而不是通过 localhost——这就是为什么 MONGODB_CONNECTION 和 MYSQL_HOST 指向的是服务名而不是 localhost,尽管本地(非 Docker)开发时使用的同一份 .env 文件中写的是 localhost。
  • 映射到主机的 MongoDB 端口(27018)与 Finch 内部连接使用的端口(27017)是不同的——应用是通过 Docker 网络以其原生端口访问 Mongo 的,而 27018 的存在只是为了方便你在主机上用本地 MongoDB 客户端连接 localhost:27018 进行调试。

Nginx 反向代理

docker/nginx.conf 会将常规 HTTP 请求,以及基于 WebSocket 的 /ws 和 /debugger 端点(分别被 WebSockets 和本地调试器使用)一并代理到 finch 容器:

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;
    }

    # 为 /ws 和 /debugger 路径提供 WebSocket 支持
    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 的含义是:当挂载的 public/ 目录中存在对应的静态文件时,Nginx 会直接提供该文件;否则才会将请求转发给 Finch(@dart)——这与在 Docker 之外使用的 Nginx for Finch 配置相同,只是这里指向的是 finch 容器,而不是本地进程。WebSocket location 块中的 proxy_buffering off 以及较长的超时设置是必需的——如果没有它们,Nginx 会缓冲长连接的 WebSocket,或者过早地将其关闭。

启动技术栈

# 构建并启动所有服务
docker-compose up --build

# 在后台运行(分离模式)
docker-compose up -d --build

# 跟踪查看某个服务的日志
docker-compose logs -f finch

# 在 Dockerfile 发生更改后,仅重新构建 finch 服务
docker-compose up -d --build finch

# 停止所有内容(仅容器——卷会被保留)
docker-compose down

# 停止所有内容并删除卷(会清空所有数据库数据)
docker-compose down -v

启动完成后,应用可以通过 http://localhost:8080(经由 Nginx)访问,也可以直接通过 http://localhost:8085(绕过 Nginx)访问——这在排查问题究竟出在 Finch 还是代理配置时很有用。

由于 ./lib/ 和 ./public/ 都以绑定挂载的方式挂载进了容器,在主机上编辑这些文件会被 Finch 的文件监听器立即感知——日常的 Dart 代码更改不需要重新构建镜像,只有当 pubspec.yaml 的依赖发生变化时才需要。

环境变量

每一个从 env.get(...)(或 env.getInt(...) / env.getBool(...))读取的 FinchConfigs 值,都可以设置为 Docker 环境变量,或者通过应用加载的 .env 文件来设置。下面是一份与上方 compose 文件相匹配的最小化 .env:

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

完整的受支持环境变量列表,包括此处未展示的服务器、模板和邮件相关设置,请参阅 Configuration。

切勿提交真实密码。 在 docker-compose.yaml 本身中,应优先使用来自已通过 .gitignore 排除的 .env 文件的 ${VAR_NAME} 插值方式,而不是像上面为了简洁而直接在 YAML 中硬编码密钥。

生产环境注意事项

  • 在生产环境中设置 ENABLE_LOCAL_DEBUGGER=false 和 LOCAL_DEBUG=false——本地调试器会向正在运行的容器暴露一个 WebSocket 终端,仅供开发使用。参见 Debugging。
  • 不要将 Dart VM service 端口(8181)暴露到你自己的网络之外;该端口会授予对正在运行的 isolate 的完整读写权限。
  • 在整个技术栈前面放置一个真正的 TLS 终止反向代理(带证书的 Nginx,或托管的负载均衡器)——这里的示例 nginx.conf 仅支持 HTTP,只是作为一个起点。
  • 在生产环境中,为 mongodb/mysql 的数据使用具名卷(或托管数据库),而不是绑定挂载主机目录,这样备份和权限管理才能在不同主机之间保持一致。
  • 如果并发用户规模超出少数几个,请调高 FinchDBConfig/FinchMysqlConfig 上的 maxConnections——参见 MongoDB 和 MySQL。