feat(backend): graceful shutdown on SIGTERM/SIGINT

Run the HTTP server in a goroutine and, on SIGINT/SIGTERM, call
srv.Shutdown so it stops accepting connections and lets in-flight
requests finish before exiting (ErrServerClosed is a clean exit). This
stops uploads/streams being cut when the container is stopped or
recreated on deploy.

The drain deadline is configurable via SHUTDOWN_TIMEOUT (default 15s).
docker-compose.yml feeds the same variable into the app's
stop_grace_period, so Docker won't SIGKILL mid-drain and the two values
can't drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 08:12:10 +03:00
parent bf7fa49a16
commit a6a46af12e
4 changed files with 55 additions and 3 deletions
+32 -3
View File
@@ -2,9 +2,12 @@ package main
import (
"context"
"errors"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/jackc/pgx/v5/stdlib"
@@ -142,9 +145,35 @@ func main() {
IdleTimeout: 120 * time.Second,
}
slog.Info("starting server", "addr", cfg.ListenAddr)
if err := srv.ListenAndServe(); err != nil {
slog.Error("server error", "err", err)
// Trigger a graceful shutdown on SIGINT/SIGTERM (the latter is what Docker
// sends when the container is stopped or recreated on deploy).
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go func() {
slog.Info("starting server", "addr", cfg.ListenAddr)
// ListenAndServe returns ErrServerClosed after a graceful Shutdown; that
// is the expected exit, not a failure.
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("server error", "err", err)
os.Exit(1)
}
}()
<-ctx.Done()
// Restore default signal handling so a second Ctrl+C / SIGTERM force-quits
// instead of waiting on the drain.
stop()
slog.Info("shutting down", "timeout", cfg.ShutdownTimeout)
// Stop accepting new connections and let in-flight requests finish, up to the
// timeout. Docker's stop grace period reads the same SHUTDOWN_TIMEOUT, so it
// won't SIGKILL before this returns.
shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
slog.Error("graceful shutdown failed", "err", err)
os.Exit(1)
}
slog.Info("shutdown complete")
}