A complete guide to set up your own Supabase instance on Oracle Cloud using Docker, Nginx, SSL certificates, and automatic updates.
- Oracle Cloud account (free tier works)
- Domain name with DNS management access
- SSH access to your Oracle Cloud VM
- Basic command line knowledge
- Login to Oracle Cloud Console
- Create a new VM instance:
- Shape: VM.Standard.E2.1.Micro (Always Free)
- Image: Ubuntu 22.04 LTS
- SSH Keys: Generate and download your key pair
- Networking: Allow HTTP (80) and HTTPS (443) traffic
- Go to Networking β Virtual Cloud Networks β Public IPs
- Click on your VM's external IP β Reserve Static IP
- Note down your static IP address (e.g.,
123.456.78.90)
- Networking β Virtual Cloud Networks β Security Lists
- Add ingress rules:
- Port 80 (HTTP): Source
0.0.0.0/0 - Port 443 (HTTPS): Source
0.0.0.0/0
- Port 80 (HTTP): Source
Set up your subdomain to point to your Oracle Cloud VM:
| Record Type | Host | Value | TTL |
|---|---|---|---|
| A | sb |
123.456.78.90 |
14400 |
Example: If your domain is example.com, create sb.example.com pointing to your VM's IP.
ssh -i ~/path/to/your-ssh-key.key [email protected]sudo apt update
sudo apt install nginx git -ysudo apt install docker.io -y
sudo systemctl start docker
sudo systemctl enable dockersudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
docker-compose --versionsudo snap install core; sudo snap refresh core
sudo snap install --classic certbotgit clone --depth 1 https://github.com/supabase/supabase.git
cd supabase/docker
cp .env.example .envEdit the .env file:
nano .envRequired Changes:
# Database Password (25+ characters, all characters allowed)
POSTGRES_PASSWORD=SecureDatabasePassword123!@#
# JWT Secret (32+ characters, letters and numbers only)
JWT_SECRET=MyJWTSecretKey1234567890ABCDEFGHIJ
# External URLs (replace with your domain)
API_EXTERNAL_URL=https://sb.example.com
SUPABASE_PUBLIC_URL=https://sb.example.com
# Dashboard Credentials
DASHBOARD_USERNAME=admin
DASHBOARD_PASSWORD=SecureAdminPassword123
# Security Keys
SECRET_KEY_BASE=SecretKey64CharactersLong123456789012345678901234567890!@#$
VAULT_ENC_KEY=VaultEncryptionKey32CharsLong123456
# File Upload Limit (0 = no limit, value in bytes)
FILE_SIZE_LIMIT=0
# Keep existing ANON_KEY and SERVICE_ROLE_KEY as they arePassword Requirements:
- POSTGRES_PASSWORD: Minimum 25 characters, any characters allowed
- JWT_SECRET: Minimum 32 characters, letters and numbers only
- SECRET_KEY_BASE: Minimum 64 characters, any characters allowed
- VAULT_ENC_KEY: Minimum 32 characters, letters and numbers only
Edit docker-compose.yml to expose Studio port:
nano docker-compose.ymlFind the studio: section (around line 12) and add ports:
studio:
container_name: supabase-studio
image: supabase/studio:2025.05.19-sha-3487831
restart: unless-stopped
ports:
- "3000:3000" # ADD THIS LINE
healthcheck:
test:
[
"CMD",
"node",
"-e",
"fetch('http://studio:3000/api/platform/profile').then((r) => {if (r.status !== 200) throw new Error(r.status)})"
]sudo docker-compose up -dThis will take 10-15 minutes on the first run.
sudo docker psAll containers should show "Up" status. Some may show "Restarting" or "Unhealthy" - this is normal for optional services like pooler and realtime.
sudo certbot certonly --nginx -d sb.example.comFollow the prompts:
- Enter your email address
- Accept terms of service
- Choose whether to share email with EFF
Certificate files will be stored at:
- Certificate:
/etc/letsencrypt/live/sb.example.com/fullchain.pem - Private Key:
/etc/letsencrypt/live/sb.example.com/privkey.pem
sudo nano /etc/nginx/sites-available/supabase.confAdd this configuration (replace sb.example.com with your subdomain):
server {
server_name sb.example.com;
gzip on;
# REST API
location ~ ^/rest/v1/(.*)$ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://localhost:8000;
proxy_redirect off;
}
# Authentication
location ~ ^/auth/v1/(.*)$ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://localhost:8000;
proxy_redirect off;
}
# Realtime
location ~ ^/realtime/v1/(.*)$ {
proxy_redirect off;
proxy_pass http://localhost:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Supabase Studio (Dashboard)
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/sb.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/sb.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
if ($host = sb.example.com) {
return 301 https://$host$request_uri;
}
listen 80;
server_name sb.example.com;
return 404;
}sudo ln -s /etc/nginx/sites-available/supabase.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxOpen your browser and navigate to: https://sb.example.com
You should see the Supabase Studio login page.
Login with:
- Username:
admin(or whatever you set as DASHBOARD_USERNAME) - Password: Your DASHBOARD_PASSWORD
Create an optimized update script that only backs up configurations:
cd ~
nano update_supabase.shAdd this content:
#!/bin/bash
LOG_FILE="/var/log/supabase_update.log"
BACKUP_DATE=$(date +'%Y-%m-%d_%H-%M-%S')
echo "=== Supabase Update Started: $(date) ===" >> $LOG_FILE
# Backup only important configs (NOT the database!)
echo "Creating config backup..." >> $LOG_FILE
mkdir -p ~/supabase_config_backup_$BACKUP_DATE
cp ~/supabase/docker/.env ~/supabase_config_backup_$BACKUP_DATE/
cp ~/supabase/docker/docker-compose.yml ~/supabase_config_backup_$BACKUP_DATE/
cp -r ~/supabase/docker/volumes/api ~/supabase_config_backup_$BACKUP_DATE/
# Navigate to supabase directory
cd ~/supabase/docker
# Stop current services
echo "Stopping Supabase services..." >> $LOG_FILE
sudo docker-compose down >> $LOG_FILE 2>&1
# Pull latest changes from repository
echo "Pulling latest Supabase updates..." >> $LOG_FILE
git pull origin master >> $LOG_FILE 2>&1
# Pull latest Docker images
echo "Pulling latest Docker images..." >> $LOG_FILE
sudo docker-compose pull >> $LOG_FILE 2>&1
# Start services with updated images
echo "Starting updated Supabase services..." >> $LOG_FILE
sudo docker-compose up -d >> $LOG_FILE 2>&1
# Wait for services to start
sleep 60
# Check if services are running
echo "Checking service status..." >> $LOG_FILE
sudo docker ps >> $LOG_FILE 2>&1
# Clean up old Docker images to save space
echo "Cleaning up old Docker images..." >> $LOG_FILE
sudo docker image prune -af >> $LOG_FILE 2>&1
# Remove old config backups (keep only last 2)
echo "Cleaning up old config backups..." >> $LOG_FILE
find ~/supabase_config_backup_* -maxdepth 0 -type d | sort | head -n -2 | xargs rm -rf
echo "=== Supabase Update Completed: $(date) ===" >> $LOG_FILE
echo "" >> $LOG_FILEchmod +x ~/update_supabase.sh
sudo touch /var/log/supabase_update.log
sudo chmod 666 /var/log/supabase_update.logOpen crontab:
sudo crontab -eAdd these lines:
# Auto-start Supabase after VM reboot
@reboot sleep 30 && cd /home/ubuntu/supabase/docker && /usr/local/bin/docker-compose up -d
# Supabase auto-update every Sunday at 3 AM German time
0 1 * * 0 /bin/bash /home/ubuntu/update_supabase.sh >/dev/null 2>&1Note: 0 1 * * 0 = 1 AM UTC = 3 AM German time (CEST)
sudo crontab -lYour Supabase instance uses these ports:
- Port 3000: Supabase Studio (Dashboard)
- Port 8000: Kong API Gateway (REST API, Auth, Realtime)
- Port 5432: PostgreSQL Database (internal)
- Port 4000: Analytics/Logflare (internal)
# Stop Supabase
cd ~/supabase/docker
sudo docker-compose down
# Start Supabase
cd ~/supabase/docker
sudo docker-compose up -d
# View Logs
cd ~/supabase/docker
sudo docker-compose logs -f
# Manual Update
sudo bash ~/update_supabase.sh# Check update logs
tail -n 50 /var/log/supabase_update.log
# List config backups
ls -la ~/supabase_config_backup_*
# Check running containers
sudo docker ps- Automatic updates every Sunday at 3 AM German time
- Auto-start after VM reboot (30 second delay)
- Config-only backups (fast, space-efficient)
- Keeps only 2 latest backups (prevents disk filling)
- Database data preserved (never copied, always persists)
- Detailed logging of all operations
- Automatic cleanup of old Docker images
- β
.envfile (passwords, keys, configuration) - β
docker-compose.yml(port mappings, service config) - β
volumes/api/kong.yml(API gateway configuration)
- πΎ Database data:
./volumes/db/data/(persists on disk) - πΎ File storage:
./volumes/storage/(your uploaded files) - πΎ All user data: Tables, users, API keys remain intact
- Check if containers are running:
sudo docker ps - Verify ports are accessible:
sudo netstat -tlnp | grep :3000andsudo netstat -tlnp | grep :8000 - Check Nginx logs:
sudo tail -f /var/log/nginx/error.log
Some containers (pooler, realtime) may show "Restarting" or "Unhealthy" status. This is normal and doesn't affect core functionality.
Certificates auto-renew. To manually renew:
sudo certbot renew
sudo systemctl reload nginxcd ~/supabase/docker
sudo docker-compose down
cp ~/supabase_config_backup_YYYY-MM-DD_HH-MM-SS/.env .
cp ~/supabase_config_backup_YYYY-MM-DD_HH-MM-SS/docker-compose.yml .
cp -r ~/supabase_config_backup_YYYY-MM-DD_HH-MM-SS/api ./volumes/
sudo docker-compose up -d- Change default passwords in
.envfile - Use strong, unique passwords for all services
- Regularly monitor update logs
- Keep your domain DNS secure
- Monitor Oracle Cloud billing (shouldn't exceed free tier)
Once installed, your Supabase API will be available at:
- REST API:
https://sb.example.com/rest/v1/ - Auth API:
https://sb.example.com/auth/v1/ - Realtime:
https://sb.example.com/realtime/v1/
Use your ANON_KEY and SERVICE_ROLE_KEY from the .env file for API authentication.
If you're running a self-hosted n8n instance and want to connect it to your self-hosted Supabase, you'll need additional configuration due to authentication header conflicts between n8n and Kong (Supabase's API gateway).
- Additional Firewall Ports: Add these to your Oracle Cloud Security Rules:
- Port 8000 (Supabase Kong API Gateway): Source
0.0.0.0/0 - Port 5432 (PostgreSQL - optional for direct DB access): Source
0.0.0.0/0
- Port 8000 (Supabase Kong API Gateway): Source
The n8n Supabase node sends both apikey and Authorization headers simultaneously. Kong prioritizes the Authorization header and ignores the apikey header, causing "Unauthorized" errors.
Solution: Configure Kong to remove the Authorization header before processing the request.
- Edit Kong configuration:
nano ~/supabase/docker/volumes/api/kong.yml- Find the
rest-v1service (around line 93) and add therequest-transformerplugin:
## Secure REST routes
- name: rest-v1
_comment: 'PostgREST: /rest/v1/* -> http://rest:3000/*'
url: http://rest:3000/
routes:
- name: rest-v1-all
strip_path: true
paths:
- /rest/v1/
plugins:
- name: cors
- name: key-auth
config:
hide_credentials: true
- name: request-transformer # ADD THIS PLUGIN
config:
remove:
headers:
- Authorization # REMOVE Authorization HEADER
- name: acl
config:
hide_groups_header: true
allow:
- admin
- anon- Ensure the
request-transformerplugin is enabled indocker-compose.yml:
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth- Restart Kong:
sudo docker-compose restart kong-
In n8n, create new Supabase credentials with:
- Host:
sb.example.com(without https:// prefix or /rest/v1/ suffix) - Service Role Secret: Your
SERVICE_ROLE_KEYfrom the.envfile
- Host:
-
Test the connection - it should now show "Connection tested successfully".
- n8n Behavior: The n8n Supabase node automatically sends both authentication headers
- Kong Priority: Kong processes the
Authorizationheader first and ignores theapikey - Header Removal: The
request-transformerplugin removes the problematicAuthorizationheader - Result: Kong only sees the
apikeyheader and authentication succeeds
Your self-hosted Supabase instance provides two types of API keys:
- Client API Key (anon): For frontend applications with limited permissions controlled by Row Level Security (RLS)
- Service Role Key: For backend services like n8n with full administrative access
Both keys are private to your installation and not publicly accessible. They are generated based on your JWT_SECRET in the .env file.
By default, Supabase tables created through the Table Editor have RLS enabled. This means:
- The
anonkey can only access data according to your RLS policies - The
service_rolekey bypasses RLS and has full access (used by n8n)
To create policies for controlled access with the anon key, use the SQL Editor in Supabase Studio.
If you prefer direct database access instead of using the Supabase API, you can connect n8n directly to PostgreSQL:
- Host:
sb.example.com - Port:
5432 - Database:
postgres - User:
postgres - Password: Your
POSTGRES_PASSWORDfrom the.envfile - SSL: Enable if connecting from external networks
This bypasses all Supabase API features but provides direct database access for simple CRUD operations.
You now have a fully automated, self-hosted Supabase instance that:
- β Runs 24/7 on Oracle Cloud free tier
- β Auto-updates every Sunday at 3 AM German time
- β Auto-starts after VM reboots
- β Keeps your data safe during updates
- β Manages disk space automatically
- β Uses SSL encryption with automatic renewal
- β Accessible via custom domain
- β Integrates with n8n for workflow automation
π Connection Methods Summary Your self-hosted Supabase supports two integration approaches: Method 1: Supabase API (Recommended)
Requirements: Host (sb.example.com) + SERVICE_ROLE_KEY Security: Very secure - keys are private, not publicly accessible Access: Only you (keys generated from your JWT_SECRET) Features: Full Supabase functionality, Row Level Security, real-time subscriptions
Method 2: Direct PostgreSQL
Requirements: Host + Port 5432 + postgres user + POSTGRES_PASSWORD Security: Less secure - bypasses all Supabase security features Access: Anyone with IP + port + credentials Features: Basic CRUD operations only
Recommendation: Use Supabase API method and close port 5432 in firewall for maximum security.
Need help? Check the official Supabase documentation or Docker self-hosting guide.