Container health checks and graceful shutdown patterns

Ryan Nakamura Feb 2026
1 tab
// Express app with health checks and graceful shutdown
const express = require('express');
const { createServer } = require('http');

const app = express();
const server = createServer(app);

// Application state
let isReady = false;
let isShuttingDown = false;
const connections = new Set();

// Track connections for graceful shutdown
server.on('connection', (conn) => {
  connections.add(conn);
  conn.on('close', () => connections.delete(conn));
});

// Health check endpoints
app.get('/health/live', (req, res) => {
  // Liveness: is the process alive and not deadlocked?
  if (isShuttingDown) {
    return res.status(503).json({
      status: 'shutting_down',
      timestamp: new Date().toISOString(),
    });
  }

  res.json({
    status: 'alive',
    uptime: process.uptime(),
    memoryUsage: process.memoryUsage(),
    timestamp: new Date().toISOString(),
  });
});

app.get('/health/ready', (req, res) => {
  // Readiness: can this instance handle requests?
  if (!isReady || isShuttingDown) {
    return res.status(503).json({
      status: 'not_ready',
      isReady,
      isShuttingDown,
    });
  }

  res.json({
    status: 'ready',
    timestamp: new Date().toISOString(),
  });
});

app.get('/health/startup', (req, res) => {
  // Startup: has the app finished initializing?
  if (!isReady) {
    return res.status(503).json({ status: 'starting' });
  }
  res.json({ status: 'started' });
});

// Middleware: reject requests during shutdown
app.use((req, res, next) => {
  if (isShuttingDown) {
    res.setHeader('Connection', 'close');
    return res.status(503).json({
      error: 'Server is shutting down',
    });
  }
  next();
});

// Application routes
app.get('/', (req, res) => {
  res.json({ message: 'Hello, World!' });
});

// Startup sequence
async function startup() {
  console.log('Starting application...');

  try {
    // Initialize database connection
    // await db.connect();
    console.log('Database connected');

    // Initialize cache
    // await redis.connect();
    console.log('Cache connected');

    // Warm up caches if needed
    // await warmupCache();

    isReady = true;
    console.log('Application is ready');
  } catch (error) {
    console.error('Startup failed:', error);
    process.exit(1);
  }
}

// Graceful shutdown
async function shutdown(signal) {
  console.log(`Received ${signal}, starting graceful shutdown...`);

  isShuttingDown = true;
  isReady = false;

  // 1. Stop accepting new connections
  server.close(() => {
    console.log('HTTP server closed');
  });

  // 2. Wait for in-flight requests (with timeout)
  const shutdownTimeout = setTimeout(() => {
    console.error('Shutdown timeout, forcing exit');
    process.exit(1);
  }, 30000); // 30 second timeout

  // 3. Close existing keep-alive connections
  for (const conn of connections) {
    conn.end();
  }

  // Give connections time to drain
  await new Promise((resolve) => setTimeout(resolve, 5000));

  // 4. Destroy remaining connections
  for (const conn of connections) {
    conn.destroy();
  }

  try {
    // 5. Close external connections
    // await db.disconnect();
    console.log('Database disconnected');

    // await redis.disconnect();
    console.log('Cache disconnected');

    clearTimeout(shutdownTimeout);
    console.log('Graceful shutdown complete');
    process.exit(0);
  } catch (error) {
    console.error('Error during shutdown:', error);
    clearTimeout(shutdownTimeout);
    process.exit(1);
  }
}

// Signal handlers
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

// Unhandled errors
process.on('uncaughtException', (error) => {
  console.error('Uncaught exception:', error);
  shutdown('uncaughtException');
});

process.on('unhandledRejection', (reason) => {
  console.error('Unhandled rejection:', reason);
});

// Start server
const PORT = process.env.PORT || 3000;

server.listen(PORT, async () => {
  console.log(`Server listening on port ${PORT}`);
  await startup();
});
1 file · javascript Explain with highlit

Health checks verify container readiness and liveness. The HEALTHCHECK Dockerfile instruction defines container-level checks. Kubernetes readinessProbe gates traffic routing—failing probes remove Pods from Service endpoints. livenessProbe detects deadlocked processes and triggers restarts. startupProbe handles slow-starting applications. HTTP probes check endpoints, TCP probes verify port connectivity, and exec probes run commands. Graceful shutdown handles SIGTERM to drain connections before stopping. The preStop lifecycle hook delays shutdown for load balancer updates. terminationGracePeriodSeconds sets the maximum shutdown time. Connection draining ensures in-flight requests complete. Proper health checks and shutdown prevent dropped requests during deployments.