Если у вас один VPS, но нельзя позволить простоя — делаем простую мультиинстансную схему: два инстанса приложения, nginx спереди и контроль работоспособности. Ни магии, ни дорогих инструментов — только systemd, порты, проверка /health и атомарные переключения.

1. Архитектура и что важно понимать

Коротко: nginx проксирует трафик на upstream из двух локальных инстансов (например, :8081 и :8082). Во время деплоя мы запускаем новый инстанс на свободном порту, ждём, пока он отвечает на /health, потом убираем старый инстанс из пула и останавливаем его. Откат — наоборот: запускаем старую сборку и возвращаем её в пул.

Главные правила:

  • инстансы должны быть независимы — свои порты и каталоги;
  • health endpoint возвращает 200 быстро, без тяжёлых проверок;
  • nginx должен поддерживать быстрый graceful reload (reload не drop соединений);
  • systemd управляет инстансами и рестартует при краше.

2. Директивы: структура каталогов и порты

Предлагаю такую простую структуру на VPS:

СервисПортsystemd unit
Инстанс A127.0.0.1:8081myapp@1.service
Инстанс B127.0.0.1:8082myapp@2.service
nginx0.0.0.0:80nginx.service

Бинарники кладём в версии-папки: /opt/myapp/releases/2026-09-15-1234/myapp, а в /opt/myapp/currentA и currentB — симлинки, указывающие на бинарь для конкретного инстанса. Так удобно откатываться: правим симлинк и перезапускаем unit.

3. systemd-юнит для инстансов

Вот минималка для шаблонного юнита myapp@.service — подставляет симлинк current%I:

systemd
[Unit]
Description=MyApp instance %i
After=network.target

[Service]
User=www-data
Group=www-data
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/current%i/myapp --port=%i
Restart=on-failure
RestartSec=2
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target

Примечание: я использую параметр %i как порт-плейсхолдер: можно запускать systemctl start myapp@8081.service и systemctl start myapp@8082.service. Альтернатива — напрямую указывать номера инстансов 1/2 и внутри unit маппить на порты.

4. Конфиг nginx и поведение при reload

Upstream с двумя локальными серверами и минимальными параметрами отказоустойчивости:

nginx
upstream backend {
    server 127.0.0.1:8081 max_fails=1 fail_timeout=5s;
    server 127.0.0.1:8082 max_fails=1 fail_timeout=5s;
}

server {
    listen 80;
    location / {
        proxy_pass http://backend;
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
        proxy_connect_timeout 2s;
        proxy_read_timeout 10s;
    }
}

nginx поддерживает graceful reload: systemctl reload nginx (или nginx -s reload) — активные соединения не рвутся, новые идут на обновлённый upstream. Мы будем менять состав upstream через перезапись конфига и reload, но проще — держать оба инстанса в upstream всегда и управлять тем, кто отвечает, через systemd (поднят/опущен).

5. Скрипты деплоя: шаги с командами

  1. Сборка локально: GOOS=linux GOARCH=amd64 go build -o myapp .
  2. Заливаем /opt/myapp/releases/2026-.../myapp и делаем права.
  3. Определяем свободный инстанс: если сейчас жив 8081 — deploy на 8082, иначе на 8081.
  4. Обновляем симлинк current{port} → новая версия.
  5. systemctl start/daemon-reload myapp@8082 (или systemctl restart). Проверяем статус systemctl status myapp@8082.
  6. Ждём пока /health даст 200. Простой чек (в скрипте):
Bash
for i in 1 2 3; do
  sleep 1
  code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8082/health || echo 000)
  [ "$code" = "200" ] && break
done
if [ "$code" != "200" ]; then
  echo "new instance failed health check"; exit 1
fi

Если чек прошёл — выключаем старую инстанцию: systemctl stop myapp@8081. nginx автоматически начнёт направлять трафик только на живой инстанс. Если нужно быстрый откат — запустить старую версию обратно и вернуть симлинк.

Примеры ловушек

  • Если оба инстанса запущены, nginx балансирует между ними. Иногда это нежелательно — тогда при деплое держите старый инстанс поднятым до полного «зелёного» трафика и затем останавливайте.
  • health должен быть дешёвым. Если там db ping, сделайте кешированный флаг или используйте быстрый self-check.

6. Откат и аварийный план

Откат — простая операция, если вы держали предыдущую сборку на диске. Алгоритм:

  1. Залить старую сборку в release-папку (или использовать уже имеющуюся).
  2. Обновить симлинк current{port} для инстанса, который вы хотите вернуть.
  3. Запустить/перезапустить systemctl start myapp@PORT и дождаться /health.
  4. Остановить проблемную инстанцию.

