Если у вас один VPS, но нельзя позволить простоя — делаем простую мультиинстансную схему: два инстанса приложения, nginx спереди и контроль работоспособности. Ни магии, ни дорогих инструментов — только systemd, порты, проверка /health и атомарные переключения.
1. Архитектура и что важно понимать
Коротко: nginx проксирует трафик на upstream из двух локальных инстансов (например, :8081 и :8082). Во время деплоя мы запускаем новый инстанс на свободном порту, ждём, пока он отвечает на /health, потом убираем старый инстанс из пула и останавливаем его. Откат — наоборот: запускаем старую сборку и возвращаем её в пул.
Главные правила:
- инстансы должны быть независимы — свои порты и каталоги;
- health endpoint возвращает 200 быстро, без тяжёлых проверок;
- nginx должен поддерживать быстрый graceful reload (reload не drop соединений);
- systemd управляет инстансами и рестартует при краше.
2. Директивы: структура каталогов и порты
Предлагаю такую простую структуру на VPS:
| Сервис | Порт | systemd unit |
|---|---|---|
| Инстанс A | 127.0.0.1:8081 | myapp@1.service |
| Инстанс B | 127.0.0.1:8082 | myapp@2.service |
| nginx | 0.0.0.0:80 | nginx.service |
Бинарники кладём в версии-папки: /opt/myapp/releases/2026-09-15-1234/myapp, а в /opt/myapp/currentA и currentB — симлинки, указывающие на бинарь для конкретного инстанса. Так удобно откатываться: правим симлинк и перезапускаем unit.
3. systemd-юнит для инстансов
Вот минималка для шаблонного юнита myapp@.service — подставляет симлинк current%I:
[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 с двумя локальными серверами и минимальными параметрами отказоустойчивости:
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. Скрипты деплоя: шаги с командами
- Сборка локально: GOOS=linux GOARCH=amd64 go build -o myapp .
- Заливаем /opt/myapp/releases/2026-.../myapp и делаем права.
- Определяем свободный инстанс: если сейчас жив 8081 — deploy на 8082, иначе на 8081.
- Обновляем симлинк current{port} → новая версия.
- systemctl start/daemon-reload myapp@8082 (или systemctl restart). Проверяем статус systemctl status myapp@8082.
- Ждём пока /health даст 200. Простой чек (в скрипте):
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. Откат и аварийный план
Откат — простая операция, если вы держали предыдущую сборку на диске. Алгоритм:
- Залить старую сборку в release-папку (или использовать уже имеющуюся).
- Обновить симлинк current{port} для инстанса, который вы хотите вернуть.
- Запустить/перезапустить systemctl start myapp@PORT и дождаться /health.
- Остановить проблемную инстанцию.
В экстренной ситуации, если 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 строк)
#!/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, а затем отработайте откат. Когда вы уже три раза успешно откатились в тесте — реальная ночь будет спокойнее.






Комментарии(2)
Чтобы участвовать в обсуждении, или .