Zero-Downtime Deployments for Self-Hosted Supabase: A Complete Guide

Learn blue-green deployment strategies for self-hosted Supabase to eliminate downtime during updates and migrations.

Cover Image for Zero-Downtime Deployments for Self-Hosted Supabase: A Complete Guide

Running a self-hosted Supabase instance comes with a persistent challenge: every update, migration, or configuration change risks downtime. If you've felt that knot in your stomach before running docker-compose down, you're not alone. Community discussions are filled with developers expressing "production anxiety" about updates being "extremely nerve-racking" due to the risk of service interruptions.

This guide covers practical strategies for achieving zero-downtime deployments with self-hosted Supabase, from simple rolling updates to full blue-green deployments.

Why Downtime Happens with Self-Hosted Supabase

When you run Supabase via Docker Compose, updating typically requires:

  1. Pulling new images
  2. Stopping existing containers
  3. Starting new containers with updated images
  4. Waiting for services to initialize

During steps 2-4, your application is unavailable. For a typical Supabase stack with 10+ services, this window can range from 30 seconds to several minutes. That's an eternity for production applications with active users.

The core problem is that standard Docker Compose deployments treat all services as a single unit. When you restart, everything goes down together.

Strategy 1: Rolling Updates with Health Checks

The simplest approach to reducing downtime is implementing proper health checks and updating services sequentially rather than all at once.

Configure Health Checks

First, add health checks to your docker-compose.yml:

services:
  kong:
    image: kong:2.8.1
    healthcheck:
      test: ["CMD", "kong", "health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 40s
    # ... other config

  rest:
    image: postgrest/postgrest:v12.0.1
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3000/ready || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 3
    depends_on:
      kong:
        condition: service_healthy

Update Services Sequentially

Create an update script that handles one service at a time:

#!/bin/bash
# update-services.sh

SERVICES="db kong rest realtime storage auth meta studio"

for service in $SERVICES; do
  echo "Updating $service..."
  docker-compose pull $service
  docker-compose up -d --no-deps $service
  
  # Wait for health check to pass
  until docker-compose ps $service | grep -q "healthy"; do
    echo "Waiting for $service to be healthy..."
    sleep 5
  done
  
  echo "$service updated successfully"
done

This approach reduces downtime significantly but doesn't eliminate it entirely. Each service still experiences a brief interruption during its individual restart.

Strategy 2: Blue-Green Deployment

For true zero-downtime updates, blue-green deployment is the gold standard. You maintain two identical environments and switch traffic between them.

Architecture Overview

                    ┌─────────────────┐
                    │  Load Balancer  │
                    │  (Traefik/Nginx)│
                    └────────┬────────┘
                             │
              ┌──────────────┼──────────────┐
              ▼                             ▼
    ┌─────────────────┐          ┌─────────────────┐
    │   Blue Stack    │          │   Green Stack   │
    │  (Production)   │          │   (Standby)     │
    │                 │          │                 │
    │ - Kong          │          │ - Kong          │
    │ - PostgREST     │          │ - PostgREST     │
    │ - Realtime      │          │ - Realtime      │
    │ - Auth          │          │ - Auth          │
    │ - Storage       │          │ - Storage       │
    └────────┬────────┘          └────────┬────────┘
             │                            │
             └──────────┬─────────────────┘
                        ▼
              ┌─────────────────┐
              │    Postgres     │
              │   (Shared DB)   │
              └─────────────────┘

The key insight: both stacks share the same PostgreSQL database. Only the stateless application services are duplicated.

Implementation with Docker Compose

Create separate compose files for each environment:

docker-compose.blue.yml:

version: '3.8'

services:
  kong-blue:
    image: kong:2.8.1
    container_name: kong-blue
    environment:
      - KONG_DECLARATIVE_CONFIG=/var/lib/kong/kong.yml
    networks:
      - supabase-blue
      - shared
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.kong-blue.rule=Host(`api.yourdomain.com`)"
      - "traefik.http.routers.kong-blue.priority=1"

  rest-blue:
    image: postgrest/postgrest:v12.0.1
    container_name: rest-blue
    environment:
      - PGRST_DB_URI=postgres://authenticator:${POSTGRES_PASSWORD}@db:5432/postgres
    networks:
      - supabase-blue
      - shared

networks:
  supabase-blue:
    name: supabase-blue
  shared:
    external: true

docker-compose.green.yml:

version: '3.8'

services:
  kong-green:
    image: kong:2.8.1
    container_name: kong-green
    environment:
      - KONG_DECLARATIVE_CONFIG=/var/lib/kong/kong.yml
    networks:
      - supabase-green
      - shared
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.kong-green.rule=Host(`api.yourdomain.com`)"
      - "traefik.http.routers.kong-green.priority=0"  # Lower priority initially

  rest-green:
    image: postgrest/postgrest:v12.0.1
    container_name: rest-green
    environment:
      - PGRST_DB_URI=postgres://authenticator:${POSTGRES_PASSWORD}@db:5432/postgres
    networks:
      - supabase-green
      - shared

networks:
  supabase-green:
    name: supabase-green
  shared:
    external: true

Traffic Switching with Traefik

Use Traefik's weighted routing for gradual traffic shifting:

# traefik-dynamic.yml
http:
  services:
    supabase:
      weighted:
        services:
          - name: blue
            weight: 100
          - name: green
            weight: 0
    
    blue:
      loadBalancer:
        servers:
          - url: "http://kong-blue:8000"
    
    green:
      loadBalancer:
        servers:
          - url: "http://kong-green:8000"

Deployment Script

#!/bin/bash
# blue-green-deploy.sh

CURRENT=$(cat /var/supabase/active-env)
TARGET=$([ "$CURRENT" = "blue" ] && echo "green" || echo "blue")

echo "Current: $CURRENT, Deploying to: $TARGET"

# Pull and start standby environment
docker-compose -f docker-compose.${TARGET}.yml pull
docker-compose -f docker-compose.${TARGET}.yml up -d

# Wait for health checks
echo "Waiting for $TARGET environment to be healthy..."
sleep 30

# Verify health
if ! curl -sf http://kong-${TARGET}:8000/health > /dev/null; then
  echo "Health check failed. Aborting deployment."
  docker-compose -f docker-compose.${TARGET}.yml down
  exit 1
fi

# Switch traffic (update Traefik config)
sed -i "s/weight: 100/weight: TEMP/g" /etc/traefik/dynamic.yml
sed -i "s/weight: 0/weight: 100/g" /etc/traefik/dynamic.yml
sed -i "s/weight: TEMP/weight: 0/g" /etc/traefik/dynamic.yml

echo "Traffic switched to $TARGET"
echo "$TARGET" > /var/supabase/active-env

# Optional: Keep old environment for quick rollback
# docker-compose -f docker-compose.${CURRENT}.yml down

Strategy 3: Database Migration Without Downtime

The trickiest part of zero-downtime deployments is database migrations. Schema changes can lock tables and break running queries.

Safe Migration Practices

1. Additive-Only Changes

When possible, make changes that only add new structures:

-- Safe: Adding a new column with a default
ALTER TABLE users ADD COLUMN last_login TIMESTAMPTZ DEFAULT NOW();

-- Safe: Creating new indexes concurrently
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);

