Установка Docker на сервер с Ubuntu 24.04: Docker Compose, порты, логи и диск

Как поставить Docker и Docker Compose на сервер с Ubuntu 24.04 из официального репозитория и обойти три проблемы, которые вылезают через неделю: порты контейнеров открыты в обход ufw, логи растут без ограничения, старые образы съедают диск. С примером на PostgreSQL и Adminer, резервной копией базы и восстановлением из неё. Docker Engine 29.8, октябрь 2026.

Что получится и что понадобится

Docker на Ubuntu 24.04 ставится из официального репозитория Docker: ключ, источник пакетов и одна команда apt install, она ставит движок вместе с плагином Compose. Документация Docker называет пакет docker.io из самой Ubuntu неофициальным и просит удалить его перед установкой. Сама установка занимает пять минут. Остальное время уходит на порты, логи и диск, про них шаги 2 и 4.

Нужен сервер с Ubuntu 24.04, на котором уже сделано всё из инструкции про первую настройку VPS: пользователь с sudo, вход по ключу, включённый ufw. На 22.04 и 26.04 команды те же, все три версии есть в списке поддерживаемых. Для Debian 12 и 13 блок установки свой, с адресом linux/debian, его надо брать со страницы документации про Debian. Astra Linux в списке Docker нет. Там Docker ставят из репозитория самой Astra по её документации, и первый шаг этой инструкции к ней не подходит. Docker Desktop и Windows тоже мимо, на сервере нужен только движок.

Проверено 2 октября 2026. Установку проверили в чистом контейнере с Ubuntu 24.04, встали Docker Engine 29.8.2 и Compose 5.5.1. Запустить сам движок внутри контейнера нельзя, поэтому контейнеры, проект Compose, копию базы и восстановление запускали на машине с Ubuntu 24.04 и Docker 29.8.1. Два места взяты из документации Docker без проверки у нас, это поведение ufw в шаге 2 и ротация логов в шаге 4. В самих шагах это отмечено.

Шаг 1. Установка Docker из репозитория Docker

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

sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc | cut -f1)

Дальше ключ репозитория, описание источника и сами пакеты. Блок из документации Docker, вставляется целиком.

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

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

Пакетов пять: движок, клиент, containerd и два плагина, buildx для сборки образов и compose. Старые руководства советуют ставить docker-compose отдельно, через pip или скачанным бинарником. Сейчас это лишнее. Compose стал плагином и вызывается через пробел, docker compose.

sudo docker run --rm hello-world
docker --version
docker compose version

Первая команда скачивает крошечный образ и печатает приветствие Hello from Docker! Раз оно появилось, движок работает и сервер достаёт до Docker Hub, хранилища образов. Служба docker на Ubuntu включена в автозапуск сразу после установки, отдельно это делать не надо.

Чтобы не писать sudo перед каждой командой, пользователя добавляют в группу docker. Документация предупреждает прямо: группа даёт права уровня root. Любой, кто попал на сервер под этим пользователем, одной командой получит через контейнер доступ ко всему диску с правами root. Если по ключу на сервер ходите только вы, это терпимо.

sudo usermod -aG docker $USER
newgrp docker
docker run --rm hello-world

В командах ниже sudo оставлен, чтобы они работали в обоих случаях.

Шаг 2. Первый контейнер и порты, которые ufw не закроет

Запустите nginx, веб-сервер, так, как это делают в большинстве примеров.

sudo docker run -d --name web --restart unless-stopped -p 8080:80 nginx
sudo docker ps

Ключ -d оставляет контейнер работать в фоне, --name даёт имя вместо случайного, -p 8080:80 пробрасывает порт 8080 сервера на порт 80 внутри контейнера, в документации это называется публикацией порта. Политика --restart unless-stopped поднимает контейнер после сбоя и после перезагрузки сервера, пока вы сами его не остановили. Без неё контейнер после перезагрузки не вернётся.

Теперь ловушка. При первой настройке вы включили ufw и открыли только SSH, порт 8080 в правилах не значится. По документации Docker страница на http://адрес-сервера:8080 всё равно откроется из интернета. Docker сам дописывает правила в сетевой фильтр Linux, и входящие соединения уходят к контейнеру раньше, чем до них доходят правила ufw. В разделе документации про ufw так и сказано, что опубликованные порты обходят правила файрвола. На своём сервере с ufw мы это не проверяли, но убедиться легко, откройте адрес с телефона через мобильную сеть.

Для nginx это не страшно, его и ставят, чтобы он был виден. Страшно, когда тем же способом публикуют порт базы данных, Redis или панели управления. Команда ufw status при этом показывает, что закрыто всё, кроме SSH, а порт 5432 отвечает всему интернету.

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

