Dokploy в продакшене: одна панель для 17 сервисов, авто-SSL и failover за 60 секунд

Веб Штурм
  • Dokploy
  • Docker Swarm
  • Traefik
  • DevOps
  • self-hosted
  • case-study
  • Let's Encrypt
  • Supabase

Мы перевели прод-инфраструктуру клиента-туроператора с трёх разрозненных Docker Compose стеков на единый Dokploy + Docker Swarm + Traefik. Получили: одну панель управления для ~17 сервисов, автоматический выпуск Let’s Encrypt сертификатов, миграцию сервиса между нодами за 60 секунд без потери трафика. Foundation-фаза заняла 45 минут wall-time. В статье — playbook миграции, 14 неочевидных гочей которые мы поймали в продакшене, метрики до/после и честные границы применимости.

Что было «до»

Инфраструктура клиента собралась исторически из трёх независимых compose-стеков на двух виртуальных хостах:

Боли, которые накопились за полгода эксплуатации:

Архитектура работала, но не масштабировалась людьми: каждое изменение требовало внимания инженера.

Почему именно Dokploy

Мы сравнили пять кандидатов self-hosted PaaS перед выбором:

КритерийDokployPortainer CECapRoverCoolifyKomodo
Docker Swarm nativepartial
Self-hosted freecommunity/paid
Traefik интегрированmanualnginx
Multi-node UI
Open source✓ MITcommunity CE
Setup time~5 мин~10 мин~15 мин~10 мин~10 мин
Footprint контрол-плейн~350 MB~250 MB~600 MB~500 MB~400 MB

Решающими стали четыре фактора:

  1. Native Docker Swarm. Coolify работает только со standalone Docker, что исключало его для multi-node deployments. CapRover поддерживает Swarm частично — без полноценных placement constraints.
  2. Traefik auto-discovery + Let’s Encrypt без танцев. Labels на deploy-секции compose-файла — и Traefik сам поднимает роут, выпускает сертификат, делает HTTP→HTTPS redirect. Нет отдельных Caddy/nginx конфигов.
  3. Минимальный footprint. Manager-нода с Dokploy + Traefik + Redis + Postgres контрол-плейна ест ~350 MB RAM и ~0.8% CPU в idle. Worker-ноды не имеют своих агентов вообще — Dokploy управляет ими через стандартный Swarm API.
  4. MIT-лицензия + активная разработка. Релизы раз в 2–3 недели на момент выбора (v0.29.x), 14k+ звёзд на GitHub, контрибьюторы активны. Если upstream остановится — текущий код самодостаточен.

Альтернативой могли быть managed Kubernetes (DigitalOcean Apps, Render) или Vercel/Railway. Отказались по двум причинам: (а) клиент работает с персональными данными по 152-ФЗ и требовал размещения в РФ; (б) co-location provider, с которым уже был контракт, не предлагал managed K8s.

Архитектура: что получилось

Целевая схема — двухнодовый Swarm-кластер (manager + worker) под управлением Dokploy:

Public DNS → Co-location ingress → Manager-нода (Traefik)

                                          │ Swarm overlay network

                                   Worker-нода (services)

                              ┌───────────┼───────────┬──────────┐
                              ▼           ▼           ▼          ▼
                        Supabase stack  Auth stack  Async   Frontends
                        (13 services)   (Keycloak)  (Hatchet)  (3 apps)

Ключевые архитектурные решения:

Playbook миграции за 45 минут

Реальный wall-time foundation-фазы — 45 минут от первой команды до зелёного smoke-теста. Разбили на две фазы — A (Foundation) и B (Apps).

Phase A — Foundation (~30 минут)

  1. Backup всего и сразу. pg_dump всех баз в Postgres-контейнере (1.7 GB сжатого), Keycloak realm export через kc.sh export, копия Caddy LE-store, копии всех .env. Сложили в /opt/backup/pre-migration-<date>/.
  2. Остановка старых стеков. docker compose down для Supabase + Keycloak. systemctl stop caddy для ingress. Старые контейнеры висят остановленные — это и есть hot-rollback на случай если новый стек не взлетит.
  3. DNS и firewall. На пограничном файрволле перебросили правила forward с frontend-хоста на manager-ноду нового кластера. DNS-записи оставили теми же — рейтинг в поисковиках не теряем.
  4. docker stack deploy для базовых сервисов. Подготовленные YAML-файлы для supabase и keycloak стеков. Swarm разворачивает их за 30–60 секунд каждый. Traefik видит labels, выпускает LE-сертификаты при первом запросе через HTTP-01.
  5. Восстановление данных и smoke. pg_restore дампа в новый Postgres-контейнер. kc.sh import realm. curl -I на все публичные хосты — все 200 (или 401 для auth-gateway, тоже ОК).

