Установка Docker на VPS: от пакета до первого контейнера


Установка Docker на VPS обычно заканчивается на hello-world: приветствие напечаталось, значит, всё работает. Для движка так и есть. Другие вопросы всплывают через месяц: какие порты сервер теперь отдаёт в интернет, куда пропали данные после обновления образа и почему у пользователя из группы docker фактически есть root. Их проще закрыть сразу, пока на сервере нет базы.
Ниже весь путь от чистого VPS с Ubuntu 24.04 LTS до веб-сервиса в Compose с отдельным томом. Нужны виртуальная машина, доступ по SSH и sudo. На Ubuntu 26.04 команды те же, отличается только кодовое имя в версиях пакетов. У Debian и других дистрибутивов свой репозиторий Docker, и смешивать инструкции не стоит.
Ставить Docker из официального репозитория или пакета ОС?
Для свежей установки берите официальный репозиторий Docker: Engine, CLI и плагины приходят из одного источника и обновляются вместе, а APT проверяет подпись ключом Docker. Пакет docker.io собирает сам Ubuntu, версии в нём обычно старше. На работающем сервере смену источника планируют отдельно.
Сначала выясните, что уже есть на сервере
Docker официально поддерживает Ubuntu 24.04 на amd64, arm64 и нескольких других архитектурах, но первым делом стоит проверить виртуализацию. Контейнеры работают на ядре самого VPS. На KVM ядро у машины своё, а при контейнерной виртуализации оно общее с хостом, и провайдер может отрезать нужные Docker возможности. Переустановкой Docker это не лечится.
Установку удобно сразу писать в журнал. Утилита script сохранит в файл все команды вместе с выводом, так что пароли и токены во время записи лучше не вводить. Закрывается запись командой exit, но это уже в самом конце.
mkdir -m 700 docker-check
cd docker-check
script -q -a install.typescript
date -Is
cat /etc/os-release
dpkg --print-architecture
uname -r
systemd-detect-virt
free -h
df -hT /
df -i /Из вывода нужны свободная память, место на диске и запас inode, записей о файлах. Про inode часто забывают, а когда они кончаются, диск перестаёт принимать новые файлы при свободных гигабайтах. Образы и логи занимают место быстрее, чем кажется в день установки.
Перед тем как установить Docker на VPS из официального репозитория, проверьте, нет ли в системе пакетов, которые с ним конфликтуют. Удалять их стоит с пониманием, что на них держится: на узле Kubernetes, например, удаление containerd остановит всю его нагрузку.
dpkg-query -W -f='${binary:Package}\t${Status}\n' \
docker.io docker-compose docker-compose-v2 docker-doc \
docker-buildx podman-docker containerd runcНенайденный пакет – хороший знак, его просто нет. Если нашёлся docker.io, сначала посмотрите, что APT собирается удалить вместе с ним, и только потом удаляйте:
sudo apt -s remove docker.io
sudo apt remove docker.ioС остальными найденными пакетами то же самое. Образы, контейнеры и тома в /var/lib/docker при удалении пакетов остаются на диске.
Один репозиторий и проверенный ключ
Настройка Docker Engine на VPS начинается с репозитория. В официальной инструкции ключ скачивают сразу в /etc/apt/keyrings. Здесь он сначала сохраняется в рабочий каталог и попадает в систему только после сверки отпечатка.
sudo apt update
sudo apt install ca-certificates curl gnupg
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o docker.asc
gpg --show-keys --fingerprint docker.ascОтпечаток ключа Docker Release (CE deb) должен совпасть с этим:
9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88Сверяйте все 40 символов: последние восемь, короткий ID, подделать гораздо проще. При несовпадении дальше не идите и сначала выясните по официальным материалам Docker, не сменился ли ключ.
После сверки ключ и описание репозитория в формате deb822 устанавливаются так:
sudo install -d -m 0755 /etc/apt/keyrings
sudo install -m 0644 docker.asc /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources >/dev/null <<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
apt-cache policy docker-ce docker-ce-cliSigned-By разрешает ключу подписывать только этот репозиторий, на остальные источники он не влияет. В выводе apt-cache policy у Candidate должен стоять download.docker.com с компонентом stable и кодовым именем noble. Ошибку подписи разбирайте до конца: отключить проверку проще, но тогда ключ теряет смысл.
Пять пакетов для установки
Самая свежая версия Engine на 14 октября 2026 года – 29.9.0. Номер в команде зафиксирован, чтобы на втором сервере встало ровно то же самое. Перед установкой сверьтесь с apt-cache madison и при необходимости замените строку целиком, с префиксом 5: и суффиксом Ubuntu.
apt-cache madison docker-ce
docker_version='5:29.9.0-1~ubuntu.24.04~noble'
sudo apt install docker-ce="$docker_version" \
docker-ce-cli="$docker_version" containerd.io \
docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker.service containerd.service
sudo systemctl is-active docker
sudo systemctl is-enabled docker
sudo docker version
sudo docker compose version
dpkg-query -W docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-pluginВ docker-ce лежит сам Engine, сервер, которым управляет CLI из docker-ce-cli. containerd.io запускает контейнеры, Buildx собирает образы, а Compose поднимает сервисы по файлу. Compose здесь работает как плагин, и команда пишется через пробел: docker compose. Старый docker-compose с дефисом из пакетов Ubuntu ставить не нужно.
В выводе docker version ищите два блока, Client и Server. Если второго нет, CLI не достучался до сервера. Статус active значит, что служба работает, enabled – что она поднимется после перезагрузки. Первый контейнер на VPS запустим позже, сначала проверим сам сервер.
Как безопасно запускать Docker без root?
Для работы без root используйте rootless-режим: в нём и сервер Docker, и контейнеры запускаются от обычного пользователя. Группа docker эту задачу не решает: её участник управляет системным сервером, а тот работает от root. Rootless требует пользовательских пространств имён, а сеть и лимиты через cgroup в нём ограничены.
Что проверить в docker info
Установка Docker из репозитория закончена, и самое время сохранить параметры сервера и свежий журнал. Если через месяц что-то сломается, будет с чем сравнить.
sudo docker info
sudo docker info --format '{{.DriverStatus}}'
sudo docker system df
sudo journalctl -u docker -u containerd -b --no-pager -n 100На чистой установке Engine 29 образы хранит containerd, а слои собирает snapshotter overlayfs: контейнер видит файловую систему из слоёв образа и пишет свои изменения поверх. После обновления со старой версии в docker info может остаться драйвер overlay2. Трогать его без причины не нужно, после смены хранилища прежние образы и контейнеры пропадут из вида.
У такой схемы есть последствие для дисков. Образы и слои лежат в /var/lib/containerd, а тома и прочие данные Docker в /var/lib/docker. Смена Docker Root Dir переносит только второй каталог, так что при разнесённом хранении место придётся смотреть на обоих.
Ещё в docker info стоит найти Cgroup Driver и Cgroup Version: от них зависят лимиты CPU и памяти. Рядом стоит Logging Driver, он решает, куда уходят логи контейнеров. Если служба не стартует, причину почти всегда видно в journalctl. Чужой daemon.json целиком лучше не копировать: одной неподходящей опции хватит, чтобы dockerd не запустился.
Группа docker даёт больше прав, чем кажется
Во всех командах этой статьи стоит sudo, и это сознательный выбор. Участник группы docker может запустить контейнер с подключённым корнем хоста и переписать любой системный файл, так что членство в группе равносильно root. По той же причине запись в /var/run/docker.sock не открывают всем и не прокидывают сокет в контейнеры без крайней нужды.
Rootless эту проблему снимает, зато приносит свои. Сервер Docker работает как пользовательская служба systemd и без включённого linger останавливается после выхода из SSH. Лимитам ресурсов нужны cgroup v2 и делегирование контроллеров, а порты ниже 1024 придётся разрешать отдельно.
Docker Compose на VPS получает ровно те полномочия, что и CLI, через который его запускают, отдельной защиты у него нет. Запуск приложения внутри контейнера от обычного пользователя настраивают в образе или compose.yaml, и к rootless на хосте он отношения не имеет.
Порт 8080 может открыться вопреки UFW
Запись 8080:80 пробрасывает порт 8080 хоста на порт 80 контейнера, причём по умолчанию на всех адресах хоста. EXPOSE в Dockerfile ничего не публикует, он только документирует container ports, которые слушает приложение. Наружу порт открывают флаг -p или секция ports в Compose.
Для теста порт привязывается к 127.0.0.1:8080:80: сервис виден с самого VPS и через SSH-туннель, но не из интернета. Для внешней публикации указывают адрес нужного интерфейса. Публичный IP за NAT провайдера может вообще не принадлежать интерфейсу машины, и привязать к нему порт не получится.
С firewall связан главный сюрприз этого раздела. Docker сам пишет правила для своих мостовых сетей, и пакет к опубликованному порту идёт по цепочке пересылки, мимо входящих правил UFW. Включённый UFW с запретом на 8080 спокойно уживается с открытым портом контейнера.
При стандартном бэкенде iptables собственные ограничения кладут в цепочку DOCKER-USER, она срабатывает раньше правил Docker. Пакет там уже несёт порт контейнера после перенаправления, поэтому для фильтра по исходному порту нужен модуль conntrack. И не путайте iptables-nft, который Ubuntu использует по умолчанию, с экспериментальным бэкендом Docker firewall-backend=nftables: в нём цепочки DOCKER-USER нет.
Как запустить и открыть наружу первый контейнер?
1. Запустите проверенный образ, явно указав адрес хоста, внешний порт и порт контейнера.
2. Ограничьте доступ firewall с учётом цепочек Docker и внешних правил провайдера.
3. С другой машины проверьте ответ с разрешённого адреса и отказ с запрещённого, затем снимите тестовую публикацию.
Тестовый Nginx и его digest
Для проверки возьмём официальный nginx:1.30.6-alpine из стабильной ветки. Если тег свежего патч-релиза ещё не появился в Docker Hub, подойдёт предыдущий. Тег с номером версии могут переопубликовать с исправлениями, поэтому из RepoDigests сохраняется digest, по которому потом запустится ровно тот же образ.
sudo docker pull nginx:1.30.6-alpine
nginx_image=$(sudo docker image inspect nginx:1.30.6-alpine \
--format '{{index .RepoDigests 0}}')
printf '%s\n' "$nginx_image"
sudo docker run -d --name vps-web-test \
-p 127.0.0.1:8080:80 --log-driver local "$nginx_image"
curl --fail --retry 10 --retry-connrefused --retry-delay 1 \
http://127.0.0.1:8080/
sudo docker port vps-web-test
sudo docker logs --tail 30 vps-web-test
sudo docker stop vps-web-test
sudo docker rm -v vps-web-testDigest выглядит как имя образа с хешем sha256 и указывает на конкретную сборку, даже если тег потом переопубликуют. Успешный curl подтверждает, что сервис отвечает по HTTP, а в журнале контейнера должна появиться строка с этим запросом. Флаг -v при удалении убирает только анонимные тома, именованные тома и сам образ остаются.
Внешнюю проверку делают отдельным контейнером: его пересоздают с адресом интерфейса вместо 127.0.0.1, а порт в firewall открывают только для своего IP. На второй машине подставьте реальный адрес VPS:
curl --connect-timeout 5 http://АДРЕС_VPS:8080/С вашего адреса должен прийти ответ, с любого другого запрос должен упереться в таймаут. Оба результата стоит записать, а тестовый контейнер удалить: дальше снова понадобится только локальная публикация.
Пока контейнер ничего не хранит, его можно удалять без сожаления. Файлы сайта замену контейнера так не переживут, для них нужны volumes, отдельные тома с данными.
Новый контейнер и прежние данные
Дальше настройки переезжают в compose.yaml. Сайт лежит в именованном томе, а журнал сервиса ограничен по размеру:
name: vps-demo
services:
web:
image: ${NGINX_IMAGE:?Укажите образ с digest}
ports:
- "127.0.0.1:8080:80"
volumes:
- site:/usr/share/nginx/html:ro
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
site:
name: vps-demo-siteЗначение nginx_image из предыдущего шага уходит в .env:
printf 'NGINX_IMAGE=%s\n' "$nginx_image" > .env
sudo docker compose config --quiet
sudo docker compose up -d
curl --fail --retry 10 --retry-connrefused --retry-delay 1 \
http://127.0.0.1:8080/При первом запуске Docker заполнит пустой том файлами из образа, и дальше том живёт отдельно: новый образ Nginx этот сайт уже не перезапишет. При смене образа или настроек Docker Compose пересоздаёт контейнер и подключает к нему тот же том. Перед обновлением запишите прежний digest, после обновления проверьте HTTP и журнал. Откат на старый образ данные в томе не вернёт, их восстанавливают из бэкапа.
Бэкап тома делают, пока в него никто не пишет. Для статического сайта хватит остановить сервис и упаковать том временным контейнером из того же образа:
sudo docker compose stop web
sudo docker run --rm -v vps-demo-site:/data:ro -v "$PWD":/backup \
"$nginx_image" tar -C /data -czf /backup/site.tar.gz .
sudo docker compose start webПока архив ни разу не разворачивали, это скорее надежда, чем бэкап. Разверните его в отдельный том, сравните контрольные суммы файлов и удалите проверочный том:
sudo docker volume create vps-demo-site-restore
sudo docker run --rm -v vps-demo-site-restore:/data -v "$PWD":/backup:ro \
"$nginx_image" tar -C /data -xzf /backup/site.tar.gz
sudo docker run --rm -v vps-demo-site:/a:ro -v vps-demo-site-restore:/b:ro \
"$nginx_image" sh -c 'cd /a && find . -type f -exec sha256sum {} + | sort > /tmp/a &&
cd /b && find . -type f -exec sha256sum {} + | sort > /tmp/b && diff /tmp/a /tmp/b && echo OK'
sudo docker volume rm vps-demo-site-restoreС базой данных так нельзя: архив тома работающей базы может оказаться несогласованным. Базу копируют её собственными средствами или после полной остановки.
Лимит журнала в compose.yaml действует только на вывод контейнера, а файлы логов, которые приложение пишет само, ограничивают отдельно. Убрать контейнеры проекта и оставить данные можно так:
sudo docker compose downС флагом -v эта команда удалит и том с сайтом, и добавлять его по привычке не стоит. Архив тома вынесите с VPS: копия на том же диске пропадёт вместе с ним.
Тестовый Nginx проверил только то, что движок работает и порт отвечает. Сервер готов к нагрузке, когда он переживает перезагрузку, порт открыт ровно туда, куда задумано, а архив тома действительно разворачивается. После установки Docker на VPS сохраните compose.yaml, .env и журнал сессии вместе с порядком восстановления, а саму запись закройте командой exit. Если у приложения есть миграции базы, откат на старую версию стоит испытать заранее: когда для него понадобится бэкап, всё записанное после его создания пропадёт.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.