راهنمای کامل داکرایز کردن پروژه‌ی لاراول، جنگو و Node.js

داکرایز کردن یک پروژه یعنی نوشتن یک Dockerfile؛ اما نوشتن Dockerfileی که در Production درست کار کند، کار دیگری است. ایمیج‌های چند گیگابایتی، اجرای اپلیکیشن با کاربر root، فایل .env که داخل ایمیج رفته و فایل‌های آپلودی که با هر استقرار پاک می‌شوند، رایج‌ترین مشکلاتی است که در پروژه‌ها می‌بینیم.

در این راهنما برای سه فریم‌ورک رایج، یعنی لاراول، جنگو و Node.js، یک Dockerfile چندمرحله‌ای (Multi-stage) می‌نویسیم و در پایان همه را با یک فایل Docker Compose شامل اپلیکیشن، دیتابیس، Redis و Nginx کنار هم می‌گذاریم. نسخه‌های ایمیج‌ها را با نسخه‌ای که پروژه‌ی شما واقعاً استفاده می‌کند هماهنگ کنید.

اصول مشترک

  • Multi-stage build: ابزارهای ساخت (Composer، npm، کامپایلرها) فقط در مرحله‌ی ساخت وجود دارند و ایمیج نهایی فقط چیزی را دارد که برای اجرا لازم است.
  • کاربر غیر root: اگر مهاجم از طریق اپلیکیشن به کانتینر نفوذ کند، با کاربر محدود کار کمتری از دستش برمی‌آید.
  • تنظیمات از محیط، نه از ایمیج: رمزها و آدرس‌ها هنگام اجرا با متغیرهای محیطی داده می‌شوند. یک ایمیج باید بتواند در Staging و Production بدون تغییر اجرا شود.
  • داده‌ی پایدار در Volume: هر چیزی که کاربر آپلود می‌کند یا باید بعد از استقرار بماند، بیرون از لایه‌های کانتینر نگه‌داری می‌شود.
  • ترتیب لایه‌ها: اول فایل‌های وابستگی (مثل composer.lock یا package-lock.json) کپی و نصب می‌شوند و بعد کد. به این ترتیب تا وقتی وابستگی‌ها تغییر نکرده، لایه‌ی نصب از کش استفاده می‌کند.

فایل .dockerignore

بدون این فایل، کل پوشه‌ی پروژه (از جمله .git، node_modules و .env) به Docker ارسال می‌شود و ممکن است وارد ایمیج شود. یک نمونه‌ی مشترک:

.git
.gitignore
.env
.env.*
!.env.example
node_modules
vendor
__pycache__
*.pyc
.venv
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
media/
dist/
coverage/
docker-compose*.yml
Dockerfile*

در پروژه‌ی Node.js اگر خروجی Build را داخل Dockerfile می‌سازید، dist/ را نادیده بگیرید؛ اگر بیرون می‌سازید، آن را از این فهرست بردارید.

لاراول: PHP-FPM و Nginx

لاراول در Production معمولاً با PHP-FPM اجرا می‌شود و یک Nginx جلوی آن فایل‌های استاتیک را سرو و درخواست‌های PHP را به FPM ارسال می‌کند. در این Dockerfile هر دو ایمیج از یک فایل ساخته می‌شوند:

# syntax=docker/dockerfile:1

# --- وابستگی‌های PHP ---
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist --no-interaction
COPY . .
RUN composer dump-autoload --optimize --no-dev --no-scripts

# --- ساخت فایل‌های فرانت (Vite) ---
FROM node:24-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# --- ایمیج اپلیکیشن ---
FROM php:8.4-fpm-alpine AS app
RUN apk add --no-cache icu-libs libzip \
 && apk add --no-cache --virtual .build-deps $PHPIZE_DEPS icu-dev libzip-dev \
 && docker-php-ext-install pdo_mysql intl zip bcmath opcache \
 && pecl install redis && docker-php-ext-enable redis \
 && apk del .build-deps
WORKDIR /var/www/html
COPY --chown=www-data:www-data . .
COPY --from=vendor --chown=www-data:www-data /app/vendor ./vendor
COPY --from=assets --chown=www-data:www-data /app/public/build ./public/build
COPY --chmod=755 docker/entrypoint.sh /usr/local/bin/entrypoint.sh
USER www-data
ENTRYPOINT ["entrypoint.sh"]
CMD ["php-fpm"]

