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-стеков на двух виртуальных хостах:
- Caddy на frontend-хосте — единый HTTPS-termination и manual cert renewal через ACME-провайдер.
- Standalone Supabase на backend-хосте — 13 контейнеров (Postgres, Kong, Auth, Storage, Realtime, Functions, Studio, Imgproxy, Meta, Vector, Supavisor, Analytics, Pooler).
- Keycloak там же — отдельный compose с собственным Postgres-volume.
- Worker для Dokploy — отдельно запущенный, без integration с management UI.
Боли, которые накопились за полгода эксплуатации:
- Два источника истины конфига. Часть прокси-правил жила в Caddyfile, часть — в
docker-compose.yml. Любое изменение требовало синхронизации в двух местах. - Нет visibility в одном месте. Чтобы понять «какие контейнеры running, сколько RAM, что в error», нужно было заходить на хост по SSH и звать
docker psплюсdocker logs. - SSL renewals каждые 60 дней руками. Caddy умеет автоматический ACME, но провайдер сертификатов выбрали с ручной выгрузкой → cron-задача напоминала о renewal, а сам процесс делал DevOps.
- Cross-host миграция = простой. Если backend-нода падала, перенос сервисов на frontend-ноду означал ручную сборку compose, копирование volumes и downtime в 30–60 минут.
Архитектура работала, но не масштабировалась людьми: каждое изменение требовало внимания инженера.
Почему именно Dokploy
Мы сравнили пять кандидатов self-hosted PaaS перед выбором:
| Критерий | Dokploy | Portainer CE | CapRover | Coolify | Komodo |
|---|---|---|---|---|---|
| Docker Swarm native | ✓ | ✓ | partial | ✗ | ✓ |
| Self-hosted free | ✓ | community/paid | ✓ | ✓ | ✓ |
| Traefik интегрирован | ✓ | manual | nginx | ✓ | ✓ |
| Multi-node UI | ✓ | ✓ | ✓ | ✗ | ✓ |
| Open source | ✓ MIT | community CE | ✓ | ✓ | ✓ |
| Setup time | ~5 мин | ~10 мин | ~15 мин | ~10 мин | ~10 мин |
| Footprint контрол-плейн | ~350 MB | ~250 MB | ~600 MB | ~500 MB | ~400 MB |
Решающими стали четыре фактора:
- Native Docker Swarm. Coolify работает только со standalone Docker, что исключало его для multi-node deployments. CapRover поддерживает Swarm частично — без полноценных placement constraints.
- Traefik auto-discovery + Let’s Encrypt без танцев. Labels на deploy-секции compose-файла — и Traefik сам поднимает роут, выпускает сертификат, делает HTTP→HTTPS redirect. Нет отдельных Caddy/nginx конфигов.
- Минимальный footprint. Manager-нода с Dokploy + Traefik + Redis + Postgres контрол-плейна ест ~350 MB RAM и ~0.8% CPU в idle. Worker-ноды не имеют своих агентов вообще — Dokploy управляет ими через стандартный Swarm API.
- 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)
Ключевые архитектурные решения:
- Manager отделён от worker по ролям. Manager держит контрол-плейн (Dokploy UI, Traefik ingress, Redis для метаданных) и не получает прикладные сервисы. Worker — рабочая лошадь, на ней placement-constraint всех app-стеков.
- Overlay-сеть для service-to-service. Все контейнеры в
dokploy-networkoverlay (создаётся Dokploy автоматически). Сервисы обращаются друг к другу по DNS-имени (postgres,kong,keycloak) — Swarm разрешает их в VIP внутри оверлея. - Placement constraints для stateful. Postgres-volume привязан к worker-ноде через
placement.constraints: node.hostname == worker-1. Фронтенды и payment-service — без constraints, могут мигрировать между нодами по решению Swarm. - Traefik labels для auto-discovery. Никакой ручной конфигурации роутера — каждый сервис в своих deploy-labels декларирует
traefik.http.routers.<name>.rule=Host(...)иtls.certresolver=letsencrypt. Traefik подхватывает изменения за секунды.
Playbook миграции за 45 минут
Реальный wall-time foundation-фазы — 45 минут от первой команды до зелёного smoke-теста. Разбили на две фазы — A (Foundation) и B (Apps).
Phase A — Foundation (~30 минут)
- Backup всего и сразу.
pg_dumpвсех баз в Postgres-контейнере (1.7 GB сжатого), Keycloak realm export черезkc.sh export, копия Caddy LE-store, копии всех.env. Сложили в/opt/backup/pre-migration-<date>/. - Остановка старых стеков.
docker compose downдля Supabase + Keycloak.systemctl stop caddyдля ingress. Старые контейнеры висят остановленные — это и есть hot-rollback на случай если новый стек не взлетит. - DNS и firewall. На пограничном файрволле перебросили правила forward с frontend-хоста на manager-ноду нового кластера. DNS-записи оставили теми же — рейтинг в поисковиках не теряем.
docker stack deployдля базовых сервисов. Подготовленные YAML-файлы для supabase и keycloak стеков. Swarm разворачивает их за 30–60 секунд каждый. Traefik видит labels, выпускает LE-сертификаты при первом запросе через HTTP-01.- Восстановление данных и smoke.
pg_restoreдампа в новый Postgres-контейнер.kc.sh importrealm.curl -Iна все публичные хосты — все 200 (или 401 для auth-gateway, тоже ОК).
Phase B — Applications (~15 минут)
- Async-слой. Hatchet Lite (workflow engine) + Gotenberg (PDF renderer) одним отдельным стеком. Подцепился к существующему Postgres через изолированную schema
hatchet. - Контейнеризация app-сервисов. Для двух фронтендов уже были Dockerfile — собрали
docker buildна worker-ноде, тегнулиprod-1. Payment-service Dockerfile написали по образу фронтенда (multi-stage builder + runner на Node 20). - Деплой фронтенд-стека. Отдельный
frontends-stack.ymlс тремя сервисами (B2C-портал, admin-портал, payment-service). Каждый со своими Traefik labels и env-секретами из единого/opt/frontends-stack/.env. - 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
Результат:
- 60 секунд от команды до
replicas: 1/1на worker-2. - Zero loss трафика при replicas=1 (есть короткое окно, когда сервис недоступен).
- Reverse-миграция за то же время.
Для 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 | невозможно без downtime | 60 секунд | новая возможность |
| Monitoring визуально | 4 разных UI + SSH | 1 (Dokploy) | UX-win |
| Footprint контрол-плейна | n/a | ~0.8% CPU, 350 MB RAM | пренебрежимо |
Disclaimer: данные «после» собраны на горизонте одной недели после миграции. Поведение под продолжительной нагрузкой и при инцидентах ещё предстоит наблюдать.
Когда Dokploy НЕ подходит
Хотим быть честными: Dokploy — отличный инструмент для конкретной ниши, но не универсальный молоток.
- Нужен Kubernetes-экосистема (Helm, ArgoCD, GitOps full). Dokploy не пытается быть K8s, у него нет Helm-charts, declarative reconciliation, custom resources. Если ваш стек уже на K8s — оставайтесь там.
- Multi-cloud orchestration. Dokploy управляет одним Swarm-кластером. Для гибридных deployments через AWS / Azure / on-prem с единым контрол-плейном нужны Rancher, Terraform или внешний инструмент.
- Строгий compliance (PCI-DSS, HIPAA с обязательным K8s). Если регуляторный аудит требует именно Kubernetes, Dokploy может не пройти проверку — даже если функционально равноценен.
- Больше 100 сервисов на одном кластере. UI Dokploy становится перегруженным при сотнях deployments. Для такого масштаба лучше иметь дашборд более высокого уровня (Grafana + Loki + Prometheus с собственными ролями).
- Нужна first-class observability с алертами. Dokploy показывает базовые метрики и логи. Для production-level алертинга, SLO, тонкого трейсинга — берите Prometheus + Grafana + Tempo поверх.
Эвристика: до 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.
Ключевые выгоды для команды:
- Один UI вместо четырёх. DevOps быстрее видит, что упало, без
ssh + docker logs. - Сертификаты не отвлекают. За месяц после деплоя — ноль ручных действий с TLS.
- Миграция между нодами стала реальной операцией. Раньше это было «отдельный проект», теперь — одна команда.
Ограничения, с которыми остались:
- Нет первоклассного алертинга — для прода с SLO нужно прикручивать Prometheus/Grafana.
- Документация Dokploy местами догоняет код — пара edge cases (типа Swarm placement constraints для stateful) пришлось узнавать из issue-трекера.
- UI на 17 сервисах ещё комфортный, на 50+ начнёт уплотняться.
Хотите похожий результат у себя? Если в вашем периметре уже есть несколько compose-стеков, и хочется свести их в управляемый кластер с авто-SSL и быстрым rollback — мы делаем это под ключ: аудит → playbook → миграция с backup-rollback гарантией. Типичный проект — от 3 до 8 недель в зависимости от количества сервисов. Свяжитесь через контакт-форму или напишите на info@webshturm.ru — пришлём примерную смету и план в течение двух рабочих дней.
Ссылки
- dokploy.com — официальная документация — установка, applications, secrets management
- traefik.io/traefik — документация Traefik v3 — labels reference и Let’s Encrypt resolver
- docker.com/docs — Docker Swarm mode — официальный гайд по Swarm
- letsencrypt.org/docs/rate-limits — лимиты Let’s Encrypt (5 issues/cert/week и др.)
- pdf-voucher-pipeline-hatchet-gotenberg — наш предыдущий разбор Hatchet + Gotenberg pipeline, который тоже теперь живёт в этом Dokploy-стеке
- claude-code-production-lessons — как мы вообще ведём такие миграции, оркестрируя работу через Claude Code