Docker на VPS: що змінюється насправді
Docker на VPS працює так само, але має 4 ризики: нестача RAM, обхід UFW опублікованими портами, зупинка після reboot і переповнення диска.
Що змінюється під час запуску Docker на VPS
Docker на VPS використовує той самий engine і ті самі images, що й Docker на вашому ноутбуці, тому всі вже відомі вам команди працюють так само. Змінюється оточення. На ноутбуці є вільна пам’ять, firewall, який ніхто не сканує, і достатньо великий диск, стан якого не потрібно постійно перевіряти. Орендований сервер має фіксований обсяг пам’яті, публічну IP-адресу, яку починають сканувати за кілька хвилин після запуску, і root filesystem, який Docker заповнить без додаткового попередження.
На невеликому сервері більшість проблем спричиняють чотири відмінності:
- Обсяг пам’яті обмежений, і kernel усуває її нестачу, завершуючи процес.
- Опублікований порт обходить UFW (uncomplicated firewall), оскільки Docker самостійно додає правила firewall.
- Контейнери не запускаються повторно після перезавантаження, якщо це не було налаштовано заздалегідь.
- Images, containers, volumes і build cache збільшуються, доки диск не заповниться.
У кожному розділі нижче описано проблему, наведено рядок, який ви фактично побачите, і вказано посібник із докладним рішенням. Якщо ви ще не створили compose file, спочатку прочитайте Основи Docker Compose на VPS, а потім поверніться. Передбачається, що ви вже вмієте запускати stack.
Скільки оперативної пам’яті використовує Docker container?
Менше, ніж очікує більшість користувачів. Container — це процес у cgroup (control group), а не virtual machine, тому в нього немає guest kernel і фіксованого обсягу пам’яті. Витрати визначаються даними, до яких звертається процес усередині. Саме тому повний стек може працювати в 2 GB, тоді як той самий стек на базі virtual machines не помістився б.
Наведені нижче значення типові для idle-режиму стандартних images в Ubuntu 24.04 із конфігурацією за замовчуванням. Їх зчитано з docker stats через кілька хвилин після запуску. Це орієнтир для планування, а не benchmark вашого навантаження. Виконайте docker stats --no-stream на власному сервері, перш ніж покладатися на будь-яке значення, зокрема на наведені тут.
The data behind this chart
[
{
"label": "nginx",
"idle_mb": 8,
"budget_mb": 64
},
{
"label": "Redis 7",
"idle_mb": 12,
"budget_mb": 128
},
{
"label": "Traefik v3",
"idle_mb": 40,
"budget_mb": 128
},
{
"label": "PostgreSQL 16",
"idle_mb": 45,
"budget_mb": 512
},
{
"label": "Uptime Kuma",
"idle_mb": 95,
"budget_mb": 256
},
{
"label": "MariaDB 11",
"idle_mb": 190,
"budget_mb": 512
},
{
"label": "Nextcloud (Apache)",
"idle_mb": 210,
"budget_mb": 768
}
]Два стовпці мають різне призначення. idle_mb показує, скільки container використовує в стані без активної роботи. budget_mb показує, скільки пам’яті потрібно закладати під час планування, оскільки реальне навантаження не є idle. PostgreSQL використовує близько 45 MB у стані idle і потребує 512 MB, коли активні підключення, сортування та cache. Плануйте за стовпцем із резервом пам’яті. Для діагностики використовуйте стовпець idle.
Зверніть увагу на розподіл цих 7 рядків. nginx використовує 8 MB у стані idle, а Nextcloud — 210 MB. Reverse proxy перед вашими застосунками майже не споживає пам’яті. Обсяг сервера визначають database та PHP-застосунок.
Зверніть увагу на docker stats: це значення пам’яті включає page cache, який завантажили власні операції читання файлів container. Тому після запуску воно певний час зростає, а потім стабілізується. Спостерігайте за ним протягом години, перш ніж робити висновок про витік пам’яті.
Розрахунок розміру VPS: що вміщується в 2 GB, 4 GB і 8 GB
Спочатку відніміть частку хоста. Ядро, systemd, journald, sshd і Docker daemon використовують ту саму RAM, що й ваші контейнери, а dockerd разом із containerd займають близько 100 MB. Також потрібна вільна пам’ять для page cache і для пікового навантаження під час збирання образу або створення дампа бази даних.
The data behind this chart
[
{
"plan": "2 GB VPS",
"total_mb": 2048,
"host_reserve_mb": 768,
"container_mb": 1280
},
{
"plan": "4 GB VPS",
"total_mb": 4096,
"host_reserve_mb": 1024,
"container_mb": 3072
},
{
"plan": "8 GB VPS",
"total_mb": 8192,
"host_reserve_mb": 1536,
"container_mb": 6656
}
]host_reserve_mb охоплює операційну систему, Docker daemon і запас пам’яті, який підтримує стабільну роботу сервера під навантаженням. Залишок — це container_mb, і лише його можна використовувати для контейнерів. Резерв збільшується разом із тарифом: від 768 MB на найменшому сервері до 1536 MB на найбільшому, оскільки більший сервер запускає більше контейнерів, записує більше журналів і потребує більше page cache.
Тариф на 2 GB залишає 1280 MB для контейнерів. Якщо витратити 512 MB на PostgreSQL і 128 MB на Traefik, половину пам’яті вже використано. Решти вистачить на два невеликі застосунки приблизно по 256 MB кожен. Це реальний і корисний сервер. Але місця для Nextcloud і кластера пошуку вже не залишиться.
Тариф на 4 GB залишає 3072 MB. Цього достатньо, щоб одночасно розмістити базу даних, reverse proxy, три застосунки та контейнер моніторингу. Це найменший рекомендований розмір для важливих систем, оскільки вільна пам’ять дає змогу пережити невдале розгортання.
Тариф на 8 GB залишає 6656 MB із 8192 MB, і обмеження зазвичай переходить із пам’яті на CPU або пропускну здатність диска. Якщо розрахунок показує, що ваш стек не вміщується, виберіть більший тариф замість спроб оптимізувати систему в межах недостатніх ресурсів: скільки насправді коштує VPS пояснює, яку щомісячну цінність дають додаткові гігабайти.
Два правила допомагають виконувати розрахунок коректно. Установіть memory limit для кожного сервісу, щоб один процес із надмірним споживанням пам’яті не вичерпав ресурси всього сервера. Також не витрачайте весь бюджет, оскільки docker compose build і pg_dump потребують пам’яті саме в момент найбільшого навантаження. У розділі Обмеження пам’яті в Docker Compose наведено синтаксис і типові проблеми.
Чому мій контейнер завершується з кодом 137?
Тому що його завершив kernel. 137 — це 128 плюс 9, а сигнал 9 — це SIGKILL. Контейнер запросив більше пам’яті, ніж йому дозволено, і OOM killer завершив його.
CONTAINER ID IMAGE STATUS
9f2c1a4d7b3e postgres:16 Exited (137) 4 minutes agoПідтвердьте причину, а не покладайтеся на припущення:
docker inspect my-db | grep -i oomkilled
sudo journalctl -k | grep -i "out of memory""OOMKilled": true означає, що контейнер досяг власного ліміту cgroup, а журнал kernel вказує процес, який було вибрано:
Memory cgroup out of memory: Killed process 2417 (postgres) total-vm:1284416kBЦе сприятливий варіант, оскільки проблема обмежилася одним контейнером. Гірший варіант — контейнер узагалі не має ліміту. Без ліміту його верхня межа — це вся машина. Тому витік пам’яті в одному сервісі виснажує ресурси host, після чого kernel вибирає процес-жертву за розміром у всій системі. У рядку журналу зникає префікс Memory cgroup, і він має вигляд Out of memory: Killed process 2417 (postgres). Часто вибраним процесом стає ваша база даних, а контейнер, у якому стався витік, продовжує працювати. Саме тому ліміт для кожного сервісу важливіший за точне значення будь-якого окремого ліміту.
Swap змінює час настання проблеми, але не сам розрахунок. У більшості образів VPS swap не налаштований. Перевірте це за допомогою swapon --show. Якщо swap відсутній, команда не виведе нічого. Файл swap дає kernel місце для переміщення неактивних сторінок пам’яті, завдяки чому у вас може бути кілька хвилин, щоб помітити проблему.
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -hfree -h тепер має показувати ненульове загальне значення в рядку Swap. Swap не збільшує обсяг RAM. Сервер під постійним тиском з боку пам’яті може сповільнитися настільки, що ви не зможете підключитися через SSH для виправлення проблеми. Тому розглядайте swap як буфер для аварійних ситуацій і виправте конфігурацію ресурсів.
Чому UFW не блокує опублікований порт Docker?
Тому що трафік не проходить через ланцюжок, який контролює UFW. Коли ви публікуєте порт за допомогою -p 5432:5432 або запису compose ports:, демон додає правило DNAT (трансляції мережевої адреси призначення) до таблиці nat і правило accept до власного ланцюжка DOCKER. Пакет, адресований контейнеру, переспрямовується до цього контейнера, а не доставляється на хост. Тому він обробляється на шляху FORWARD і не проходить через правила INPUT, які додає UFW.
Це можна побачити на сервері:
sudo ufw status | grep 5432
sudo iptables -t nat -L DOCKER -n | grep 5432UFW може показувати 5432 DENY IN Anywhere, тоді як у таблиці nat є правило DNAT tcp ... to:172.18.0.2:5432 для того самого порту. З іншої машини nc -vz your.server.ip 5432 усе одно встановлює з’єднання. База даних доступна з публічного інтернету, хоча firewall показує, що це не так.
Рішення — публікувати менше портів. Контейнери в одному compose-проєкті спільно використовують мережу та звертаються один до одного за іменем сервісу. Тому базі даних, яка обслуговує лише розташований поруч застосунок, запис ports: узагалі не потрібен. Якщо локальний доступ потрібен, прив’яжіть опублікований порт до loopback:
services:
db:
image: postgres:16
ports:
- "127.0.0.1:5432:5432"Після docker compose up -d підключення через nc -vz your.server.ip 5432 ззовні не працює, а psql -h 127.0.0.1 -p 5432 на самому сервері й далі працює. У невеликому стеку з правильною конфігурацією лише reverse proxy публікує порти 80 і 443. У матеріалі Чому опубліковані порти Docker обходять UFW описано ланцюжок DOCKER-USER для випадків, коли порт потрібно опублікувати й водночас фільтрувати. Основи firewall UFW і правила для хоста описано в Основи firewall UFW.
Чому мої контейнери зникли після перезавантаження?
Тому що ніщо не вказало їм запуститися знову. Для контейнера встановлено політику перезапуску no, якщо ви не задали іншу. Тому після перезавантаження він залишається зупиненим, а daemon не втручається. Перезавантаження VPS трапляються регулярно: через оновлення ядра з unattended upgrades, технічне обслуговування провайдера або описану вище послідовність OOM. Усі ці причини зрештою призводять до перезавантаження.
Мають виконуватися дві умови. Daemon має запускатися під час завантаження системи:
systemctl is-enabled dockerУ стандартній інсталяції Ubuntu команда виводить enabled. Після цього для кожного сервісу потрібно задати політику:
services:
app:
image: ghcr.io/example/app:1.4
restart: unless-stoppedunless-stopped запускає контейнер після перезавантаження та враховує контейнер, який ви навмисно зупинили. always також перезапускає контейнери, які ви зупинили навмисно, що може бути несподіваним під час налагодження. Самого редагування файлу недостатньо, оскільки політика перезапуску задається під час створення контейнера. Виконайте docker compose up -d, щоб створити його заново, а потім перевірте поточне значення:
docker inspect my-app | grep -A3 RestartPolicyПісля цього навмисно перезавантажте сервер і виконайте docker compose ps у каталозі проєкту. Стек, який переживає планове перезавантаження, переживе й непланове. Якщо стеку потрібна гарантована послідовність запуску або одноразове завдання під час завантаження, краще використати unit systemd: у матеріалі запуск Docker Compose під час завантаження наведено файл unit. Щоб перевірити, чи контейнер, який запустився знову, справді обслуговує запити, додайте healthchecks у Compose.
Чому диск мого VPS заповнений?
Тому що Docker зберігає все, доки ви явно не вкажете йому цього не робити. Кожен тег образу, який ви коли-небудь завантажили, кожен зупинений контейнер, кожен анонімний том, що залишився після повторного створення, і кожен шар кешу збірки залишаються на диску. На root-файловій системі обсягом 40 GB або 80 GB, що є нормальним для тарифів такого розміру, це спричиняє відмову в роботі через місяці, а не через роки.
Заповнений диск не виглядає як аварійне завершення роботи. Протягом однієї години ви отримуєте no space left on device від контейнера, від apt, від journald і від docker pull. PostgreSQL припиняє приймати записи. Сервер залишається доступним, тому проблему важче помітити, ніж цикл перезавантажень.
Спочатку перевірте, а вже потім видаляйте:
docker system df
df -h /docker system df розподіляє загальний обсяг між образами, контейнерами, локальними томами та кешем збірки й показує поруч із кожним обсяг, який можна звільнити. На сервері, де образи збираються локально, кеш збірки зазвичай займає найбільше місця.
docker image prune -a
docker builder prune
docker system dfdocker image prune -a видаляє всі образи, які не використовує жоден контейнер. docker builder prune очищає кеш збірки. Обидві команди безпечні під час роботи сервісів, оскільки використовувані об’єкти пропускаються. Небезпечно виконувати docker system prune --volumes: ця команда видаляє всі томи, на які зараз не посилається жоден контейнер. Стек, який ви зупинили на вихідні, має саме таку структуру, і його том бази даних буде видалено. Перш ніж указувати цей прапорець, прочитайте bind mounts і іменовані томи та спочатку створіть резервну копію.
Журнали контейнерів збільшуються непомітніше. Драйвер json-file за замовчуванням не має обмеження розміру, тому один контейнер, який генерує багато повідомлень, записує гігабайти в /var/lib/docker/containers. Установіть обмеження для кожного контейнера в /etc/docker/daemon.json:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}Застосуйте зміни за допомогою sudo systemctl restart docker. Ця команда перезапускає контейнери, тому виберіть відповідний момент. Обмеження застосовується до контейнерів, створених після зміни. Тому повторно створіть запущені контейнери за допомогою docker compose up -d --force-recreate і перевірте результат:
docker inspect my-app | grep -A5 LogConfig
sudo du -sh /var/lib/docker/containersУ виведенні inspect має бути встановлено max-size. Якщо значення порожнє, цей контейнер було створено до внесення зміни, і він досі записує журнали без обмеження.
Звички, які допомагають підтримувати невеликий Docker-сервер у справному стані
Для цього не потрібні dashboard або інструмент, який доведеться вивчати.
- Запускайте
docker system dfіdf -h /першого числа кожного місяця. Дві команди, тридцять секунд — і ви побачите тенденцію задовго до збою. - Встановіть memory limit для кожного сервісу, навіть для тих, які здаються гарантовано малими. Обмеження перетворює збій усього хоста на перезапуск одного контейнера.
- Моніторте сервер з іншого місця, щоб дізнатися про дефіцит пам’яті або дискового простору до того, як відреагує kernel. Uptime Kuma працює в контейнері та споживає в режимі простою приблизно 95 MB.
- Створюйте резервні копії volumes, а не контейнерів. Контейнер можна видалити й створити заново, а volume — ні. резервне копіювання за допомогою restic на VPS охоплює розклад і перевірку відновлення.
- Фіксуйте image tags у compose-файлі та оновлюйте їх у визначений вами день. З
latestверсія, яку ви отримаєте під час наступногоdocker compose pull, буде тією, що вийшла того ранку.
Невеликий VPS із Docker роками працює стабільно, якщо чотири показники залишаються в межах норми: бюджет пам’яті, список опублікованих портів, політика перезапуску кожного сервісу та вільний дисковий простір. Усе інше — це той самий Docker, який ви вже використовуєте вдома.
FAQ
Скільки RAM потрібно для запуску Docker на VPS?
Сам Docker споживає мало ресурсів. Daemon і containerd разом займають близько 100 MB, а решта залежить від ваших контейнерів. Спочатку закладіть частку хоста: 768 MB на 2048 MB для операційної системи, daemon і запасу, після чого залишиться 1280 MB для контейнерів. База даних на 512 MB, reverse proxy на 128 MB і два невеликі застосунки в цей обсяг помістяться. Вимірюйте споживання власного стеку за допомогою docker stats --no-stream, а не покладайтеся на опубліковані значення.
Чи можна запускати Docker на VPS із 1 GB?
Так, якщо йдеться про один або два легкі контейнери. Перед запуском додайте swap-файл. Після запуску операційної системи та Docker daemon приблизно половина 1 GB буде зайнята. Залишеного обсягу вистачить для невеликого застосунку та reverse proxy, але не для бази даних під реальним навантаженням. Створення образів на сервері такого розміру може завершитися помилкою або призвести до зупинки іншого процесу, тому створюйте образи в іншому місці та завантажуйте готовий образ.
Чи захищає UFW контейнер Docker?
Ні, якщо порти контейнера опубліковані. Docker створює власні правила DNAT і пересилання, тому пакет, надісланий на опублікований порт контейнера, переспрямовується до контейнера, а не доставляється хосту. Правила INPUT, якими керує UFW, такий пакет не бачать. ufw deny 5432 може бути активним, але цей порт усе одно відповідатиме із мережі Інтернет. Публікуйте порт на loopback за допомогою 127.0.0.1:5432:5432, не публікуйте внутрішні сервіси або фільтруйте трафік у ланцюжку DOCKER-USER.
Чи перезапустяться мої контейнери після перезавантаження VPS?
Лише якщо під час їх створення було задано політику перезапуску. Встановіть restart: unless-stopped для кожного сервісу, виконайте docker compose up -d, щоб контейнери було створено повторно з цією політикою, і переконайтеся, що systemctl is-enabled docker виводить enabled. Потім навмисно перезавантажте сервер і перевірте docker compose ps. Політика перезапуску, яку ви ніколи не тестували, не є надійною політикою перезапуску.
Як часто потрібно очищати образи Docker?
Для більшості невеликих серверів достатньо робити це щомісяця або щоразу, коли docker system df показує обсяг, який можна звільнити й який вам потрібен. docker image prune -a і docker builder prune безпечно виконувати під час роботи сервісів, оскільки образи та кеш, які використовуються, буде пропущено. Не використовуйте docker system prune --volumes, якщо точно не знаєте, які томи не мають посилань, оскільки ця команда видаляє дані стеку, який випадково зупинено.