HostiServer
2026-09-26 11:18
Docker у production: reverse proxy, SSL
📚 Серія «Docker на VPS», частина 3 з 5:
Docker у production: reverse proxy, SSL, автозапуск
У попередній статті ми звели застосунок, базу даних і кеш в один docker-compose.yml, і стек піднімається однією командою. Але в тому вигляді, в якому він є, до production йому ще далеко. Порт 3000 відкритий напряму в інтернет, трафік іде тільки по HTTP, а другий проєкт на тому самому VPS одразу вперся б у зайнятий порт. І якщо сервер перезавантажиться — наприклад, після планового оновлення ядра — стек сам не підніметься: потрібно вручну зайти по SSH і виконати docker compose up -d.
У цій статті закриваємо три задачі одразу. Перша — Nginx як reverse proxy: один сервер, один IP, стандартні порти 80 і 443, скільки завгодно застосунків за ним. Друга — SSL-сертифікат від Let's Encrypt, безкоштовний і з автоматичним поновленням. Третя — systemd-юніт, який піднімає весь стек після перезавантаження сервера без участі людини. Бонусом розберемо ліміти ресурсів, щоб один контейнер, що вийшов з-під контролю, не поклав увесь сервер, і ротацію логів, яка інакше рано чи пізно забиває диск.
2. Nginx як reverse proxy в Docker
2.1 Nginx як окремий сервіс
Nginx додається в docker-compose.yml як ще один сервіс, і саме він тепер єдиний, хто має відкриті назовні порти — 80:80 і 443:443. Секція ports: прибирається із сервісу app: застосунок більше не доступний напряму з інтернету, тільки через проксі.
2.2 Спільна мережа
Nginx і app лишаються в тій самій мережі app-net, що й раніше. Nginx резолвить ім'я app:3000 точно так само, як сам app резолвив db:5432 у попередній статті — через вбудований DNS Compose.
2.3 Мінімальний HTTP-конфіг для старту
Перед тим як отримувати сертифікат, потрібен робочий HTTP-конфіг — саме через нього Let's Encrypt буде перевіряти, що ви дійсно володієте доменом:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://app:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Кожен із заголовків має конкретну причину бути там: Host передає застосунку оригінальне ім'я домену, яке інакше загубилося б за проксі; X-Real-IP — реальну IP-адресу відвідувача, бо без нього застосунок бачив би тільки внутрішню IP контейнера Nginx; X-Forwarded-Proto повідомляє, чи запит прийшов по HTTP, чи по HTTPS — застосунку це потрібно, наприклад, щоб правильно формувати абсолютні посилання.
2.4 Залежність від app
У сервісі nginx додається depends_on: [app], щоб Compose піднімав застосунок раніше за проксі. За потреби сюди ж можна додати умову service_healthy, якщо у застосунку є ендпоінт на кшталт /health, який healthcheck зможе опитувати.
3. SSL: Let's Encrypt + Certbot
3.1 Передумова — DNS
Перш ніж запускати Certbot, домен має вказувати на IP-адресу сервера. Перевірити це можна одразу:
dig +short example.com
У виводі має бути саме IP вашого VPS. Якщо запис ще не поширився по DNS, Certbot гарантовано не пройде перевірку — і варто почекати, а не намагатись налагодити щось у конфігурації Nginx.
3.2 Certbot на хості, не в контейнері
Certbot встановлюється прямо на сервер, а не запускається як окремий Docker-контейнер — так простіше з подальшим автоматичним поновленням, і не потрібно прокидати сокети чи томи туди-сюди. Найпоширеніший спосіб — webroot: Certbot кладе тестовий challenge-файл у визначену директорію, а Nginx, у якого ця директорія примонтована, роздає його назовні у відповідь на запит Let's Encrypt.
3.3 Альтернатива — standalone
Якщо не хочеться налаштовувати webroot окремо, є режим standalone: Certbot сам піднімає тимчасовий вебсервер на порту 80. Для цього Nginx доведеться зупинити приблизно на п'ять секунд — прийнятно для першого отримання сертифіката, але незручно для регулярного поновлення на роботаючому production-сервері.
3.4 Куди потрапляє результат
Після успішного випуску сертифікат і приватний ключ з'являються за шляхом /etc/letsencrypt/live/example.com/{fullchain.pem,privkey.pem}. Ця директорія монтується всередину контейнера Nginx як bind mount, обов'язково в режимі :ro (read-only) — Nginx має право читати сертифікат, але не повинен мати можливість його змінити.
4. Nginx HTTPS конфіг
4.1 Два server-блоки
Робочий конфіг складається з двох блоків: перший слухає порт 80 і одразу віддає редирект return 301 https://$host$request_uri;, другий слухає 443 і містить сам SSL:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
location / {
proxy_pass http://app:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}
4.2 Чому TLSv1.2 і TLSv1.3, і не нижче
ssl_protocols TLSv1.2 TLSv1.3; — мінімальна планка, нижче якої опускатись не варто: TLS 1.0 і 1.1 офіційно визнані застарілими і не проходять сучасні security-сканери. ssl_prefer_server_ciphers off; відповідає рекомендаціям Mozilla Intermediate-профілю — при сучасних клієнтах вибір шифру краще довірити клієнту, а не примусово нав'язувати порядок сервера.
4.3 HSTS та інші security-заголовки
Strict-Transport-Security каже браузеру запам'ятати, що цей домен має відвідуватись тільки по HTTPS, навіть якщо користувач введе адресу без https://. Ризик тут реальний: значення max-age=63072000 — це два роки, і якщо протягом цього часу HTTPS раптом стане недоступним (наприклад, сертифікат прострочився і його вчасно не поновили), браузери просто відмовляться відкривати сайт по HTTP як запасний варіант. Вмикати HSTS варто тільки тоді, коли автоматичне поновлення сертифіката вже перевірене і працює. X-Content-Type-Options і X-Frame-Options — додаткові, менш ризиковані заголовки проти MIME-sniffing і клікджекінгу.
4.4 proxy_read_timeout
Значення за замовчуванням у Nginx — 60 секунд, і для більшості API цього достатньо. Збільшувати варто тільки під конкретні довгі операції: генерація звіту, обробка великого файлу, довгий запит до зовнішнього API. Піднімати timeout глобально "про запас" не варто — зависла з якоїсь причини бекенд-задача краще обірветься за хвилину, ніж буде тримати з'єднання відкритим невизначено довго.
4.5 Застосування конфігу без даунтайму
docker compose exec nginx nginx -s reload
nginx -s reload перечитує конфігурацію і сертифікати, зберігаючи вже відкриті з'єднання — на відміну від повного перезапуску контейнера (docker compose restart nginx), який на коротку мить обірве всі активні підключення.
5. Автоматичне поновлення SSL
5.1 Термін дії сертифіката
Сертифікати Let's Encrypt видаються на 90 днів. Команда certbot renew перевіряє всі наявні сертифікати і оновлює тільки ті, у яких лишилось менше 30 днів — тобто її можна безпечно запускати хоч щодня, вона не буде оновлювати те, що ще не потребує оновлення.
5.2 Чому потрібен reload після renewal
Nginx завантажує сертифікат у пам'ять один раз при старті чи при reload і не перечитує файл на диску сам по собі. Тому навіть після успішного certbot renew Nginx продовжить використовувати старий сертифікат, доки йому явно не скажуть перечитати конфігурацію.
5.3 Deploy hook
Certbot вміє запускати довільний скрипт одразу після успішного поновлення — саме сюди і виноситься reload:
certbot renew --deploy-hook "docker compose exec nginx nginx -s reload"
5.4 Cron-завдання
Достатньо запускати перевірку раз на добу, бажано вночі при низькому навантаженні:
0 3 * * * certbot renew --quiet --deploy-hook "docker compose exec nginx nginx -s reload"
Прапор --quiet прибирає вивід у випадках, коли оновлення не потрібне — інакше системна пошта чи лог cron забиваються однаковими повідомленнями щодня.
5.5 Перевірка, що механізм справді працює
certbot renew --dry-run
certbot certificates
--dry-run прогонює весь процес поновлення без реальної видачі нового сертифіката — безпечний спосіб переконатись, що webroot і deploy hook налаштовані правильно. certbot certificates показує список сертифікатів і дату їхнього завершення — варто перевірити хоча б одного разу вручну після налаштування cron.
6. Автозапуск після reboot
6.1 restart: unless-stopped vs restart: always
Різниця проявляється тільки в одному сценарії: якщо стек зупинили вручну командою docker compose stop. З unless-stopped Docker запам'ятає, що контейнер зупинили свідомо, і не підніме його автоматично навіть після перезавантаження демона. З always контейнер підніметься в будь-якому випадку, ігноруючи те, що його зупинили вручну — для production-стека це рідко бажана поведінка.
6.2 Обмеження restart policy
Жодна з restart policy не допоможе, якщо стек зупинили через docker compose down: ця команда не просто зупиняє, а видаляє контейнери повністю. Перезапускати вже нічого — Docker навіть не знає, що такий контейнер мав існувати. Це саме той сценарій, де потрібен зовнішній механізм, який виконає docker compose up -d заново.
6.3 systemd-юніт для Compose
[Unit]
Description=My App (Docker Compose)
After=docker.service network-online.target
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/app
ExecStart=/usr/bin/docker compose up -d
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=300
[Install]
WantedBy=multi-user.target
Type=oneshot з RemainAfterExit=yes тут ключові: команда docker compose up -d сама виходить одразу після запуску контейнерів у фоні, і без цих двох параметрів systemd вважав би сервіс завершеним і "неактивним" одразу після старту. After=docker.service network-online.target гарантує, що юніт стартує вже після того, як сам Docker daemon і мережа готові — інакше docker compose up -d впаде одразу після boot, коли Docker ще не встиг піднятись.
6.4 Увімкнення і перевірка
sudo systemctl enable app.service
sudo systemctl start app.service
journalctl -u app.service
Найнадійніший спосіб перевірити, що все дійсно спрацює — реальний тест: sudo reboot, зачекати, зайти знову і подивитись docker compose ps. Довіряти конфігурації на слово, не перевіривши перезавантаженням хоча б раз, не варто.
6.5 Два рівні разом
restart: unless-stopped у compose-файлі та systemd-юніт вирішують різні задачі і працюють разом, а не замість одне одного: systemd відповідає за те, щоб стек піднявся заново після перезавантаження хоста, а restart: unless-stopped — за те, щоб окремий контейнер, який впав усередині вже працюючого стека, піднявся сам без перезапуску всього іншого.
7. Ліміти ресурсів
7.1 Навіщо це потрібно
Без явних лімітів memory leak в одному сервісі чи процес, що вийшов з-під контролю, здатен використати всю пам'ять сервера і покласти VPS цілком — типово це стається вночі, коли ніхто не дивиться на графіки. Ліміти обмежують збиток одним конкретним контейнером: впаде він, а не сусідні сервіси й не хост.
7.2 Синтаксис у Compose v2
Без Swarm ліміти задаються через deploy.resources.limits прямо в стандартному docker-compose.yml:
services:
app:
deploy:
resources:
limits:
memory: 384m
cpus: "0.5"
7.3 Різна поведінка при перевищенні
Ці два ліміти працюють по-різному. Перевищення limits.memory призводить до SIGKILL — ядро вбиває процес усередині контейнера (OOM killer), і якщо задано restart: unless-stopped, Docker підніме контейнер заново. Перевищення limits.cpus не вбиває нічого — сервіс просто throttle'иться, отримуючи менше процесорного часу, і продовжує працювати повільніше.
7.4 Орієнтовний розподіл для VPS на 2 GB RAM
| Сервіс | Ліміт пам'яті |
|---|---|
| nginx | 64m |
| app | 384m |
| db | 512m |
| cache | 128m |
Це орієнтир, а не універсальне правило — конкретні цифри залежать від навантаження. Секція reservations поруч із limits задає не жорстку межу, а гарантований мінімум і корисна радше як документація очікуваного споживання, ніж як механізм захисту.
7.5 Моніторинг у реальному часі
docker stats
Показує живе споживання CPU, пам'яті і мережі кожним контейнером — перший інструмент, до якого варто звертатись, підбираючи реалістичні значення лімітів замість цифр навмання.
8. Ротація логів
8.1 Проблема за замовчуванням
Драйвер логування json-file, який Docker використовує за замовчуванням, нічим не обмежений — файл логів росте необмежено, доки не скінчиться місце на диску. Перевірити поточний стан:
du -sh /var/lib/docker/containers/*/
8.2 Обмеження на рівні сервісу
services:
app:
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Це обмежує кожен лог-файл 10 мегабайтами і зберігає не більше трьох файлів по черзі — старіші видаляються автоматично. Той самий результат можна отримати глобально для всіх контейнерів хоста через /etc/docker/daemon.json, якщо не хочеться прописувати це в кожному сервісі окремо.
8.3 Як це виглядає при перегляді логів
docker compose logs -f поводиться однаково незалежно від того, чи налаштована ротація: команда читає поточні активні файли логів прозоро для користувача, ротація впливає лише на те, скільки історії фізично зберігається на диску.
9. Traefik як альтернатива Nginx
9.1 Ключова відмінність підходу
Замість окремого файлу конфігурації Traefik читає налаштування прямо з labels на самих сервісах у docker-compose.yml. ACME-клієнт для Let's Encrypt вбудований одразу — окремий Certbot не потрібен.
9.2 Коли Traefik виграє
Перевага стає відчутною, коли мікросервісів багато і вони часто додаються чи видаляються: правити labels конкретного сервіса простіше, ніж щоразу редагувати спільний nginx.conf і не забути про reload.
9.3 Компроміс
За зручність доводиться платити доступом: Traefik потребує змонтованого /var/run/docker.sock усередині свого контейнера, щоб бачити labels інших сервісів — а це фактично root-доступ до хоста, за тією ж логікою, що й членство в групі docker з першої статті цієї серії. Nginx такого доступу не потребує взагалі.
10. Висновок
Стек пройшов шлях від відкритого напряму порту 3000 до повноцінної production-конфігурації: Nginx перед застосунком, SSL-сертифікат від Let's Encrypt з автоматичним поновленням, systemd-юніт для автозапуску після перезавантаження, ліміти ресурсів і ротація логів.
Архітектура тепер виглядає так: інтернет потрапляє тільки на порти 80 і 443, де його зустрічає Nginx; він проксіює запити на app:3000 усередині мережі app-net; db і cache ззовні недосяжні взагалі.
Короткий чек-лист перед тим, як вважати стек готовим:
- DNS домену вказує на IP сервера (
dig +short) - Certbot випустив сертифікат, HTTPS відкривається в браузері без попереджень
- Deploy hook викликає
nginx -s reload, перевірено черезcertbot renew --dry-run - systemd-юніт увімкнено (
systemctl enable) - Стек дійсно піднімається сам після реального
reboot, а не тільки в теорії - Ліміти пам'яті виставлені і звірені з реальним споживанням через
docker stats
Стек готовий до production-навантаження. Лишається останній ручний крок — деплой нової версії застосунку все ще означає SSH на сервер і виконання команд руками. Наступна стаття закриває саме це: автоматичний деплой через GitHub Actions чи GitLab CI, який після кожного merge сам збирає образ, заливає його в registry і оновлює стек на сервері.
📚 Навігація серії:
Ви читаєте частину 3 з 5 «Docker у production».
Попередня: Частина 2. Docker Compose: кілька сервісів разом ←
Далі: Частина 4. Docker + CI/CD: автодеплой з GitHub Actions / GitLab CI →
🚀 Production-сервер під ваш Docker-стек
Reverse proxy, SSL-термінація, кілька контейнерів під навантаженням і ліміти, які мають сенс лише тоді, коли ресурсів дійсно достатньо. Hostiserver дає передбачувану інфраструктуру саме під такий сценарій.
🖥️ Виділені Сервери
- Від $90/міс, повний контроль над залізом для production-навантаження
- Без сусідів по ресурсах: ліміти контейнерів рахуються від реальних, а не спільних потужностей
- 24/7 підтримка: допоможемо з systemd-юнітом, Nginx і поновленням сертифікатів
💻 Cloud (VPS) Хостинг
- Від $19.95/міс, KVM-ізоляція, виділені vCPU і RAM
- Виділена IP-адреса: коректна робота DNS і видача сертифіката Let's Encrypt без сюрпризів
- NVMe-диски: логи, сертифікати і дані бази без просідання швидкодії
- Легко масштабувати: більше RAM під нові ліміти, коли стек росте
💬 Не впевнені, який варіант потрібен?
💬 Напишіть нам — допоможемо з вибором!
Часті запитання
- Чому Certbot ставлять на хост, а не запускають у контейнері?
Так простіше з автоматичним поновленням: не потрібно прокидати додаткові томи чи сокети між контейнером Certbot і Nginx, cron-завдання на хості напряму викликає системну команду і одразу перезавантажує Nginx через
docker compose exec. Контейнеризований Certbot теж можливий, але додає рухомих частин без відчутної переваги для одного сервера.
- restart: always чи unless-stopped — яка різниця на практиці?
Різниця проявляється тільки якщо стек зупинили вручну через
docker compose stop. ЗalwaysDocker підніме контейнер знову, навіть якщо його зупинили свідомо для обслуговування. Зunless-stopped— не підніме, доки ви самі не запустите його знову. Для production-стекаunless-stopped, як правило, безпечніший вибір.
- Ми зробили docker compose down, і стек не піднявся сам — це помилка restart policy?
Ні, це очікувана поведінка.
docker compose downвидаляє контейнери повністю, а не просто зупиняє — restart policy стосується існуючих контейнерів і тут просто нічого перезапускати. Саме для цього сценарію і потрібен systemd-юніт, який виконує повнийdocker compose up -dзаново після перезавантаження хоста.
- Nginx чи Traefik для невеликого self-hosted проєкту?
Для одного-двох застосунків на одному сервері Nginx простіший і не потребує доступу до Docker socket. Traefik починає виграти, коли сервісів багато і вони часто змінюються — тоді конфігурація через labels економить час порівняно з ручним редагуванням
nginx.confпри кожній зміні.
- Як перевірити, що ліміт пам'яті реально працює?
docker statsпоказує поточне споживання в реальному часі — і якщо контейнер впритул наближається до заданогоlimits.memory, можна побачити це до того, як станеться SIGKILL. Сам факт спрацювання ліміту видно вdocker compose psчиdocker inspect: контейнер перезапуститься з кодом завершення, характерним для OOM.
- Чи можна оновити SSL-сертифікат без даунтайму?
Так, це стандартний сценарій.
certbot renewоновлює файли сертифіката на диску без зупинки Nginx, а deploy hook викликаєnginx -s reload, який перечитує конфігурацію й сертифікати, зберігаючи вже відкриті з'єднання. Даунтайм виникає лише при виборі standalone-режиму отримання першого сертифіката, коли Nginx доводиться зупиняти на кілька секунд.
- Після reboot стек не піднявся автоматично — з чого почати діагностику?
Спочатку
systemctl status app.serviceіjournalctl -u app.service— вони покажуть, чи взагалі запускався юніт і на якому кроці впав. Типові причини: юніт не був увімкнений черезsystemctl enable, або він стартував раніше за сам Docker daemon через відсутність правильногоAfter=у секції[Unit].