Центр помощи

Пошаговые руководства по серверам, сетям и аккаунту.

Linux-сервер

Docker на небольшом VPS: установка, лимиты логов и памяти, ловушки файрвола и очистка диска

Docker на небольшом VPS: установка, лимиты логов и памяти, ловушки файрвола и очистка диска

Docker нормально работает даже на VPS с 1 GB, если установить его из официального репозитория Docker, ограничить логи контейнеров, задать каждому контейнеру лимит памяти, публиковать порты на 127.0.0.1 за обратным прокси (иначе Docker обходит UFW и firewalld) и регулярно удалять старые образы. В этой инструкции каждый шаг разобран для Debian 12/13, Ubuntu 22.04/24.04 и Rocky Linux / AlmaLinux.

Шаг 1: Установите Docker из официального репозитория

Пакет docker.io в Debian и Ubuntu часто устаревший и без плагина Compose. Используйте собственный репозиторий Docker. В Debian и Ubuntu выполните от root:

# Debian 12/13 and Ubuntu 22.04/24.04 - remove distro packages that conflict
for p in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
  apt-get remove -y "$p" 2>/dev/null
done

apt-get update
apt-get install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
. /etc/os-release        # sets ID (debian/ubuntu) and VERSION_CODENAME
curl -fsSL "https://download.docker.com/linux/$ID/gpg" -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

cat > /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/$ID
Suites: $VERSION_CODENAME
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

В Rocky Linux и AlmaLinux на некоторых образах уже установлен Podman, который конфликтует с пакетами Docker, поэтому сначала удалите его:

# Rocky Linux / AlmaLinux 8, 9, 10
dnf remove -y podman buildah runc 2>/dev/null
dnf install -y dnf-plugins-core
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker

В Debian и Ubuntu служба запускается автоматически. Проверьте, что всё работает:

systemctl is-active docker
docker version
docker compose version
docker run --rm hello-world
Участники группы docker управляют демоном, а это равносильно root-доступу к серверу. Добавляйте только доверенных пользователей командой usermod -aG docker username и никогда не открывайте API Docker (/var/run/docker.sock или TCP-порт 2375) в интернет.

Шаг 2: Ограничьте логи контейнеров и включите live-restore

По умолчанию Docker хранит каждую строку, которую выводит контейнер, без ограничения размера. «Болтливый» контейнер способен за несколько дней заполнить диск на 20 GB. Задайте общий лимит в /etc/docker/daemon.json:

# /etc/docker/daemon.json
{
  "log-driver": "local",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "live-restore": true
}
  • Драйвер логов local: сжатые файлы с ротацией; здесь не больше 3 × 10 MB на контейнер. docker logs работает как прежде.
  • live-restore: контейнеры продолжают работать, пока демон Docker перезапускается или обновляется, так что обновление пакетов не роняет сайт.

Если какой-то инструмент на сервере читает JSON-логи напрямую, используйте json-file с теми же лимитами:

  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" },

Проверьте файл и перезапустите Docker. При этом первом перезапуске работающие контейнеры один раз остановятся:

dockerd --validate --config-file /etc/docker/daemon.json
systemctl restart docker
docker info --format '{{.LoggingDriver}} live-restore={{.LiveRestoreEnabled}}'
Настройки логов действуют только для контейнеров, созданных после изменения. Пересоздайте существующие командой docker compose up -d --force-recreate.

Шаг 3: Создайте compose-файл с лимитами памяти и CPU

Без лимитов один контейнер с утечкой памяти может загнать весь VPS в swap или заставить ядро убить sshd или базу данных. С лимитом будет убит и перезапущен только этот контейнер. Небольшое приложение с MariaDB на тарифе 1 GB может выглядеть так:

# /opt/app/compose.yaml
services:
  app:
    image: ghcr.io/example/app:1.4.2      # pin a version, avoid :latest
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"             # reachable only from the VPS itself
    mem_limit: 256m
    memswap_limit: 256m                   # no extra swap for this container
    cpus: 1.0
    pids_limit: 200
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 30s
    depends_on:
      db:
        condition: service_healthy

  db:
    image: mariadb:11.4
    restart: unless-stopped
    command: ["--innodb-buffer-pool-size=64M"]
    environment:
      MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MARIADB_DATABASE: app
    volumes:
      - db_data:/var/lib/mysql
    mem_limit: 300m
    memswap_limit: 300m
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 30s
      timeout: 5s
      retries: 5
      start_period: 60s

