Комерційні платформи безсерверного хостингу (Vercel, AWS Amplify, Netlify) надають інженерам високий рівень абстракції та автоматизації: ефемерні прев'ю-середовища, глобальний CDN та масштабування з коробки. Проте в міру зростання проєкту до десятків мільйонів переглядів сторінок на місяць абстракція Serverless перетворюється на експоненційне зростання інфраструктурних витрат.
Детальний фінансовий аналіз витрат виявляє три головні статті переплат у комерційних Serverless PaaS:
GB-sec при постійному навантаженні обходиться в 5–10 разів дорожче за оренду виділених ядер процесора (vCPU).Next.js підтримує Standalone Mode (output: standalone), перетворюючи застосунок на компактний герметичний бандл, готовий до пакування в Docker-контейнери та розгортання на власних віртуальних (VPS) або виділених серверах через сучасні оркестратори: Coolify, Kamal або Dokku.
Архітектура промислового Self-Hosting кластера будується на поєднанні сучасного зворотного проксі Traefik v3 та ізольованих Docker-контейнерів Next.js:
Вхідний клієнтський HTTPS-трафік (Порти 80, 443 / HTTP/3 QUIC)
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Traefik v3 Edge Reverse Proxy │
│ - Автоматичний випуск і ротація TLS-сертифікатів Let's Encrypt │
│ - Динамічне виявлення контейнерів через Docker Socket та Labels │
│ - Термінація TLS 1.3, HTTP/2, HTTP/3, стиснення Zstd / Brotli / Gzip │
│ - Глобальний Rate Limiting, захист від сканування та DDoS │
└─────────────┬────────────────────────────────────────────┬─────────────┘
│ Внутрішня мережа Docker Bridge │
▼ ▼
┌──────────────────────────────┐ ┌──────────────────────────┐
│ Next.js Container A (Blue) │ │ Next.js Container B (Green)
│ - Node.js 22 LTS Alpine │ │ - Нова версія додатку │
│ - Непривілейований nextjs │ │ - Плавне перемикання │
│ - Внутрішній порт 3000 │ │ - Zero-Downtime Rollout │
└──────────────┬───────────────┘ └────────────┬─────────────┘
│ │
└─────────────────────┬─────────────────────┘
│ Монтування тому
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Docker Persistent Volume (/cache) │
│ - Збереження сторінок Incremental Static Regeneration (ISR) │
│ - Спадковість кешу між оновленнями версій додатку │
│ - Збереження метаданих fetch та внутрішнього кешу компілятора │
└────────────────────────────────────────────────────────────────────────┘Вказівка output: 'standalone' у файлі next.config.ts активує вбудовану утиліту @vercel/nft (Node File Trace). Цей модуль будує абстрактне синтаксичне дерево (AST) усіх серверних файлів і точно визначає використовувані імпорти import та require.
У результаті в теку .next/standalone потрапляють:
server.js;node_modules, які реально викликаються в рантаймі (dev-залежності, компілятор TypeScript та лінтери відсікаються).# ---------------------------------------------------------
# ЕТАП 1: Базовий образ з Node.js 22 LTS на Alpine Linux
# ---------------------------------------------------------
FROM node:22-alpine AS base
# Встановлення libc6-compat для сумісності з нативними бінарниками (sharp)
RUN apk add --no-cache libc6-compat
WORKDIR /app
# ---------------------------------------------------------
# ЕТАП 2: Встановлення залежностей із кешуванням шарів
# ---------------------------------------------------------
FROM base AS deps
WORKDIR /app
# Копіювання маніфестів пакетних менеджерів
COPY package.json pnpm-lock.yaml* yarn.lock* package-lock.json* ./
RUN \
if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
elif [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
else echo "Lockfile not found." && exit 1; \
fi
# ---------------------------------------------------------
# ЕТАП 3: Компіляція застосунку Next.js
# ---------------------------------------------------------
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Вимкнення телеметрії для прискорення складання
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_ENV=production
# Компіляція у standalone-бандл
RUN \
if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
elif [ -f yarn.lock ]; then yarn build; \
else npm run build; \
fi
# ---------------------------------------------------------
# ЕТАП 4: Мінімальний робочий образ рантайму (Runner)
# ---------------------------------------------------------
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
# Створення безпечної системної групи та користувача без root-прав
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
# Копіювання публічних статичних ресурсів
COPY --from=builder /app/public ./public
# Налаштування прав доступу до каталогу кешу ISR
RUN mkdir -p .next/cache && chown -R nextjs:nodejs .next
# Копіювання автономного сервера та клієнтських ассетів
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Перемикання на непривілейованого користувача
USER nextjs
EXPOSE 3000
# Прямий запуск сервера Node.js без shell-обгорток
CMD ["node", "server.js"]Traefik відстежує події Docker через системний сокет /var/run/docker.sock. При старті нового контейнера Next.js Traefik зчитує мітки labels, реєструє маршрутизатор, автоматично отримує сертифікат Let's Encrypt і плавно перемикає трафік після проходження перевірки здоров'я:
Демон Docker сповіщає Traefik про запуск нового контейнера з мітками (traefik.http.routers.nextjs.rule=Host(`example.com`)).
Traefik виконує перевірку HTTP-01 або TLS-ALPN-01, генерує валідний сертифікат TLS 1.3 та записує його в acme.json.
Traefik перевіряє внутрішній маршрут /api/health контейнера. Трафік не спрямовується до отримання HTTP 200.
Новий контейнер додається до пулу балансування, тоді як старий отримує сигнал SIGTERM для плавного дренажу з'єднань.
Нижче наведено еталонний маніфест docker-compose.prod.yml для виробничого розгортання на VPS. Він включає налаштування Traefik v3 з автоматичним випуском SSL Let's Encrypt, ізоляцію мереж, ліміти пам'яті та постійний том для кешу ISR.
services:
# -------------------------------------------------------
# 1. ЗВОРОТНИЙ ПРОКСІ-СЕРВЕР TRAEFIK V3
# -------------------------------------------------------
traefik:
image: traefik:v3.1
container_name: production_traefik
restart: always
command:
# Вимкнення анонімної телеметрії
- "--global.sendAnonymousUsage=false"
# Увімкнення провайдера Docker для читання міток
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
# Точки входу для HTTP, HTTPS та HTTP/3 QUIC
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.websecure.http3=true"
# Глобальне автоматичне перенаправлення з HTTP на HTTPS
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
# Автоматичний випуск SSL-сертифікатів Let's Encrypt
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "[email protected]"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
- "443:443/udp" # Порт UDP для роботи протоколу HTTP/3 QUIC
volumes:
# Доступ до сокета Docker у режимі тільки для читання (:ro)
- /var/run/docker.sock:/var/run/docker.sock:ro
# Персистентне сховище для SSL-сертифікатів
- traefik_certs:/letsencrypt
networks:
- web_gateway
security_opt:
- no-new-privileges:true
# -------------------------------------------------------
# 2. АВТОНОМНИЙ ЗАСТОСУНОК NEXT.JS 15
# -------------------------------------------------------
nextjs_app:
build:
context: .
dockerfile: Dockerfile
target: runner
container_name: production_nextjs
restart: unless-stopped
environment:
- NODE_ENV=production
- PORT=3000
- HOSTNAME=0.0.0.0
- DATABASE_URL=postgresql://user:password@postgres:5432/app_db
# Ліміти системних ресурсів для запобігання падінню сервера
deploy:
resources:
limits:
cpus: '2.0'
memory: 2048M
reservations:
cpus: '0.5'
memory: 512M
volumes:
# КРИТИЧНО ВАЖЛИВО: збереження кешу ISR між оновленнями контейнера
- nextjs_cache:/app/.next/cache
networks:
- web_gateway
labels:
# Активація маршрутизації через Traefik
- "traefik.enable=true"
# Правила маршрутизації за доменами
- "traefik.http.routers.nextjs.rule=Host(`example.com`) || Host(`www.example.com`)"
- "traefik.http.routers.nextjs.entrypoints=websecure"
- "traefik.http.routers.nextjs.tls=true"
- "traefik.http.routers.nextjs.tls.certresolver=letsencrypt"
# Внутрішній мережевий порт контейнера
- "traefik.http.services.nextjs.loadbalancer.server.port=3000"
# Конфігурація опитування доступності (Healthcheck)
- "traefik.http.services.nextjs.loadbalancer.healthcheck.path=/api/health"
- "traefik.http.services.nextjs.loadbalancer.healthcheck.interval=10s"
- "traefik.http.services.nextjs.loadbalancer.healthcheck.timeout=3s"
# Підключення middleware стиснення, rate limit та заголовків безпеки
- "traefik.http.routers.nextjs.middlewares=nextjs-compress,nextjs-ratelimit,nextjs-headers"
# Стиснення трафіку (Brotli / Gzip)
- "traefik.http.middlewares.nextjs-compress.compress=true"
# Обмеження частоти запитів (Rate Limiting)
- "traefik.http.middlewares.nextjs-ratelimit.ratelimit.average=100"
- "traefik.http.middlewares.nextjs-ratelimit.ratelimit.burst=50"
# Заголовки безпеки HSTS та захисту від атак
- "traefik.http.middlewares.nextjs-headers.headers.sslredirect=true"
- "traefik.http.middlewares.nextjs-headers.headers.stsSeconds=63072000"
- "traefik.http.middlewares.nextjs-headers.headers.stsIncludeSubdomains=true"
- "traefik.http.middlewares.nextjs-headers.headers.stsPreload=true"
- "traefik.http.middlewares.nextjs-headers.headers.customresponseheaders.X-Robots-Tag=all"
# ---------------------------------------------------------
# ПЕРСИСТЕНТНІ ТОМИ ТА МЕРЕЖІ
# ---------------------------------------------------------
volumes:
traefik_certs:
driver: local
nextjs_cache:
driver: local
networks:
web_gateway:
driver: bridgeРозгортання контейнера без підключення тому до теки .next/cache: docker run -p 3000:3000 my-nextjs. При кожному оновленні коду старий контейнер видаляється разом із файловою системою, стираючи весь кеш згенерованих сторінок та відповідей fetch.
Одразу після деплою тисячі відвідувачів одночасно запитують динамічні маршрути. Це породжує шторм одночасних важких SSR-рендерингів (Cache Stampede), навантажуючи процесор на 100% та перевантажуючи базу даних.
# ❌ КАТАСТРОФІЧНА ПОМИЛКА: Каталог кешу знищується при кожному релізі
services:
nextjs:
image: my-app:latest
# ✅ ВИРОБНИЧЕ РІШЕННЯ: Явне монтування Persistent Volume до .next/cache
services:
nextjs:
image: my-app:latest
volumes:
- next_cache_vol:/app/.next/cache # Зберігає кеш сторінок між деплоямиПередача паролів бази даних або API-ключів через інструкції ARG та ENV у Dockerfile під час збірки: ARG DATABASE_URL і ENV DATABASE_URL=$DATABASE_URL.
Навіть якщо ці змінні не використовуються у фінальному контейнері, будь-хто, хто має доступ до Docker Registry, може прочитати їх за допомогою команди docker history --no-trunc <image-id>.
# ❌ ВРАЗЛИВІСТЬ: Секрети запікаються в історію шарів Docker
ARG DATABASE_URL
ENV DATABASE_URL=$DATABASE_URL
RUN npm run build
# ✅ ПРАВИЛЬНЕ РІШЕННЯ: Використання Docker Build Secrets або рантайм-конфігурації
# RUN --mount=type=secret,id=DATABASE_URL \
# DATABASE_URL=$(cat /run/secrets/DATABASE_URL) npm run build
# Або чітке розділення: під час збірки передаються лише NEXT_PUBLIC_* змінні,
# а серверні секрети надаються в рантаймі через docker-compose environment!Пропуск директиви USER nextjs у Dockerfile. За замовчуванням процеси всередині контейнера виконуються під root (UID 0).
Якщо в якійсь npm-бібліотеці буде виявлено вразливість віддаленого виконання коду (RCE) або Path Traversal, зловмисник отримає повні права root усередині контейнера, що спрощує атаку виходу з контейнера (Container Escape) на операційну систему хоста.
# ❌ НЕБЕЗПЕЧНО: Виконання під root
CMD ["node", "server.js"]
# ✅ БЕЗПЕЧНО: Створення ізольованого системного користувача
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
USER nextjs
CMD ["node", "server.js"]Запуск контейнера через команду CMD ["npm", "start"]. Утиліта npm не транслює вхідні POSIX-сигнали дочірньому процесу Node.js. Під час зупинки контейнер зависає на 10 секунд до примусового SIGKILL, аварійно обриваючи активні транзакції користувачів.
Завжди викликайте бінарний файл Node безпосередньо: CMD ["node", "server.js"] у поєднанні з логікою плавного закриття з'єднань (Graceful Shutdown).
Налаштування фаєрвола UFW (порти 22, 80, 443), вимкнення авторизації SSH за паролями, активація fail2ban та встановлення Docker Engine з плагіном Docker Compose.
Активація output: 'standalone' у next.config.ts, додавання перевірки доступності /api/health та складання оптимізованого багатоетапного Dockerfile.
Створення внутрішньої мережі, запуск контейнера Traefik із монтуванням /var/run/docker.sock у режимі :ro та налаштування генерації сертифікатів Let's Encrypt.
Створення іменованого Docker тома для каталогу /app/.next/cache для збереження згенерованих сторінок та тегів між викатками релізів.
Автоматизація в GitHub Actions: складання образів, пуш у GHCR та запуск безшовного перемикання трафіку через Kamal або вебхук Coolify.