SSD Nodes Learn 🎉 VPS от $4.99/мес
Руководства Matt ConnorАвтор: Matt Connor

Docker на VPS: что меняется на практике

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

Что меняется при запуске Docker на VPS

Docker на VPS использует тот же движок и те же образы, что и Docker на вашем ноутбуке, поэтому все знакомые вам команды по-прежнему работают. Меняется окружающая среда. На ноутбуке есть свободная память, firewall, который никто не сканирует, и достаточно большой диск, за которым не приходится следить. У арендованного сервера фиксированный объём памяти, публичный IP-адрес, который начинают сканировать через несколько минут после запуска, и корневая файловая система, которую Docker заполнит без предупреждения.

На небольшом сервере большинство проблем вызывают четыре различия:

  • Память ограничена, и при её нехватке kernel завершает процесс.
  • Опубликованный порт проходит напрямую через UFW (uncomplicated firewall), потому что Docker добавляет собственные правила firewall.
  • Контейнеры не запускаются снова после перезагрузки, если это заранее не настроить.
  • Образы, контейнеры, volumes и build cache растут, пока диск не заполнится.

В каждом разделе ниже указаны причина сбоя, строка, которую вы фактически увидите, и подробное руководство по устранению проблемы. Если вы ещё не создали compose-файл, сначала прочитайте Основы Docker Compose на VPS, а затем вернитесь сюда. Предполагается, что вы уже умеете запускать stack.

Сколько оперативной памяти использует Docker-контейнер?

Меньше, чем ожидает большинство пользователей. Контейнер — это процесс в cgroup (control group), а не виртуальная машина. Поэтому у него нет гостевого ядра и фиксированного объёма памяти. Затраты определяются тем, к чему обращается процесс внутри контейнера. Поэтому полный стек помещается в 2 GB, хотя тот же стек на виртуальных машинах потребовал бы больше памяти.

Ниже приведены типичные значения в состоянии простоя для стандартных образов на Ubuntu 24.04 с конфигурацией по умолчанию. Они получены из docker stats через несколько минут после запуска. Используйте их только как исходные данные для планирования, а не как результат тестирования вашей нагрузки. Перед тем как полагаться на любые значения, включая эти, выполните docker stats --no-stream на собственном сервере.

ChartTypical idle container memory, stock images, megabytes
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 показывает, сколько памяти контейнер использует в состоянии простоя. budget_mb показывает, сколько памяти нужно закладывать при планировании, поскольку реальная нагрузка не равна простою. PostgreSQL использует около 45 MB в состоянии простоя и требует 512 MB после установления соединений, выполнения сортировок и заполнения кэша. Планируйте по столбцу с резервом. Для отладки используйте столбец простоя.

Обратите внимание на распределение значений в этих 7 строках. nginx использует 8 MB в состоянии простоя, а Nextcloud — 210 MB. Reverse proxy перед приложениями почти не требует памяти. Размер сервера определяется базой данных и PHP-приложением.

У docker stats есть одна особенность. Показатель памяти включает page cache, заполненный в результате чтения собственных файлов контейнера. Поэтому после запуска он некоторое время растёт, а затем стабилизируется. Наблюдайте за ним в течение часа, прежде чем решать, что произошла утечка памяти.

Подбор VPS: что помещается в 2 GB, 4 GB и 8 GB

Сначала вычтите долю хоста. Kernel, systemd, journald, sshd и Docker daemon используют ту же RAM, что и контейнеры, а dockerd вместе с containerd занимает около 100 MB. Также нужна свободная память для page cache и для скачка потребления при сборке image или создании database dump.

ChartRAM budget by plan size, megabytes
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 и search cluster поверх этого уже не останется.

Тариф на 4 GB оставляет 3072 MB. Этого достаточно, чтобы одновременно разместить database, reverse proxy, три приложения и контейнер мониторинга. Это минимальный размер для важных сервисов, потому что свободная память поглощает последствия неудачного deploy.

Тариф на 8 GB оставляет 6656 MB из 8192 MB. Обычно ограничением становится уже не память, а CPU или пропускная способность диска. Если расчёт показывает, что ваш стек не помещается, выберите более дорогой тариф вместо попыток обойти ограничение настройками: сколько на самом деле стоит VPS показывает, сколько стоят дополнительные гигабайты в месяц.

Два правила помогают сохранить точность расчётов. Установите memory limit для каждого сервиса, чтобы один процесс с неконтролируемым потреблением памяти не вывел из строя весь сервер. Не расходуйте весь бюджет, потому что docker compose build и pg_dump обоим требуется память в самый неподходящий момент. В разделе Ограничения памяти в Docker Compose описаны синтаксис и типичные ошибки.

Почему мой контейнер завершается с кодом 137?

Потому что его завершило ядро. 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, а в журнале ядра указано, какой процесс был выбран:

Memory cgroup out of memory: Killed process 2417 (postgres) total-vm:1284416kB

Это хороший вариант, поскольку проблема ограничилась одним контейнером. Плохой вариант — контейнер вообще без ограничения. В этом случае его пределом становится объём памяти всей машины. Утечка памяти в одном сервисе лишает ресурсов весь хост, после чего ядро выбирает процесс для завершения по его размеру во всей системе. В строке журнала отсутствует префикс Memory cgroup, и она выглядит как Out of memory: Killed process 2417 (postgres). Чаще всего выбранным процессом оказывается база данных, а контейнер с утечкой продолжает работать. Поэтому ограничение для каждого сервиса важнее точного значения любого отдельного ограничения.

