Running Supabase locally with docker compose up is straightforward. Running it in production? That's where things get interesting. The default configuration works fine for development, but deploying to production requires careful attention to security, performance, and operational reliability.
This guide covers everything you need to know about hardening your Supabase Docker Compose deployment for production. Whether you're deploying on a VPS provider or your own infrastructure, these best practices will help you avoid common pitfalls.
The Default Configuration Isn't Production-Ready
Supabase's official Docker Compose file is designed for getting started quickly, not for running in production. The official documentation explicitly warns: "While we provided example placeholder passwords and keys in the .env.example file, you should NEVER start your self-hosted Supabase using these defaults."
Here's what needs attention:
- Placeholder secrets: Default passwords and API keys are publicly known
- No SSL termination: The default setup exposes unencrypted HTTP
- All services enabled: You're running components you might not need
- No backup configuration: Data loss is one misconfiguration away
- Missing monitoring: You won't know when things break
Let's fix each of these.
Security Hardening
Replace Every Default Secret
The .env file contains secrets for JWT signing, database access, and service communication. Every single one must be regenerated before production deployment.
Generate cryptographically secure secrets:
# Generate a 32-character random string openssl rand -base64 32 # For JWT secrets, use a longer key openssl rand -base64 64
Critical secrets to replace in your .env:
# NEVER use defaults for these POSTGRES_PASSWORD=<generate-unique-password> JWT_SECRET=<generate-64-char-minimum> ANON_KEY=<generate-new-jwt> SERVICE_ROLE_KEY=<generate-new-jwt> DASHBOARD_PASSWORD=<strong-password>
Use a secrets manager like HashiCorp Vault, AWS Secrets Manager, or even a password manager to generate and store these values. Never commit them to version control.
Generate New JWT Keys
The ANON_KEY and SERVICE_ROLE_KEY are JWTs that must be regenerated with your new JWT_SECRET. You can use the Supabase JWT generator or create them manually:
// Using jsonwebtoken in Node.js
const jwt = require('jsonwebtoken');
const JWT_SECRET = 'your-new-jwt-secret';
const anonKey = jwt.sign(
{
role: 'anon',
iss: 'supabase',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (10 * 365 * 24 * 60 * 60) // 10 years
},
JWT_SECRET
);
const serviceKey = jwt.sign(
{
role: 'service_role',
iss: 'supabase',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (10 * 365 * 24 * 60 * 60)
},
JWT_SECRET
);
Secure the Dashboard
The Supabase Studio dashboard should never be publicly accessible without authentication. Options include:
- Set a strong dashboard password in your environment variables
- Use a reverse proxy with additional authentication (HTTP Basic Auth, OAuth)
- Restrict access by IP using firewall rules or your reverse proxy
- Disable it entirely if you don't need it in production
For most production deployments, disabling the dashboard and using migrations for schema changes is the safest approach.
SSL/TLS Configuration
Production deployments must use HTTPS. You have two options:
Option 1: Reverse Proxy (Recommended)
Place a reverse proxy like Nginx, Caddy, or Traefik in front of Kong. This handles SSL termination, certificate renewal, and can add additional security features.
Caddy is particularly attractive because it handles Let's Encrypt certificates automatically:
# Caddyfile
api.yourdomain.com {
reverse_proxy kong:8000
}
studio.yourdomain.com {
reverse_proxy studio:3000
basicauth * {
admin $2a$14$hashedpasswordhere
}
}
Option 2: Configure Kong Directly
Kong can handle SSL on port 8443. Mount your certificates and update the configuration:
# docker-compose.yml snippet
kong:
volumes:
- ./certs/fullchain.pem:/etc/ssl/certs/fullchain.pem:ro
- ./certs/privkey.pem:/etc/ssl/private/privkey.pem:ro
ports:
- "8443:8443"
The reverse proxy approach is generally preferred because it's easier to manage certificate renewals and provides an additional security layer.
Resource Optimization
Disable Unused Services
Supabase includes many services you might not need. Each consumes memory and CPU. The default stack includes:
- Realtime: WebSocket subscriptions for live updates
- Storage: S3-compatible file storage with imgproxy
- Edge Functions: Deno runtime for serverless functions
- Analytics/Logflare: Usage analytics and log aggregation
- Vector: Log collection and forwarding
If you're not using a service, remove it from docker-compose.yml along with its dependencies. A minimal API-focused deployment might only need:
- PostgreSQL (database)
- PostgREST (REST API)
- GoTrue (authentication)
- Kong (API gateway)
This can reduce memory usage from 4GB+ to under 1GB.
Configure Resource Limits
Docker Compose supports resource constraints. Set them based on your server capacity and expected load:
services:
db:
deploy:
resources:
limits:
memory: 2G
cpus: '2'
reservations:
memory: 1G
cpus: '1'
PostgreSQL typically needs the most resources. A good starting point for a small to medium workload:
| Service | Memory Limit | CPU Limit |
|---|---|---|
| PostgreSQL | 2-4 GB | 2 cores |
| PostgREST | 512 MB | 0.5 cores |
| GoTrue | 256 MB | 0.25 cores |
| Kong | 512 MB | 0.5 cores |
| Realtime | 512 MB | 0.5 cores |
| Storage | 512 MB | 0.5 cores |
Adjust based on your actual usage patterns after monitoring in production.
Tune PostgreSQL
The default PostgreSQL configuration is conservative. For production, tune these settings in a custom postgresql.conf or via environment variables:
# Based on 4GB RAM server POSTGRES_SHARED_BUFFERS=1GB POSTGRES_EFFECTIVE_CACHE_SIZE=3GB POSTGRES_MAINTENANCE_WORK_MEM=256MB POSTGRES_WORK_MEM=16MB
Use PGTune to generate settings based on your server specs and workload type.
Backup Strategy
This is where most self-hosted deployments fail. A production deployment without automated backups is a disaster waiting to happen.
Database Backups
The minimum viable backup strategy uses pg_dump:
#!/bin/bash # backup.sh TIMESTAMP=$(date +%Y%m%d_%H%M%S) BACKUP_DIR=/backups docker exec supabase-db pg_dump -U postgres \ --format=custom \ --compress=9 \ postgres > "$BACKUP_DIR/supabase_$TIMESTAMP.dump" # Retain last 7 days find $BACKUP_DIR -name "*.dump" -mtime +7 -delete
Run this via cron:
0 2 * * * /opt/supabase/backup.sh
For serious production deployments, consider WAL-G for point-in-time recovery with continuous archiving.
Storage Backups
Database backups don't include files uploaded to Supabase Storage. If you're using local storage, back up the volumes:
docker run --rm \ -v supabase_storage:/data \ -v /backups:/backup \ alpine tar czf /backup/storage_$(date +%Y%m%d).tar.gz /data
Better yet, configure Storage to use S3-compatible object storage with versioning enabled.
Don't Forget: Test Your Restores
A backup you haven't tested restoring is not a backup. Schedule regular restore tests to a separate environment.
For comprehensive backup strategies including one-click restore, check out our backup and restore guide.
Monitoring and Health Checks
Add Health Checks to Compose
Docker Compose supports health checks that automatically restart unhealthy containers:
services:
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
rest:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
interval: 30s
timeout: 10s
retries: 3
External Monitoring
Health checks handle container-level issues, but you need external monitoring for:
- Uptime monitoring: Is your API responding?
- Performance metrics: Response times, error rates
- Resource usage: CPU, memory, disk space
- Certificate expiry: Don't let SSL certificates expire
Tools like Uptime Kuma, Prometheus + Grafana, or managed services like Better Uptime work well.
Log Management
By default, Docker logs can grow unbounded and fill your disk. Configure log rotation:
# docker-compose.yml
services:
db:
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "3"
For centralized logging, consider forwarding to Loki, Elasticsearch, or a managed logging service.
Deployment Workflow
Version Control Everything
Your entire Supabase configuration should be in version control:
supabase-production/
├── docker-compose.yml
├── docker-compose.override.yml # Local overrides (gitignored)
├── .env.example # Template (never real secrets)
├── kong/
│ └── kong.yml
├── postgres/
│ └── postgresql.conf
└── migrations/
└── *.sql
The actual .env with secrets should never be committed.
Zero-Downtime Updates
Updating Supabase requires pulling new images and recreating containers. To minimize downtime:
# Pull new images without stopping services docker compose pull # Recreate containers one at a time docker compose up -d --no-deps db docker compose up -d --no-deps rest docker compose up -d --no-deps auth # Continue for each service
For true zero-downtime deployments, you'd need a blue-green or rolling deployment strategy with a load balancer, which is beyond the scope of a single Docker Compose file.
Database Migrations
Schema changes should be managed through migrations, not the dashboard. Supabase CLI helps with this:
# Generate migration from local changes supabase db diff -f my_migration # Apply to production supabase db push --db-url postgres://user:pass@host:5432/postgres
Test migrations against a backup of production data before applying them.
Common Pitfalls to Avoid
1. Running as root: Create a dedicated user for Docker and the Supabase services.
2. Exposing ports directly: Only Kong (your API gateway) should be accessible from the internet. Other services communicate internally.
3. Ignoring disk space: PostgreSQL, logs, and storage can fill your disk. Monitor usage and set up alerts.
4. No restart policy: Containers should restart automatically after server reboots:
services:
db:
restart: unless-stopped
5. Skipping RLS: Row Level Security is disabled by default on new tables. This is the most common security mistake in Supabase deployments—83% of exposed databases involve RLS misconfigurations.
When Docker Compose Isn't Enough
Docker Compose works well for single-server deployments. You'll outgrow it when:
- Traffic exceeds one server's capacity: You need load balancing and horizontal scaling
- You need high availability: Single server = single point of failure
- Management overhead becomes painful: Updating 10 Docker Compose deployments manually doesn't scale
At that point, consider Kubernetes, Docker Swarm, or a management platform.
Simplifying Production Deployments
Managing production Docker Compose deployments involves significant operational overhead: generating secrets, configuring SSL, setting up backups, monitoring health. It's achievable, but time-consuming.
Supascale handles the operational complexity of self-hosted Supabase. Instead of manually configuring Docker Compose files and managing infrastructure, you get:
- Automated deployments with proper security defaults
- Built-in backups to S3-compatible storage with one-click restore
- Custom domains with automatic SSL certificate management
- OAuth configuration through a UI instead of environment variables
- Selective services: Only run what you need
For a one-time $39.99 purchase, you can deploy unlimited projects without the operational burden. If you're spending hours on Docker Compose configuration, check if Supascale makes sense for your use case.
Summary
Production-ready Supabase with Docker Compose requires:
- Replace all default secrets with cryptographically secure values
- Configure SSL via reverse proxy or Kong
- Disable unused services to reduce resource consumption
- Set resource limits to prevent runaway containers
- Implement automated backups and test restores regularly
- Add health checks and monitoring for visibility
- Enable RLS on every table containing user data
The effort is significant, but self-hosting gives you full control, predictable costs, and data sovereignty. For teams managing multiple projects or wanting to reduce operational overhead, tools like Supascale can automate much of this configuration.