volumes:
  db_data:
  • mem_limit / memswap_limit: жёсткий лимит памяти. Одинаковые значения запрещают контейнеру использовать swap. Оставьте место системе: на 1 GB держите сумму всех лимитов в районе 600–700 MB.
  • cpus: 1.0 — одно полное ядро. Так загруженный контейнер не выведет весь VPS за среднюю загрузку CPU по правилам fair use.
  • restart: unless-stopped: контейнеры поднимаются после сбоя или перезагрузки, но не после того, как вы остановили их сами.
  • healthcheck: помечает контейнер как healthy или unhealthy, а depends_on ждёт готовности базы. Команда проверки (здесь wget) должна быть внутри образа. Учтите: обычный Docker не перезапускает unhealthy-контейнер, а лишь показывает статус.

Храните секреты в файле .env рядом с compose.yaml и запустите стек:

cd /opt/app
echo 'DB_ROOT_PASSWORD=REPLACE_WITH_A_LONG_RANDOM_PASSWORD' > .env
chmod 600 .env
docker compose up -d
docker compose ps            # STATUS should become "healthy"
docker stats --no-stream     # memory use against each limit

Шаг 4: Не попадитесь на обход файрвола (UFW, firewalld и Docker)

Это самая частая ошибка безопасности с Docker. Когда вы публикуете порт через -p 8080:8080 или "8080:8080", Docker добавляет свои правила NAT и пересылки. Такой трафик не проходит через правила INPUT, которыми управляют UFW и firewalld, поэтому порт открыт всему интернету, даже если ufw status показывает, что он закрыт.

Решения в порядке предпочтения:

  1. Публикуйте на 127.0.0.1 ("127.0.0.1:8080:8080", как в compose-файле выше). Порт доступен только с самого VPS.
  2. Поставьте перед контейнером обратный прокси. Nginx или Caddy на хосте слушает 80/443 (их вы открываете в UFW или firewalld как обычно) и передаёт запросы на 127.0.0.1:8080. Заодно HTTPS настраивается в одном месте.
  3. Вообще не публикуйте внутренние сервисы. Базам данных и кэшам достаточно сети Compose: контейнеры одного проекта находят друг друга по имени сервиса (db:3306).

