После того как мы создали первую версию нашего приложения, создали Dockerfile, проверили работоспособность приложения в контейнере, встает вопрос как это выложить на prod, и самое главное как его обновлять далее.
Можно пойти в Kubernetes, но если у вас не большое количество сервисов и нет отдельного отдела для обслуживания Kubernetes, то следуют выбрать что-то попроще.
Экосистема Docker предлагает нам воспользоваться режимом swarm mode. В этом режиме, можно объединять несколько серверов в один кластер и запускать в нем приложения точно так же, как на одном сервере.
Удобные особенности swarm mode, которые облегчают поддержку и обновление сервисов запускаемых через docker:
- deploy без downtime из коробки (правильные порядок запуска и старт новой версии с последующей остановкой старой)
- объединение нескольких серверов в кластер
- overlay сеть поверх всех хостов
- автоматическое распределение сервисов по серверам
Установка Docker
Для начала необходимо установить Docker и Docker compose на сервер.
Переключение в swarm mode
docker swarm init
В консоли отобразиться команда для добавления в кластер других серверов.
Рекомендую на будущее создать overlay сети, чтобы в позже можно было легко подключать дополнительные сервера.
docker network create --driver overlay --attachable cluster-network
Proxy сервис
Если нам на одном сервере необходимо использовать больше одного приложения, которые слушают 80 или 443 порты, то необходимо использовать прокси nginx, Traefik, Envoy или другой. Такой прокси сервис принимает http запросы и на основании url распределяют запросы по сервисам.
В качестве gateway рекомендую использовать Traefik.
Он удобен:
- автоматически считывает конфигурацию из labels контейнеров
- умеет проксировать трафик до наших сервисов и распределять нагрузку
- самостоятельно создает и обновляет сертификат через letsencrypt.org
Для этой роли можем использовать и nginx, но количество ручной работы будет сильно больше.
Для публикации сервиса достаточно прописать labels, например так:
services:
strapi:
deploy:
labels:
- traefik.http.routers.strapi.rule=Host(`strapi.project.ru`)
- traefik.http.services.strapi.loadbalancer.server.port=1337
- traefik.http.routers.strapi.entrypoints=websecure
- traefik.http.routers.strapi.tls.certresolver=default
Этот сервис будет принимать весь трафик с доменом strapi.project.ru по https, так же автоматически будет создан сертификат.
Подготовка docker-compose.yaml
Что и как запускать описывается в уже знакомом формате docker-compose.yaml.
services:
proxy:
image: traefik:v2.9
ports:
- "80:80"
- "443:443"
networks:
- global
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- proxy-acme:/acme
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.swarmMode=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--certificatesresolvers.default.acme.httpchallenge=true"
- "--certificatesresolvers.default.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.default.acme.email=notify@email.ru"
- "--certificatesresolvers.default.acme.storage=/acme/acme.json"
deploy:
mode: replicated
replicas: 1
update_config:
parallelism: 2
delay: 15s
order: start-first
postgres:
image: postgres:16.2
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- global
environment:
TZ: "Europe/Moscow"
POSTGRES_DB: strapi
POSTGRES_USER: strapi
POSTGRES_PASSWORD: strapi
deploy:
mode: replicated
replicas: 1
update_config:
parallelism: 1
order: stop-first
strapi:
image: updev/strapi
networks:
- global
environment:
APP_URL: "https://strapi.project.ru"
APP_KEYS: 33h8bQmgX7sRpIFzEAYSiA==,jn0G/M0akKqWgq73iC+n4w==,o7ZmUZONopLPZiwuoRJ9XA==,hU+7Ra9xwrN+7ZtWXeO+7g==
API_TOKEN_SALT: K/QGU......3Gw==
ADMIN_JWT_SECRET: 1PI.....Gvw==
TRANSFER_TOKEN_SALT: ni......hA==
DATABASE_CLIENT: postgres
DATABASE_HOST: postgres
DATABASE_PORT: 5432
DATABASE_NAME: strapi
DATABASE_USERNAME: strapi
DATABASE_PASSWORD: strapi
YC_BUCKET: strapi-data
YC_ACCESS_KEY_ID: YC....Yk
YC_ACCESS_SECRET: YCO....LyaE
JWT_SECRET: 0wd.....tGKg==
depends_on:
- postgres
deploy:
labels:
- traefik.enable=true
- traefik.docker.network=cluster-network
- traefik.http.routers.strapi.rule=Host(`strapi.project.ru`)
- traefik.http.services.strapi.loadbalancer.server.port=1337
- traefik.http.routers.strapi.entrypoints=websecure
- traefik.http.routers.strapi.tls.certresolver=default
mode: replicated
replicas: 1
update_config:
parallelism: 2
delay: 60s
order: start-first
failure_action: rollback
restart_policy:
condition: on-failure
delay: 15s
volumes:
postgres-data:
proxy-acme:
networks:
global:
external: true
name: cluster-network
В нем описаны:
- Прокси сервис - Traefik
- База данных PostgresSQL
- Strapi
Вместо notify@email.ru
, strapi.project.ru
укажите свои данные.
У сервиса strapi укажите свои секреты и ключи в секции environment.
Запуск сервиса
Для запуска всех сервисов достаточно выполнить команду:
docker stack deploy --compose-file ./docker-swarm/docker-compose.yaml strapi
Эта команда запустит все сервисы, описанные в файле.
Обновление сервиса
Для обновления какого либо сервиса достаточно выполнить такую же команду, как и при запуске.
Сервисы обновятся только те, у которых:
- изменилась конфигурация в docker-compose.yaml
- есть более новая версия образа
За логику обновления отвечает секция update_config
в docker-compose.yaml, документация по этой секции тут.
Разберем на примере:
deploy:
mode: replicated
replicas: 1
update_config:
parallelism: 2
delay: 15s
order: start-first
failure_action: rollback
restart_policy:
condition: on-failure
delay: 15s
max_attempts: 3
mode
- способ запуска в кластере (global - по одному на каждом сервере, replicated - указанное количество контейнеров).
replicas
- сколько копий сервиса запускать. Трафик будет распределяться между ними.
update_config
- описывает как обновлять сервис. В данном случае сначала будет запущена новая версия сервиса,
после того как он будет готов к работе он заменяет старый и через 15 секунд если с новым сервисом все хорошо - старый останавливается.
Если новая версия контейнера не запускается или падает, то возвращается в строй предыдущая версия.
Таким образом достигается обновление сервиса без простоя.
restart_policy
- описывает что делать, если контейнер останавливается.
В данном примере контейнер будет перезапущен до 3 раз, с промежутками в 15 секунд.
Исходный код руководства на GitHub.