sudo docker rm -f web
sudo docker run -d --name web --restart unless-stopped -p 127.0.0.1:8080:80 nginx
sudo docker ps
curl -I http://127.0.0.1:8080

В колонке PORTS теперь стоит 127.0.0.1:8080->80/tcp. С самого сервера nginx отвечает HTTP/1.1 200 OK, снаружи порта нет. Со своего компьютера до него можно добраться через туннель SSH.

ssh -L 8080:127.0.0.1:8080 user@адрес-сервера

Пока это соединение открыто, http://localhost:8080 в вашем браузере показывает страницу с сервера. Файрвол в панели хостера, если он есть, стоит перед сервером, и Docker на него не влияет: это второй способ закрыть порт. Напоследок посмотрите журнал контейнера и уберите его за собой.

sudo docker logs --tail 20 web
sudo docker stop web
sudo docker rm web

Шаг 3. Установка Docker Compose и первый проект: база и панель в одном файле

Compose уже установлен, он пришёл пакетом docker-compose-plugin в первом шаге. Нужен он потому, что команду docker run с пятью ключами через месяц уже не вспомнить. Compose хранит то же самое в файле: какие контейнеры, с какими портами, томами и переменными. Том здесь значит отдельное хранилище Docker для данных, которое живёт дольше контейнера. Для примера возьмите PostgreSQL и веб-панель Adminer к ней, то есть два контейнера, один том с данными и пароль в отдельном файле.

mkdir -p ~/stack && cd ~/stack
tee .env > /dev/null <<'EOF'
POSTGRES_PASSWORD=придумайте-длинный-пароль
EOF
chmod 600 .env

Файл .env Compose читает сам, если он лежит рядом с compose.yaml. Теперь описание контейнеров, файл compose.yaml в той же папке.

services:
  db:
    image: postgres:18
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - db_data:/var/lib/postgresql

  adminer:
    image: adminer
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    depends_on:
      - db

volumes:
  db_data:

У базы нет раздела ports, и это намеренно. Контейнеры одного проекта видят друг друга по именам сервисов из compose.yaml, здесь это db и adminer. Adminer обращается к базе по имени db, а на самом сервере порт 5432 не открыт, из интернета его тем более не видно. Панель опубликована на 127.0.0.1 по правилу из второго шага. Данные лежат в томе с именем db_data и переживают пересоздание контейнера.

Том подключён в /var/lib/postgresql, хотя в большинстве руководств стоит /var/lib/postgresql/data. Это не опечатка. Начиная с PostgreSQL 18 образ держит данные в подпапке с номером версии, и его описание на Docker Hub просит монтировать том уровнем выше. Для версии 17 и старше путь прежний, с data на конце. Если перепутать, база молча окажется во временном безымянном томе, и после пересоздания контейнера вы увидите пустой сервер.

sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs --tail 20 db

В выводе ps оба сервиса должны быть в состоянии running, в логе базы последняя строка database system is ready to accept connections. Откройте туннель из второго шага и зайдите на http://localhost:8080. В форме Adminer выберите систему PostgreSQL, сервер db, пользователя postgres и пароль из .env.

В простое эта пара заняла у нас около 40 МБ памяти: 31 МБ PostgreSQL и 8 МБ Adminer по docker stats. Диска образы берут заметно больше. По docker image ls postgres:18 весит 650 МБ, adminer 164 МБ, nginx из второго шага 242 МБ.

Команда sudo docker compose down останавливает и удаляет контейнеры и сеть проекта, том остаётся. Мы проверили это так: создали в базе таблицу, сделали down и снова up -d, таблица осталась. Данные стирает только down с ключом -v, и набирать его по привычке не стоит.

Шаг 4. Логи и диск

Всё, что контейнер печатает в консоль, Docker складывает в JSON-файл в /var/lib/docker. Ротации, то есть удаления старых записей по достижении лимита, по умолчанию нет. В документации сказано, что такие файлы могут занять весь диск, а поведение оставлено ради совместимости со старыми версиями. Разговорчивое приложение за пару месяцев наговаривает гигабайты. Ограничение задаётся в настройках демона, фоновой службы Docker.

Сначала проверьте командой ls /etc/docker/daemon.json, нет ли уже такого файла. Если он есть, блок ниже его затрёт, поэтому допишите в него два ключа руками. Между ключами в JSON нужна запятая, без неё демон не запустится. Блок взят из документации Docker, на живом сервере мы его не запускали.

sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
EOF
sudo systemctl restart docker
sudo docker info --format '{{.LoggingDriver}}'

