Supabase es una alternativa de código abierto a Firebase construida sobre PostgreSQL: combina base de datos, API REST generada automáticamente (PostgREST), autenticación (GoTrue), almacenamiento de archivos, Realtime y el panel Studio. En este tutorial desplegarás Supabase en Ubuntu 24.04 con Docker Compose, generarás tus propias claves, lo publicarás detrás de Nginx con HTTPS y comprobarás la API con una tabla protegida por Row Level Security.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 4 GB de RAM (8 GB recomendados), 2 vCPU y 40 GB de disco. Supabase levanta más de diez contenedores.
- Un usuario no root con privilegios
sudo. - Un subdominio con un registro A apuntando al servidor. En esta guía se usa
supabase.tudominio.com. - Docker Engine y el plugin de Docker Compose instalados desde el repositorio oficial de Docker. Esta guía usa
!overrideen un archivo de Compose, que requiere Docker Compose 2.24.4 o posterior.
Comprueba la versión de Compose:
sudo docker compose version
Docker Compose version v2.39.2
Paso 1: Descargar los archivos de Supabase
La configuración oficial para autoalojar Supabase está en el directorio docker del repositorio del proyecto. Clónalo y copia esos archivos a un directorio de trabajo propio, para poder actualizar el repositorio sin pisar tu configuración:
sudo apt update
sudo apt install git
git clone --depth 1 https://github.com/supabase/supabase
mkdir ~/supabase-project
cp -rf supabase/docker/* ~/supabase-project
cp supabase/docker/.env.example ~/supabase-project/.env
cd ~/supabase-project
Comprueba que tienes el archivo de Compose, el .env y el directorio de volúmenes:
ls -a
. .. .env docker-compose.yml volumes ...
Paso 2: Generar secretos y claves JWT
El .env de ejemplo trae contraseñas y claves públicas conocidas por todo el mundo. Nunca arranques Supabase con ellas en un servidor accesible desde Internet.
ANON_KEY y SERVICE_ROLE_KEY son tokens JWT firmados con JWT_SECRET. Crea un pequeño script en Python (incluido en Ubuntu 24.04) que los genere:
nano ~/gen-jwt.py
import base64, hashlib, hmac, json, sys, time
def b64url(data):
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
secret, role = sys.argv[1], sys.argv[2]
now = int(time.time())
header = {"alg": "HS256", "typ": "JWT"}
payload = {"role": role, "iss": "supabase", "iat": now, "exp": now + 5 * 365 * 24 * 3600}
signing_input = ".".join(
b64url(json.dumps(part, separators=(",", ":")).encode()) for part in (header, payload)
)
signature = hmac.new(secret.encode(), signing_input.encode(), hashlib.sha256).digest()
print(f"{signing_input}.{b64url(signature)}")
En la misma sesión de terminal, genera los secretos y las dos claves:
JWT_SECRET=$(openssl rand -hex 32)
ANON_KEY=$(python3 ~/gen-jwt.py "$JWT_SECRET" anon)
SERVICE_ROLE_KEY=$(python3 ~/gen-jwt.py "$JWT_SECRET" service_role)
Escribe todos los valores en .env. La contraseña de PostgreSQL usa solo caracteres hexadecimales porque forma parte de URLs de conexión:
sed -i \
-e "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(openssl rand -hex 24)|" \
-e "s|^JWT_SECRET=.*|JWT_SECRET=${JWT_SECRET}|" \
-e "s|^ANON_KEY=.*|ANON_KEY=${ANON_KEY}|" \
-e "s|^SERVICE_ROLE_KEY=.*|SERVICE_ROLE_KEY=${SERVICE_ROLE_KEY}|" \
-e "s|^DASHBOARD_PASSWORD=.*|DASHBOARD_PASSWORD=$(openssl rand -hex 16)|" \
-e "s|^SECRET_KEY_BASE=.*|SECRET_KEY_BASE=$(openssl rand -hex 32)|" \
-e "s|^VAULT_ENC_KEY=.*|VAULT_ENC_KEY=$(openssl rand -hex 16)|" \
.env
Comprueba que los valores han cambiado:
grep -E '^(POSTGRES_PASSWORD|JWT_SECRET|DASHBOARD_USERNAME|DASHBOARD_PASSWORD)=' .env
POSTGRES_PASSWORD=5f0c3b9e8a...
JWT_SECRET=9d41e6c07b...
DASHBOARD_USERNAME=supabase
DASHBOARD_PASSWORD=a73be21f04...
Anota DASHBOARD_PASSWORD y ANON_KEY: los necesitarás para entrar en Studio y para probar la API. Si tu .env contiene otras variables de secreto de ejemplo (por ejemplo PG_META_CRYPTO_KEY o LOGFLARE_PRIVATE_ACCESS_TOKEN), sustitúyelas también por valores generados con openssl rand -hex 32.
Importantesi más adelante cambias
JWT_SECRET, tendrás que regenerarANON_KEYySERVICE_ROLE_KEYcon el nuevo secreto. Con claves firmadas por otro secreto, todas las peticiones fallarán con errores de JWT.
Paso 3: Configurar las URLs públicas
Supabase necesita conocer la URL por la que se accede a la API y la URL de tu aplicación, a la que GoTrue redirige tras un inicio de sesión o una confirmación de correo. Edita .env:
nano .env
Ajusta estas variables con tus dominios:
SITE_URL=https://app.tudominio.com
API_EXTERNAL_URL=https://supabase.tudominio.com
SUPABASE_PUBLIC_URL=https://supabase.tudominio.com
Para que los correos de confirmación y recuperación de contraseña funcionen, completa también las variables SMTP_ADMIN_EMAIL, SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS y SMTP_SENDER_NAME con los datos de tu servidor de correo.
Paso 4: Limitar los puertos publicados a localhost
Por defecto, Compose publica la API (Kong, puertos 8000 y 8443) y el pooler de PostgreSQL (Supavisor, puertos 5432 y 6543) en todas las interfaces. Docker gestiona sus propias reglas de iptables, así que UFW no los bloquea. Como Nginx será la única entrada pública, crea un archivo de override que los publique solo en 127.0.0.1:
nano docker-compose.override.yml
services:
kong:
ports: !override
- "127.0.0.1:8000:8000/tcp"
- "127.0.0.1:8443:8443/tcp"
supavisor:
ports: !override
- "127.0.0.1:5432:5432"
- "127.0.0.1:6543:6543"
docker compose carga docker-compose.override.yml automáticamente, y !override sustituye la lista de puertos original en lugar de añadirse a ella.
Paso 5: Arrancar Supabase
Descarga las imágenes (varios GB, tarda unos minutos) y arranca los servicios:
sudo docker compose pull
sudo docker compose up -d
Espera un minuto y comprueba el estado. Todos los contenedores deben aparecer como running y los que tienen comprobación de salud como healthy:
sudo docker compose ps --format 'table {{.Name}}\t{{.Status}}'
NAME STATUS
supabase-auth Up 2 minutes (healthy)
supabase-db Up 2 minutes (healthy)
supabase-kong Up 2 minutes (healthy)
supabase-rest Up 2 minutes
supabase-storage Up 2 minutes (healthy)
supabase-studio Up 2 minutes (healthy)
...
Verifica que los puertos solo escuchan en localhost:
sudo ss -tlnp | grep -E ':(8000|5432|6543) '
Todas las líneas deben mostrar 127.0.0.1 como dirección local. Prueba el servicio de autenticación a través de Kong con tu ANON_KEY (en la misma sesión en la que la generaste):
curl -s http://127.0.0.1:8000/auth/v1/health -H "apikey: ${ANON_KEY}"
{"version":"v2.177.0","name":"GoTrue","description":"GoTrue is a user registration and authentication API"}
Paso 6: Publicar Supabase con Nginx y HTTPS
Instala Nginx y Certbot, y abre los puertos web en UFW:
sudo apt install nginx certbot python3-certbot-nginx
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
Crea el sitio de Nginx. Todo el tráfico va a Kong, que enruta internamente a la API REST, Auth, Storage, Realtime y Studio. Las cabeceras Upgrade y Connection son necesarias para los WebSockets de Realtime:
sudo nano /etc/nginx/sites-available/supabase
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name supabase.tudominio.com;
client_max_body_size 50m;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
}
}
Activa el sitio, comprueba la sintaxis y recarga Nginx:
sudo ln -s /etc/nginx/sites-available/supabase /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Solicita el certificado. Certbot añade el bloque HTTPS y la redirección desde HTTP:
sudo certbot --nginx -d supabase.tudominio.com
Abre https://supabase.tudominio.com en el navegador. Kong te pedirá usuario y contraseña: usa DASHBOARD_USERNAME (por defecto supabase) y DASHBOARD_PASSWORD. Verás el panel Studio.
Paso 7: Crear una tabla y probar la API REST
En Studio, abre SQL Editor y ejecuta este SQL. Crea una tabla, activa Row Level Security y añade una política que permite leer las filas con la clave anónima:
create table public.notas (
id bigint generated always as identity primary key,
texto text not null,
creada_en timestamptz not null default now()
);
alter table public.notas enable row level security;
create policy "lectura publica de notas"
on public.notas for select
to anon
using (true);
insert into public.notas (texto) values ('Hola desde Supabase');
PostgREST expone la tabla automáticamente en /rest/v1/notas. Consúltala desde tu equipo:
curl -s "https://supabase.tudominio.com/rest/v1/notas?select=*" \
-H "apikey: TU_ANON_KEY"
[{"id":1,"texto":"Hola desde Supabase","creada_en":"2026-09-25T10:12:31.482+00:00"}]
Prueba ahora a insertar con la clave anónima. Como no existe ninguna política insert, RLS lo rechaza:
curl -s -X POST "https://supabase.tudominio.com/rest/v1/notas" \
-H "apikey: TU_ANON_KEY" \
-H "Content-Type: application/json" \
-d '{"texto":"intento anónimo"}'
{"code":"42501","details":null,"hint":null,"message":"new row violates row-level security policy for table \"notas\""}
Así debe comportarse: la ANON_KEY se usa en el navegador y solo puede hacer lo que permitan tus políticas. La SERVICE_ROLE_KEY se salta RLS, así que guárdala solo en tu backend.
Paso 8: Mantenimiento básico
Para aplicar cambios en .env, recrea los contenedores. docker compose restart no vuelve a leer el archivo:
cd ~/supabase-project
sudo docker compose up -d
Haz una copia de seguridad lógica de la base de datos con pg_dump dentro del contenedor:
sudo docker exec supabase-db pg_dump -U postgres -d postgres -Fc > supabase-$(date +%F).dump
Para actualizar, revisa las notas de versión de Supabase, actualiza el repositorio clonado, compara su docker-compose.yml con el tuyo y después ejecuta:
sudo docker compose pull
sudo docker compose up -d
Solución de problemas
Algún contenedor se queda en unhealthy o reiniciándose. Consulta sus logs usando el nombre del servicio de Compose, por ejemplo sudo docker compose logs db --tail 50 o sudo docker compose logs auth --tail 50. Con menos de 4 GB de RAM es habitual que el sistema mate procesos por falta de memoria; compruébalo con sudo dmesg | grep -i oom.
La API responde Invalid authentication credentials o JWSInvalidSignature. ANON_KEY y SERVICE_ROLE_KEY no están firmadas con el JWT_SECRET actual. Regenera ambas con el script del paso 2 y ejecuta sudo docker compose up -d.
Si cambiaste POSTGRES_PASSWORD después del primer arranque, los servicios no conectan a la base de datos. La contraseña se fija al inicializar el volumen de datos en volumes/db/data; cambiar .env después no la actualiza. En una instalación nueva sin datos puedes parar todo con sudo docker compose down -v, borrar volumes/db/data y arrancar de nuevo.
Conclusión
Tienes Supabase autoalojado en Ubuntu 24.04, con secretos propios, la API y Studio publicados solo a través de Nginx con HTTPS, y una tabla protegida por Row Level Security. Como siguientes pasos, configura el SMTP para los correos de autenticación, programa el pg_dump con cron y copia los volcados fuera del servidor, y conecta tu aplicación con el cliente @supabase/supabase-js usando https://supabase.tudominio.com y tu ANON_KEY.
