Что нужно сделать перед развёртыванием Docker в продакшене

Источник: «What you should do before deploying Docker to production»
Краткое руководство, показывающее, как перейти от локальной разработки к продакшену с помощью Docker, включая рабочие процессы развёртывания и CI/CD.

Если вы начинаете путь от локальной разработки к продакшену с Docker и не знаете всех необходимых шагов, это руководство для вас.

Мы рассмотрим некоторые тонкости перехода, такие как создание производственных Docker-файлов и настройка конфигураций. Затем мы поработаем над рабочими процессами развёртывания и закончим CI/CD. К концу мы должны без проблем перенести ваше Docker-приложение в стабильную, живую среду.

Давайте начнём!

Создание файла .dockerignore

Прежде чем приступить к созданию образов Docker и развёртыванию контейнеров в продакшене, необходимо убедиться, что мы добавляем в них только необходимые файлы. Чаще всего при локальной разработке у нас есть файлы и каталоги, которые не нужны в продакшене и занимают довольно много места. Такие вещи, как каталоги вендоров и, конечно, печально известные node_modules.

Если вы знакомы с файлом .gitignore, используемым в системе контроля версий, то файл .dockerignore работает примерно так же. Указание шаблонов для файлов и каталогов позволяет игнорировать их при сборке образа Docker. Таким образом, мы можем уменьшить общий размер конечного образа и повысить производительность сборки.

Вот базовый пример того, что можно включить в файл .dockerignore:

node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
.docker/
.vscode/
.idea/
.DS_Store

Это хорошее начало, охватывающее широкий спектр файлов, часто встречающихся при разработке, но не являющихся необходимыми для продакшена.

Давайте попробуем что-нибудь более конкретное.

Если мы создаём образ для приложения Laravel, в наш файл .dockerignore должны быть добавлены дополнительные файлы и каталоги.

.env
bootstrap/cache/
storage/
vendor/

# ... дополнительные базовые файлы и каталоги, указанные выше

А файл .dockerignore для контейнера приложений Next.js будет содержать другие файлы и каталоги, которые необходимо игнорировать.

node_modules/
out/
.cache/
.next/

# ... дополнительные базовые файлы и каталоги, указанные выше

Файлы и каталоги, специфичные для фреймворка или языка, на котором вы работаете, можно игнорировать отдельно для каждого проекта. В качестве альтернативы можно создать единый файл .dockerignore, содержащий игнорирование для нескольких фреймворков и языков. Это зависит от ваших предпочтений.

Создание конфигурационных файлов для продакшена/рабочей среды

В локальной среде разработки наше приложение может полагаться на определённые параметры и конфигурации, такие как опции отладки, базы данных для разработки или менее строгие параметры безопасности. Однако в продакшене эти конфигурации должны быть точно настроены для оптимальной производительности, безопасности и надёжности.

Для устранения этих различий мы можем создать отдельные версии конфигурационных файлов, предназначенные для развёртывания в продакшене. Сюда могут входить новые файлы конфигурации для веб-серверов, таких, как Nginx или Apache, настройки PHP и любые другие файлы, которые отличаются в среде dev и prod.

Давайте рассмотрим несколько небольших примеров.

При подготовке к созданию рабочего образа мы должны убедиться, что наш файл конфигурации указывает на живой URL и содержит информацию о конфигурациях SSL/HTTPS

server {
listen 443 ssl;
server_name my-prod-domain.com;

location / {
proxy_pass backend:9000;
}

ssl_certificate my-prod-domain.com.crt;
ssl_certificate_key my-prod-domain.com.key;
}

Для PHP вы можете настроить параметры, связанные с сообщениями об ошибках, временем выполнения, путями к логам и кэшированием.

display_errors = Off
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
log_errors = On
error_log = /var/log/php/error.log

max_execution_time = 30
max_input_time = 60
memory_limit = 256M
post_max_size = 20M
upload_max_filesize = 20M

opcache.enable = On

После того как вы создали готовые к продакшену конфигурации, следующим шагом будет их интеграция в образ Docker. Для этого нужно скопировать эти файлы в соответствующие места в образе в процессе сборки.

Сборка образа для продакшена

Теперь, когда мы точно настроили файлы конфигурации для продакшена, следующим шагом будет создание отдельных образов Docker, предназначенных для использования в продакшене.

Если вы ещё не сделали этого, создайте Dockerfile для развёртывания в продакшене. Если в процессе разработки вы используете предварительно созданный образ из Docker Hub, вам придётся начать с нуля. В противном случае можно просто скопировать образ из разработки в новый файл.

Обычно я называю свои файлы как-то вроде local.Dockerfile и prod.Dockerfile.

