Опубликовано 2026-10-27 · Обновлено 2026-10-27
Как безопасно развернуть приложение Node или Python на VPS
Коротко: чтобы безопасно развернуть приложение Node или Python на VPS, входите по ключу SSH под обычным пользователем с sudo, включите файрвол с запретом по умолчанию и автоматические обновления безопасности, запустите приложение как службу systemd без привилегий на 127.0.0.1, храните секреты в файле с правами 600, поставьте перед ним nginx с HTTPS и проверьте снаружи, что отвечают только порты 22, 80 и 443.
VPS (виртуальный выделенный сервер) это арендованный Linux-сервер в интернете, которым вы управляете сами. Это руководство служит главой о деплое к чек-листу безопасности вайб-кодинга. Каждый шаг это команда, которую можно скопировать, с объяснением в одну строку. Порядок важен: некоторые шаги, сделанные раньше времени, закроют вам доступ. Команды рассчитаны на Ubuntu и Debian.
В этой статье
Шаг 1: Что нужно перед началом Шаг 2: Создайте пользователя с sudo и перейдите на ключи SSH Шаг 3: Включите файрвол Шаг 4: Включите автоматические обновления безопасности Шаг 5: Запустите приложение как службу systemd без привилегий Шаг 6: Храните секреты в защищённом файле окружения Шаг 7: Поставьте перед приложением nginx и HTTPS Шаг 8: Проверьте снаружи Шаг 9: Обновление и безопасный откат Шаг 10: Что делать дальше Часто задаваемые вопросыБыстрый результат (20 минут)
- Выполните
sudo ss -tlnpи проверьте, что приложение слушает127.0.0.1, а не0.0.0.0. См. шаг 8. - С вашего компьютера выполните
ssh -o PubkeyAuthentication=no -l deploy 203.0.113.10. Вход по паролю должен быть отклонён. - Выполните
sudo ufw status verboseи убедитесь, что входящие разрешены только на портах 22, 80 и 443. - Выполните
sudo ls -l /etc/myapp/myapp.envи проверьте, что права-rw-------. - Выполните
sudo certbot renew --dry-runи убедитесь, что проверка проходит.
Шаг 1: Что нужно перед началом
Подготовьте до того, как откроете терминал:
- IP-адрес сервера (в руководстве адрес для документации
203.0.113.10). - Доступ root или sudo к новому серверу.
- Домен (здесь
example.com) с A-записью, указывающей на этот IP. - Приложение в репозитории Git с lock-файлом (
package-lock.jsonилиrequirements.txt) и терминал на вашем компьютере.
В примерах приложение называется myapp, порт 3000 (Node) или 8000 (Python). Общий список мер для сервера есть в чек-листе безопасности VPS.
Два правила действуют на протяжении всего руководства. Первое: никогда не запускайте сервер разработки в продакшене. Документация Flask говорит, что встроенный сервер не рассчитан на безопасность, стабильность и эффективность, а справочник CLI Next.js описывает next start после next build как режим продакшена. Второе: в интернет смотрит только nginx, а с приложением он общается через loopback-адрес. CDN вроде Cloudflare лишь добавляет один узел перед nginx.
Шаг 2: Создайте пользователя с sudo и перейдите на ключи SSH
Замените вход под root от провайдера обычным пользователем и ключом SSH (полное руководство по усилению SSH). Не закрывайте первую сессию root, пока не пройдёт последняя проверка. Если потеряли доступ, см. руководство по восстановлению доступа.
2a. Создайте пользователя. Войдите под root командой ssh -l root 203.0.113.10 и выполните (на Debian сначала установите sudo: apt install sudo):
adduser deploy
usermod -aG sudo deploy
adduser создаёт учётную запись и запрашивает пароль; usermod -aG sudo добавляет её в группу sudo.
2b. Создайте ключ на своём компьютере и выведите открытую часть:
ssh-keygen -t ed25519
cat ~/.ssh/id_ed25519.pub
Затем на сервере под root создайте файл ключей и вставьте в него выведенную строку:
install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
install -m 600 -o deploy -g deploy /dev/null /home/deploy/.ssh/authorized_keys
nano /home/deploy/.ssh/authorized_keys
2c. Проверьте во втором терминале. Выполните ssh -l deploy 203.0.113.10, затем sudo -v, чтобы убедиться, что sudo работает. Не продолжайте, пока оба шага не сработают.
2d. Отключите вход root и по паролю. sshd_config(5) берёт первое найденное значение каждого параметра и читает файлы Include в лексикографическом порядке, поэтому файл с именем 00- перекрывает значения из образа облака, загружаемые позже:
sudo tee /etc/ssh/sshd_config.d/00-hardening.conf >/dev/null <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
EOF
sudo sshd -t && sudo systemctl reload ssh
По sshd(8), sshd -t только проверяет конфигурацию, так что опечатка не остановит SSH. Действующие значения: sudo sshd -T | grep -Ei 'passwordauthentication|permitrootlogin'. Если в /etc/ssh/sshd_config нет строки Include для sshd_config.d, поместите эти три строки в его начало.
2e. Проверьте ещё раз из третьего терминала, не закрывая прежние сессии. ssh -l deploy 203.0.113.10 должен работать, а ssh -o PubkeyAuthentication=no -l deploy 203.0.113.10 и ssh -l root 203.0.113.10 должны быть отклонены. Чтобы всё отменить, выполните sudo rm /etc/ssh/sshd_config.d/00-hardening.conf и перезагрузите ssh. О группе sudo: sudo и минимальные привилегии.
Шаг 3: Включите файрвол
Файрвол решает, на каких портах сервер принимает соединения снаружи. ufw («простой файрвол») это его удобная надстройка в Ubuntu; в Debian выполните sudo apt install ufw. Разрешите SSH до включения, потому что справочник ufw предупреждает: включение может оборвать существующие соединения, например SSH.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose
Если вы перенесли SSH на другой порт, разрешите его вместо 22. Порта приложения (3000 или 8000) в списке нет намеренно: nginx обращается к нему локально. Если у провайдера есть сетевой файрвол, повторите там те же три порта. Подробнее в руководстве по файрволу Linux.
Шаг 4: Включите автоматические обновления безопасности
Большинство взломов используют ошибки, для которых исправление уже есть. Документация Ubuntu Server сообщает, что unattended-upgrades установлен по умолчанию и раз в день ставит обновления безопасности. Проверьте это, а в Debian установите пакет:
sudo apt update
sudo apt install unattended-upgrades
sudo dpkg-reconfigure unattended-upgrades
cat /etc/apt/apt.conf.d/20auto-upgrades
sudo unattended-upgrade -v --dry-run
В выводе cat для Update-Package-Lists и Unattended-Upgrade должно стоять "1", а пробный запуск ничего не меняет. Вики Debian сообщает, что по умолчанию ставятся только обновления безопасности. Это касается только операционной системы. Пакеты npm и pip обновляете вы сами; см. руководстве по управлению патчами.
Шаг 5: Запустите приложение как службу systemd без привилегий
Никогда не запускайте приложение от root или от вашего рабочего пользователя. Создайте системного пользователя, который ничем не владеет и нигде не может войти. По useradd(8), системная учётная запись не получает домашний каталог, если его не запросить. Затем создайте каталог для релизов, который пишете вы (пользователь deploy), а служба только читает:
sudo useradd --system --user-group --shell /usr/sbin/nologin myapp
sudo mkdir -p /srv/myapp/releases
sudo chown -R deploy:myapp /srv/myapp
sudo chmod 750 /srv/myapp
Установите актуальную LTS-версию Node.js (официальные загрузки) или sudo apt install python3-venv, для всей системы: копия в /home/deploy будет невидима службе, потому что юнит ниже скрывает /home. Затем создайте первый релиз под пользователем deploy. Для приватных репозиториев нужен ключ развёртывания (deploy key); GitHub по умолчанию делает его доступным только для чтения.
REL=/srv/myapp/releases/$(date +%Y%m%d-%H%M%S)
git clone --depth 1 https://github.com/YOUR_ACCOUNT/YOUR_REPO.git "$REL"
cd "$REL" && npm ci && npm run build
ln -sfn "$REL" /srv/myapp/current
Согласно документации npm, npm ci ставит ровно то, что записано в lock-файле, и завершается ошибкой, если lock-файл и package.json расходятся. ln -sfn переставляет ссылку current. Для Python замените npm ci && npm run build на python3 -m venv .venv && .venv/bin/pip install -r requirements.txt gunicorn.
Теперь служба. Создайте файл /etc/systemd/system/myapp.service через sudo nano:
[Unit]
Description=myapp web application
After=network.target
[Service]
User=myapp
Group=myapp
WorkingDirectory=/srv/myapp/current
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/srv/myapp/current/node_modules/.bin/next start -H 127.0.0.1 -p 3000
Restart=on-failure
RestartSec=5
StateDirectory=myapp
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
[Install]
WantedBy=multi-user.target
Ключевые строки по systemd.exec(5): User= задаёт, кто запускает процесс (по умолчанию root). NoNewPrivileges=true не даёт процессу и его потомкам получать привилегии через биты setuid или файловые capabilities. ProtectSystem=strict делает файловую систему только для чтения, кроме /dev, /proc и /sys, ProtectHome=true скрывает /home и /root, а PrivateTmp=true даёт службе собственный /tmp. StateDirectory=myapp создаёт /var/lib/myapp с владельцем служебным пользователем: туда кладите загрузки, файлы SQLite и кэши. Restart=on-failure перезапускает упавший процесс через RestartSec= секунд (systemd.service(5)). Остальные строки делают параметры ядра только для чтения, запрещают загрузку модулей и оставляют только локальные и IP-сокеты.
Самое важное для безопасности это адрес: -H 127.0.0.1 привязывает Next.js к loopback, потому что в документации имя хоста по умолчанию 0.0.0.0. В обычном Node передайте '127.0.0.1' как аргумент host в listen(): по документации Node.js без host сервер слушает все адреса; запуск: ExecStart=/usr/bin/node server.js. Для Flask: ExecStart=/srv/myapp/current/.venv/bin/gunicorn -w 2 -b 127.0.0.1:8000 'app:app', для FastAPI: .venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000.
Сначала выполните шаг 6, ведь файл окружения должен существовать. Затем запустите службу:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp
sudo journalctl -u myapp -n 50 --no-pager
enable --now включает запуск при загрузке и сразу запускает службу. Если в журнале ошибка «read-only file system», разрешите именно этот путь через ReadWritePaths= и оставьте остальную песочницу. Чтобы увидеть, что ещё можно ужесточить, выполните systemd-analyze security myapp: он оценивает юнит от 0.0 до 10.0 (systemd-analyze(1)). Хотите контейнеры? Сначала прочитайте Docker Compose на VPS без лишней открытости.
Шаг 6: Храните секреты в защищённом файле окружения
Секреты (пароли баз данных, ключи API, ключи сессий) хранят в файле окружения вне папки с кодом и веб-корня, доступном только служебному пользователю. Создайте его с нужным владельцем и правами до заполнения:
sudo install -d -m 750 -o root -g myapp /etc/myapp
sudo install -m 600 -o myapp -g myapp /dev/null /etc/myapp/myapp.env
sudo nano /etc/myapp/myapp.env
NODE_ENV=production
SESSION_SECRET=paste_output_of_openssl_rand_hex_32_here
STRIPE_SECRET_KEY=sk_test_EXAMPLE_NOT_A_REAL_KEY
Правила для этого файла:
- По одной паре
NAME=valueв строке, безexport. Если файла нет, служба не запустится. - Права
600, проверка:sudo ls -l /etc/myapp/myapp.env. systemd читает файл сам, поэтому подойдёт и владелец root. - Никогда в Git. Добавьте
.envв.gitignoreдо первого коммита и прочитайте, как находят утечки, в статье открытые файлы .env и .git. NODE_ENV=productionрекомендует документация Node.js, потому что некоторые библиотеки по умолчанию работают в режиме разработки.- В Next.js переменные с префиксом
NEXT_PUBLIC_вшиваются в JavaScript-бандл при сборке, то есть публичны. Никогда не кладите в них секреты. - После любой правки перезапускайте службу. Если ключ когда-либо попал в репозиторий, чат или лог, перевыпустите его: что делать при утечке ключа API.
Шаг 7: Поставьте перед приложением nginx и HTTPS
Обратный прокси принимает публичные запросы и передаёт их приложению на loopback-адресе. Руководство по самостоятельному хостингу Next.js советует его, потому что он справляется с некорректными запросами, медленными соединениями и слишком большими телами запросов. Установите nginx и создайте /etc/nginx/conf.d/myapp.conf (когда DNS заработает; проверка: getent hosts example.com):
sudo apt install nginx
sudo nano /etc/nginx/conf.d/myapp.conf
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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 nginx, proxy_pass без части URI передаёт путь запроса без изменений, а заголовок Host по умолчанию содержит адрес бэкенда, поэтому задаётся Host $host. Для Python используйте порт 8000. Загрузите конфигурацию, затем получите сертификат по официальной инструкции Certbot для nginx (она начинается с установки snapd):
sudo nginx -t && sudo systemctl reload nginx
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot
sudo certbot --nginx
sudo certbot renew --dry-run
Certbot спросит домены, получит сертификат и изменит файл nginx: добавит listen 443 ssl, строки ssl_certificate и ssl_certificate_key с путями в /etc/letsencrypt/live/example.com/ и перенаправление с порта 80 (точные строки зависят от версии).
Certbot хранит сертификаты в /etc/letsencrypt/live/ и ставит таймер или задание cron для продления. Пробный запуск доказывает, что продление сработает; расписание проверьте командой systemctl list-timers. Сроки жизни сертификатов сокращаются, см. сертификаты Let's Encrypt на 45 дней. Наконец, сообщите приложению, что перед ним стоит прокси, иначе оно будет видеть всех посетителей как 127.0.0.1: в Express используйте app.set('trust proxy', 1) (число переходов объясняет руководство Express), а во Flask оберните приложение в Werkzeug ProxyFix с одним прокси, как показано в документации Flask. Больше заголовков и лимитов: рекомендации по безопасности nginx.
Шаг 8: Проверьте снаружи
Проверьте настройку глазами атакующего. Сначала на сервере выведите слушающие TCP-сокеты (-t TCP, -l слушающие, -n числовой вывод, -p процесс, по ss(8)):
sudo ss -tlnp
Ожидаемо: 0.0.0.0:22, :80 и :443 (sshd и nginx), приложение на 127.0.0.1:3000, а любая база данных только на 127.0.0.1. База данных на публичном адресе это отдельная аварийная ситуация: открытые базы данных. Затем с вашего компьютера, не с сервера:
curl -I https://example.com
curl -I http://example.com
curl -m 5 -I http://203.0.113.10:3000
nc -vz -w 5 203.0.113.10 3000
curl -sI https://example.com/.env
curl -sI https://example.com/.git/HEAD
- Первый вызов успешен по действительному HTTPS; второй перенаправляет на HTTPS кодом 301.
- Порт приложения должен быть недоступен:
curlзавершается по тайм-ауту, аncсообщает об отсутствии соединения. Файрвол отбрасывает пакеты, поэтому ждите тайм-аут, а не быстрый отказ. /.envи/.git/HEADне должны возвращать 200. Если у приложения есть универсальный маршрут, который отдаёт главную страницу на любой путь, получите тело черезcurl -sи убедитесь, что в нём нет строкNAME=valueи текстаref:.
Нужен второй взгляд на ваш сервер?
Бесплатная проверка в режиме только чтения покажет, что открыто на сервере и что исправлять в первую очередь.
Получить бесплатную проверку сервера Связаться с инженером по безопасностиШаг 9: Обновление и безопасный откат
Каждый деплой собирает новую папку релиза, переключает ссылку current, перезапускает службу и проверяет приложение, а предыдущий релиз остаётся на диске. Сохраните скрипт как /srv/myapp/deploy.sh (запускайте от deploy):
#!/bin/bash
set -euo pipefail
APP=/srv/myapp
REL=$APP/releases/$(date +%Y%m%d-%H%M%S)
PREV=$(readlink -f $APP/current)
git clone --depth 1 https://github.com/YOUR_ACCOUNT/YOUR_REPO.git "$REL"
cd "$REL" && npm ci && npm run build
ln -sfn "$REL" $APP/current
sudo systemctl restart myapp
sleep 5
if ! curl -fsS -m 5 http://127.0.0.1:3000/ >/dev/null; then
echo "Health check failed, rolling back to $PREV"
ln -sfn "$PREV" $APP/current
sudo systemctl restart myapp
exit 1
fi
echo "Deployed $REL"
Это git pull и сборка в новую папку, поэтому неудачная сборка не трогает работающую копию. Подставьте свою строку установки для Python и свой адрес проверки. Откат вручную:
ls -1t /srv/myapp/releases
ln -sfn /srv/myapp/releases/PREVIOUS_RELEASE /srv/myapp/current
sudo systemctl restart myapp
Откат не отменяет изменения в базе данных и правку файла окружения, поэтому делайте бэкап базы перед любой миграцией. Храните от трёх до пяти последних папок релизов. Бэкапы самих данных организуйте по стратегии бэкапов 3-2-1.
Шаг 10: Что делать дальше
Теперь у сервера разумная базовая защита. Добавьте следующие уровни:
- Блокируйте повторяющихся нарушителей с помощью fail2ban.
- Следите за логами.
journalctl -u myappпоказывает работу приложения, а мониторинг сервера и анализ логов подскажут, на что настроить оповещения. - Напишите план на пять строк заранее: как обнаружить взлом и отреагировать.
Если в приложении лежат данные клиентов, а безопасностью сервера никто не занимается, просите помощи заранее. Бесплатная проверка в режиме только чтения покажет, что видит интернет на вашем сервере.
Ещё для вайб-кодеров: все руководства серии собраны в центре безопасности для вайб-кодеров, а пункты можно отмечать в интерактивном чек-листе.
Часто задаваемые вопросы
Как безопасно развернуть приложение Node на сервере Ubuntu?
Создайте пользователя с sudo и входом по ключу SSH, включите файрвол с запретом по умолчанию и автоматические обновления, затем запустите приложение отдельной службой systemd без привилегий на 127.0.0.1. Поставьте перед ним nginx с HTTPS и храните секреты в файле окружения с правами 600 вне папки с кодом.
Можно ли просто запустить npm run dev или flask run на сервере?
Нет. Обе команды запускают серверы разработки. Документация Flask говорит, что встроенный сервер не рассчитан на безопасность, стабильность и эффективность, а Next.js для продакшена описывает next build и затем next start. Используйте производственную команду запуска под systemd.
Почему приложение должно слушать 127.0.0.1, а не 0.0.0.0?
Адрес 127.0.0.1 доступен только с самого сервера, поэтому к приложению можно обратиться лишь через nginx. Адрес 0.0.0.0 принимает соединения на всех интерфейсах и позволяет посетителям обойти прокси, о чём прямо предупреждает документация Flask.
Нужен ли nginx, если у фреймворка есть свой веб-сервер?
Это настоятельно рекомендуется. Обратный прокси берёт на себя TLS, некорректные запросы, медленные соединения и лимиты размера, а приложение работает без прав root на высоком порту. Руководство Next.js по самостоятельному хостингу советует прокси именно по этим причинам.
Нужно ли запускать приложение от root или через sudo?
Нет. Ошибка в приложении, работающем от root, отдаёт злоумышленнику весь сервер. Запускайте его от отдельного системного пользователя без оболочки входа и добавьте опции systemd, например NoNewPrivileges и ProtectSystem, чтобы ещё сильнее ограничить ущерб.
Как хранить ключи API и пароли на VPS?
Положите их в файл окружения вроде /etc/myapp/myapp.env с владельцем служебным пользователем и правами 600 и подключите через EnvironmentFile= в юните systemd. Никогда не коммитьте этот файл в Git и перевыпускайте любой ключ, попавший в репозиторий или лог.
Как откатить неудачный деплой?
Выкладывайте каждую версию в отдельную папку релиза и направляйте на неё символическую ссылку current. Для отката переставьте ссылку на предыдущую папку командой ln -sfn и выполните sudo systemctl restart myapp. Изменения базы данных не отменяются, поэтому делайте бэкап перед миграциями.
Не уверены, в каком состоянии ваш сервер?
Получите бесплатную проверку безопасности в режиме только чтения или поговорите с инженером по безопасности об управляемой защите.
Получить бесплатную проверку сервера Связаться с инженером по безопасности