استقرار Next.js با Docker، nginx و SSL

این راهنما یک الگوی رایج و تمیز برای استقرار برنامه‌های Node و Next.js است: برنامه در کانتینر روی 127.0.0.1 اجرا می‌شود، nginx در جلوی آن SSL و کش را مدیریت می‌کند و به‌روزرسانی با یک دستور انجام می‌شود.

DevOpsپیشرفته20 دقیقه

۱Dockerfile چندمرحله‌ای (ایمیج کوچک)

با حالت standalone در Next.js فقط فایل‌های لازم به ایمیج نهایی می‌روند. در next.config مقدار output: 'standalone' را بگذارید.

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

FROM node:22-alpine AS builder
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production PORT=3000 HOSTNAME=0.0.0.0
RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD node -e "fetch('http://127.0.0.1:3000/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "server.js"]

۲docker-compose.prod.yml

پورت فقط روی loopback منتشر می‌شود تا از بیرون مستقیم در دسترس نباشد.

yaml
services:
  app:
    build: .
    image: myapp:latest
    container_name: myapp
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      NODE_ENV: production
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

۳بیلد و اجرا روی سرور

پروژه را به سرور منتقل کنید و بالا بیاورید.

bash
rsync -az --delete --exclude node_modules --exclude .next --exclude .git ./ deploy@SERVER:/opt/myapp/
ssh deploy@SERVER 'cd /opt/myapp && docker compose -f docker-compose.prod.yml up -d --build'
ssh deploy@SERVER 'docker compose -f /opt/myapp/docker-compose.prod.yml ps'

۴کانفیگ nginx (reverse proxy و کش)

فایل‌های _next/static نام هش‌دار دارند و می‌توانند یک سال کش شوند. بقیهٔ درخواست‌ها به برنامه می‌روند.

nginx
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host              $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;
    }

    location /_next/static/ {
        proxy_pass http://127.0.0.1:3000;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

۵فعال‌سازی SSL

certbot کانفیگ را به HTTPS ارتقا می‌دهد و ریدایرکت را تنظیم می‌کند.

bash
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d example.com -d www.example.com --redirect

۶اسکریپت deploy یک‌دستوری

این اسکریپت را روی کامپیوتر خودتان بگذارید: کد را می‌فرستد، بیلد می‌کند، کانتینر را جایگزین می‌کند و سایت را بررسی می‌کند.

bash
#!/usr/bin/env bash
set -euo pipefail
SERVER="deploy@203.0.113.10"
DIR="/opt/myapp"

rsync -az --delete --exclude node_modules --exclude .next --exclude .git ./ "$SERVER:$DIR/"
ssh "$SERVER" "cd $DIR && docker compose -f docker-compose.prod.yml build && docker compose -f docker-compose.prod.yml up -d"
sleep 5
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 https://example.com/)
[ "$code" = "200" ] && echo "deploy OK" || { echo "deploy FAILED ($code)"; exit 1; }

۷لاگ و مانیتورینگ

لاگ برنامه و nginx و وضعیت سلامت کانتینر:

bash
docker logs -f --tail 100 myapp
sudo tail -f /var/log/nginx/error.log
docker inspect -f '{{.State.Health.Status}}' myapp
docker stats --no-stream

۸بازگشت به نسخهٔ قبل (Rollback)

قبل از هر deploy ایمیج فعلی را با یک برچسب نگه دارید تا در صورت مشکل سریع برگردید.

bash
docker tag myapp:latest myapp:previous
# ... deploy جدید ...
# اگر مشکل بود:
docker tag myapp:previous myapp:latest
docker compose -f docker-compose.prod.yml up -d

پرسش‌های پرتکرار دربارهٔ استقرار Next.js با Docker، nginx و SSL

چرا پورت را فقط روی 127.0.0.1 منتشر می‌کنیم؟

تا برنامه مستقیم از اینترنت در دسترس نباشد و همهٔ ترافیک از nginx (با SSL و محدودیت‌ها) بگذرد. همچنین Docker با -p ساده قوانین ufw را دور می‌زند و پورت را باز می‌کند.