Значения пишутся в кавычках, числа тоже, иначе демон не запустится. Три файла по 10 МБ дают потолок в 30 МБ логов на контейнер. Перезапуск службы останавливает все контейнеры. Те, у кого задана политика restart, поднимутся сами, остальные придётся запускать руками. Настройка действует только на контейнеры, созданные после неё, старые надо пересоздать: sudo docker compose up -d --force-recreate в папке проекта. Вместо json-file документация советует драйвер local, он ротирует логи сам и держит до 100 МБ на контейнер в сжатом виде. Мы оставили json-file, потому что его файлы читаются глазами.

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

sudo docker system df
sudo docker image prune -a

В колонке RECLAIMABLE видно, сколько вернёт уборка. Команда image prune -a удаляет образы, которые не использует ни один контейнер, включая остановленные, и перед этим спрашивает подтверждение. Контейнеры и тома она не трогает. Есть команда шире, docker system prune. Она убирает ещё остановленные контейнеры, неиспользуемые сети и кэш сборки образов. С ней аккуратнее, если остановленный контейнер вам ещё нужен.

Обновление, бэкап и типичные ошибки

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

cd ~/stack
sudo docker compose pull
sudo docker compose up -d

Образ с тегом, то есть меткой версии, postgres:18 получает исправления внутри восемнадцатой версии. Переход на 19 заменой тега не делается. Новая версия не откроет каталог данных старой, нужен дамп, текстовая копия всех баз, и восстановление из него. Дамп снимается с работающего контейнера.

sudo docker compose exec -T db pg_dumpall -U postgres | gzip > backup-$(date +%F).sql.gz

Файл появится в папке проекта. Копию держите не на этом же сервере, иначе вместе с VPS пропадут и база, и бэкап. Восстановление мы проверили на пустой базе: стёрли том командой down -v, подняли проект заново и влили дамп обратно. В команде ниже подставьте имя своего файла.

gunzip -c backup-2026-10-02.sql.gz | sudo docker compose exec -T db psql -U postgres

В начале вывода будет строка ERROR: role "postgres" already exists. Это не поломка: дамп пытается создать пользователя postgres, а он в свежей базе уже есть. Таблицы и данные после неё восстанавливаются, у нас строка из тестовой таблицы вернулась на место.

permission denied while trying to connect to the Docker daemon socket. Команда запущена без sudo пользователем, которого нет в группе docker. Добавьте sudo или вернитесь к концу первого шага.

The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string. Compose не нашёл файл .env: вы запускаете команду не из папки проекта или файл назван иначе. Перейдите в ~/stack и повторите.

port is already allocated или address already in use. Порт на сервере занят другим контейнером или программой. Кто его держит, покажет sudo ss -ltnp | grep ':8080 '.

no space left on device. Кончился диск, чаще всего из-за образов и логов. Смотрите sudo docker system df и четвёртый шаг.

Контейнер не поднялся после перезагрузки сервера. У него нет политики перезапуска. Работающему контейнеру её добавляют без пересоздания: sudo docker update --restart unless-stopped имя-контейнера.

Контейнер в состоянии Restarting. Он падает сразу после старта, и Docker поднимает его по кругу. Причина почти всегда написана в последних строках sudo docker logs имя-контейнера.

Какой сервер под это нужен

Сам Docker почти ничего не просит, считать надо то, что в контейнерах: память под сервисы и диск под образы. Три образа из этой инструкции вместе заняли больше гигабайта, а памяти в простое пара из базы и панели взяла 40 МБ. В нашем каталоге на 28 сентября 2026 у 11 провайдеров в России нашлось 188 Linux-тарифов без видеокарты и без условия о предоплате. У 18 тарифов диск меньше 20 ГБ, у восьми всего 10 ГБ. На систему и пару готовых образов этого хватит, для сборки своих образов уже тесно. С 2 ГБ памяти и диском от 20 ГБ остаётся 166 тарифов у 10 провайдеров. Дешевле 500 ₽ в месяц из них три: Selectel «VDS 1-2-25» за 250 ₽ с диском 25 ГБ, Selectel «VDS 2-2-40» за 400 ₽ с диском 40 ГБ и AdminVPS «Promo (3,6 ГГц)» за 429 ₽ с диском 30 ГБ NVMe. На 1 ГБ спокойно живут nginx и небольшой сервис. Базе вместе с приложением лучше дать 2 ГБ. Карточки ниже показывают самые дешёвые варианты каталога с такими параметрами.

инструкцииdockerubuntu

Серверы под Docker

Linux в России, от 2 ГБ памяти и 20 ГБ диска: место под образы и логи кончается раньше, чем память.

Коды площадок

RU
— Россия

Цены из каталога, обновляются вместе с ним. Остальные варианты — в каталоге.

Сравнить тарифы VPS

Весь каталог с фильтрами и честной сортировкой — цены обновляются автоматически.

Открыть каталог →