Приложение, которое работает на 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 и перезапуск. Дальше остаётся добавить мониторинг и бэкапы, и про сервер можно забыть до следующего релиза.
Выбрать сервер под приложение →

