Docker и Docker Compose на VPS с Ubuntu: установка и первый запуск
Docker на VPS ставят из официального репозитория Docker, а не из пакета docker.io, который лежит в Ubuntu: версия там свежее, а Compose приходит готовым плагином. Установка — это несколько команд, которые можно копировать подряд, ниже они разобраны по шагам. А дальше идёт то, из-за чего новички обычно теряют вечер: первый проект на Compose, который переживает перезагрузку, порты, которые Docker открывает в обход файрвола, журналы без ограничения размера и диск, который незаметно заполняется.
Команды из статьи мы выполняли на Ubuntu 24.04 (Docker 29.9.2 и Compose 5.6.1 на 30 октября 2026 года). В документации Docker та же схема указана для Ubuntu 22.04 и 26.04, но на них мы не пробовали.
Зачем Docker на VPS
Docker запускает программу вместе со всем, что ей нужно, в изолированной среде — контейнере. Контейнер создаётся из образа: это готовый шаблон с программой и её библиотеками. Контейнер не мешает соседям: у него своя файловая система и свои версии библиотек, а убрать его можно одной командой, не оставив мусора в системе. Поэтому в Docker удобно держать то, что капризно ставится обычным способом: n8n, Nextcloud, базы данных, ботов.
Docker Compose — надстройка над Docker. В одном файле вы описываете, какие контейнеры нужны, как они связаны и где хранят данные, а потом поднимаете всё одной командой. Без Compose для каждого контейнера пришлось бы помнить длинную строку с десятком флагов.
Самому Docker много памяти не нужно. Диск расходуется быстрее: образ n8n весит 1,81 ГБ, образ Nextcloud 2,13 ГБ, и на небольшом диске это заметно. Ниже есть раздел про то, как смотреть и чистить занятое место.
Что понадобится
- VPS с 64-битной Ubuntu 22.04, 24.04 или 26.04. Подойдёт любой тариф VPS UFO.Hosting. Для пары небольших контейнеров хватит и скромной конфигурации, а если планируете несколько сервисов, берите от 2 ГБ памяти. Как выбрать тариф, описано в отдельной статье.
- Доступ по SSH. Если ещё не подключались, начните со статьи про SSH.
- Права
sudoили вход подroot. В командах ниже стоитsudo, под root его можно убрать.
Установка Docker из официального репозитория
Репозиторий — это сервер, с которого apt берёт пакеты. Сначала уберите старые версии Docker, если они уже были. На новом сервере шаг можно пропустить, а если каких-то пакетов нет, apt просто сообщит, что удалять нечего:
sudo apt remove docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc
Теперь подключаем репозиторий Docker. Два пакета, которые мы ставим первыми, нужны, чтобы безопасно скачать ключ по HTTPS. Ключ позволяет apt убедиться, что пакеты выпущены именно Docker и не подменены по дороге:
sudo apt update
sudo apt install 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
Команда install -m 0755 -d создаёт каталог для ключей с нужными правами, а chmod a+r разрешает apt этот ключ читать. Дальше описание самого репозитория. Версию Ubuntu и архитектуру процессора команда подставит сама, править в ней ничего не нужно, а строка Signed-By привязывает репозиторий к скачанному ключу:
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
Если в выводе apt update есть строка с download.docker.com, репозиторий подключён. Ставим Docker:
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Что здесь ставится. docker-ce — сам Docker, фоновая служба (её называют демоном). docker-ce-cli — команда docker, которую вы вводите в терминале. containerd.io — движок, который запускает контейнеры. docker-compose-plugin — Compose. Пакет docker-buildx-plugin нужен для сборки собственных образов и в этой статье не пригодится. Служба docker после установки уже включена в автозапуск и работает:
systemctl is-enabled docker
enabled
systemctl is-active docker
active
docker --version
Docker version 29.9.2, build a82e528
docker compose version
Docker Compose version v5.6.1
Пишите docker compose через пробел. Команда docker-compose через дефис — старая отдельная программа первой версии, её поддержку прекратили в 2023 году. Если инструкция из интернета просит поставить именно её, ищите другую.
Последняя проверка — запустить контейнер, который сам сообщает, что всё работает:
sudo docker run --rm hello-world
...
Hello from Docker!
This message shows that your installation appears to be working correctly.
Docker скачал образ, запустил контейнер, получил от него текст и удалил контейнер (за это отвечает --rm). Сам образ остался на диске и пригодится при следующем запуске.
Docker без sudo: чем это опасно
Если запустить docker ps под обычным пользователем, будет отказ:
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock
Лечится добавлением пользователя в группу docker: sudo usermod -aG docker ваш_пользователь. Ключ -aG добавляет пользователя в группу, не удаляя из остальных. После этого нужно выйти и войти по SSH заново, потому что группы читаются только при входе. Делайте так осознанно: участник группы docker фактически равен root. Он может запустить контейнер, подключить в него всю файловую систему сервера и менять там что угодно. На личном сервере это нормально. А давать эту группу другим людям или программам стоит так же осторожно, как пароль root.
Первый проект на Compose
Возьмём самый простой пример: nginx отдаёт одну страницу из папки. Создайте каталог проекта и файл страницы:
mkdir -p ~/hello/site && cd ~/hello
echo '<!doctype html><meta charset="utf-8"><h1>Привет из контейнера</h1>' > site/index.html
Теперь файл compose.yaml (создайте его любым редактором, например nano compose.yaml). Так сегодня называют файл Compose, старое имя docker-compose.yml тоже работает.
services:
web:
image: nginx:1.28-alpine
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"
volumes:
- ./site:/usr/share/nginx/html:ro
По строкам:
services— список контейнеров проекта. У нас один, он называетсяweb.image: nginx:1.28-alpine— из какого образа его создать. После двоеточия стоит тег, метка версии. Её лучше указывать явно: с тегомlatestпри очередном обновлении можно незаметно получить новую крупную версию с несовместимыми изменениями. Словоalpineозначает облегчённую сборку образа.restart: unless-stopped— поднимать контейнер заново, если он упал или сервер перезагрузился, кроме случая, когда вы сами его остановили. Без этой строки после перезагрузки сервера контейнер сам не запустится.ports— какой порт сервера перенаправить в контейнер. Запись127.0.0.1:8080:80значит: порт 80 внутри контейнера доступен только на самом сервере, на порту 8080. Почему не просто8080:80, объясняет раздел про файрвол ниже.volumes— какую папку сервера показать контейнеру. Наша страница лежит в./site, а nginx ищет файлы в/usr/share/nginx/html. Приписка:roразрешает только читать. Так подключают и обычные папки, и тома (volumes) — хранилища данных вне контейнера, которые остаются, когда контейнер удалён. Именованные тома мы встретим в следующих статьях цикла.
Проверим файл и запустим:
docker compose config --quiet && echo OK
OK
docker compose up -d
Container hello-web-1 Created
Container hello-web-1 Started
Ключ -d оставляет контейнер работать в фоне. Без него Compose займёт терминал и остановит всё, когда вы закроете окно. Команда config --quiet ничего не запускает, а только проверяет файл, и молчит, если ошибок нет.
docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
hello-web-1 nginx:1.28-alpine "/docker-entrypoint.…" web 2 seconds ago Up 2 seconds 127.0.0.1:8080->80/tcp
curl -s localhost:8080
<!doctype html><meta charset="utf-8"><h1>Привет из контейнера</h1>
Страница отвечает. Из браузера на вашем компьютере её не открыть: порт привязан к 127.0.0.1, поэтому проверяем с самого сервера. Откуда имя hello-web-1? Compose собирает его из названия папки проекта (hello), названия сервиса (web) и номера экземпляра. Название папки поэтому важно: два проекта в одноимённых папках будут мешать друг другу.
Перезагрузка, обновление, остановка
Чтобы убедиться, что restart: unless-stopped работает, перезагрузите сервер (sudo reboot) и после входа выполните docker compose ps в папке проекта. Контейнер должен снова быть в статусе Up. Сама служба Docker при загрузке стартует сама, это видно выше по is-enabled.
Повседневных команд немного. Выполняйте их в папке, где лежит compose.yaml:
| Команда | Что делает |
|---|---|
docker compose ps |
Показывает контейнеры проекта и их статус. |
docker compose logs -f web |
Выводит журнал контейнера web и следит за новыми строками. Выход — Ctrl+C. |
docker compose restart web |
Перезапускает один контейнер. |
docker compose exec web sh |
Открывает оболочку внутри работающего контейнера. |
docker compose pull, затем docker compose up -d |
Обновление: сначала скачивает свежие образы, потом пересоздаёт те контейнеры, у которых образ изменился. Остальные не трогает. |
docker compose down |
Останавливает и удаляет контейнеры и сеть проекта. Образы и данные в томах остаются. |
У down есть опасная версия. Обычный docker compose down оставляет именованный том с данными на месте, а docker compose down -v удаляет и его. На тестовом томе мы это проверили: после down том остался в списке, после down -v исчез. Если в нём лежала база данных, вернуть её будет нечем. Пишите -v только тогда, когда хотите начать с нуля.
Больше команд самого Docker собрано в гайде по основным командам.
Файрвол и порты: чего не ждёшь от Docker
Здесь легко попасться и опытному администратору. Допустим, вы включили ufw и разрешили в нём только SSH. Логика подсказывает, что снаружи закрыто всё остальное. С Docker это не так: когда контейнер публикует порт, Docker сам добавляет правила в iptables, сетевой фильтр ядра, и они срабатывают раньше правил ufw. В документации Docker сказано прямо: для ufw трафик к опубликованным портам обходит его настройки.
Проверили на сервере. В ufw разрешён только порт 22:
sudo ufw status
Status: active
To Action From
-- ------ ----
22/tcp ALLOW Anywhere
22/tcp (v6) ALLOW Anywhere (v6)
Контейнер с nginx опубликован как "80:80". Запрос с другого сервера (ключ -w печатает код ответа, а 000 значит, что соединения не было совсем):
curl -s -o /dev/null -w "http=%{http_code}\n" http://IP_СЕРВЕРА/
http=200
Порт 80 в ufw не разрешён, а сайт открыт всему интернету. Теперь тот же контейнер, но в compose.yaml порт записан как "127.0.0.1:80:80":
curl -s -o /dev/null -w "http=%{http_code}\n" http://IP_СЕРВЕРА/
http=000
Снаружи порт закрыт, а на самом сервере curl http://127.0.0.1/ возвращает 200. Для проверки мы брали порт 80: на нашем тестовом сервере нестандартные порты вроде 8080 снаружи не открывались сетевым фильтром, и это сбивало картину. У вас фильтр может отличаться, на сам обход ufw это не влияет.
Итог: порт, который не должен быть виден из интернета (база данных, панель, внутренний сервис), публикуйте только на 127.0.0.1. А то, что должно быть открыто, пропускайте через обратный прокси. Это программа, которая принимает запросы из интернета на портах 80 и 443 и передаёт их нужным контейнерам. Именно так устроены следующие статьи цикла, про n8n и Nextcloud.
Оговорка про версии. По документации Docker, до версии 28.0.0 порт, опубликованный на 127.0.0.1, могли достать другие машины из того же сетевого сегмента. В Docker 29, который ставит эта статья, такого уже нет. Если у вас docker.io из Ubuntu, проверьте версию командой docker --version.
Свои правила для контейнеров можно писать и отдельно: в документации Docker для этого предусмотрена цепочка DOCKER-USER. Но проще и надёжнее начать с привязки к 127.0.0.1. А настройку ufw всё равно стоит сделать: он по-прежнему защищает сам сервер, просто не контейнеры.
Журналы: настройте ротацию сразу
По умолчанию Docker пишет журнал каждого контейнера в файл и никогда его не обрезает. Занятый сервис за несколько месяцев набирает гигабайты. Посмотреть, как сейчас:
docker info --format '{{.LoggingDriver}}'
json-file
Драйвер json-file без ограничения размера на небольшом диске — плохой выбор. Создайте файл /etc/docker/daemon.json (по умолчанию его нет), например командой sudo nano /etc/docker/daemon.json:
{
"log-driver": "local",
"log-opts": { "max-size": "10m", "max-file": "3" }
}
Драйвер local хранит журнал в компактном виде, а max-size и max-file ограничивают его тремя файлами по 10 МБ. Применить:
sudo systemctl restart docker
docker compose up -d --force-recreate
После перезапуска Docker настройка действует для новых контейнеров. У уже существующих остаётся старая, поэтому мы их пересоздаём ключом --force-recreate (команду запускайте в папке проекта). Проверим результат:
docker inspect -f '{{json .HostConfig.LogConfig}}' hello-web-1
{"Type":"local","Config":{"max-file":"3","max-size":"10m"}}
Есть оговорка: файлы в формате local руками не прочитать. Это не мешает, docker compose logs работает как обычно.
Место на диске
Образы, остановленные контейнеры и тома копятся. Посмотреть, что занимает место:
docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 2 1 93.42MB 25.87kB (0%)
Containers 1 1 81.92kB 0B (0%)
Local Volumes 0 0 0B 0B
Build Cache 0 0 0B 0B
В столбце RECLAIMABLE написано, сколько можно освободить. Чистят так:
docker system pruneубирает остановленные контейнеры, неиспользуемые сети и «висячие» образы без имени. Команда просит подтверждения, запущенные контейнеры и тома не трогает. Остановленный контейнер, который вы собирались запустить снова, она всё же удалит, поэтому читайте список перед согласием.docker system prune -aдополнительно убирает все образы, которые не нужны ни одному контейнеру. Нужные потом скачаются заново.- Ключ
--volumesдобавляет удаление томов, на которые не ссылается ни один контейнер, то есть данных. Пользуйтесь им, только если точно знаете, что в этих томах ничего ценного нет.
Если что-то пошло не так
Порт занят. Текст ошибки зависит от того, кто его занял. Если это другой контейнер, Docker пишет так:
docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint clash (...): Bind for 127.0.0.1:80 failed: port is already allocated
А если порт держит программа на самом сервере, например nginx или Apache, ответ другой:
docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint clash2 (...): failed to bind host port 0.0.0.0:8099/tcp: address already in use
Узнать, кто занял порт, можно командой sudo ss -tlnp | grep ':80 '. Она показывает программы, которые слушают TCP-порты. Дальше остановите нарушителя или выберите другой порт.
Предупреждение про version. Во многих старых инструкциях compose.yaml начинается со строки version: "3.8". Современный Compose пишет об этом так:
level=warning msg="/opt/vol/compose.yaml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion"
Это не ошибка. Строку можно просто удалить, на работу она не влияет.
Если Docker отвечает «permission denied», вернитесь к разделу про sudo и группу docker.
Частые вопросы
Нужен ли отдельный пакет docker-compose?
Нет. Compose ставится плагином вместе с Docker (пакет docker-compose-plugin) и запускается командой docker compose через пробел.
Можно ли поставить Docker командой apt install docker.io?
Можно, это рабочий вариант. На Ubuntu 24.04 в нём Docker 29.1.3 против 29.9.2 у Docker, а Compose идёт отдельным пакетом docker-compose-v2 версии 2.40.3 (в репозитории Docker сегодня 5.6.1). Для учебного сервера разницу вы не заметите, но обновления будете получать позже. Мы берём репозиторий Docker: Docker и Compose обновляются одной командой apt upgrade.
Контейнер упал, что смотреть?
Сначала docker compose ps -a, чтобы увидеть статус, потом docker compose logs --tail 50 имя_сервиса, чтобы прочитать последние строки журнала. Причина почти всегда написана в конце.
Что дальше
Когда Docker стоит, а порты под контролем, на нём можно запускать готовые сервисы. В следующих статьях цикла мы ставим n8n с HTTPS и автозапуском и собственное облако на Nextcloud. Данные из контейнеров стоит сразу включить в резервные копии.
Похожее
Все статьи
Nextcloud на VPS: своё облако через Docker Compose
Nextcloud — это собственное облако: файлы с синхронизацией на телефон и компьютер, календарь, контакты, общий доступ по ссылке. На VPS его поднимают через Docker Compose из пяти контейнеров (подробности ниже): Nextcloud, база PostgreSQL, кэш Redis, контейнер для фоновых задач и…
n8n на VPS: установка через Docker Compose, HTTPS и автозапуск
n8n на своём VPS поднимается через Docker Compose за один вечер: два контейнера (сам n8n и обратный прокси Caddy, который сам выпускает HTTPS-сертификат), три небольших файла и одна команда запуска. Ниже — готовые файлы, объяснение каждой переменной, первый сценарий с…