В экстренной ситуации, если nginx или systemd ведут себя странно, можно временно поставить maintenance страницу через nginx и дать команде время вернуть сервис вручную.

7. Что я делаю у себя в продакшне и практические советы

  • Храню последние 5 релизов в releases — хватает для быстрого отката и не сжирает диск.
  • Всегда тестирую health в CI перед заливкой на сервер — экономит 90% возни.
  • Логи пишу в stdout — systemd+journald удобно просматриваются и не надо настраивать доп. ротацию.

Сделайте health простым и детерминированным, держите симлинки для каждой инстанции и управляйте запусками systemd. Это даёт понятный, отлаживаемый процесс деплоя без простоя на одном VPS. Совет: напишите маленький deploy-скрипт (10-20 строк) и тестируйте rollback раз в месяц — тогда неприятные сюрпризы не встретят вас в 3 утра.

8. Реальные числа и требования к ресурсам (чтобы не «упереться» в VPS)

Пару практических цифр по моему опыту на VPS с 2 vCPU и 2 GB RAM, где сидит Go-сервис с лёгким HTTP-подходом и небольшим в памяти кэшем:

  • Один инстанс — ~120-200 MB RSS на старте, два — ~250-400 MB совокупно. Планируйте margin 30–50% под GC и spike-память.
  • Средняя нагрузка: 100–300 RPS держится уверенно на таком наборе, если обработка запроса <30 ms. Если у вас долгоиграющие запросы (секунды), RPS сильно падает — тогда лучше увеличить число потоков/инстансов или уменьшить keepalive.
  • LimitNOFILE=65536 в unit — полезно, но мониторьте socket usage: ulimit и netstat показывают, если вы приближаетесь к лимиту.

Если место на диске ограничено, храните не больше 3–5 релизов и делайте cron-очистку старых. При моём опыте 5 релизов на бинарь 30–50MB — безопасно для VPS с 20–40 GB.

9. Readiness vs liveness: зачем два эндпоинта

Классическая ошибка — делать /health, который инициализирует соединение к базе и тормозит старт. Я разделяю проверки:

  • /liveness — простая проверка процесса (вернуть 200 если процесс жив). Используется systemd для перезапуска и внешний мониторинг для алертов.
  • /readiness — более подробная, но быстрая проверка: кэш и внешние зависимости только если они уже проинициализированы. Nginx, в нашем случае, мы не настраиваем на чтение readiness напрямую, но в скрипте деплоя ждём именно этот эндпоинт.

Практика: readiness должен быть готов максимум за 2–3 секунды. Если инициализация >10 секунд, думайте о фоновой инициализации и возвращении ready как только минимальные компоненты подняты.

10. Пример небольшого deploy-скрипта (реальный, ~20 строк)

Bash
#!/bin/bash
NEWREL=/opt/myapp/releases/$(date +%F-%H%M)
mkdir -p "$NEWREL"
scp ./myapp root@vps:$NEWREL/
ssh root@vps <<'EOF'
ln -sfn "$NEWREL" /opt/myapp/current_new
TARGET_PORT=8082
systemctl start myapp@${TARGET_PORT}
# wait readiness
for i in {1..10}; do
  sleep 1
  if curl -s -f http://127.0.0.1:${TARGET_PORT}/readiness; then break; fi
done
if ! curl -s -f http://127.0.0.1:${TARGET_PORT}/readiness; then
  echo 'deploy failed'; exit 1
fi
systemctl stop myapp@8081
EOF

Это упрощённый пример, но он показывает ключевые моменты: атомарный перенос релиза (ln -sfn), старт нового инстанса, ожидание readiness и останов старого. Добавляйте логирование и уведомления по результатам deploy — мне пару раз спасло Slack-сообщение с «deploy success/fail».

11. Мониторинг и алерты (минимальный набор)

Чтобы не просыпаться от пустоты мониторинга, рекомендую минимум:

  • метрики обработки запросов: latency p50/p95 и ошибки (5xx) — графики в любой системе (Prometheus/Grafana или простой Datadog);
  • health-проверки и алерты: если оба инстанса DOWN — pager; если p95 растёт в два раза — warning;
  • логирование ошибок с контекстом request_id — чтобы при rollback было проще понять причину.

У меня одно простое правило: если откат занимает меньше 2 минут — всё в порядке. Если дольше — процесс деплоя/плейбука нужно упрощать.

Мой конкретный совет: выделите час и напишите простую автоматизацию (тот самый скрипт), прогоните её на staging, а затем отработайте откат. Когда вы уже три раза успешно откатились в тесте — реальная ночь будет спокойнее.