Docker برای فینچ

فینچ طوری طراحی شده که به‌خوبی درون Docker اجرا شود. ایمیج رسمی uproid/finch شامل Dart SDK، ابزار خط فرمان finch، و هر چیزی است که برای build و سرویس‌دهی یک اپلیکیشن فینچ لازم است. پروژه example/ در این ریپازیتوری یک docker-compose.yaml کامل ارائه می‌دهد که می‌توانید آن را به‌عنوان نقطه شروع برای MongoDB، MySQL، SQLite، یک Nginx reverse proxy، و build کردن asset‌های Tailwind — همه در یک stack — کپی کنید.

ساختار پروژه

یک پروژه معمولی فینچ مبتنی بر Docker از این فایل‌ها استفاده می‌کند:

my-app/
  Dockerfile             # ایمیج container فینچ را می‌سازد
  docker-compose.yaml    # تمام سرویس‌ها را orchestrate می‌کند
  docker/
    nginx.conf           # پیکربندی Nginx reverse proxy
  lib/                   # کد Dart اپلیکیشن
  public/                # فایل‌های استاتیک سرویس‌دهی‌شده توسط Nginx
  sqlite/                # فایل‌های دیتابیس SQLite (volume-mounted)
  mongodb/               # فایل‌های داده MongoDB (volume-mounted)
  mysql/                 # فایل‌های داده MySQL (volume-mounted)

Dockerfile

فایل Dockerfile نمونه از ایمیج پایه رسمی فینچ استفاده می‌کند و در زمان build وابستگی‌ها را نصب می‌کند:

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/

# Build: باینری اپلیکیشن فینچ را تولید کرده و مسیرها/widgetها را از پیش کامپایل می‌کند
RUN finch -u

# پورت اپلیکیشن (8085) و پورت Dart VM service/DevTools (8181)
EXPOSE 8085 8181

# در زمان شروع: اپلیکیشن را اجرا کرده و migrationهای در انتظار MySQL و SQLite را اعمال می‌کند
CMD ["finch", "serve", "-p", "/www/lib/watcher.dart", "--args=\"migrate --init --and migrate_sqlite --init\""]

finch -u (به Finch CLI مراجعه کنید) خودِ CLI سراسری‌نصب‌شده‌ی finch را دوباره فعال می‌کند — معادل اجرای دوباره‌ی dart pub global activate finch — تا ابزار build/serve داخل ایمیج با آخرین نسخه‌ی منتشرشده‌ی Finch هم‌راستا باشد، نه نسخه‌ای که به‌صورت اتفاقی در image پایه‌ی uproid/finch:latest جاسازی شده است. CMD اپلیکیشن را از طریق finch serve اجرا می‌کند، lib/watcher.dart را نظارت می‌کند (به Run App مراجعه کنید) و migrate --init / migrate_sqlite --init را پاس می‌دهد تا migrationهای دیتابیس (به Database Migration مراجعه کنید) به‌صورت خودکار در هر شروع container اعمال شوند — اجرای مکرر آن امن است، چون migrationهای از قبل اعمال‌شده رد می‌شوند.

اگر local debugger (ENABLE_LOCAL_DEBUGGER=true) را درون یک container فعال می‌کنید، EXPOSE 8282 را هم اضافه کرده و آن را در docker-compose.yaml منتشر کنید — این پورت ترمینال تعاملی debugger را روی یک WebSocket حمل می‌کند و به‌صورت پیش‌فرض expose نمی‌شود.

نکته: فایل‌های Dockerfile و docker-compose-live.yaml در ریشه این ریپازیتوری، خودِ framework فینچ را برای نمونه live خودش build می‌کنند و قرار نیست در یک پروژه اپلیکیشن کپی شوند — همیشه از example/Dockerfile و example/docker-compose.yaml شروع کنید.

docker-compose.yaml

استک نمونه کامل دارای پنج سرویس است: finch (اپلیکیشن شما)، nginx (reverse proxy)، mongodb، mysql، و nodejs (برای build کردن asset‌های Tailwind CSS):

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   # از hostname سرویس استفاده کنید، نه localhost
      MONGODB_PORT: 27017           # پورت داخلی container (نه پورت 27018 که به host نگاشت شده)
      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 کردن فایل‌های سورس بدون rebuild کردن ایمیج
      - ./public/:/www/public/
      - ./sqlite/:/www/sqlite/     # نگه‌داشتن داده‌های SQLite بین راه‌اندازی‌های مجدد container

  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)
    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 روی host، برای جلوگیری از تداخل با یک نصب محلی 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 نمونه مراجعه کنید)؛ اگر asset‌های frontend را build نمی‌کنید، این سرویس را کاملاً حذف کنید.
  • mongodb و mysql مستقل از یکدیگرند — فقط دیتابیس‌هایی را که اپلیکیشن شما واقعاً استفاده می‌کند فعال کنید، با تنظیم enable: false روی FinchDBConfig/FinchMysqlConfig/FinchSqliteConfig مربوطه در configs.dart (به Configuration مراجعه کنید)، و بلاک سرویس استفاده‌نشده را در اینجا حذف کنید.
  • درون شبکه Docker، containerها یکدیگر را با نام سرویس (mongodb، mysql، nginx) پیدا می‌کنند، هرگز با localhost — به همین دلیل MONGODB_CONNECTION و MYSQL_HOST به نام‌های سرویس اشاره می‌کنند، نه localhost، حتی اگر همان فایل .env مورد استفاده برای توسعه محلی (بدون Docker) مقدار localhost را داشته باشد.
  • پورت MongoDB که به host نگاشت شده (27018) با پورتی که فینچ به‌صورت داخلی به آن متصل می‌شود (27017) متفاوت است — اپلیکیشن روی شبکه Docker با پورت native خودش با Mongo صحبت می‌کند، در حالی که 27018 فقط برای این وجود دارد که بتوانید یک کلاینت محلی MongoDB را از دستگاه host خود به localhost:27018 برای دیباگ متصل کنید.