Phase B — Applications (~15 минут)

  1. Async-слой. Hatchet Lite (workflow engine) + Gotenberg (PDF renderer) одним отдельным стеком. Подцепился к существующему Postgres через изолированную schema hatchet.
  2. Контейнеризация app-сервисов. Для двух фронтендов уже были Dockerfile — собрали docker build на worker-ноде, тегнули prod-1. Payment-service Dockerfile написали по образу фронтенда (multi-stage builder + runner на Node 20).
  3. Деплой фронтенд-стека. Отдельный frontends-stack.yml с тремя сервисами (B2C-портал, admin-портал, payment-service). Каждый со своими Traefik labels и env-секретами из единого /opt/frontends-stack/.env.
  4. Verify. docker service ls показывает replicas: 1/1 на всех 17 сервисах. curl -I на публичные эндпоинты — 200. Логи без errors.

Каноничный паттерн Traefik labels в Swarm — на deploy.labels, не на services.<name>.labels. Это Swarm-специфичное требование:

services:
  app:
    image: app:prod-1
    deploy:
      replicas: 1
      labels:
        traefik.enable: "true"
        traefik.http.routers.app.rule: "Host(`app.example.com`)"
        traefik.http.routers.app.tls.certresolver: "letsencrypt"
        traefik.http.routers.app.entrypoints: "websecure"
        traefik.http.services.app.loadbalancer.server.port: "3000"
      restart_policy:
        condition: any
        delay: 10s

Похожая инфраструктурная задача? Мы делаем инфраструктурный аудит — два-три дня, на выходе диаграмма текущего стека, точки риска, оценка трудозатрат на миграцию. Это бесплатно, если в результате не находим, что хотим стартовать проект.

14 неочевидных гочей из реальной миграции

Самая ценная часть — не «как правильно», а «во что мы наступили». 14 находок из четырёх фаз работы, каждая ловила нас по часу или больше.

1. live-restore: true несовместим с Docker Swarm init

При попытке docker swarm init Docker возвращает:

Error response from daemon: --live-restore daemon configuration is incompatible with swarm mode

live-restore позволяет контейнерам жить, когда демон Docker рестартует — но Swarm требует жёсткой связи между демоном и Swarm-state. Фикс делать до swarm init: jq '."live-restore"=false' /etc/docker/daemon.json > /tmp/d && mv /tmp/d /etc/docker/daemon.json && systemctl restart docker.

2. pg_restore --clean --if-exists пишет тысячи «errors», но данные восстанавливаются

При накатывании дампа Supabase Postgres скрипт выдал 4109 ошибок — и при этом таблицы заполнены, RLS работает, foreign keys в порядке. Причина: дамп пытается воссоздать event triggers и выполнить superuser-only statements, на которые встроенный postgres user не имеет прав в Supabase. Это не ошибка миграции, а архитектурное ограничение managed-PG. Проверяем \dt и SELECT count(*) — данные есть, миграция прошла.

3. Healthcheck по service-DNS-name в Swarm overlay = бесконечный цикл

В Supabase docker-compose некоторые сервисы делают curl http://storage:5000/health для проверки. В Swarm overlay-network storage резолвится в VIP, который при single-replica указывает на self → loopback и timeout. Сервис помечается unhealthy, Swarm перезапускает его, история повторяется. Фикс: docker service update --no-healthcheck supabase_storage или замена URL на localhost:5000 внутри healthcheck.

4. Swarm молча игнорирует depends_on.condition

В docker-compose v2 можно написать depends_on: { db: { condition: service_healthy } } — сервис стартует только когда db healthy. В Swarm (docker stack deploy) условие игнорируется без ошибки, только warning в логах. Сервисы стартуют параллельно. Компенсация: restart_policy.condition: any с delay: 10s — упавший на старте сервис автоматически перезапускается, пока зависимость не поднимется.

5. Compose v3 ports.published — число, не строка

После прохождения compose-файла через docker compose config --env-file (типичная команда для предпросмотра) интерполированные значения становятся строками. Swarm на это ругается: ports.published must be int, got string. Решение: либо не использовать --env-file препроцессинг, либо вручную привести типы в YAML после генерации.

6. pnpm@latest через corepack ломает Node 20

Dockerfile с RUN corepack enable && corepack prepare pnpm@latest --activate после очередного pnpm-релиза начал падать: ERR_UNKNOWN_BUILTIN_MODULE node:sqlite. Причина: pnpm 11+ требует node:sqlite builtin, который доступен только в Node 22+. Pin до конкретной major: corepack prepare pnpm@10.22.0 --activate. Универсальный совет — никогда не использовать @latest в Dockerfile, фиксировать версии всего.

7. .dockerignore patterns без **/ не рекурсивны

