Установка 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 ГБ. Карточки ниже показывают самые дешёвые варианты каталога с такими параметрами.