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:
- Pulling new images
- Stopping existing containers
- Starting new containers with updated images
- 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:
- New connections go to the new environment
- Existing connections remain on the old environment
- 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:
- Quickly spin up alternate environments in different regions
- Switch traffic without service interruption
- 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.
