12 KiB
INSTALL_SUPABASE.md — Supabase self-hosted en Dokploy v0.29.4
Guía operativa basada en la instalación real del proyecto "Datos País sobre el Cuidado" (junio 2026). Registra los pasos exactos, errores conocidos y sus soluciones.
Decisiones de arquitectura
- Supabase self-hosted completo (Postgres + Auth + Storage + Realtime + Studio + Kong).
- Partida limpia — no se migran datos desde Cloud, se aplican las migraciones del repo.
- Gestión vía Git — el compose y los volumes viven en un repo dedicado; Dokploy clona ese repo. Las variables de entorno viven en Dokploy (no en el repo).
- Backups locales diarios, retención 30 días. Remoto: TECH-DEBT-006.
- Studio no expuesto a internet. Acceso solo vía SSH tunnel.
Requisitos previos
- VPS Ubuntu 22.04+ con Docker 29+ instalado.
- Dokploy v0.29.4 instalado y accesible.
- Repo Git disponible (Gitea o GitHub) con acceso desde el VPS y desde Dokploy.
- Acceso SSH al VPS.
- Supabase CLI en la máquina de desarrollo local.
PARTE 1 — Preparar el repo de Supabase
1.1 Descargar el stack oficial en el VPS
curl -fsSL https://supabase.link/setup.sh | sh
Crea ~/supabase-project/ con esta estructura:
~/supabase-project/
├── docker-compose.yml
├── volumes/
│ ├── api/
│ │ ├── kong.yml <- API gateway (EDITAR: ver 1.2)
│ │ └── kong-entrypoint.sh
│ ├── db/
│ │ ├── _supabase.sql <- CRITICO: crea la base _supabase
│ │ ├── roles.sql
│ │ ├── jwt.sql
│ │ ├── realtime.sql
│ │ ├── pooler.sql
│ │ ├── webhooks.sql
│ │ ├── logs.sql
│ │ └── init/data.sql
│ └── pooler/
│ └── pooler.exs
Estos archivos son los scripts de inicializacion que Postgres ejecuta la primera vez. Sin ellos la base _supabase no se crea y el stack no funciona. Por eso NO se puede usar "docker raw" en Dokploy — ese modo no tiene contexto del directorio.
1.2 Ajustar kong.yml
El kong.yml define las credenciales del API gateway. Verificar que use los mismos nombres de variables que se definen en Dokploy:
grep -n "keyauth" ~/supabase-project/volumes/api/kong.yml
Deben quedar dos bloques con UNA sola entrada cada uno:
keyauth_credentials:
- key: $ANON_KEY # nombre exacto de la variable en Dokploy
keyauth_credentials:
- key: $SERVICE_ROLE_KEY # nombre exacto de la variable en Dokploy
ERROR comun: si los dos bloques resuelven al mismo valor JWT, Kong falla con "uniqueness violation". Verificar que ANON_KEY y SERVICE_ROLE_KEY son distintos.
1.3 Crear .gitignore
cat > ~/supabase-project/.gitignore << 'EOF'
.env
.env.local
volumes/db/data/
volumes/storage/
volumes/logs/
*.log
EOF
1.4 Push al repo Git
cd ~/supabase-project
git init
git add docker-compose.yml volumes/api/ volumes/db/ volumes/pooler/ .gitignore
git commit -m "chore: supabase self-hosted stack inicial"
git remote add origin https://TU_GIT/usuario/supabase-repo.git
git push -u origin master
El .env con los secrets NO se sube (esta en .gitignore).
PARTE 2 — Generar los secrets
Usar siempre openssl rand -hex (solo hex, sin caracteres especiales que rompan el escaping de Docker).
2.1 Valores base (en el VPS)
echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)"
echo "JWT_SECRET=$(openssl rand -hex 40)"
echo "DASHBOARD_PASSWORD=$(openssl rand -hex 16)"
echo "SECRET_KEY_BASE=$(openssl rand -hex 40)"
echo "VAULT_ENC_KEY=$(openssl rand -hex 20)"
echo "PG_META_CRYPTO_KEY=$(openssl rand -hex 24)"
Guardar todos en un gestor de contrasenas antes de continuar.
2.2 Generar ANON_KEY y SERVICE_ROLE_KEY
Con el JWT_SECRET generado, usar Node.js en el VPS:
node -e "
const crypto = require('crypto');
const secret = 'PEGAR_AQUI_JWT_SECRET';
function b64url(obj) {
return Buffer.from(JSON.stringify(obj)).toString('base64url');
}
function makeJWT(role) {
const h = b64url({alg:'HS256',typ:'JWT'});
const p = b64url({role,iss:'supabase',iat:1700000000,exp:2000000000});
const sig = crypto.createHmac('sha256',secret).update(h+'.'+p).digest('base64url');
return h+'.'+p+'.'+sig;
}
console.log('ANON_KEY='+makeJWT('anon'));
console.log('SERVICE_ROLE_KEY='+makeJWT('service_role'));
"
PARTE 3 — Configurar el servicio en Dokploy
3.1 Crear el servicio
- Projects -> seleccionar proyecto -> Create Service -> Docker Compose.
- Provider: Gitea (o GitHub) -> seleccionar el repo del paso 1.4.
- Branch: master | Compose Path: ./docker-compose.yml
- IMPORTANTE: desactivar Autodeploy (toggle OFF).
3.2 Variables de entorno (pestaña Environment)
Pegar el bloque completo reemplazando los valores:
POSTGRES_PASSWORD=<openssl rand -hex 24>
POSTGRES_HOST=db
POSTGRES_DB=postgres
POSTGRES_PORT=5432
JWT_SECRET=<openssl rand -hex 40>
JWT_EXPIRY=3600
ANON_KEY=<JWT anon>
SERVICE_ROLE_KEY=<JWT service_role>
SUPABASE_ANON_KEY=<igual a ANON_KEY>
SUPABASE_SERVICE_KEY=<igual a SERVICE_ROLE_KEY>
DASHBOARD_USERNAME=admin
DASHBOARD_PASSWORD=<openssl rand -hex 16>
KONG_HTTP_PORT=8000
KONG_HTTPS_PORT=8443
SECRET_KEY_BASE=<openssl rand -hex 40>
VAULT_ENC_KEY=<openssl rand -hex 20>
PG_META_CRYPTO_KEY=<openssl rand -hex 24>
SITE_URL=http://localhost:3000
API_EXTERNAL_URL=http://localhost:8000
SUPABASE_PUBLIC_URL=http://localhost:8000
STUDIO_DEFAULT_ORGANIZATION=NombreOrg
STUDIO_DEFAULT_PROJECT=NombreProyecto
SMTP_ADMIN_EMAIL=admin@example.com
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
SMTP_SENDER_NAME=
ENABLE_EMAIL_SIGNUP=false
ENABLE_EMAIL_AUTOCONFIRM=false
ENABLE_PHONE_SIGNUP=false
ENABLE_PHONE_AUTOCONFIRM=false
DISABLE_SIGNUP=true
ENABLE_ANONYMOUS_USERS=false
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false
MAILER_URLPATHS_CONFIRMATION=/auth/v1/verify
MAILER_URLPATHS_INVITE=/auth/v1/verify
MAILER_URLPATHS_RECOVERY=/auth/v1/verify
MAILER_URLPATHS_EMAIL_CHANGE=/auth/v1/verify
ADDITIONAL_REDIRECT_URLS=
POOLER_TENANT_ID=pooler
POOLER_DEFAULT_POOL_SIZE=20
POOLER_MAX_CLIENT_CONN=100
POOLER_PROXY_PORT_TRANSACTION=6543
POOLER_DB_POOL_SIZE=10
STORAGE_TENANT_ID=default
GLOBAL_S3_BUCKET=
REGION=
S3_PROTOCOL_ACCESS_KEY_ID=
S3_PROTOCOL_ACCESS_KEY_SECRET=
LOGFLARE_LOGGER_BACKEND_API_KEY=logflare-local-key
LOGFLARE_API_KEY=logflare-local-key
FUNCTIONS_VERIFY_JWT=true
IMGPROXY_ENABLE_WEBP_DETECTION=true
IMGPROXY_AUTO_WEBP=true
PGRST_DB_SCHEMAS=public,storage,graphql_public
3.3 Deploy y validacion
Click en Deploy. Cuando termine verificar:
docker ps --format "table {{.Names}}\t{{.Status}}" | grep supabase
docker exec supabase-db psql -U postgres -c 'SELECT version();'
curl -s http://localhost:8000/rest/v1/ -H "apikey: <ANON_KEY>" | head -c 200
docker exec supabase-db psql -U postgres -c '\l' | grep supabase
Contenedores esperados healthy: supabase-db, supabase-kong, supabase-studio, supabase-auth, supabase-rest, supabase-storage, supabase-meta, supabase-pooler, supabase-imgproxy, realtime-dev.supabase-realtime. supabase-edge-functions puede quedar en Restarting si no se usa Edge Functions (no critico).
PARTE 4 — Aplicar las migraciones del proyecto
El puerto 5432 no esta expuesto externamente. Las migraciones se aplican desde el VPS.
4.1 Copiar migraciones al VPS (desde la maquina de desarrollo)
ssh administrator@<VPS_IP> "mkdir -p ~/migrations"
scp -r supabase/migrations/ administrator@<VPS_IP>:~/migrations/
4.2 Aplicar en orden
for f in $(ls ~/migrations/*.sql | sort); do
echo "Aplicando: $f"
docker exec -i supabase-db psql -U postgres -d postgres < "$f"
echo "---"
done
4.3 Verificar
docker exec supabase-db psql -U postgres -d postgres -c \
"SELECT tablename, policyname FROM pg_policies WHERE schemaname='public' ORDER BY tablename;"
Nota: auth.jwt() en GoTrue v2.189+
GoTrue v2.189+ elimino auth.jwt(). Las funciones disponibles son auth.uid(), auth.role(). Las politicas que usen custom claims JWT (app_role, org_id) fallaran. Ver TECH-DEBT-007. No es bloqueante para el piloto porque el dashboard usa service_role que bypasa RLS.
PARTE 5 — Backups locales
5.1 Crear el script
sudo mkdir -p /var/backups/supabase-prod
sudo chmod 700 /var/backups/supabase-prod
sudo tee /usr/local/bin/supabase-backup.sh > /dev/null << 'SCRIPT'
#!/usr/bin/bash
set -euo pipefail
TS=$(date +%Y%m%d_%H%M%S)
OUT=/var/backups/supabase-prod/supabase_$TS.sql.gz
docker exec supabase-db pg_dump -U postgres postgres | gzip > "$OUT"
chmod 600 "$OUT"
find /var/backups/supabase-prod -name "supabase_*.sql.gz" -mtime +30 -delete
echo "Backup OK: $OUT ($(du -h "$OUT" | cut -f1))"
SCRIPT
sudo chmod +x /usr/local/bin/supabase-backup.sh
5.2 Probar y configurar cron
sudo bash /usr/local/bin/supabase-backup.sh
sudo crontab -e
# Agregar: 0 3 * * * /usr/bin/bash /usr/local/bin/supabase-backup.sh >> /var/log/supabase-backup.log 2>&1
5.3 Probar restore (obligatorio)
docker exec supabase-db createdb -U postgres restore_test
gunzip < $(ls /var/backups/supabase-prod/*.sql.gz | tail -1) | \
docker exec -i supabase-db psql -U postgres restore_test
docker exec supabase-db psql -U postgres -d restore_test -c \
"SELECT count(*) FROM pg_tables WHERE schemaname='public';"
docker exec supabase-db dropdb -U postgres restore_test
PARTE 6 — Dominio y SSL (pendiente)
En Dokploy, pestaña Domains del servicio supabase:
- Agregar dominio db.dominio.com -> apuntar a supabase-kong:8000 -> activar SSL.
- Studio NO tiene dominio publico. Acceso solo via SSH tunnel: ssh -L 3001:localhost:3001 administrator@VPS_IP -> abrir http://localhost:3001
Actualizar variables de entorno:
API_EXTERNAL_URL=https://db.dominio.com
SUPABASE_PUBLIC_URL=https://db.dominio.com
PARTE 7 — Conectar la app Next.js
Actualizar variables de entorno de la app Next.js en Dokploy:
NEXT_PUBLIC_SUPABASE_URL=https://db.dominio.com
NEXT_PUBLIC_SUPABASE_ANON_KEY=<ANON_KEY>
SUPABASE_SERVICE_ROLE_KEY=<SERVICE_ROLE_KEY>
Errores conocidos y soluciones
| Error | Causa | Solucion |
|---|---|---|
| password authentication failed for supabase_storage_admin | POSTGRES_PASSWORD con caracteres especiales | Usar openssl rand -hex 24 |
| database "_supabase" does not exist | volumes/db/ no accesible | Usar modo Git en Dokploy, no docker raw |
| keyauth_credentials uniqueness violation | ANON_KEY = SERVICE_ROLE_KEY o duplicados en kong.yml | Una sola entrada por bloque en kong.yml |
| exec: kong-entrypoint.sh is a directory | Volumen corrupto | docker volume rm del volumen afectado |
| GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: converting to bool | Variable vacia | Agregar GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false |
| GOTRUE_SMTP_PORT: converting to int | SMTP_PORT vacio | Agregar SMTP_PORT=587 |
| auth.jwt() does not exist | GoTrue v2.189+ lo elimino | Ver TECH-DEBT-007 |
| Conflicto de nombres de contenedores | Stack manual y Dokploy corriendo en paralelo | docker compose down desde ~/supabase-project |
Deuda tecnica
- TECH-DEBT-006: Backup remoto (S3) — bloqueante antes del primer dato real de empresa.
- TECH-DEBT-007: Migrar auth.jwt() a alternativa compatible con GoTrue v2.189+.
Datos operativos de esta instalacion
- VPS: inq-mart-2 - IP VPN: 172.20.5.11
- Dokploy: https://172.20.5.11 (acceso via VPN)
- Repo Supabase: https://git.inquality.com.py/marcos/supabase-motor-fomularios
- Backups: /var/backups/supabase-prod/ (diarios, retencion 30 dias)
- Studio: ssh -L 3001:localhost:3001 administrator@172.20.5.11 -> http://localhost:3001