Swap меняет время возникновения проблемы, но не сам расчёт. В большинстве образов VPS swap отсутствует. Проверьте это с помощью swapon --show: если swap отсутствует, команда ничего не выводит. Swap-файл даёт ядру место для перемещения неиспользуемых страниц памяти. Это позволяет выиграть несколько минут и заметить проблему.

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 -h

Теперь free -h должен показывать ненулевой общий объём в строке Swap. Swap не добавляет оперативную память. При постоянной нехватке памяти система может замедлиться настолько, что вы не сможете подключиться по 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 5432

UFW может вывести 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 описаны базовые правила хоста.

Почему контейнеры исчезают после перезагрузки?

Потому что ничто не указало им запускаться снова. Если не задать политику перезапуска, контейнер создаётся с политикой no. Поэтому после перезагрузки он остаётся остановленным, а daemon не предпринимает никаких действий. Перезагрузки не редкость на VPS: к ним приводят обновления ядра через unattended upgrades, техническое обслуживание у провайдера и описанная выше последовательность OOM.

Должны выполняться два условия. Daemon должен запускаться при загрузке системы:

systemctl is-enabled docker

В стандартной установке Ubuntu эта команда выводит enabled. Затем для каждого сервиса нужно задать политику:

services:
  app:
    image: ghcr.io/example/app:1.4
    restart: unless-stopped

unless-stopped запускает контейнер после перезагрузки и не трогает контейнер, который вы остановили вручную. always также запускает контейнеры, которые вы намеренно остановили, при каждом перезапуске daemon. Во время отладки это может быть неожиданно. Одного изменения файла недостаточно, поскольку политика перезапуска задаётся при создании контейнера. Выполните docker compose up -d, чтобы пересоздать контейнер, затем проверьте фактическое значение:

docker inspect my-app | grep -A3 RestartPolicy

После этого намеренно перезагрузите сервер и выполните docker compose ps в каталоге проекта. Если стек переживает плановую перезагрузку, он переживёт и неплановую. Если стеку нужны гарантированный порядок запуска или однократная задача при загрузке, лучше использовать systemd unit: в материале запуск Docker Compose при загрузке приведён файл unit. Чтобы проверить, действительно ли вернувшийся контейнер обслуживает запросы, добавьте healthcheck в 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 разделяет общий объём между образами, контейнерами, локальными томами и кэшем сборки. Для каждого типа отображается столбец RECLAIMABLE. На сервере, где образы собираются локально, кэш сборки обычно занимает больше всего места.

docker image prune -a
docker builder prune
docker system df

docker 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 / первого числа каждого месяца. Две команды и тридцать секунд — и вы увидите тенденцию задолго до сбоя.
  • Задайте лимит памяти для каждого сервиса, включая те, которые, как вам кажется, потребляют мало памяти. Лимит превращает отказ всего хоста в перезапуск одного контейнера.
  • Контролируйте сервер из другого места, чтобы узнать о нехватке памяти или дискового пространства до того, как начнёт действовать kernel. Uptime Kuma запускается в контейнере и в режиме простоя потребляет около 95 MB.
  • Создавайте резервные копии volumes, а не контейнеров. Контейнер можно удалить и создать заново, а volume — нет. В разделе резервное копирование restic на VPS описаны расписание и проверка восстановления.
  • Фиксируйте теги образов в compose-файле и обновляйте их в выбранный день. При использовании latest версия, которую вы получите из следующего docker compose pull, будет соответствовать выпуску этого утра.

Небольшой VPS с Docker годами работает стабильно, если в норме остаются четыре показателя: выделенный объём памяти, список опубликованных портов, политика перезапуска каждого сервиса и свободное место на диске. Всё остальное — тот же Docker, который вы уже используете дома.

FAQ

Сколько оперативной памяти нужно для запуска 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 file. После запуска операционной системы и Docker daemon примерно половина VPS с 1 GB уже будет занята. Оставшегося объёма хватит для небольшого приложения и reverse proxy, но не для базы данных под реальной нагрузкой. Сборка образов на таком VPS может завершиться ошибкой или остановить другой процесс. Выполняйте сборку в другом месте и загружайте готовый образ.

Защищает ли UFW контейнер Docker?

Не защищает порты, опубликованные наружу. Docker создаёт собственные правила DNAT и пересылки. Поэтому пакет, направленный на опубликованный порт контейнера, пересылается в контейнер, а не доставляется хосту. Правила INPUT, которыми управляет UFW, такой пакет не видят. ufw deny 5432 может быть активен, пока этот порт доступен из Интернета. Выполните публикацию на loopback с помощью 127.0.0.1:5432:5432, не публикуйте внутренние сервисы или фильтруйте трафик в цепочке DOCKER-USER.

Перезапустятся ли контейнеры после перезагрузки VPS?

Только если при их создании была задана restart policy. Укажите restart: unless-stopped для каждого сервиса, выполните docker compose up -d, чтобы пересоздать контейнеры с этой настройкой, и убедитесь, что systemctl is-enabled docker выводит enabled. Затем намеренно перезагрузите VPS и проверьте docker compose ps. Непроверенная restart policy не гарантирует перезапуск.

Как часто следует удалять неиспользуемые образы Docker?

Для большинства небольших серверов достаточно выполнять очистку раз в месяц. Её также следует выполнять, когда docker system df показывает reclaimable space, который вам нужен. Команды docker image prune -a и docker builder prune безопасны при работающих сервисах, поскольку используемые образы и cache пропускаются. Не используйте docker system prune --volumes, если точно не знаете, какие volumes не имеют ссылок. Эта команда удаляет данные любого стека, который в данный момент остановлен.