← Все статьи
Полезные статьи

Деплой приложения на Node.js и Python на VPS

Как выложить приложение на Node.js или Python на сервер: systemd, gunicorn, nginx как reverse proxy, SSL, переменные окружения и обновление кода без простоя.

Приложение, которое работает на localhost:3000, до боевого сервера доезжает не сразу. Нужно, чтобы оно поднималось после перезагрузки, переживало падения, отвечало на 443 порту по HTTPS и обновлялось без долгого простоя.

Разбираем полный путь для обоих стеков. Схема одинаковая: приложение слушает локальный порт, nginx принимает запросы снаружи и передаёт их внутрь, systemd следит, чтобы процесс был жив.

Схема, к которой идём

Интернет → nginx (80/443, SSL) → приложение (127.0.0.1:3000) → база
                                   ↑
                              systemd следит

Почему именно так, а не «приложение сразу на 80 порту»:

  • nginx берёт на себя SSL, сжатие и отдачу статики — он делает это быстрее и экономнее
  • приложение остаётся недоступным снаружи напрямую, что закрывает целый класс проблем
  • на одном сервере можно держать несколько приложений на разных портах и доменах

Что понадобится

  • VPS с Ubuntu 24.04 — для небольшого приложения хватает «Лёгкого» (2 ядра, 4 ГБ, 10 €), для нагруженного берите «Оптимальный»
  • домен, направленный на сервер A-записью
  • код в Git-репозитории — так обновления будут занимать одну команду

Базовая защита сервера — до всего остального: 7 шагов безопасности.

Общая часть: пользователь и код

Приложение не должно работать от root. Заведите отдельного пользователя:

adduser --system --group --home /opt/app app
mkdir -p /opt/app && cd /opt/app
git clone https://github.com/ваш-аккаунт/проект.git .
chown -R app:app /opt/app

Секреты — в файл окружения, а не в код:

nano /opt/app/.env
DATABASE_URL=postgresql://user:пароль@localhost/dbname
SECRET_KEY=длинная_случайная_строка
PORT=3000
chmod 600 /opt/app/.env
chown app:app /opt/app/.env

Файл .env обязательно должен быть в .gitignore — иначе ключи уедут в репозиторий при первом же коммите.


Вариант 1. Node.js

Установка

curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt install nodejs -y
node -v

Ставьте зависимости и собирайте проект:

cd /opt/app
npm ci --omit=dev
npm run build      # если есть сборка

npm ci вместо npm install — правильный выбор для сервера: ставит строго то, что зафиксировано в package-lock.json, без неожиданных обновлений версий.

Служба systemd

nano /etc/systemd/system/app.service
[Unit]
Description=Node.js App
After=network.target

[Service]
Type=simple
User=app
WorkingDirectory=/opt/app
EnvironmentFile=/opt/app/.env
ExecStart=/usr/bin/node /opt/app/server.js
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now app
systemctl status app

Приложение должно слушать 127.0.0.1, а не 0.0.0.0 — тогда снаружи к нему не подключиться в обход nginx:

app.listen(process.env.PORT || 3000, "127.0.0.1");

systemd или pm2

Частый вопрос. Оба инструмента делают одно и то же — держат процесс живым.

systemd уже есть в системе, единообразно управляет всеми службами и пишет логи в общий журнал. Для одного приложения этого достаточно, и лишней зависимости не появляется.

pm2 удобнее, когда нужен кластерный режим на нескольких ядрах и перезапуск без разрыва соединений при обновлении. Если берёте pm2 — всё равно оберните его в службу systemd, иначе после перезагрузки сервера он не поднимется.

Для большинства проектов хватает systemd.


Вариант 2. Python

Установка

apt install python3 python3-pip python3-venv -y
cd /opt/app
python3 -m venv venv
./venv/bin/pip install -r requirements.txt
./venv/bin/pip install gunicorn

Виртуальное окружение изолирует библиотеки проекта от системных — без него второй проект на сервере рано или поздно сломает первый.

Gunicorn

Встроенный сервер Django и Flask (runserver, flask run) для боевой эксплуатации не предназначен: он однопоточный и не рассчитан на реальную нагрузку. Рабочий вариант — gunicorn.

Число процессов считается по формуле 2 × ядра + 1: на двух ядрах это 5.

nano /etc/systemd/system/app.service
[Unit]
Description=Python App
After=network.target

[Service]
Type=notify
User=app
WorkingDirectory=/opt/app
EnvironmentFile=/opt/app/.env
ExecStart=/opt/app/venv/bin/gunicorn \
    --workers 5 \
    --bind 127.0.0.1:3000 \
    --timeout 60 \
    --access-logfile - \
    проект.wsgi:application
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Для Django в конце указывается проект.wsgi:application, для Flask — app:app или как называется объект приложения.

systemctl daemon-reload
systemctl enable --now app

Для Django не забудьте собрать статику и применить миграции:

./venv/bin/python manage.py collectstatic --noinput
./venv/bin/python manage.py migrate

Если приложение асинхронное (FastAPI, Starlette), вместо обычных воркеров указывается uvicorn:

--worker-class uvicorn.workers.UvicornWorker

Общая часть: nginx и SSL

Конфиг

apt install nginx -y
nano /etc/nginx/sites-available/app
server {
    listen 80;
    server_name app.example.com;

    client_max_body_size 32M;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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_read_timeout 60s;
    }

    location /static/ {
        alias /opt/app/static/;
        expires 30d;
        access_log off;
    }
}