Первое, что нам нужно сделать, это создать легковесный базовый образ и добавить в него конфигурационные файлы, созданные ранее.

# Используйте минимальный базовый образ, подходящий для продакшена, для этого хорошо подходит alpine
FROM nginx:alpine

# Копирование конфигурации для продакшена
COPY nginx-production.conf /etc/nginx/conf.d/default.conf
COPY php-production.ini /etc/php/8.2/fpm/conf.d/production.ini

В отличие от локальной разработки, где могут быть удобны тома Docker Compose, образы для продакшена должны хранить кодовую базу приложения целиком. Поэтому необходимо включить код для добавления в наш исходник, чтобы конечный образ был полностью самодостаточным.

# ... предыдущее содержимое из верхнего блока

# Копирование исходного кода приложения
COPY . /var/www/html

Команды, точки входа и открытые порты, скорее всего, останутся неизменными между разработкой и продакшеном.

Когда мы подготовили Dockerfile(ы), пришло время собрать образы для продакшена. Мы можем использовать команду docker build, указать путь к директории, содержащей Dockerfile, и указать тэг с именем и версией образа. Это поможет нам различать образы локально и упростит контроль версий.

В терминале мы можем запустить следующую команду.

docker build -t my-app:1.0.0 -f prod.Dockerfile .

Если мы выполним команду docker image ls, то сможем увидеть наш новый образ в локальном реестре. Мы можем запустить его, выполнив docker run my-app, который должен запустить наш продакшен образ.

На этом этапе нужно подумать где будем хранить образ. Мы можем просто загружать его на сервер каждый раз, когда хотим использовать, но практичнее использовать сервис, специализирующийся на хранении образов Docker.

Они называются реестрами контейнеров, и мы используем их для хранения и получения образов Docker в безопасном месте. Этот шаг необязателен, мы можем просто загружать образ на сервер каждый раз, когда хотим его использовать, но он может быть очень полезен для версионирования, совместной работы и автоматизации развёртывания.

Рекомендуемые варианты — Docker Hub или GitHub Container Registry.

После создания учётной записи под одним из вышеперечисленных адресов мы можем отправить наш продакшен образ в реестр с помощью следующей команды.

docker push your-registry-url/my-app:1.0.0

Примечание: Замените your-registry-url на URL, предоставленный вашим реестром контейнеров.

Создав продакшен образы и разместив их в реестре, вы стали на шаг ближе к развёртыванию своего приложения, созданного с помощью Docker, в продакшене!

Модификация файла Docker Compose

Вполне вероятно, что во время разработки (и, соответственно, продакшена) у вас есть несколько сервисов, оркестрированных вместе. Обычно для этого используется что-то вроде встроенного Docker Compose, и в этом случае нам нужно подготовить его для использования в продакшене.

Если это так, скопируйте файл docker-compose.yml в docker-compose.prod.yml. Это будет наш продакшен файл, используемый при развёртывании.

Сначала замените все образы на те, которые мы создали ранее.

version: '3'
services:
app:
image: my-registry-url/my-app:1.0.0
# ...другие опции конфигурации

mysql:
image: mysql:latest
# ...другие опции конфигурации

В отличие от локальных сред разработки, где монтирование томов может быть удобно для перезагрузки кода в реальном времени, в продакшене рекомендуется удалить монтирование томов из конфигурации Docker Compose. Это гарантирует, что продакшен будет полагаться исключительно на образы, созданные в предыдущем шаге. Это делает развёртывание более предсказуемым и воспроизводимым.

version: '3'
services:
app:
image: my-registry-url/my-app:1.0.0
volumes:
- ./src:/var/www/html # удалите меня!
- ./logs:/var/logs # удалите меня!
# ...другие опции конфигурации

Вполне вероятно, что переменные среды для приложений в продакшене будут отличаться от переменных среды в локальной среде разработки. Такие детали, как учётные данные сервера, API-ключи сторонних разработчиков и уровни отладки, вероятно, будут отличаться при переходе на продакшен.

Вместо того чтобы добавлять продакшен env-файл в образ Docker, мы можем сделать его более безопасным, используя атрибут env_file в нашем продакшен файле docker-compose.yml.

version: '3'
services:
app:
image: my-registry-url/my-app:1.0.0
env_file: .env.prod
# ...другие опции конфигурации

mysql:
image: mysql:latest
# ...другие опции конфигурации

Осталось только перенести этот файл на сервер и запустить docker-compose up -d, чтобы наш стек был полностью готов к работе!