پراکسی معکوس Nginx

docker/nginx.conf هم درخواست‌های معمولی HTTP و هم endpointهای مبتنی بر WebSocket یعنی /ws و /debugger (که توسط WebSockets و local debugger استفاده می‌شوند) را به container به نام 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;
    }

    # پشتیبانی WebSocket برای مسیرهای /ws و /debugger
    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 یعنی Nginx فایل‌های استاتیک را مستقیماً از دایرکتوری mount‌شده public/ سرویس‌دهی می‌کند اگر وجود داشته باشند، و در غیر این صورت درخواست را فقط به فینچ (@dart) فوروارد می‌کند — این همان پیکربندی Nginx for Finch است که خارج از Docker استفاده می‌شود، فقط به‌جای یک process محلی به container finch اشاره می‌کند. proxy_buffering off و timeoutهای طولانی روی بلاک location مربوط به WebSocket ضروری هستند — بدون آن‌ها Nginx اتصالات طولانی‌مدت WebSocket را buffer می‌کند یا زودهنگام می‌بندد.

راه‌اندازی Stack

# ساخت (build) و راه‌اندازی همه سرویس‌ها
docker-compose up --build

# اجرا در پس‌زمینه (detached)
docker-compose up -d --build

# دنبال کردن لاگ‌های یک سرویس
docker-compose logs -f finch

# rebuild کردن فقط سرویس finch پس از تغییر Dockerfile
docker-compose up -d --build finch

# توقف همه‌چیز (فقط containerها — volumeها نگه داشته می‌شوند)
docker-compose down

# توقف همه‌چیز و حذف volumeها (تمام داده‌های دیتابیس از بین می‌رود)
docker-compose down -v

پس از اجرا، اپلیکیشن از طریق http://localhost:8080 (از طریق Nginx) یا مستقیماً از http://localhost:8085 (با دور زدن Nginx، مفید هنگام دیباگ اینکه مشکل در فینچ است یا در پیکربندی پراکسی) در دسترس است.

از آنجا که ./lib/ و ./public/ به‌صورت bind-mount درون container قرار می‌گیرند، ویرایش این فایل‌ها روی host بلافاصله توسط file watcher فینچ تشخیص داده می‌شود — برای تغییرات روزمره کد Dart نیازی به rebuild کردن ایمیج ندارید، فقط زمانی که وابستگی‌های pubspec.yaml تغییر می‌کنند.

متغیرهای محیطی

هر مقدار از FinchConfigs که از env.get(...) (یا env.getInt(...) / env.getBool(...)) خوانده می‌شود، می‌تواند به‌عنوان یک متغیر محیطی Docker یا از طریق یک فایل .env که توسط اپلیکیشن بارگذاری می‌شود تنظیم شود. یک .env مینیمال منطبق با فایل compose بالا:

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 مراجعه کنید.

هرگز رمزهای عبور واقعی را commit نکنید. در خودِ docker-compose.yaml، به‌جای hardcode کردن مستقیم اطلاعات محرمانه در YAML (همان‌طور که در بالا برای اختصار نشان داده شد)، درون‌یابی (interpolation) ${VAR_NAME} را از یک فایل .env که از طریق .gitignore مستثنا شده ترجیح دهید.

ملاحظات محیط Production

  • در محیط production، ENABLE_LOCAL_DEBUGGER=false و LOCAL_DEBUG=false را تنظیم کنید — local debugger یک ترمینال WebSocket به داخل container در حال اجرا expose می‌کند و فقط برای توسعه در نظر گرفته شده است. به Debugging مراجعه کنید.
  • پورت Dart VM service (8181) را خارج از شبکه خودتان منتشر نکنید؛ این پورت دسترسی کامل خواندن/نوشتن به isolate در حال اجرا می‌دهد.
  • یک reverse proxy واقعی با پایان‌دهی TLS (Nginx به‌همراه یک certificate، یا یک load balancer مدیریت‌شده) جلوی stack قرار دهید — nginx.conf نمونه در اینجا فقط HTTP است و به‌عنوان نقطه شروع در نظر گرفته شده.
  • برای داده‌های mongodb/mysql در production، به‌جای bind-mount کردن یک دایرکتوری host، از named volume (یا یک دیتابیس مدیریت‌شده) استفاده کنید، تا backupها و permissionها به‌طور یکسان در همه hostها مدیریت شوند.
  • اگر فراتر از تعداد کمی کاربر هم‌زمان مقیاس می‌گیرید، maxConnections را روی FinchDBConfig/FinchMysqlConfig افزایش دهید — به MongoDB و MySQL مراجعه کنید.