Две детали, которые обычно и становятся причиной странных багов:

  • заголовки Upgrade и Connection нужны для веб-сокетов — без них соединение рвётся
  • X-Forwarded-Proto сообщает приложению, что оригинальный запрос шёл по HTTPS; без него Django и другие фреймворки строят ссылки с http:// и уходят в бесконечный редирект

Статику отдаём напрямую через nginx — приложение не должно тратить на это процессы.

ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

Сертификат

apt install certbot python3-certbot-nginx -y
certbot --nginx -d app.example.com

Certbot настроит HTTPS и редирект, продление произойдёт само.

Firewall

ufw allow 'Nginx Full'
ufw allow OpenSSH
ufw enable

Порт 3000 наружу не открываем — приложение слушает только локально, и это правильно.


Обновление кода

Когда всё настроено, выкладка новой версии — три команды:

cd /opt/app
git pull
systemctl restart app

Для Node.js, если менялись зависимости:

npm ci --omit=dev && npm run build && systemctl restart app

Для Python:

./venv/bin/pip install -r requirements.txt
./venv/bin/python manage.py migrate
./venv/bin/python manage.py collectstatic --noinput
systemctl restart app

Удобно собрать это в скрипт:

nano /opt/deploy.sh
#!/bin/bash
set -e
cd /opt/app
git pull
npm ci --omit=dev
npm run build
systemctl restart app
echo "Готово: $(git log -1 --format=%s)"
chmod +x /opt/deploy.sh

Строка set -e останавливает скрипт при первой ошибке — иначе сломавшаяся сборка приведёт к перезапуску приложения со сломанным кодом.

Перезапуск без разрыва

Обычный systemctl restart даёт паузу в доли секунды — для большинства проектов это незаметно. Если нужен перезапуск вообще без потери запросов, gunicorn умеет перезагружать воркеров по очереди:

systemctl reload app

Для этого в службе должно быть:

ExecReload=/bin/kill -s HUP $MAINPID

Логи

Всё уходит в общий журнал systemd:

journalctl -u app -f              # в реальном времени
journalctl -u app -n 100          # последние 100 строк
journalctl -u app -p err --since today   # только ошибки за сегодня

Частые проблемы

502 Bad Gateway. Приложение не запустилось или слушает не тот порт. Проверьте systemctl status app и ss -tulpn | grep 3000.

Приложение работает, но статика не грузится. Неверный путь в alias или не выполнен collectstatic.

Бесконечный редирект после подключения HTTPS. Не передан заголовок X-Forwarded-Proto, приложение считает соединение небезопасным и снова шлёт на HTTPS.

Веб-сокеты не подключаются. Не хватает заголовков Upgrade и Connection в блоке location.

После перезагрузки сервера ничего не поднялось. Не выполнена команда systemctl enable app. Проверить: systemctl is-enabled app.

Ошибка доступа к файлам. Приложение работает от пользователя app, а файлы принадлежат root. Исправляется через chown -R app:app /opt/app.

Несколько приложений на одном сервере

Каждому — свой порт, своя служба, свой конфиг nginx:

/opt/app-one/   → app-one.service   → 127.0.0.1:3000 → one.example.com
/opt/app-two/   → app-two.service   → 127.0.0.1:3001 → two.example.com

Если у приложений разные версии Node или Python, удобнее разнести их по контейнерам — тогда окружения не пересекаются вовсе: Docker на VPS.

Частые вопросы

Какой тариф нужен? Небольшое приложение с базой — «Лёгкий» за 10 €. Если пользователей много или нужна сборка прямо на сервере — «Оптимальный» за 20 €.

Нужен ли Docker? Для одного приложения — нет, systemd проще и легче. Docker окупается, когда проектов несколько или нужно повторяемо разворачивать одно и то же окружение на разных машинах.

Где хранить базу — на том же сервере? Для небольших проектов да, это проще и быстрее. Выносят базу отдельно, когда она становится узким местом: Выделенный сервер или VPS.

Как настроить автоматический деплой из Git? Простой вариант — SSH-ключ для деплоя и вызов /opt/deploy.sh из GitHub Actions. Для начала хватает и запуска скрипта руками.

Сколько воркеров ставить gunicorn? Формула 2 × ядра + 1 — хорошая отправная точка. Больше не значит лучше: каждый воркер занимает память, и на 2 ГБ их не должно быть много.

Что с бэкапами? Код лежит в репозитории, а вот база и загруженные пользователями файлы — только у вас. Настройте копии сразу: Бэкапы VPS.

Итог

Рабочая схема одинакова для обоих стеков: приложение на локальном порту под присмотром systemd, nginx снаружи с SSL и отдачей статики, секреты в .env с правами 600, обновление одной командой.

Настройка занимает час, после чего выкладка новой версии — это git pull и перезапуск. Дальше остаётся добавить мониторинг и бэкапы, и про сервер можно забыть до следующего релиза.

Выбрать сервер под приложение →

Команда Puws.Cloud
Puws.Cloud
Начать

СЕРВЕР ДЛЯ ВАШЕГО ПРОЕКТА

Начните
с тарифа.

ПОЛЕЗНОЕ О СЕРВЕРАХИнструкции
и советы.
Открыть блог
Наш Аптайм