Небольшое замечание: использование Docker Compose для оркестрации контейнеров в продакшене — это совершенно нормально. Я запускал с его помощью несколько сторонних проектов с умеренным трафиком. Однако он подходит не для всех задач, поэтому, возможно, стоит рассмотреть другие варианты, например Kubernetes, если вы считаете, что вам нужна более мощная система.

В заключительном разделе мы рассмотрим настройку автоматизированного рабочего процесса, чтобы обеспечить плавный и эффективный процесс развёртывания вашего докеризованного приложения.

Настройка процесса развёртывания(deployment workflow)

Теперь, когда наши продакшен-контейнеры Docker установлены и работают на сервере, неплохо бы разработать надёжную стратегию управления и развёртывания изменений в приложении.

Мы можем использовать ручной подход, где при каждом изменении кода мы собираем и присваиваем тег новому образу Docker. Как это будет выглядеть?

Сначала мы собираем и присваиваем тег новому образу и (по желанию) размещаем его в нашем реестре.

docker build -t my-app:1.1.0 -f prod.Dockerfile .
docker push your-registry-url/my-app:1.1.0

После создания нового образа необходимо перезапустить сервисы на сервере. Это также должно загрузить последний образ.

docker-compose -f docker-compose.prod.yml up -d --no-deps

Хотя такой ручной подход прост, он становится непрактичным, когда приложение усложняется, частота обновлений увеличивается или вы работаете в команде, состоящей из более чем нескольких человек.

Для упрощения этого процесса мы можем использовать CI/CD-платформу, например GitHub Actions. Она позволяет автоматизировать рабочий процесс сборки и развёртывания при автоматической загрузке изменений в репозиторий.

Давайте рассмотрим пример базового рабочего процесса (workflow), чтобы выполнить то, что мы сделали выше.

name: CI/CD

on:
push:
branches:
- main

jobs:
build-and-deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout Repository
uses: actions/checkout@v2

- name: Build and Push Docker Image
run: |
docker build -t my-app:${{ github.sha }} -f prod.Dockerfile .
docker push my-registry-url/my-app:${{ github.sha }}


- name: SSH into Production Server and Update Containers
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.PRODUCTION_SERVER }}
username: ${{ secrets.PRODUCTION_SERVER_USERNAME }}
key: ${{ secrets.PRODUCTION_SERVER_KEY }}
script: |
docker-compose -f /path/to/your/docker-compose.prod.yml up -d --no-deps

Этот пример рабочего процесса выполняет следующее:

  1. Ждёт, когда код будет загружен или слит в основную ветку.
  2. Проверяет код.
  3. Собирает и размещает новый образ Docker с уникальным тегом, основанным на SHA коммита.
  4. Заходит по SSH на рабочий сервер и обновляет контейнеры последними образами.

Чувствительная информация, например данные о рабочем сервере, надёжно хранится в виде секретов в репозитории GitHub.

Используя автоматизированный CI/CD, мы создали оптимизированный и эффективный процесс развёртывания, упростив управление обновлениями и изменениями Docker-образов и приложения в целом.

Следующие шаги

Вот и все! Вы узнали, что нужно для модификации локальной установки Docker в нечто готовое для продакшена, и развернули его на живом сервере. По мере того как вы будете продолжать совершенствовать и поддерживать своё докеризованное приложение, вот некоторые ключевые соображения и дальнейшие шаги:

Мы уже говорили об использовании переменных среды и файлов .env для настройки, но для управления секретами можно использовать сторонние сервисы, такие как HashiCorp Vault или AWS Secrets Manager. Они позволят вам хранить и управлять секретами отдельно от файлов Docker Compose и повысят общую безопасность вашего приложения.

Хотя я не продемонстрировал сопутствующий сервис MySQL в нашей рабочей установке Docker Compose, вы можете использовать внешние сервисы баз данных в качестве альтернативы.

Ранее я уже говорил, что смонтированные тома следует удалять в продакшене. Однако бывают случаи, когда вам нужно обмениваться данными между контейнерами или хранить такие вещи, как изображения на сервере за пределами контейнера.

Для этого мы можем использовать том Docker, убедившись, что монтируемый путь находится в пределах области действия.

services:
app:
image: my-app:1.0.0
volumes:
- my_images:/var/www/html/storage/app/images

volumes:
my_images:
driver: local

Помните, что путешествие не заканчивается на этапе развёртывания!

Придерживайтесь принципа постоянного совершенствования, будьте в курсе новых достижений в Docker и контейнеризации в целом. Адаптируйте новые стратегии развёртывания по мере развития вашего приложения.

Дополнительные материалы

Предыдущая Статья

Основы TypeScript: компилятор TypeScript (tsc) и tsconfig.json

Следующая Статья

Основы TypeScript: JavaScript в сравнении с TypeScript