# --- ایمیج Nginx با فایل‌های public ---
FROM nginx:1.27-alpine AS web
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=app /var/www/html/public /var/www/html/public

فایل docker/entrypoint.sh کش تنظیمات را هنگام اجرا می‌سازد، چون متغیرهای محیطی در زمان Build وجود ندارند:

#!/bin/sh
set -e
php artisan config:cache
php artisan route:cache
php artisan view:cache
exec "$@"

Migration را داخل entrypoint اجرا نکنید؛ اگر چند نسخه از کانتینر هم‌زمان بالا بیایند، هم‌زمان Migration می‌زنند. آن را یک مرحله‌ی جداگانه در استقرار بگذارید: docker compose run --rm app php artisan migrate --force.

فایل docker/nginx.conf:

server {
    listen 80;
    root /var/www/html/public;
    index index.php;
    client_max_body_size 20m;

    location /storage/ {
        alias /var/www/html/storage/app/public/;
    }

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass app:9000;
        fastcgi_param SCRIPT_FILENAME /var/www/html/public$fastcgi_script_name;
        include fastcgi_params;
    }
}

لاگ‌ها را با LOG_CHANNEL=stderr به خروجی استاندارد بفرستید تا docker logs آن‌ها را ببیند و فایل لاگ داخل کانتینر بزرگ نشود. لاراول ۱۱ به بعد مسیر /up را برای Health Check دارد که در Compose از آن استفاده می‌کنیم.

جنگو: Gunicorn با venv جداگانه

# syntax=docker/dockerfile:1
FROM python:3.13-slim AS builder
ENV PIP_NO_CACHE_DIR=1 PIP_DISABLE_PIP_VERSION_CHECK=1
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install -r requirements.txt

FROM python:3.13-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PATH="/opt/venv/bin:$PATH"
RUN useradd --create-home --uid 10001 app
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
COPY --chown=app:app . .
RUN DJANGO_SECRET_KEY=build-only python manage.py collectstatic --noinput
RUN mkdir -p /app/media && chown app:app /app/media
USER app
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz/')" || exit 1
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3", "--access-logfile", "-"]

چند نکته:

  • در requirements.txt از psycopg[binary] یا پکیج‌هایی با Wheel آماده استفاده کنید تا به کامپایلر در ایمیج نیاز نباشد. اگر پکیجی کامپایل لازم دارد، ابزار ساخت را فقط در مرحله‌ی builder نصب کنید.
  • collectstatic نیاز دارد STATIC_ROOT در تنظیمات تعریف شده باشد و تنظیمات جنگو هنگام Build بدون رمزهای واقعی بارگذاری شود؛ برای همین یک SECRET_KEY موقت فقط برای این دستور داده‌ایم.
  • مسیر /healthz/ را خودتان یک View ساده تعریف کنید و 127.0.0.1 را به ALLOWED_HOSTS اضافه کنید.
  • فایل‌های استاتیک را یا با WhiteNoise از خود اپلیکیشن سرو کنید، یا Nginx را مثل نمونه‌ی لاراول جلوی Gunicorn بگذارید. فایل‌های آپلودی (MEDIA_ROOT) حتماً روی Volume یا Object Storage باشند.

Node.js: نصب وابستگی، Build و حذف devDependencies

# syntax=docker/dockerfile:1
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM deps AS build
COPY . .
RUN npm run build && npm prune --omit=dev

FROM node:24-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build --chown=node:node /app/package.json ./
COPY --from=build --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist
USER node
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD wget -qO- http://127.0.0.1:3000/healthz || exit 1
CMD ["node", "dist/server.js"]
  • ایمیج رسمی Node کاربر غیر root به نام node را از پیش دارد.
  • اپلیکیشن را مستقیم با node اجرا کنید، نه با npm start؛ در این حالت سیگنال توقف (SIGTERM) مستقیم به پروسه‌ی شما می‌رسد و می‌توانید اتصال‌ها را تمیز ببندید.
  • اگر پروژه TypeScript نیست و مرحله‌ی Build ندارد، خط npm run build و کپی dist را حذف کنید و به‌جای آن کد را کپی کنید.

تنظیمات با متغیرهای محیطی

یک فایل .env.example بدون مقادیر واقعی در مخزن کد نگه دارید و فایل .env واقعی فقط روی سرور باشد (و در .gitignore). Compose آن را با env_file به کانتینر می‌دهد. برای اسرار حساس‌تر، به‌جای فایل env از Docker Secrets یا ابزار مدیریت اسرار استفاده کنید. جزئیات بیشتر در مقاله‌ی ۱۰ اشتباه رایج Docker Compose در Production آمده است.

