Docker на одном сервере - это не оркестрация и не облако. Это способ установить программу, не устанавливая её: одна команда скачивает проверенный образ, запускает его как изолированное дерево процессов со своей файловой системой и оставляет в вашей операционной системе только сам Docker. Ничто не разворачивается за вас, ничто не масштабируется, ничто не чинится само. Вы получаете повторяемость - один и тот же контейнер ведёт себя одинаково и на вашем ноутбуке, и на машине в Германии, - а заодно возможность держать две версии одной и той же базы данных без драки.
Вся работа на одной машине сводится к пяти решениям на каждый контейнер: какой образ и какой тег, какие порты опубликованы и на какой адрес, где лежат данные, что происходит, когда контейнер завершается, и что не даёт ему съесть диск. В этой статье все пять разобраны по порядку, с командами, а в конце - способы, которыми теряют данные.
Что Docker меняет на одном сервере#
Честный итог того, что вы получаете:
- Установка превращается в pull. Никаких PPA, никакой компиляции, никаких «нужен Python 3.9, а на машине 3.11». Образ несёт с собой собственное окружение.
- Версии перестают конфликтовать. PostgreSQL 15 и 17 могут работать бок о бок на одной машине, каждая со своим каталогом данных и своим портом, и ни одна не знает о другой.
- Удаление полное.
docker rmуносит и процесс, и файловую систему. В/usr,/etcи базе пакетов ничего не остаётся. - Настройка становится командой, которую можно записать. Строка
docker run- это инструкция по установке, и её можно хранить в файле; именно этим занимается Docker Compose для небольших стеков.
А вот чего Docker не даёт, и это важнее, потому что люди предполагают обратное. Контейнеры - не виртуальные машины: они используют ядро хоста, поэтому паника ядра или нехватка памяти на хосте кладёт всё разом. Они не служат границей безопасности против root: любой, кто может запускать docker, способен смонтировать файловую систему хоста в контейнер и прочитать что угодно. Они не ускоряют программы: накладные расходы близки к нулю, но и выигрыш тоже. И они ничего не резервируют.
Одно условие: нужно собственное настоящее ядро. На KVM, VMware, Hyper-V и Xen Docker работает. На контейнерной виртуализации вроде OpenVZ или LXC он в лучшем случае неудобен, а в худшем невозможен. Выполните systemd-detect-virt, прежде чем что-либо планировать: в статье VPS, VDS или выделенный сервер разобрано, что это единственное слово в выводе говорит о купленном вами тарифе.
Установка Docker и группа, которая на самом деле root#
Есть два поддерживаемых пути. Скрипт для удобной установки подходит для машины, которая принадлежит вам:
$ curl -fsSL https://get.docker.com -o get-docker.sh$ sudo sh get-docker.sh$ sudo systemctl enable --now docker$ docker versionУстановка из репозитория даёт те же пакеты, но в несколько шагов, и именно она нужна во всём, что вы будете повторять. В любом случае на выходе получаются docker-ce (демон), docker-ce-cli (команда), containerd.io (среда выполнения под ними), а также подкоманды docker-buildx-plugin и docker-compose-plugin. Не берите пакет docker.io из репозитория вашего дистрибутива: он отстаёт по версии и не всегда приносит с собой плагин Compose, который вам понадобится через неделю.
Теперь то, что все пропускают:
$ sudo usermod -aG docker deploy$ newgrp docker$ docker run --rm hello-worldЕсли docker run hello-world печатает приветствие, значит, демон работает, сеть работает и скачивание образов работает. Это три проверки одной командой, и стоит сделать её раньше, чем разбираться с чем-то посложнее.
Образы и контейнеры: первый запуск#
Образ - это стопка слоёв файловой системы только для чтения плюс метаданные о том, что запускать. Контейнер - один работающий экземпляр образа с тонким слоем для записи сверху. Удаление контейнера удаляет и этот слой, поэтому всё, что вам дорого, должно лежать в томе - о нём через три раздела.
$ docker run -d \ --name web \ --restart unless-stopped \ -p 127.0.0.1:8080:80 \ -v /srv/web/html:/usr/share/nginx/html:ro \ nginx:1.27-alpineЧитаем эту строку слева направо: запустить в фоне, назвать web, поднимать после перезагрузки, опубликовать порт 80 контейнера как порт 8080 только на loopback, смонтировать каталог хоста только для чтения, из образа nginx с тегом 1.27-alpine.
Тег - это решение, а не украшение:
| Стиль тега | Пример | Когда использовать |
|---|---|---|
latest | nginx | Никогда на сервере |
| Major | nginx:1 | Вы доверяете обещанию проекта о совместимости |
| Minor | nginx:1.27 | Разумный вариант по умолчанию: исправления безопасности без сюрпризов |
| Точный | nginx:1.27.2 | Вы хотите сами выбирать каждое обновление |
| Digest | nginx@sha256:... | Сборка должна совпадать байт в байт каждый раз |
latest - не канал обновлений и не даёт никаких обещаний. Это просто тег, который подставляется, когда никто не указал другой, и он сдвигается, когда сопровождающему вздумается. docker pull через полгода может принести новую major-версию, и вы узнаете об этом по контейнеру, который не запускается. На всём, что хранит данные, закрепляйте хотя бы minor-версию.
Повседневные команды, которыми вы будете пользоваться каждый час:
$ docker ps # запущенные контейнеры$ docker ps -a # включая остановленные$ docker logs -f --tail 100 web # следить за выводом$ docker exec -it web sh # оболочка внутри контейнера$ docker stop web && docker rm web$ docker inspect web # все настройки в виде JSONdocker stop посылает SIGTERM, ждёт десять секунд, затем посылает SIGKILL. Если приложению нужно больше времени на чистое завершение - сбросить запись, закончить запрос, сохранить мир, - увеличьте его через docker stop -t 60 и убедитесь, что процесс действительно обрабатывает SIGTERM. В статье корректное завершение и проверки работоспособности показано, как выглядит правильная обработка в коде приложения.
Порты, публикация и файрвол#
-p настраивает destination NAT с порта хоста на порт контейнера. Запись -p 8080:80 привязывается ко всем адресам машины, включая публичный. Запись -p 127.0.0.1:8080:80 привязывается только к loopback.
Предпочитайте вторую форму. Почти всё, что вы запускаете, должно стоять за чем-то ещё: за обратным прокси для всего, что говорит по HTTP, и вообще ни за чем для базы данных, которой пользуется только ваше приложение. Правило такое: публикуйте порт, только если интернет должен добраться до него напрямую.
Это не вопрос стиля. Опубликованные порты обходят UFW. Контейнер, опубликованный через -p 8080:80, доступен из интернета, даже когда ufw status показывает политику «запретить всё» без правила для 8080: пакет пересылается в контейнер, а не доставляется на сам хост, и правила пересылки Docker вычисляются первыми. Полное объяснение и четыре способа обойти это - в руководстве по файрволу UFW; в одну строку это звучит так: публикуйте на 127.0.0.1 и отдайте публичную сторону прокси на хосте.
Контейнеры, которым нужно общаться друг с другом, должны делить пользовательскую сеть, а не публиковать что-либо:
$ docker network create appnet$ docker run -d --name db --network appnet -e POSTGRES_PASSWORD=... postgres:17$ docker run -d --name app --network appnet -p 127.0.0.1:3000:3000 my/app:1.4В пользовательской сети Docker запускает встроенный DNS-сервер, так что приложение подключается к имени хоста db на порт 5432, и наружу ничего не выставлено. Сеть bridge по умолчанию такого разрешения имён не делает, поэтому «в Compose работает, а через docker run нет» почти всегда означает пропущенный docker network create.
Тома и где на самом деле лежат ваши данные#
Слой для записи в контейнере умирает вместе с контейнером. Сохранить данные можно двумя способами, и они предназначены для разных задач.
Именованные тома управляются Docker и хранятся в /var/lib/docker/volumes/. Используйте их для всего, чем владеет программа и что вы трогаете только через неё: файлы базы данных, игровой мир, каталог состояния приложения.
$ docker volume create pgdata$ docker run -d --name db -v pgdata:/var/lib/postgresql/data postgres:17$ docker volume ls$ docker volume inspect pgdataBind-монтирования отображают путь хоста в контейнер. Используйте их для того, что вы хотите править собственным редактором: файлы конфигурации, HTML статического сайта, каталог загруженных файлов, который вы к тому же отдаёте через nginx.
$ docker run -d --name web -v /srv/web/html:/usr/share/nginx/html:ro nginx:1.27-alpineТри практических замечания. Суффикс :ro делает монтирование только для чтения и ничего не стоит - ставьте его везде, где контейнеру нечего записывать. Bind-монтирования переносят в контейнер владельцев с хоста, поэтому процесс с UID 1000 внутри не может писать в каталог, принадлежащий root снаружи; либо выполните chown каталога на нужный числовой ID, либо запускайте контейнер с --user "$(id -u):$(id -g)". И именованный том получает начальное содержимое из образа только тогда, когда он пуст, поэтому изменение конфигурации образа по умолчанию после первого запуска, кажется, ничего не делает.
Резервная копия тома - это контейнер, который монтирует его и записывает архив tar в другое место:
$ docker run --rm -v pgdata:/data:ro -v /srv/backups:/out alpine \ tar czf /out/pgdata-$(date +%F).tar.gz -C /data .Для базы данных лучше пользоваться её собственным инструментом дампа, а не копировать файловую систему работающего каталога данных: pg_dump через docker exec даёт файл, который надёжно восстанавливается, а архив файлов, в которые идёт запись, - нет. Флаги описаны в статье pg_dump и pg_restore.
Политики перезапуска, перезагрузки и проверки состояния#
Контейнер сам не возвращается. По умолчанию, когда процесс завершился или машина перезагрузилась, он остаётся выключенным.
| Политика | При сбое | При docker stop | После перезагрузки |
|---|---|---|---|
no (по умолчанию) | остаётся выключенным | остаётся выключенным | остаётся выключенным |
on-failure:5 | перезапускается, до 5 раз | остаётся выключенным | перезапускается, если он падал |
always | перезапускается | остаётся выключенным до перезапуска демона | перезапускается |
unless-stopped | перезапускается | остаётся выключенным | остаётся выключенным, если вы его останавливали |
unless-stopped - правильный ответ почти для всего на небольшом сервере. Он переживает перезагрузки и сбои и уважает то, что вы остановили контейнер намеренно; always этого не делает и с радостью поднимает контейнер, остановленный час назад, при следующем запуске демона.
Политику работающего контейнера можно менять без пересоздания:
$ docker update --restart unless-stopped webПолитики перезапуска зависят от того, что служба Docker запускается при загрузке, поэтому убедитесь, что systemctl is-enabled docker отвечает enabled. И помните, чем политика перезапуска не является: она не проверка состояния. Контейнер, чей процесс жив, но завис - приложение на Node перестало принимать соединения, игровой сервер застрял на сохранении, - полностью удовлетворяет политике перезапуска. Собственный HEALTHCHECK в Docker помечает такой контейнер как unhealthy, но не перезапускает его. Если вам нужно «перезапускать, когда перестал отвечать», это работа для watchdog или для юнита systemd, оборачивающего контейнер нужной вам логикой проверки; файл юнита для такого случая есть в статье службы systemd для ваших приложений.
Лимиты и логи: две вещи, которые тихо заполняют машину#
По умолчанию контейнер может использовать всю память машины и весь процессор. На сервере, где работает одно приложение, это нормально. На сервере с шестью один разбушевавшийся процесс утянет за собой остальные.
$ docker run -d --name app \ --memory=1g --memory-swap=1g --cpus=1.5 \ my/app:1.4$ docker stats --no-streamЕсли задать --memory-swap равным --memory, для этого контейнера отключается swap, и обычно это то, что нужно: процесс, загнанный в swap, тормозит так, что диагностировать это сложнее, чем прямой отказ. Когда контейнер превышает лимит памяти, ядро убивает процесс внутри него, контейнер завершается с кодом 137, и docker inspect это записывает:
$ docker inspect -f '{{.State.OOMKilled}}' apptrueКод завершения 137 - это 128 + 9, то есть SIGKILL. Код 143 - 128 + 15, чистый SIGTERM, обычно это вы сами его остановили. Эти два числа сами по себе отвечают на большинство вопросов «почему он умер». В статье swap в Linux и OOM killer объяснено, что именно решает ядро, выбирая жертву.
Более тихая проблема - логи. Драйвер логирования Docker по умолчанию записывает всё, что печатает контейнер, в JSON-файл в /var/lib/docker/containers/, вообще без ограничения размера. Разговорчивое приложение заполнит диск за недели или месяцы, а симптомом станет сервер, который одновременно ломается в нескольких несвязанных местах. Исправьте это глобально:
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }}Затем sudo systemctl restart docker. Обратите внимание, что это относится к контейнерам, созданным после изменения; уже существующие сохраняют настройки, с которыми были созданы, поэтому пересоздайте их или укажите --log-opt max-size=10m для каждого. Посмотреть, что вы носите на себе сегодня, можно командой sudo du -sh /var/lib/docker/containers/*.
Обновление, очистка и место на диске#
Обновление контейнера - это не апгрейд на месте. Вы скачиваете новый образ, уничтожаете контейнер и создаёте новый с теми же томами и той же командой запуска:
$ docker pull nginx:1.27$ docker stop web && docker rm web$ docker run -d --name web --restart unless-stopped \ -p 127.0.0.1:8080:80 -v /srv/web/html:/usr/share/nginx/html:ro nginx:1.27Именно поэтому важно записывать строку запуска, и именно поэтому существует Compose: docker compose pull && docker compose up -d делает те же три шага по уже имеющемуся файлу. Держите прежний тег, пока новый контейнер не проработает сутки; тогда откат - это просто запуск старого тега.
Место на диске утекает сразу в четырёх местах. Вот как узнать, где именно:
$ docker system dfTYPE TOTAL ACTIVE SIZE RECLAIMABLEImages 14 4 6.2GB 4.9GB (79%)Containers 7 4 112MB 43MB (38%)Local Volumes 6 3 2.1GB 820MB (38%)Build Cache 41 0 3.3GB 3.3GBdocker image prune -a удаляет образы, которыми не пользуется ни один контейнер. docker builder prune очищает кэш сборки, который на машине, собирающей собственные образы, нередко оказывается самой большой статьёй. docker container prune удаляет остановленные контейнеры. Запускайте эти три команды по расписанию, и вы почти перестанете об этом думать. Запускайте docker system prune --volumes от случая к случаю, и рано или поздно вы удалите базу данных. Это различие стоит держать в пальцах.
Когда контейнер не запускается#
Он сразу завершается, а `docker ps` ничего не показывает. Посмотрите в docker ps -a код завершения, а в docker logs <name> - причину. Контейнер, у которого нет долгоживущего процесса на переднем плане, завершается, как только заканчивается его команда; это не сбой, а замысел.
Порт уже занят. Порт хоста занят чем-то другим. sudo ss -lntp | grep 8080 покажет, чем именно. Либо остановите это, либо опубликуйте на другом порту хоста; порт контейнера менять никогда не нужно.
Permission denied на смонтированном каталоге. Несовпадение UID в bind-монтировании. Проверьте ls -ln для каталога на хосте и USER, от имени которого работает образ, и приведите их в соответствие.
Образ не скачивается. Убедитесь, что тег действительно существует, затем проверьте, не упёрлись ли вы в ограничение по частоте: реестры ограничивают анонимные загрузки по адресу, а docker login снимает это ограничение. На свежей машине проверьте и DNS: контейнеры по умолчанию используют резолвер хоста, и сломанный /etc/resolv.conf проявляется здесь первым.
Вчера работало, а сегодня другая версия. Вы использовали latest. См. таблицу тегов.
Всё тормозит, а диск заполнен. Логи или кэш сборки. См. предыдущий раздел.
Управляемые тарифы RE:NODE - тоже контейнеры: панель представляет собой доработанный Pterodactyl с одним контейнером на сервер, жёстким ограничением процессора в пределах купленной доли и тем же поведением при нехватке памяти, что описано выше, только весь контейнер останавливается на лимите и запускается заново с чистого листа, а не уходит в swap. Своего образа там принести нельзя, так что если программы, которую вы хотите запустить, нет в каталоге, честный ответ - VDS с root-доступом и Docker на нём. В статье выбор между VDS и игровой панелью оба варианта поставлены рядом, а в материале что такое хостинг-панель на самом деле разобрана управляемая сторона.
FAQ#
Нужен ли Docker на небольшом VDS?
Нет, а для одного приложения это может оказаться лишним слоем, который надо изучать ради небольшой пользы. Он оправдывает себя, как только вы запускаете вторую программу, нуждаетесь в версии, которой нет в пакетах вашего дистрибутива, или хотите перенести ту же конфигурацию на другую машину без изменений.
Сколько накладных расходов добавляет контейнер?
По процессору и памяти - почти ничего: это то же ядро, выполняющее тот же процесс с другими пространствами имён. Сеть контейнеров добавляет небольшую задержку из-за NAT, а перекрывающая (overlay) файловая система для записи медленнее bind-монтирования при интенсивной случайной записи, и это ещё одна причина держать базы данных на томах.
Где мои данные, если я удалю контейнер?
В его томах, которые остаются. Всё, что записано внутри контейнера, но вне тома, исчезает вместе со слоем для записи. Перед удалением посмотрите через docker inspect, что монтирует контейнер.
Можно ли запускать Docker внутри управляемого тарифа?
Нет. Управляемый игровой тариф или тариф для приложений уже сам является контейнером, и у вас внутри него нет ни собственного ядра, ни собственного демона. Возможность запускать Docker - одна из конкретных причин взять VDS.
Почему мой контейнер доступен из интернета, когда файрвол включён?
Потому что вы опубликовали порт, а опубликованные порты пересылаются мимо входящих правил файрвола хоста. Привязывайте публикацию к 127.0.0.1, если порт не должен быть по-настоящему публичным, и ставьте обратный прокси перед всем, что должно.
Стоит ли контейнерам обновляться автоматически?
Не на сервере, который вам дорог. Автоматическое обновление образов означает смену major-версии без присмотра в час, когда никто не смотрит. Закрепите minor-тег, обновляйтесь осознанно и держите прежний тег достаточно долго, чтобы откатиться.




Комментарии
Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.