2. Expand-Contract Pattern

For breaking changes, use a multi-phase approach:

Phase 1 (Expand): Add new column, keep old column

ALTER TABLE orders ADD COLUMN status_new TEXT;

Phase 2 (Migrate): Backfill data

UPDATE orders SET status_new = status::TEXT WHERE status_new IS NULL;

Phase 3 (Switch): Update application to use new column

Deploy new application code

Phase 4 (Contract): Remove old column

ALTER TABLE orders DROP COLUMN status;

3. Use Migration Tools

Consider tools like pgroll for complex migrations that require zero downtime. If you're managing multiple Supabase projects, tools like Supascale can help coordinate migrations across instances.

Handling Stateful Services

PostgreSQL

Never run multiple PostgreSQL primaries against the same data directory. Instead:

  • Keep a single PostgreSQL instance
  • Use streaming replication for read replicas
  • Consider connection pooling with PgBouncer or Supavisor for connection management during switchovers

Storage (S3-Compatible)

Supabase Storage is stateless when backed by S3-compatible storage:

storage:
  environment:
    - STORAGE_BACKEND=s3
    - AWS_S3_BUCKET=your-bucket
    - AWS_REGION=us-east-1

Both blue and green environments can safely share the same storage backend.

Realtime

Supabase Realtime maintains WebSocket connections. During switchovers:

  1. New connections go to the new environment
  2. Existing connections remain on the old environment
  3. Implement client-side reconnection logic:
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY, {
  realtime: {
    reconnectAfterMs: (attempts) => Math.min(1000 * 2 ** attempts, 30000),
  },
})

Monitoring and Rollback

Health Check Endpoints

Create a comprehensive health check endpoint:

// /health endpoint
app.get('/health', async (req, res) => {
  const checks = {
    database: await checkDatabase(),
    storage: await checkStorage(),
    auth: await checkAuth(),
    realtime: await checkRealtime(),
  }
  
  const healthy = Object.values(checks).every(c => c.status === 'ok')
  res.status(healthy ? 200 : 503).json(checks)
})

Automated Rollback

If the new deployment fails health checks, automatically roll back:

#!/bin/bash
# health-monitor.sh

while true; do
  if ! curl -sf http://localhost/health > /dev/null; then
    FAIL_COUNT=$((FAIL_COUNT + 1))
    
    if [ $FAIL_COUNT -ge 3 ]; then
      echo "Health check failed 3 times. Rolling back..."
      ./rollback.sh
      break
    fi
  else
    FAIL_COUNT=0
  fi
  
  sleep 10
done

When Zero-Downtime Is Worth the Complexity

Blue-green deployments add operational complexity. They require:

  • Double the compute resources during deployments
  • More sophisticated load balancer configuration
  • Careful handling of database migrations
  • Additional monitoring and alerting

For many self-hosted Supabase deployments, a 30-second maintenance window during off-peak hours is acceptable. Reserve zero-downtime strategies for:

  • Production applications with 24/7 global traffic
  • SLA requirements that mandate high availability
  • Applications where any downtime has significant business impact

If you're managing multiple projects and want to simplify deployment operations, Supascale's automated deployment features can handle much of this complexity for you.

Lessons from the India Supabase Block

The recent Supabase block in India highlighted why robust deployment strategies matter. Developers with self-hosted instances and proper blue-green setups were able to:

  1. Quickly spin up alternate environments in different regions
  2. Switch traffic without service interruption
  3. Maintain service continuity while managed Supabase users scrambled

This incident reinforced that self-hosting isn't just about cost savings - it's about operational independence and resilience.

Conclusion

Zero-downtime deployments for self-hosted Supabase range from simple rolling updates to full blue-green architectures. Start with proper health checks and sequential service updates. As your availability requirements grow, implement blue-green deployments with automated traffic switching.

The investment in zero-downtime infrastructure pays dividends not just in uptime metrics, but in peace of mind. No more 3 AM maintenance windows or "please wait" notices for your users.

Ready to simplify your self-hosted Supabase operations? Supascale provides automated backups, one-click deployments, and management tools that reduce operational burden while you focus on building your application.


Further Reading