Compose کامل: اپلیکیشن، دیتابیس، Redis و Nginx

نمونه‌ی زیر برای لاراول است؛ برای جنگو و Node.js در ادامه تفاوت‌ها را می‌گوییم:

services:
  app:
    build:
      context: .
      target: app
    image: registry.example.com/myapp/app:1.4.0
    env_file: .env
    restart: unless-stopped
    volumes:
      - uploads:/var/www/html/storage/app/public
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  web:
    build:
      context: .
      target: web
    image: registry.example.com/myapp/web:1.4.0
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - uploads:/var/www/html/storage/app/public:ro
    depends_on:
      - app
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1/up"]
      interval: 30s
      timeout: 5s
      retries: 3

  db:
    image: mysql:8.4
    env_file: .env.db
    restart: unless-stopped
    volumes:
      - dbdata:/var/lib/mysql
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p\"$$MYSQL_ROOT_PASSWORD\" --silent"]
      interval: 10s
      timeout: 5s
      retries: 10

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5

volumes:
  uploads:
  dbdata:
  redisdata:

چند نکته درباره‌ی این فایل:

  • دیتابیس و Redis هیچ پورتی روی سرور باز نکرده‌اند و فقط از شبکه‌ی داخلی Compose در دسترس‌اند. در .env اپلیکیشن، DB_HOST=db و REDIS_HOST=redis بگذارید.
  • فایل .env.db شامل MYSQL_ROOT_PASSWORD، MYSQL_DATABASE، MYSQL_USER و MYSQL_PASSWORD است.
  • در محیط واقعی، SSL را یا با یک Reverse Proxy جلوی سرویس web، یا با گواهی داخل همین Nginx فراهم کنید.
  • برای مقیاس‌پذیری و جدا کردن فایل‌ها از سرور، فایل‌های آپلودی را به Object Storage ببرید؛ روش آن را در انتقال آپلودهای وردپرس و لاراول به S3 توضیح داده‌ایم.

برای جنگو: سرویس app را با ایمیج جنگو جایگزین کنید، Volume مدیا را روی /app/media بگذارید، دیتابیس را معمولاً postgres:17 با Health Check pg_isready انتخاب کنید و در Nginx به‌جای fastcgi_pass از proxy_pass http://app:8000; استفاده کنید.

برای Node.js: مشابه جنگو، Nginx با proxy_pass http://app:3000; جلوی اپلیکیشن قرار می‌گیرد و سرویس app پورتی روی میزبان باز نمی‌کند.

ساخت و اجرا

docker compose build
docker compose up -d
docker compose run --rm app php artisan migrate --force
docker compose ps
docker compose logs -f app

در ستون STATUS خروجی docker compose ps باید healthy را ببینید.

نکته‌ی مخصوص سرورهای ایران

ایمیج‌های پایه (php، python، node، composer) از Docker Hub دریافت می‌شوند و پکیج‌های npm و pip و Composer هم از مخازن خارجی. اگر Build روی سرور داخل ایران با خطای 403 یا قطعی شبکه متوقف می‌شود، راه‌حل‌ها را در دسترسی به Docker Hub از ایران و راه‌اندازی Nexus به‌عنوان مخزن مرکزی ببینید.

جمع‌بندی

یک Dockerfile خوب کوچک است، با کاربر غیر root اجرا می‌شود، هیچ رازی داخلش نیست و داده‌ی پایدار را بیرون از کانتینر نگه می‌دارد. با این الگوها، همان ایمیجی که روی سیستم توسعه‌دهنده تست شده، بدون تغییر در Production اجرا می‌شود. قدم بعدی، خودکار کردن ساخت و استقرار است که در راه‌اندازی CI/CD با GitLab و Docker توضیح داده‌ایم.

نمی‌خواهید این کار را خودتان انجام دهید؟ تیم مروانت پروژه‌ی لاراول، جنگو یا Node.js شما را داکرایز می‌کند، Compose یا CI/CD آن را راه می‌اندازد و مستندات را تحویل تیمتان می‌دهد. صفحه‌ی برون‌سپاری پروژه‌های Docker را ببینید.

دیدگاهتان را بنویسید

جستجو

فهرست مطالب