Как установить Docker на VPS
Устанавливаем Docker и Compose на Ubuntu 24.04, запускаем приложение и открываем его для посетителей по HTTPS через Nginx. Проверяем API и сохранение данных.

Docker на VPS позволяет запускать приложение вместе с его окружением и переносить проект между серверами. В этой инструкции установим Docker Engine из официального репозитория на Ubuntu 24.04, запустим приложение через Compose и откроем его в интернете по домену с HTTPS. Затем пересоздадим контейнер и убедимся, что данные SQLite остались в отдельном томе.
Команды выполнены на учебном VPS с Ubuntu 24.04.5 LTS, 2 vCPU и 2 ГБ памяти. Для практики используем уже работающий сервер, чтобы не оплачивать отдельный VPS для каждого урока. Панель конкретного провайдера для установки Docker не требуется.
Что получится и что нужно подготовить
После урока у вас будут Docker Engine, плагин Compose и проект из четырёх файлов. Посетители смогут открыть приложение по HTTPS. Nginx будет принимать внешние запросы на порту 443 и передавать их контейнеру на 127.0.0.1:8082. Приложение будет проверять своё состояние и хранить счётчик вне контейнера. На нашем стенде можно открыть готовое приложение по HTTPS; исходная страница Nginx остаётся на главной.
Понадобятся Ubuntu 24.04, доступ по SSH, права root или sudo, свободное место под образы и исходящий доступ к репозиторию Docker и Docker Hub. Для этого небольшого примера 2 ГБ памяти достаточно; потребности настоящего проекта зависят от приложения, базы и сборки. При выборе нового сервера можно начать с подбора конфигурации и инструкции по созданию VPS в Timeweb Cloud.
Если сервер новый, сначала подключитесь по SSH, создайте пользователя с sudo и настройте UFW. Для заключительной части нужны домен с A-записью на ваш VPS, работающий Nginx и сертификат HTTPS. Это предыдущие шаги серии: если ещё не выполняли их, закончите до публикации приложения. На сервере с важными данными подготовьте резервную копию и проверку восстановления.
Команды ниже выполняются в Bash от root. При входе под обычным администратором откройте оболочку командой sudo -i, а после работы выйдите из неё через exit. Это особенно удобно для блоков, которые записывают файлы в /etc.
Как устроены образ, контейнер, Compose и том
Образ содержит приложение и его окружение. Контейнер — запущенный экземпляр образа. Из одного образа можно создать несколько контейнеров, однако изменения внутри удалённого контейнера обычно пропадут вместе с ним.
Compose описывает запуск проекта в файле compose.yaml: какой образ собрать, какие порты опубликовать, где хранить данные и как проверять работоспособность. Именованный том хранится отдельно от контейнера. В нашем случае это место для файла SQLite.
Контейнеры используют ядро хостовой ОС и не заменяют виртуальную машину. Установка Docker сама по себе также не делает приложение безопасным: имеют значение права процесса, опубликованные порты, подключённые каталоги и обновления образов.
Docker Hub — каталог и хранилище образов. Оттуда сервер скачивает hello-world и python:3.12-slim; посетители сайта к Docker Hub не подключаются. Путь посетителя в этом уроке выглядит так: браузер → HTTPS на VPS → Nginx → контейнер приложения. Публиковать API управления Docker ради работы сайта не требуется.
Проверьте Ubuntu и существующие установки
Посмотрите систему, архитектуру и свободный диск:
cat /etc/os-release
dpkg --print-architecture
df -h /
На нашем VPS это Ubuntu 24.04 и архитектура amd64. Репозиторий ниже выбирает кодовое имя системы и архитектуру автоматически. Не подставляйте название Ubuntu наугад, если повторяете урок на другой ОС.
Убедитесь, что на сервере ещё нет Docker или другой среды, которую вы можете затронуть:
command -v docker || true
dpkg-query -W docker.io docker-compose docker-compose-v2 \
podman-docker containerd runc 2>/dev/null || true
Если Docker уже есть, сначала выясните способ его установки, список контейнеров и назначение данных. Пакеты containerd и runc могут использоваться другими системами. Не удаляйте их по готовой команде на рабочем сервере. Дальнейшие шаги рассчитаны на Ubuntu без существующей установки Docker; порядок перехода с конфликтующих пакетов описан в официальной документации, указанной в источниках.
Установите Docker из официального репозитория
Обновите список пакетов и установите инструменты для HTTPS:
apt-get update
apt-get install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
Добавьте источник пакетов в формате deb822. Это один блок команды: строка EOF в конце должна стоять отдельно.
cat > /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
Установите Engine, среду выполнения, Buildx и Compose:
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker
Здесь docker-compose-plugin даёт команду docker compose с пробелом. Отдельный старый бинарный файл docker-compose для этого урока не нужен. Общее обновление всех пакетов сервера не является частью этих команд.
Проверьте Docker Engine и Compose
docker --version
docker compose version
systemctl is-active docker
docker run --rm hello-world
Ожидаемый результат: версии Docker и Compose, состояние active и сообщение Hello from Docker!. Параметр --rm удаляет проверочный контейнер после завершения, но скачанный образ остаётся в кеше.
На учебном сервере установились Docker Engine 29.9.2 и Compose 5.6.1. В вашем репозитории версии могут быть новее. Важны успешный запуск службы и выполнение тестового контейнера.
Мы используем root или sudo для управления Docker. Добавление пользователя в группу docker фактически предоставляет ему полномочия уровня root через Docker daemon. Не добавляйте в неё всех пользователей ради устранения ошибки permission denied и не выставляйте сокету /var/run/docker.sock права 666.
Создайте учебное приложение
Приложение использует стандартную библиотеку Python: дополнительных Python-пакетов и отдельного сервера базы данных не требуется. Страница / показывает счётчик, /api возвращает его в JSON, а /health проверяет доступность приложения и базы, не увеличивая число просмотров.
Выберите свободный каталог и порт:
ss -lnt 'sport = :8082'
mkdir -p /srv/hostfera-lab/docker-demo
cd /srv/hostfera-lab/docker-demo
Если каталог уже содержит проект, используйте другой. Если порт занят, замените 8082 в compose.yaml и командах проверки. Если под заголовком таблицы ss нет строк, слушателя на этом порту сейчас нет.
Создайте файл app.py, например через nano app.py, и вставьте код целиком:
import json
import sqlite3
from contextlib import closing
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
DATABASE = Path('/data/demo.sqlite3')
def visits(increment=False):
with closing(sqlite3.connect(DATABASE, timeout=10)) as db:
with db:
if increment:
db.execute('UPDATE counter SET value = value + 1 WHERE id = 1')
return db.execute('SELECT value FROM counter WHERE id = 1').fetchone()[0]
class Handler(BaseHTTPRequestHandler):
server_version = 'HostferaDemo'
sys_version = ''
def do_GET(self):
self.respond()
def do_HEAD(self):
self.respond(head=True)
def respond(self, head=False):
path = self.path.split('?', 1)[0]
if path == '/health':
visits()
payload = b'{"status":"ok"}'
content_type = 'application/json'
elif path in ('/', '/api'):
count = visits(increment=not head)
if path == '/api':
payload = json.dumps({'message': 'Docker Compose works', 'visits': count}).encode()
content_type = 'application/json'
else:
page = '''<!doctype html><html lang="ru"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><title>Hostfera Lab — Docker Compose</title><style>body{margin:0;min-height:100vh;display:grid;place-items:center;background:#071320;color:#edf7ff;font:18px/1.6 system-ui,sans-serif}main{max-width:720px;padding:36px;margin:20px;border:1px solid #245976;border-radius:18px;background:#0b1d2c}small{color:#4ed6ff;letter-spacing:.15em}h1{line-height:1.2;font-size:38px}p{color:#bfd2df}strong{color:#fff}a{color:#4ed6ff}</style><main><small>HOSTFERA LAB</small><h1>Приложение работает в Docker</h1><p>Запущено через Compose. Данные счётчика хранятся в отдельном томе SQLite.</p><p>Просмотры: <strong>COUNT</strong></p><p>Обновите страницу: счётчик увеличится.</p><a href="health">Проверить состояние</a></main></html>'''
payload = page.replace('COUNT', str(count)).encode('utf-8')
content_type = 'text/html; charset=utf-8'
else:
self.send_error(404)
return
self.send_response(200)
self.send_header('Content-Type', content_type)
self.send_header('Content-Length', str(len(payload)))
self.send_header('X-Robots-Tag', 'noindex, nofollow')
self.send_header('X-Content-Type-Options', 'nosniff')
self.send_header('X-Frame-Options', 'DENY')
self.send_header('Referrer-Policy', 'no-referrer')
self.send_header('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; frame-ancestors 'none'")
self.end_headers()
if not head:
self.wfile.write(payload)
def log_message(self, message, *args):
print(message % args, flush=True)
if __name__ == '__main__':
with closing(sqlite3.connect(DATABASE)) as db:
with db:
db.execute('CREATE TABLE IF NOT EXISTS counter (id INTEGER PRIMARY KEY CHECK (id = 1), value INTEGER NOT NULL)')
db.execute('INSERT OR IGNORE INTO counter VALUES (1, 0)')
ThreadingHTTPServer(('0.0.0.0', 8000), Handler).serve_forever()
Это небольшой учебный HTTP-сервер: мы откроем его через Nginx, чтобы проверить публикацию приложения целиком. Он поддерживает GET и HEAD, а ссылка проверки состояния работает и в подкаталоге /app/. Для своего Django, FastAPI или другого проекта используйте рекомендуемый сервер приложения, обработку ошибок и нужную авторизацию; исходники этого счётчика замените своим приложением.
Создайте рядом Dockerfile:
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
WORKDIR /app
RUN mkdir -p /data && chown 10001:10001 /data
COPY --chown=10001:10001 app.py /app/app.py
USER 10001:10001
EXPOSE 8000
CMD ["python", "app.py"]
Образ использует Python 3.12. Приложение запускается от UID/GID 10001, а каталог /data заранее подготовлен для записи этим пользователем. Инструкция EXPOSE 8000 описывает порт внутри контейнера, но сама не открывает его в интернете.
Добавьте .dockerignore, чтобы локальные служебные файлы и базы не попадали в контекст сборки:
.git
__pycache__
*.pyc
.env
*.sqlite3
*.sqlite3-*
Тег python:3.12-slim обновляется. Для воспроизводимой поставки рабочего проекта фиксируют проверенную версию или digest образа и планируют обновление. В уроке используется актуальный образ этой ветки.
Опишите запуск в compose.yaml
Создайте файл compose.yaml в том же каталоге:
name: hostfera-docker-lab
services:
app:
build: .
ports:
- "127.0.0.1:8082:8000"
volumes:
- app-data:/data
restart: unless-stopped
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
pids_limit: 100
mem_limit: 128m
cpus: 0.5
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2).read()"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
logging:
driver: json-file
options:
max-size: "5m"
max-file: "2"
volumes:
app-data:
В этой конфигурации:
build: .собирает образ из текущего каталога;127.0.0.1:8082:8000связывает локальный порт VPS с портом приложения;app-data:/dataвыносит SQLite в именованный том;restart: unless-stoppedвключает перезапуск после сбоя или перезагрузки, если контейнер не остановлен вручную;read_only,cap_dropиno-new-privilegesограничивают права контейнера; запись разрешена в том и временный/tmp;mem_limit: 128m,cpus: 0.5иpids_limit: 100ограничивают ресурсы именно этого небольшого приложения;healthcheckпроверяет HTTP и доступ к SQLite; журнал ограничен двумя файлами по 5 МБ.
Лимиты из примера не подходят автоматически любой CMS или базе. Если приложение завершается по нехватке памяти, нужно изучить потребление и подобрать ресурсы. Состояние unhealthy само по себе не заставляет обычный Docker Compose перезапускать работающий процесс: для этого нужны отдельное наблюдение и корректная логика приложения.
Соберите и запустите проект
Из каталога с compose.yaml выполните:
docker compose config --quiet
docker compose build
docker compose up -d --wait --wait-timeout 90
docker compose ps
config --quiet проверяет структуру файла; при успехе команда ничего не печатает. Во время первой сборки Docker скачает базовый образ. up создаст сеть, том и контейнер, а --wait дождётся успешной проверки состояния.
В docker compose ps найдите сервис app, состояние healthy и публикацию 127.0.0.1:8082->8000/tcp.
Проверьте ответ внутри VPS:
curl -fsS http://127.0.0.1:8082/health; echo
curl -fsS http://127.0.0.1:8082/api; echo
docker compose port app 8000
Первый запрос возвращает {"status":"ok"}, второй — сообщение Docker Compose works и число просмотров. Повторный запрос к /api увеличит счётчик.
Для необязательной проверки до настройки Nginx можно открыть другой локальный терминал и создать SSH-туннель. Вместо ADMIN и VPS_IP подставьте своего пользователя и адрес:
ssh -N -L 127.0.0.1:18082:127.0.0.1:8082 ADMIN@VPS_IP
Пока SSH-соединение работает, откройте http://127.0.0.1:18082/ на компьютере. Если локальный порт занят, выберите другой. Закрытие туннеля через Ctrl+C не останавливает контейнер. Для доступа обычных посетителей выполните следующий раздел: после него туннель не понадобится.
Почему нельзя полагаться только на UFW
Запись 8082:8000 без адреса обычно публикует порт на всех интерфейсах хоста. Docker создаёт собственные сетевые правила, и опубликованный порт может оказаться доступен извне, даже если его нет в обычном списке разрешений UFW.
Поэтому в учебном проекте указан 127.0.0.1:8082:8000. Открывать 8082 через UFW не нужно. Не отключайте управление iptables у Docker ради согласования с UFW: это может нарушить работу сетей контейнеров.
Теперь настроим Nginx как обратный прокси: публичные HTTP/HTTPS-порты принимают посетителей, а внутренний порт приложения используется для передачи запроса внутри сервера.
Откройте приложение по домену с HTTPS
На стенде домен lab.hostfera.ru уже указывает на VPS, а Nginx обслуживает его с действующим сертификатом. Для вашего проекта используйте своё имя и его конфигурационный файл. Если домен пока не открывается по HTTPS, сначала исправьте DNS, сетевые правила и сертификат по предыдущим урокам. В UFW и внешнем firewall провайдера должны быть разрешены 80 и 443.
Сохраните копию конфигурации и откройте её:
cp -p /etc/nginx/sites-available/hostfera-lab.conf \
/root/hostfera-lab-before-docker-$(date +%Y%m%d-%H%M%S).conf
nano /etc/nginx/sites-available/hostfera-lab.conf
Внутри существующего блока server, который слушает 443 ssl, добавьте следующие два location. Не помещайте их в HTTP-блок, который только перенаправляет на HTTPS, и не создавайте второй server с тем же именем.
location = /app { return 308 /app/; }
location /app/ {
proxy_pass http://127.0.0.1:8082/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
Остальные настройки сертификата и действующий location / сохраните. Здесь /app перенаправляется на /app/, а завершающий слеш в proxy_pass http://127.0.0.1:8082/; убирает префикс /app/ при передаче запроса. Поэтому публичный /app/health попадает во внутренний обработчик /health.
Проверьте синтаксис и примените изменения только после успешной проверки:
nginx -t && systemctl reload nginx
Если nginx -t показывает ошибку, не перезапускайте службу. Исправьте указанный файл и строку или верните сохранённую копию, затем снова проверьте конфигурацию.
Откройте в браузере https://lab.hostfera.ru/app/, подставив свой домен. На странице должен появиться заголовок «Приложение работает в Docker». Обновление страницы увеличит число просмотров; ссылка «Проверить состояние» откроет /app/health с ответом {"status":"ok"}. Эта проверка не требует SSH-туннеля.
Если весь отдельный домен предназначен для приложения, его можно обслуживать от /: вместо двух правил /app и /app/ замените прежний location / на приведённый блок прокси, изменив первую строку на location / {. В одном блоке server должен остаться только один location /. Тогда адресом сайта будет https://ВАШ_ДОМЕН/. Такой вариант подходит для приложения, рассчитанного на корневой путь; произвольной CMS для работы в подкаталоге может потребоваться настройка её базового URL.
Проверьте сайт с другого компьютера
Проверка только с самого VPS не подтверждает внешний доступ. Выполните команды с компьютера или другого сервера, заменив домен:
curl -fsS https://lab.hostfera.ru/app/health; echo
curl -fsS https://lab.hostfera.ru/app/api; echo
curl -I https://lab.hostfera.ru/app/
curl -sS -o /dev/null -w '%{redirect_url}\n' http://lab.hostfera.ru/app/
Должны получиться ответ ok, JSON со счётчиком, HTTP 200 для HTTPS и адрес перенаправления на HTTPS. Сертификат проверяется обычным клиентом: параметр -k не нужен. На нашем стенде внешние запросы прошли, повторный GET увеличил счётчик, а HEAD вернул 200 без увеличения числа просмотров.
Если внутри VPS приложение отвечает, а снаружи нет: таймаут обычно требует проверки DNS и сетевых правил; 502 — запущенного контейнера и адреса proxy_pass; 404 — выбранного server_name, пути и завершающего слеша. При ошибке сертификата проверьте имя домена и цепочку. Смотрите журнал docker compose logs --tail=50 app и error_log именно этого виртуального хоста.
Учебный адрес отдаёт noindex, чтобы демонстрация не участвовала в поиске. Для своего готового сайта настройку индексации выбирают отдельно; на возможность открыть приложение в браузере она не влияет.
Проверьте сохранение данных после пересоздания
Сначала прочитайте счётчик напрямую из SQLite. Эта команда не увеличивает число просмотров:
docker compose exec -T app python -c "import sqlite3; c=sqlite3.connect('/data/demo.sqlite3'); print(c.execute('SELECT value FROM counter WHERE id=1').fetchone()[0])"
Пересоздайте контейнер и повторите запрос:
docker compose up -d --force-recreate --wait --wait-timeout 90
docker compose exec -T app python -c "import sqlite3; c=sqlite3.connect('/data/demo.sqlite3'); print(c.execute('SELECT value FROM counter WHERE id=1').fetchone()[0])"
На нашем VPS значение осталось 1, хотя идентификатор контейнера изменился. Если в это время открыть страницу или /api, число увеличится — это работа счётчика, а не потеря данных. Проверка /health его не меняет.
Посмотрите права процесса, ограничения и созданный том:
docker compose exec -T app id
docker inspect --format 'ReadOnly={{.HostConfig.ReadonlyRootfs}} Memory={{.HostConfig.Memory}} Privileged={{.HostConfig.Privileged}}' $(docker compose ps -q app)
docker volume ls --filter label=com.docker.compose.project=hostfera-docker-lab
Ожидаем UID 10001, ReadOnly=true, память 134217728 байт и Privileged=false. Имя тома начинается с имени проекта: hostfera-docker-lab_app-data.
Том защищает данные от обычного пересоздания контейнера, но не является резервной копией. При повреждении или удалении тома данные будут потеряны. Таймер из предыдущего урока копирует перечисленные там файлы сайта и отдельную демонстрационную SQLite-базу; новый Docker-том автоматически в эту копию не входит. Для него нужно отдельно настроить согласованную копию базы, хранение вне VPS и пробное восстановление.
Логи, остановка и повторный запуск
Для обычной диагностики достаточно:
docker compose ps
docker compose logs --tail=50 app
docker compose stats --no-stream
docker system df
logs показывает сообщения приложения, stats — текущее потребление, system df — место, занятое Docker. Если /health не проходит, сначала посмотрите журнал, доступность тома и ограничения памяти. При ошибке скачивания образа проверьте доступ к Docker Hub и DNS; не подменяйте официальный источник случайным зеркалом.
После изменения app.py или Dockerfile образ нужно собрать заново:
docker compose up -d --build --wait --wait-timeout 90
Для паузы используйте docker compose stop, для возвращения — docker compose start. Команда docker compose down удаляет контейнеры и сеть проекта, сохраняя именованный том. После docker compose up -d --wait данные снова подключатся.
Не добавляйте -v к down, если данные нужны: этот параметр удаляет тома проекта. Также не запускайте общую очистку томов на сервере с другими приложениями без проверки их назначения.
Итоговая проверка после установки
На учебном VPS мы проверили hello-world, состояние healthy, публичную страницу приложения по HTTPS, внешний API, сохранение SQLite после обновления и пересоздания контейнера. Nginx принимает посетителей на 443, а внутренний 8082 не отвечает напрямую из интернета. Контейнер работает от обычного UID с ограниченной памятью, без привилегированного режима и без доступа к Docker socket.
Дополнительно проверили ранее настроенные службы:
systemctl is-active docker nginx hostfera-lab-backup.timer
curl -fsS -o /dev/null -w 'HTTPS %{http_code}\n' https://lab.hostfera.ru/
ufw status
Docker, Nginx и таймер активны, исходная страница отвечает HTTP 200, правила UFW для SSH, HTTP и HTTPS сохранены. Файлы исходного статического сайта не менялись; в его виртуальный хост добавлены правила проксирования /app/.
Теперь Docker установлен, Compose-проект открыт для посетителей через Nginx с HTTPS, а сохранение данных проверено на практике. Следующие задачи для своего приложения — независимая копия данных тома, обработка ошибок и управляемое обновление образов. Возвращаться к остальным шагам можно через начало серии о первом VPS.
Перед выбором конфигурации прочитайте, какие ресурсы нужны первому VPS и из чего складывается месячный счёт.
Перед объявлением запуска пройдите проверку сайта без входа администратора. Для появления материалов в поиске отдельно проверьте robots.txt и sitemap.xml.
Варианты запуска без контейнеров
Если контейнеры вашему проекту пока не нужны, можно запустить Node.js через systemd или FastAPI с Uvicorn и Nginx. Это отдельные проверенные сценарии.
Первоисточники
Сообщите адрес статьи и фрагмент через контакты редакции.