Паттерн *.test.ts исключает только ./foo.test.ts, но не ./src/routes/+page.server.test.ts. SvelteKit 2.49+ строже относится к route-файлам и падает на сборке, если +page.server.test.ts попадает в Docker-контекст. Решение: **/*.test.ts и **/*.spec.ts. Та же логика для node_modules (**/node_modules), dist, .git, тестовых фикстур.

8. Workspace workspace:* зависимости ломают pnpm install в Docker

В monorepo package.json пакета содержит "@org/shared": "workspace:*". Это работает локально, но pnpm install внутри Docker-контейнера (без корневого pnpm-workspace.yaml) падает с EUNSUPPORTED workspace protocol. Три варианта решения: (1) pnpm deploy для построения изолированного пакета; (2) build context = root монорепо вместо подпапки; (3) убрать workspace deps + Vite alias на .docker-deps/ с явной копией shared-кода. Мы выбрали (3) для максимального контроля.

9. CREATE DATABASE OWNER=user требует superuser

В Supabase встроенный postgres пользователь — не superuser (это сделано умышленно для безопасности). При попытке создать дополнительную базу под другого владельца получаем permission denied. Использовать supabase_admin (он superuser в составе self-hosted дистрибутива) либо отдельный admin-аккаунт. Документация Supabase это упоминает мельком, обычно ловится в момент миграции.

10. Traefik Let’s Encrypt — лимит 5 issues / cert / неделю

