# 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 ```bash 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: ```bash grep -n "keyauth" ~/supabase-project/volumes/api/kong.yml ``` Deben quedar dos bloques con UNA sola entrada cada uno: ```yaml 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 ```bash cat > ~/supabase-project/.gitignore << 'EOF' .env .env.local volumes/db/data/ volumes/storage/ volumes/logs/ *.log EOF ``` ### 1.4 Push al repo Git ```bash 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) ```bash 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: ```bash 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 1. Projects -> seleccionar proyecto -> Create Service -> Docker Compose. 2. Provider: Gitea (o GitHub) -> seleccionar el repo del paso 1.4. 3. Branch: master | Compose Path: ./docker-compose.yml 4. IMPORTANTE: desactivar Autodeploy (toggle OFF). ### 3.2 Variables de entorno (pestaña Environment) Pegar el bloque completo reemplazando los valores: ```env POSTGRES_PASSWORD= POSTGRES_HOST=db POSTGRES_DB=postgres POSTGRES_PORT=5432 JWT_SECRET= JWT_EXPIRY=3600 ANON_KEY= SERVICE_ROLE_KEY= SUPABASE_ANON_KEY= SUPABASE_SERVICE_KEY= DASHBOARD_USERNAME=admin DASHBOARD_PASSWORD= KONG_HTTP_PORT=8000 KONG_HTTPS_PORT=8443 SECRET_KEY_BASE= VAULT_ENC_KEY= PG_META_CRYPTO_KEY= 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: ```bash 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: " | 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) ```bash ssh administrator@ "mkdir -p ~/migrations" scp -r supabase/migrations/ administrator@:~/migrations/ ``` ### 4.2 Aplicar en orden ```bash 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 ```bash 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 ```bash 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 ```bash 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) ```bash 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= SUPABASE_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