Минимальный сайт Nginx для приложения (HTTPS через Let's Encrypt добавляется, как описано в нашей инструкции по Nginx и Certbot):

# /etc/nginx/sites-available/app.example.com  (Rocky/Alma: /etc/nginx/conf.d/app.conf)
server {
    listen 80;
    server_name app.example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Проверьте, что на самом деле открыто наружу:

# On the VPS: what is published, and on which address?
docker ps --format '{{.Names}}\t{{.Ports}}'
ss -tlnp | grep docker-proxy

# From ANOTHER machine: this must fail for a port bound to 127.0.0.1
nc -vz 203.0.113.10 8080

Если порт действительно должен быть публичным, но только для определённых адресов, добавьте правила в цепочку DOCKER-USER — Docker проверяет её раньше своих правил. --ctorigdstport сопоставляет порт в том виде, в каком его отправил клиент, до трансляции адресов Docker:

# Allow new connections to published port 8080 only from 203.0.113.0/24
iptables -I DOCKER-USER -i eth0 ! -s 203.0.113.0/24 -p tcp \
  -m conntrack --ctstate NEW --ctorigdstport 8080 -j DROP
iptables -L DOCKER-USER -n -v --line-numbers
Правила в DOCKER-USER не сохраняются автоматически и пропадают после перезагрузки, а ошибочное правило может отрезать контейнеры от сети. Привязка к 127.0.0.1 проще и переживает перезагрузку. Настройка самого файрвола хоста описана в инструкции по UFW и firewalld.

Шаг 5: Следите за чистотой диска

Старые образы, остановленные контейнеры и кэш сборки быстро накапливаются на диске в 10–40 GB. Сначала посмотрите, сколько занимает Docker:

docker system df          # images, containers, volumes, build cache
docker system df -v       # per image / container / volume
df -h / && du -sh /var/lib/docker

Затем чистите поэтапно — от безопасного к рискованному:

# Safe: stopped containers, unused networks, dangling images, build cache
docker system prune

# Unused images (not used by any container) older than 7 days
docker image prune -a --filter "until=168h"

# Build cache only
docker builder prune

# CAREFUL: deletes volumes not used by any container - this is data
docker volume ls
docker volume prune
docker volume prune и docker system prune --volumes удаляют тома, которые в этот момент не использует ни один контейнер. Если вы остановили и удалили контейнер с базой, его том считается неиспользуемым, и данные пропадут. Всегда сначала выполняйте docker volume ls и делайте бэкап нужного.

Еженедельная очистка, которая никогда не трогает тома:

# /etc/cron.d/docker-prune - weekly, never touches volumes
0 4 * * 0 root docker image prune -af --filter "until=168h" >/dev/null && docker builder prune -f --filter "until=168h" >/dev/null

Если диск уже заполнен, см. как найти и освободить место на диске.

Шаг 6: Swap или zram на тарифах 512 MB и 1 GB

Немного swap защищает небольшой VPS от внезапных убийств по нехватке памяти, когда пики у нескольких контейнеров совпадают. Классический вариант — swap-файл на диске (см. как добавить swap). В Debian и Ubuntu хорошим дополнением будет zram: сжатый swap прямо в RAM, он гораздо быстрее и не создаёт нагрузки на диск:

# Debian / Ubuntu: compressed swap in RAM
apt install -y zram-tools
cat > /etc/default/zramswap <<'EOF'
ALGO=zstd
PERCENT=50
PRIORITY=100
EOF
systemctl restart zramswap
swapon --show

# Prefer RAM, swap only under pressure
echo 'vm.swappiness=10' > /etc/sysctl.d/90-swappiness.conf
sysctl --system

В Rocky Linux и AlmaLinux используйте swap-файл. Swap — это страховка, а не дополнительная память: если free -h показывает, что swap занят постоянно, тариф мал для ваших контейнеров.

Шаг 7: Обновляйте контейнеры безопасно

Закрепляйте версии образов вместо :latest: тогда обновление происходит только по вашему решению, и вы всегда знаете, на какую версию откатиться:

cd /opt/app
# 1. Back up first (database dump; replace "app" with your database)
docker compose exec -T db sh -c 'mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" --single-transaction app' > /root/app-$(date +%F).sql

# 2. Change the image tag in compose.yaml (e.g. 1.4.2 -> 1.5.0), then:
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=50 app

# 3. Roll back if needed: put the old tag back and run "docker compose up -d"

Сам Docker обновляйте вместе с системными пакетами. Благодаря live-restore контейнеры во время обновления продолжают работать:

apt-get update && apt-get install --only-upgrade -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Rocky / AlmaLinux
dnf upgrade -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Не используйте автоматические обновлялки для баз данных и приложений с состоянием: новая мажорная версия может изменить формат данных. Прочитайте release notes, сделайте бэкап и обновляйте вручную. Большие образы собирайте на своём компьютере или в CI и скачивайте готовыми, а не собирайте на VPS с 1 GB.

FAQ

Хватит ли тарифа с 512 MB для Docker?

Для одного-двух лёгких контейнеров (статический сайт, небольшой прокси или бот) — да, при наличии swap и строгих лимитов памяти. База данных плюс веб-приложение гораздо комфортнее работают на 1–2 GB.

Почему контейнер постоянно перезапускается?

Посмотрите docker compose logs app и docker inspect $(docker compose ps -q app) --format '{{.State.OOMKilled}}'. Если ответ true, контейнер упёрся в лимит памяти: увеличьте mem_limit, уменьшите потребление памяти приложением или повысьте тариф на странице услуги.

Может ли JUSTG помочь с контейнерами?

Проблемы самого VPS, например с сетью или доступом к консоли, решаются через тикет в Technical support. У JUSTG работает AI-поддержка 24/7 по тикетам, а инженеры подключаются, когда нужен человек.

Помог ли вам данный ответ?
Центр помощи
Linux-сервер Вход по SSH-ключу в Linux: настройка ключей ed25519 и отключение входа по паролю Любой сервер с публичным IPv4 через несколько минут после запуска начинают атаковать боты, перебирающие SSH-пароли. В этой инструкции… Linux-сервер Как безопасно сменить порт SSH и пароль root на Linux VPS Перенос SSH с порта 22 не остановит целенаправленную атаку, но уберёт из логов большую часть автоматического сканирования, а смена пароля… Linux-сервер Настройка файрвола в Linux: UFW и firewalld на VPS (IPv4 и IPv6) Только что установленный Linux-сервер отвечает на любом порту, который открыла какая-либо служба, поэтому файрвол — одна из первых вещей,… Linux-сервер Как добавить swap на Linux VPS (тарифы с 512 МБ и 1 ГБ ОЗУ) На облачном сервере с 512 МБ или 1 ГБ ОЗУ база данных, пул PHP-FPM и обновление пакетов легко исчерпывают память одновременно. Тогда OOM… Linux-сервер Диск заполнен в Linux: как найти и освободить место на VPS Ошибка «No space left on device» ломает на сервере почти всё: базы данных перестают писать, сайты отдают ошибки, обновления пакетов не…
AlipayUnionPayVISAMastercardPayPalUSDTstripe