VPS

Как установить 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. В вашем репозитории версии могут быть новее. Важны успешный запуск службы и выполнение тестового контейнера.

Версии Docker Engine и Compose, служба active и успешный запуск hello-world
Docker Engine и Compose установлены; проверочный контейнер завершился успешно.

Мы используем 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.

Результат docker compose up и ps: приложение healthy на локальном порту 127.0.0.1:8082
Compose создал сеть, том и контейнер; приложение перешло в состояние healthy.

Проверьте ответ внутри 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 увеличит счётчик.

Ответы health и api, счётчик просмотров и локальный адрес опубликованного порта Docker
Приложение отвечает на проверки состояния и запрос JSON через локальный порт VPS.

Для необязательной проверки до настройки 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 показывает ошибку, не перезапускайте службу. Исправьте указанный файл и строку или верните сохранённую копию, затем снова проверьте конфигурацию.

Настройки proxy_pass к контейнеру Docker и успешный nginx -t
Nginx передаёт запросы /app/ контейнеру; проверка конфигурации успешна.

Откройте в браузере https://lab.hostfera.ru/app/, подставив свой домен. На странице должен появиться заголовок «Приложение работает в Docker». Обновление страницы увеличит число просмотров; ссылка «Проверить состояние» откроет /app/health с ответом {"status":"ok"}. Эта проверка не требует SSH-туннеля.

Страница приложения Docker в браузере: заголовок, счётчик просмотров и ссылка проверки состояния
Приложение открыто по HTTPS с другого компьютера; ссылка проверки состояния работает.

Если весь отдельный домен предназначен для приложения, его можно обслуживать от /: вместо двух правил /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 без увеличения числа просмотров.

Внешний адрес Docker-приложения: health ok, API со счётчиком, HTTPS 200 и редирект HTTP
Публичные проверки: API отвечает, HTTPS возвращает 200, HTTP перенаправляется на HTTPS.

Если внутри 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 его не меняет.

Чтение счётчика до и после force-recreate: оба раза 1, пользователь контейнера UID 10001
После пересоздания контейнера счётчик в SQLite сохранил значение 1.

Посмотрите права процесса, ограничения и созданный том:

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.

UID 10001, ReadOnly true, лимит памяти 128 МБ, Privileged false и именованный том Docker
Процесс работает без root, корневая файловая система доступна только для чтения.

Том защищает данные от обычного пересоздания контейнера, но не является резервной копией. При повреждении или удалении тома данные будут потеряны. Таймер из предыдущего урока копирует перечисленные там файлы сайта и отдельную демонстрационную 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, Nginx и таймер active, исходный HTTPS 200 и действующие правила UFW
Исходный HTTPS-сайт, Nginx и таймер резервного копирования продолжают работать.

Теперь Docker установлен, Compose-проект открыт для посетителей через Nginx с HTTPS, а сохранение данных проверено на практике. Следующие задачи для своего приложения — независимая копия данных тома, обработка ошибок и управляемое обновление образов. Возвращаться к остальным шагам можно через начало серии о первом VPS.

Перед выбором конфигурации прочитайте, какие ресурсы нужны первому VPS и из чего складывается месячный счёт.

Перед объявлением запуска пройдите проверку сайта без входа администратора. Для появления материалов в поиске отдельно проверьте robots.txt и sitemap.xml.

Варианты запуска без контейнеров

Если контейнеры вашему проекту пока не нужны, можно запустить Node.js через systemd или FastAPI с Uvicorn и Nginx. Это отдельные проверенные сценарии.

Первоисточники

Нашли неточность?

Сообщите адрес статьи и фрагмент через контакты редакции.