Production-эндпоинт LE даёт максимум 5 выпусков одного сертификата за 7 дней. Если в процессе отладки Traefik перезапускался много раз и каждый раз выпускал новый сертификат — упрётесь в лимит на самом интересном месте. Профилактика: для итеративной настройки использовать staging endpoint LE (https://acme-staging-v02.api.letsencrypt.org/directory), для прод-выпуска переключать только когда роуты стабильны.

11. DNS TTL — главный bottleneck cutover

Старая запись с TTL = 6 часов кэшируется у провайдеров и пользователей до полного истечения. Даже если на authoritative-сервере DNS обновлён мгновенно — реальные клиенты могут видеть старый IP ещё 6 часов. Best practice: за 24+ часа до миграции снизить TTL до 300 секунд, провести cutover, восстановить TTL обратно после стабилизации. Если забыли — у части пользователей сайт «лежит» половину суток.

12. Cross-host SCP в Swarm — нет встроенных SSH-ключей между нодами

Когда нужно перенести файл с manager на worker, инстинктивно набираешь scp manager:file worker:file. В стандартном Swarm у нод нет SSH-доступа друг к другу — это плоская сеть на Docker API. Workaround: ssh manager 'cat file' | ssh worker 'cat > file' (через managing machine как через гейтвей). Альтернатива — кидать файлы через Swarm secrets или общий Docker volume.

13. Hairpin NAT ломает доступ контейнеров к собственным publicly-exposed сервисам

Контейнер на worker-ноде делает fetch('https://api.example.com/...'). DNS резолвится в публичный IP, traffic идёт в файрвол, файрвол должен сделать «петлю» обратно в этот же сегмент — большинство SOHO-роутеров и часть enterprise-firewalls этого не умеют (hairpin / NAT-reflection отключен). В итоге запрос вешается. Решение: extra_hosts в compose с явным mapping публичного hostname на internal IP кластера. У нас это работающий паттерн для server-side fetch между сервисами одной инфраструктуры.

14. Restart Docker daemon без restart: always убивает контейнеры

Стандартный сценарий: меняешь /etc/docker/daemon.json (например, добавляешь log-driver), делаешь systemctl restart docker. Все контейнеры compose-стека, у которых не было restart: always или unless-stopped, не поднимаются автоматически. Думаешь «ну и ладно, сейчас compose up подниму» — а в продакшене это незапланированный downtime. Привычка: проверять restart: always на всех контейнерах перед любым редактированием daemon-конфига.

Migration capability test: 60 секунд

После того как стек заработал, мы хотели проверить главное: реально ли Dokploy упрощает миграцию сервиса между нодами. Выбрали Gotenberg (stateless PDF-конвертер, replicas=1) и сменили placement constraint с worker-1 на worker-2:

docker service update --constraint-rm 'node.hostname == worker-1' \
                      --constraint-add 'node.hostname == worker-2' \
                      hatchet_gotenberg

Результат:

Для replicas≥2 миграция была бы rolling — без окна недоступности вообще. Это паттерн для planned maintenance: апгрейд ядра ноды, замена железа, расширение кластера.

Метрики «до» и «после»

МетрикаДоПослеДельта
Deploy нового сервиса~30 минут (compose + nginx reload)~2 минуты (UI или docker stack deploy)×15
SSL renewalкаждые 60 дней рукамиавтоматический LE
Rollback версии~10 минут (compose pull + restart)1 клик в UI×10
Cross-node migrationневозможно без downtime60 секундновая возможность
Monitoring визуально4 разных UI + SSH1 (Dokploy)UX-win
Footprint контрол-плейнаn/a~0.8% CPU, 350 MB RAMпренебрежимо

Disclaimer: данные «после» собраны на горизонте одной недели после миграции. Поведение под продолжительной нагрузкой и при инцидентах ещё предстоит наблюдать.

Когда Dokploy НЕ подходит

Хотим быть честными: Dokploy — отличный инструмент для конкретной ниши, но не универсальный молоток.

Эвристика: до 30–50 сервисов на 1–3 нодах с одной командой DevOps — Dokploy сладкая точка. Дальше — растёт сложность управления и стоит смотреть в сторону K8s.

FAQ

Подходит ли Dokploy для production?

Да, при правильной конфигурации. Dokploy — это управляемая обвязка над Docker Swarm и Traefik. В нашем проде на нём крутится ~17 сервисов: 13 контейнеров Supabase, 2 контейнера Keycloak, Hatchet, Gotenberg, 3 SvelteKit-фронтенда и payment-service. Стабильность определяется Swarm и Traefik (зрелые компоненты), Dokploy добавляет только UI и метаданные.

В чём отличие Dokploy от Coolify, CapRover и Portainer?

Coolify и CapRover работают со standalone Docker, в Swarm — ограниченно. Portainer умеет управлять Swarm, но без интегрированного Traefik и автоматического Let’s Encrypt — нужен ручной reverse proxy. Dokploy сразу даёт Swarm-native deployment, Traefik auto-discovery и LE-сертификаты из коробки.

Можно ли мигрировать с docker-compose без даунтайма?

Частично. Stateless-сервисы (фронтенды, API gateway) переключаются blue/green через смену DNS и Traefik labels — даунтайм 0. Stateful (Postgres, файловые storage) требует короткого окна (5–10 минут) на pg_dump → pg_restore. С replicas=2 после миграции последующие обновления идут rolling без перерыва.

Сколько RAM и CPU требует сам Dokploy?

Контрол-плейн (Dokploy UI, Traefik, Redis, Postgres под метаданные) ест ~350 MB RAM и ~0.8% CPU на manager-ноде в idle. Worker-ноды Dokploy не имеют своих агентов — управление идёт через нативный Docker Swarm API. Накладные расходы пренебрежимо малы относительно типичных рабочих нагрузок.

Поддерживает ли Dokploy Docker Swarm с несколькими нодами?

Да, нативно. Manager-нода с Dokploy раздаёт деплои на все воркеры через стандартный Docker Swarm API. Placement constraints (привязка stateful-сервиса к конкретной ноде) задаются в Swarm-секции compose-файла и Dokploy уважает их при rolling-update.

Как получить автоматический SSL для всех доменов?

Через Traefik labels в Swarm deploy.labels. Добавляешь traefik.http.routers.<svc>.tls.certresolver=letsencrypt — Traefik сам запрашивает HTTP-01 challenge при первом запросе и обновляет сертификат за 30 дней до истечения. На наш стек выпуск ~12 сертификатов прошёл за 4 минуты.

Что если Dokploy upstream перестанет развиваться?

Риск минимальный: Dokploy — open-source MIT, исходники на GitHub. Если разработка остановится — текущая версия продолжит работать (Swarm и Traefik самодостаточны), форк всегда возможен. Сервисы под управлением Dokploy — это стандартные Docker Swarm стеки, их можно поднять и без Dokploy через docker stack deploy.

Можно ли откатить деплой одним кликом?

Да. Dokploy хранит историю билдов и тегов для каждого application. Откат = выбор предыдущей версии в UI или docker service update --image <old-tag> через CLI. Время отката равно времени health-check rolling update — у нас ~30–60 секунд для типичного фронтенда.

Итог и что дальше

За полтора месяца от старта до production-ready стека прошло чуть больше, чем мы планировали — но не из-за Dokploy, а из-за второстепенных задач (сертификаты, сетевые правила в файрволле, тесты с реальной нагрузкой). Сама миграция в день X заняла 45 минут foundation + 15 минут apps. С учётом теста миграции между нодами — 1 час wall-time.

Ключевые выгоды для команды:

Ограничения, с которыми остались:

Хотите похожий результат у себя? Если в вашем периметре уже есть несколько compose-стеков, и хочется свести их в управляемый кластер с авто-SSL и быстрым rollback — мы делаем это под ключ: аудит → playbook → миграция с backup-rollback гарантией. Типичный проект — от 3 до 8 недель в зависимости от количества сервисов. Свяжитесь через контакт-форму или напишите на info@webshturm.ru — пришлём примерную смету и план в течение двух рабочих дней.

Ссылки