---
title: "Autoaloja Convex en 2026: Guía de Docker Compose y Dokploy"
description: "Aprende a autoalojar Convex en tu VPS con Docker Compose o Dokploy. Cubre SQLite, PostgreSQL, MySQL, proxy inverso, almacenamiento S3 y métricas de Prometheus."
date: 2026-10-21
categories: ["self-hosting"]
tags: ["convex","docker","dokploy"]
---

import Button from "@components/widgets/Button.astro";
import Notice from "@components/widgets/Notice.astro";
import ListCheck from "@components/widgets/ListCheck.astro";
import Accordion from "@components/widgets/Accordion.astro";
import Tabs from "@components/widgets/Tabs.astro";
import Tab from "@components/widgets/Tab.astro";
import YouTubeEmbed from "@components/widgets/YouTubeEmbed.astro";

Si estás construyendo aplicaciones en tiempo real con bases de datos, probablemente te has topado con [Convex](https://go.bitdoze.com/convex). Es una plataforma backend-as-a-service que combina una base de datos en tiempo real con funciones serverless. El nivel gratuito de la nube te da 0,5 GB de almacenamiento de base de datos y 1M de llamadas a funciones al mes, lo que funciona para proyectos pequeños. Autoalojarlo elimina esos límites y te da control sobre tus datos y tu infraestructura.

La vía autoalojada ahora es oficial. Convex liberó el código del backend en noviembre de 2025, y el repositorio (`convex/convex-backend`) ronda las 12.700 estrellas en GitHub con lanzamientos `precompiled-*` casi semanales. El paquete npm `convex` está en la versión 1.47.0 con alrededor de 1,1M de descargas semanales. Los mantenedores afirman que el repositorio público se sincroniza con su código interno en cuestión de días, así que no estás ejecutando un fork abandonado.

En esta guía te muestro cómo autoalojar Convex usando Dokploy (más sencillo) o Docker Compose (más control). SQLite cubre instalaciones pequeñas; PostgreSQL o MySQL cubre producción. Un VPS de 5-10 $ es suficiente para un despliegue en solitario.

## ¿Qué es Convex?

[Convex](https://go.bitdoze.com/convex) es una plataforma de backend que combina bases de datos, servidores de API y capas de caché en un solo sistema. Gestiona la sincronización de datos en tiempo real, funciones serverless y caché mediante una API de TypeScript. La versión autoalojada es el mismo código que ejecuta la nube, con licencia FSL 1.1, que se convierte en Apache 2.0 completa dos años después de cada lanzamiento.

### Características clave de Convex

<ListCheck>

- **Base de datos en tiempo real**: Sincronización automática de datos entre todos los clientes con consultas reactivas
- **TypeScript primero**: Seguridad de tipos de extremo a extremo, del backend al frontend
- **Funciones serverless**: Escribe consultas, mutaciones y acciones en TypeScript sin gestionar servidores
- **Programación integrada**: Trabajos cron y funciones programadas sin servicios externos
- **Almacenamiento de archivos**: Subida y almacenamiento de archivos integrados con distribución CDN
- **Búsqueda de texto completo**: Capacidades de búsqueda nativas sin Elasticsearch
- **Autenticación**: Sistema de autenticación flexible compatible con varios proveedores
- **Transacciones atómicas**: Garantías ACID para la consistencia de los datos
- **Viaje en el tiempo**: Consulta datos históricos y depura con consultas retrospectivas
- **Búsqueda vectorial**: Base de datos vectorial integrada para aplicaciones de IA

</ListCheck>

### ¿Por qué autoalojar Convex?

**Ventajas del autoalojamiento**:

- Eres dueño de los datos y controlas dónde residen
- Sin límites de ancho de banda ni de ejecución de funciones
- Personaliza la infraestructura y el escalado
- Menores costes a gran escala
- Ejecútalo on-premises o en redes privadas

**Los planes en la nube en 2026** son así: Free y Starter incluyen 1M de llamadas a funciones, 0,5 GB de almacenamiento de base de datos y 1 GB de almacenamiento de archivos, y después facturan los excedentes con pago por uso. Professional empieza en 25 $ por desarrollador al mes y añade 25M de llamadas a funciones, 50 GB de almacenamiento de base de datos, copias de seguridad diarias y dominios personalizados. Business y Enterprise empiezan en 2.500 $ al mes para SAML/SSO, SLA y despliegues dedicados.

<Notice type="info" title="Cuándo compensa autoalojar">
La nube de pago empieza en 25 $/desarrollador/mes. Un VPS de 5-10 $ cubre un despliegue autoalojado en solitario. Autoaloja cuando llegues a los límites del nivel gratuito, necesites controlar la residencia de los datos o quieras una sola máquina de producción en lugar de una factura por asiento. Para ver cifras de una prueba de carga real, consulta [nuestras benchmarks de Convex autoalojado frente a la nube](/convex-self-hosted-benchmark/).
</Notice>

### Cómo funciona Convex autoalojado

Un despliegue autoalojado son tres servicios de Convex más tu frontend. El backend escucha en el puerto 3210 para la API y la sincronización de consultas. Las acciones HTTP van en el puerto 3211. El dashboard escucha en el 6791 y se comunica con el backend a través de una URL accesible desde el navegador. Los datos viven en un archivo SQLite dentro de un volumen de Docker o en una instancia externa de Postgres/MySQL. La CLI `npx convex` sube funciones y lee datos usando una clave de administrador.

<figure>
<svg viewBox="0 0 900 340" width="100%" role="img" aria-label="Arquitectura de Convex autoalojado: la app y la CLI npx convex llegan al backend por el puerto 3210, el dashboard corre en el 6791, las acciones HTTP corren en el 3211, y el backend guarda los datos en un volumen SQLite o en una base de datos Postgres o MySQL externa" font-family="system-ui, -apple-system, sans-serif">
  <defs>
    <marker id="cvxArchArrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
      <path d="M0,0 L8,4 L0,8 Z" fill="#4F46E5" />
    </marker>
    <marker id="cvxArchArrowGray" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
      <path d="M0,0 L8,4 L0,8 Z" fill="#64748B" />
    </marker>
  </defs>
  <rect x="0" y="0" width="900" height="340" rx="12" fill="#F8FAFC" />
  <rect x="30" y="30" width="200" height="80" rx="10" fill="#ffffff" stroke="#CBD5E1" />
  <text x="130" y="62" text-anchor="middle" font-size="14" font-weight="600" fill="#0F172A">Tu app / navegador</text>
  <text x="130" y="84" text-anchor="middle" font-size="11" fill="#64748B">consultas, mutaciones, acciones HTTP</text>
  <rect x="30" y="140" width="200" height="70" rx="10" fill="#EEF2FF" stroke="#4F46E5" />
  <text x="130" y="170" text-anchor="middle" font-size="14" font-weight="600" fill="#0F172A">npx convex CLI</text>
  <text x="130" y="192" text-anchor="middle" font-size="11" fill="#4F46E5">deploy + export con clave admin</text>
  <rect x="330" y="30" width="220" height="80" rx="10" fill="#ffffff" stroke="#4F46E5" stroke-width="2" />
  <text x="440" y="62" text-anchor="middle" font-size="14" font-weight="600" fill="#0F172A">Convex backend :3210</text>
  <text x="440" y="84" text-anchor="middle" font-size="11" fill="#4F46E5">CONVEX_CLOUD_ORIGIN</text>
  <rect x="330" y="140" width="220" height="70" rx="10" fill="#ffffff" stroke="#CBD5E1" />
  <text x="440" y="170" text-anchor="middle" font-size="14" font-weight="600" fill="#0F172A">Acciones HTTP :3211</text>
  <text x="440" y="192" text-anchor="middle" font-size="11" fill="#64748B">CONVEX_SITE_ORIGIN</text>
  <rect x="330" y="240" width="220" height="70" rx="10" fill="#ffffff" stroke="#CBD5E1" />
  <text x="440" y="270" text-anchor="middle" font-size="14" font-weight="600" fill="#0F172A">Dashboard :6791</text>
  <text x="440" y="292" text-anchor="middle" font-size="11" fill="#64748B">NEXT_PUBLIC_DEPLOYMENT_URL</text>
  <rect x="660" y="70" width="210" height="90" rx="10" fill="#FEF3C7" stroke="#D97706" />
  <text x="765" y="105" text-anchor="middle" font-size="14" font-weight="600" fill="#0F172A">Almacenamiento</text>
  <text x="765" y="128" text-anchor="middle" font-size="11" fill="#92400E">Volumen SQLite, o</text>
  <text x="765" y="145" text-anchor="middle" font-size="11" fill="#92400E">Postgres / MySQL</text>
  <line x1="230" y1="70" x2="330" y2="70" stroke="#4F46E5" stroke-width="2" marker-end="url(#cvxArchArrow)" />
  <line x1="230" y1="175" x2="330" y1="175" stroke="#4F46E5" stroke-width="2" marker-end="url(#cvxArchArrow)" />
  <line x1="230" y1="100" x2="310" y2="100" stroke="#CBD5E1" stroke-width="2" />
  <path d="M310,100 L310,275 L330,275" fill="none" stroke="#CBD5E1" stroke-width="2" marker-end="url(#cvxArchArrowGray)" />
  <text x="262" y="290" text-anchor="middle" font-size="10" fill="#64748B">el navegador llega al dashboard</text>
  <line x1="550" y1="115" x2="660" y2="115" stroke="#D97706" stroke-width="2" marker-end="url(#cvxArchArrow)" />
  <text x="605" y="105" text-anchor="middle" font-size="10" fill="#92400E">datos</text>
</svg>
<figcaption class="mt-2 text-center text-sm text-gray-500 dark:text-gray-400">Tres servicios de Convex y un backend de almacenamiento. Las variables de entorno deciden bajo qué hostname se publica cada puerto.</figcaption>
</figure>

<YouTubeEmbed
  url="https://www.youtube.com/embed/rEcOmzYyb1g"
  label="Autoaloja Convex en tu propio servidor: guía completa paso a paso"
/>

## Requisitos previos

Necesitarás:

<ListCheck>

- **Un VPS o servidor**: 2 GB de RAM mínimo, 2 núcleos de CPU (4 GB de RAM recomendados para PostgreSQL)
- **Un nombre de dominio**: Tres hostnames es la configuración limpia (`api`, `actions`, `dashboard` bajo tu dominio)
- **Docker instalado**: Docker y el plugin `docker compose` (Dokploy incluye Docker)
- **Conocimientos básicos de línea de comandos**: Para ejecutar comandos
- **openssl**: Para generar `INSTANCE_SECRET`
- **Node.js 18+ y npx**: La CLI de Convex lo necesita para despliegues, exportaciones y el trabajo con claves de administrador
- **Registros DNS A**: Apunta los hostnames al VPS antes del paso del proxy inverso

</ListCheck>

<Button text="Prueba Hetzner Cloud ahora" link="https://go.bitdoze.com/hetzner" variant="solid" color="blue" size="lg" external={true} icon="rocket-launch" />
<Button text="Prueba el VPS de Hostinger" link="https://go.bitdoze.com/hostinger-vps" variant="solid" color="green" size="lg" external={true} icon="rocket-launch" />

_Los precios de los VPS subieron en todos los proveedores en 2026. Si estás reconsiderando una máquina alquilada, mira [qué cambió y cuándo gana un mini PC](/es/subidas-precios-vps/)._

<Notice type="info" title="Recomendaciones de hosting">
Para desarrollo, un VPS básico con 2 GB de RAM funciona bien con SQLite. Para producción, usa 4 GB de RAM con PostgreSQL alojado en la misma región. Hetzner, [DigitalOcean](https://go.bitdoze.com/do) y AWS funcionan igual de bien. Decide tus nombres de dominio ahora: cambiar `CONVEX_CLOUD_ORIGIN` o `CONVEX_SITE_ORIGIN` más tarde implica reiniciar el backend y volver a desplegar tus funciones.
</Notice>

## Opción 1: Desplegar con Dokploy (el método más fácil)

Dokploy es una Plataforma como Servicio de código abierto para desplegar aplicaciones, e incluye una plantilla de Convex. Uso Dokploy por defecto para despliegues en un solo servidor porque gestiona Traefik, SSL y redespliegues sin herramientas adicionales. Si quieres la comparativa de las opciones PaaS, lee [la comparación de Coolify vs Dokploy vs Kamal 2](/es/coolify-vs-dokploy-vs-kamal-2/). Si aún no has configurado Dokploy, usa la [guía de instalación de Dokploy](/dokploy-install/).

### Método A: Usar la plantilla integrada de Dokploy (recomendado)

Esta es la forma más rápida de desplegar Convex. La plantilla te deja funcionando en minutos, pero actualmente está desactualizada, así que editarás el archivo compose antes de desplegar. Si eres nuevo en las apps basadas en compose dentro de Dokploy, [cómo desplegar una app Docker Compose en Dokploy](/dokploy-docker-compose-app/) cubre el flujo genérico.

**Paso 1: Instala Dokploy** (si aún no está instalado)

```sh
curl -sSL https://dokploy.com/install.sh | sh
```

Accede a Dokploy en `http://tu-ip-vps:3000` y completa la configuración.

**Paso 2: Despliega desde la plantilla**

1. Inicia sesión en el dashboard de Dokploy
2. Haz clic en «**Create Project**» y ponle nombre (por ejemplo, «Convex»)
3. Haz clic en «**Templates**» en la barra lateral izquierda
4. Busca «**Convex**» en la galería de plantillas
5. Haz clic en «**Deploy**» en la plantilla de Convex

**Paso 3: Sustituye el archivo Compose**

<Notice type="warning" title="La plantilla de Dokploy está desactualizada">
El blueprint oficial todavía fija imágenes del antiguo namespace `ghcr.io/get-convex`, usa la variable eliminada `ACTIONS_USER_TIMEOUT_SECS`, arrastra una `DATABASE_URL` heredada y solo incluye SQLite. El namespace antiguo ahora rechaza las descargas anónimas. Abre la pestaña «**Docker Compose**» antes de desplegar y pega el compose corregido de abajo.
</Notice>

Este es el compose que yo ejecutaría desde la pestaña Docker Compose de Dokploy:

```yaml
services:
  backend:
    image: ghcr.io/convex/convex-backend:latest   # pin backend+dashboard to the SAME SHA for prod
    volumes:
      - data:/convex/data
    environment:
      - INSTANCE_NAME=${INSTANCE_NAME:-convex-self-hosted}
      - INSTANCE_SECRET=${INSTANCE_SECRET}
      - CONVEX_CLOUD_ORIGIN=${CONVEX_CLOUD_ORIGIN:-http://127.0.0.1:3210}
      - CONVEX_SITE_ORIGIN=${CONVEX_SITE_ORIGIN:-http://127.0.0.1:3211}
      - POSTGRES_URL=${POSTGRES_URL:-}
      - DISABLE_BEACON=${DISABLE_BEACON:-true}
      - V8_ACTION_USER_TIMEOUT_SECS=${V8_ACTION_USER_TIMEOUT_SECS:-}
      - NODE_ACTION_USER_TIMEOUT_SECS=${NODE_ACTION_USER_TIMEOUT_SECS:-}
      - DISABLE_METRICS_ENDPOINT=${DISABLE_METRICS_ENDPOINT:-true}
      - DOCUMENT_RETENTION_DELAY=${DOCUMENT_RETENTION_DELAY:-172800}
      - RUST_LOG=${RUST_LOG:-info}
      - DO_NOT_REQUIRE_SSL=${DO_NOT_REQUIRE_SSL:-1}
    healthcheck:
      test: curl -f http://localhost:3210/version
      interval: 5s
      start_period: 5s

  dashboard:
    image: ghcr.io/convex/convex-dashboard:latest  # must match backend version
    environment:
      - NEXT_PUBLIC_DEPLOYMENT_URL=${NEXT_PUBLIC_DEPLOYMENT_URL:-http://127.0.0.1:3210}
      - NEXT_PUBLIC_LOAD_MONACO_INTERNALLY=${NEXT_PUBLIC_LOAD_MONACO_INTERNALLY:-}
    depends_on:
      backend:
        condition: service_healthy
```

<Notice type="info" title="Flexibilidad de la plantilla">
La plantilla de Dokploy es un punto de partida. Edita la configuración de Docker Compose y las variables de entorno para que coincidan con este fragmento, y luego añade el servicio Postgres de la Opción 2 si quieres una base de datos de producción.
</Notice>

<Notice type="info" title="SQLite vs PostgreSQL vs MySQL">
Deja `POSTGRES_URL` y `MYSQL_URL` vacías y Convex usará SQLite. Para producción, define `POSTGRES_URL` para PostgreSQL o `MYSQL_URL` para MySQL. El soporte de MySQL se añadió en febrero de 2025.
</Notice>

### Configura las variables de entorno (Dokploy)

Ve a la pestaña «**Environment**» y añade estas variables.

**Obligatorias:**

- `INSTANCE_SECRET`: Genérala con `openssl rand -hex 32`

<Notice type="warning" title="INSTANCE_SECRET es tu credencial raíz">
Las claves de administrador se derivan de `INSTANCE_NAME` + `INSTANCE_SECRET`. Si pierdes el secreto no podrás generar nuevas claves de administrador. Si lo rotas, todas las claves de administrador existentes se rompen. Guárdalo en tu gestor de contraseñas y en tus copias de seguridad.
</Notice>

**URLs**: elige una opción.

**Opción A: Los dominios Traefik gratuitos de Dokploy** (inicio rápido)

```sh
NEXT_PUBLIC_DEPLOYMENT_URL=http://shhosted-convex-cf33fb-91-98-95-196.traefik.me
CONVEX_CLOUD_ORIGIN=http://shhosted-convex-cf33fb-91-98-95-196.traefik.me
CONVEX_SITE_ORIGIN=http://shhosted-convex-59a34c-91-98-95-196.traefik.me
```

`CONVEX_SITE_ORIGIN` es un hostname distinto de los otros dos. Debe llegar al puerto 3211 (acciones HTTP), no al 3210.

**Opción B: Tu propio dominio** (producción)

Configura primero los registros DNS A y después:

```sh
CONVEX_CLOUD_ORIGIN=https://api.convex.yourdomain.com
CONVEX_SITE_ORIGIN=https://actions.convex.yourdomain.com
NEXT_PUBLIC_DEPLOYMENT_URL=https://api.convex.yourdomain.com
```

`CONVEX_CLOUD_ORIGIN` y `NEXT_PUBLIC_DEPLOYMENT_URL` apuntan al hostname que llega al 3210. `CONVEX_SITE_ORIGIN` apunta al hostname que llega al 3211. Si `NEXT_PUBLIC_DEPLOYMENT_URL` se queda en `127.0.0.1`, el dashboard cargará en tu navegador pero todas las consultas se quedarán colgadas.

**PostgreSQL** (opcional, déjalo vacío para usar SQLite):

```sh
DB_PASSWORD=your-strong-password
POSTGRES_URL=postgresql://postgres:your-strong-password@postgres:5432
```

Pasa `POSTGRES_URL` sin nombre de base de datos y sin parámetros de consulta. Convex nombra la base de datos según la instancia, mapeando `-` a `_`, así que la instancia por defecto `convex-self-hosted` usa la base de datos `convex_self_hosted`.

### Despliega, genera la clave de administrador y verifica

1. En la pestaña «**Domains**», añade tus hostnames. El Traefik de Dokploy emite los certificados SSL.
2. Haz clic en «**Deploy**» y espera a que los servicios estén saludables.
3. Abre la «**Terminal**» en el contenedor del backend y ejecuta:

```sh
cd convex
./generate_admin_key.sh
```

4. Guarda la clave de administrador de forma segura. Trátala como una contraseña root.

Verifica el despliegue antes de conectar cualquier app:

<ListCheck>

- Dokploy muestra el servicio backend como saludable
- `curl http://localhost:3210/version` desde el VPS devuelve un JSON con la versión
- El dashboard carga en `:6791` o en tu dominio del dashboard
- La clave de administrador está guardada y empieza por `convex-self-hosted|` (o lo que hayas puesto en `INSTANCE_NAME`)

</ListCheck>

Si `/version` no responde, el backend no está levantado. Lee `docker compose logs backend` o el panel de logs de Dokploy antes de tocar nada más.

## Opción 2: Desplegar solo con Docker Compose

Si prefieres desplegar sin Dokploy o quieres más control, usa Docker Compose directamente.

### Paso 1: Prepara tu servidor

Actualiza el sistema e instala Docker. El instalador oficial incluye el plugin de Compose, así que no hay nada extra que instalar:

```sh
# Update packages
sudo apt update && sudo apt upgrade -y

# Install Docker (includes the docker compose plugin)
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# Confirm compose is available
docker compose version
```

`docker-compose` (con guion) es la antigua herramienta v1 y está en fin de vida. Todo lo siguiente usa `docker compose` (con espacio).

### Paso 2: Crea el directorio del proyecto

```sh
mkdir -p ~/convex-backend
cd ~/convex-backend
```

### Paso 3: Crea el archivo Docker Compose

Elige una de las dos variantes y guárdala como `docker-compose.yml` (o `compose.yml`).

<Tabs>
  <Tab name="SQLite (sencillo)">

```yaml
services:
  backend:
    image: ghcr.io/convex/convex-backend:latest
    stop_grace_period: 10s
    stop_signal: SIGINT
    ports:
      - "3210:3210"
      - "3211:3211"
    volumes:
      - data:/convex/data
    environment:
      - INSTANCE_NAME=${INSTANCE_NAME:-convex-self-hosted}
      - INSTANCE_SECRET=${INSTANCE_SECRET}
      - CONVEX_CLOUD_ORIGIN=${CONVEX_CLOUD_ORIGIN:-http://127.0.0.1:3210}
      - CONVEX_SITE_ORIGIN=${CONVEX_SITE_ORIGIN:-http://127.0.0.1:3211}
      - V8_ACTION_USER_TIMEOUT_SECS=${V8_ACTION_USER_TIMEOUT_SECS:-}
      - NODE_ACTION_USER_TIMEOUT_SECS=${NODE_ACTION_USER_TIMEOUT_SECS:-}
      - DISABLE_METRICS_ENDPOINT=${DISABLE_METRICS_ENDPOINT:-true}
      - DOCUMENT_RETENTION_DELAY=${DOCUMENT_RETENTION_DELAY:-172800}
      - RUST_LOG=${RUST_LOG:-info}
      - DISABLE_BEACON=${DISABLE_BEACON:-false}
    healthcheck:
      test: curl -f http://localhost:3210/version
      interval: 5s
      start_period: 10s

  dashboard:
    image: ghcr.io/convex/convex-dashboard:latest
    stop_grace_period: 10s
    stop_signal: SIGINT
    ports:
      - "6791:6791"
    environment:
      - NEXT_PUBLIC_DEPLOYMENT_URL=${NEXT_PUBLIC_DEPLOYMENT_URL:-http://127.0.0.1:3210}
      - NEXT_PUBLIC_LOAD_MONACO_INTERNALLY=${NEXT_PUBLIC_LOAD_MONACO_INTERNALLY:-}
    depends_on:
      backend:
        condition: service_healthy

volumes:
  data:
```

  </Tab>
  <Tab name="PostgreSQL (producción)">

```yaml
services:
  backend:
    image: ghcr.io/convex/convex-backend:latest
    stop_grace_period: 10s
    stop_signal: SIGINT
    ports:
      - "3210:3210"
      - "3211:3211"
    environment:
      - INSTANCE_NAME=${INSTANCE_NAME:-convex-self-hosted}
      - INSTANCE_SECRET=${INSTANCE_SECRET}
      - CONVEX_CLOUD_ORIGIN=${CONVEX_CLOUD_ORIGIN:-http://127.0.0.1:3210}
      - CONVEX_SITE_ORIGIN=${CONVEX_SITE_ORIGIN:-http://127.0.0.1:3211}
      - POSTGRES_URL=${POSTGRES_URL}
      - DO_NOT_REQUIRE_SSL=${DO_NOT_REQUIRE_SSL:-1}
      - V8_ACTION_USER_TIMEOUT_SECS=${V8_ACTION_USER_TIMEOUT_SECS:-}
      - NODE_ACTION_USER_TIMEOUT_SECS=${NODE_ACTION_USER_TIMEOUT_SECS:-}
      - DISABLE_METRICS_ENDPOINT=${DISABLE_METRICS_ENDPOINT:-true}
      - DOCUMENT_RETENTION_DELAY=${DOCUMENT_RETENTION_DELAY:-172800}
      - RUST_LOG=${RUST_LOG:-info}
      - DISABLE_BEACON=${DISABLE_BEACON:-false}
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: curl -f http://localhost:3210/version
      interval: 5s
      start_period: 10s

  dashboard:
    image: ghcr.io/convex/convex-dashboard:latest
    stop_grace_period: 10s
    stop_signal: SIGINT
    ports:
      - "6791:6791"
    environment:
      - NEXT_PUBLIC_DEPLOYMENT_URL=${NEXT_PUBLIC_DEPLOYMENT_URL:-http://127.0.0.1:3210}
      - NEXT_PUBLIC_LOAD_MONACO_INTERNALLY=${NEXT_PUBLIC_LOAD_MONACO_INTERNALLY:-}
    depends_on:
      backend:
        condition: service_healthy

  postgres:
    image: postgres:17-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=convex_self_hosted
    volumes:
      - postgres-data:/var/lib/postgresql/data
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  postgres-data:
```

  </Tab>
</Tabs>

Si el puerto 3210, 3211 o 6791 ya está ocupado en el host, compruébalo con `ss -tlnp | grep 3210`. Los puertos del lado del host se pueden cambiar con las variables de entorno `PORT`, `SITE_PROXY_PORT` y `DASHBOARD_PORT` en el backend y el dashboard.

### Paso 4: Crea el archivo de entorno

```sh
nano .env
```

Añade tu configuración:

```sh
# Instance Configuration
INSTANCE_NAME=convex-self-hosted
INSTANCE_SECRET=<generate-with-openssl-rand-hex-32>

# Public URLs (update these for production)
CONVEX_CLOUD_ORIGIN=http://127.0.0.1:3210
CONVEX_SITE_ORIGIN=http://127.0.0.1:3211
NEXT_PUBLIC_DEPLOYMENT_URL=http://127.0.0.1:3210

# PostgreSQL (only if using the PostgreSQL variant)
DB_PASSWORD=<your-secure-db-password>
POSTGRES_URL=postgresql://postgres:${DB_PASSWORD}@postgres:5432
DO_NOT_REQUIRE_SSL=1

# Optional: Logging
RUST_LOG=info

# Optional: Disable telemetry beacon
DISABLE_BEACON=false
```

Genera el secreto:

```sh
openssl rand -hex 32
```

`DO_NOT_REQUIRE_SSL=1` es para configuraciones locales o con Postgres sin SSL. Mantén el SSL obligatorio (`DO_NOT_REQUIRE_SSL=0`) para Postgres gestionado a través de internet público.

### Paso 5: Arranca Convex

```sh
# Start services
docker compose up -d

# View logs
docker compose logs -f

# Check status
docker compose ps
```

Verifica de inmediato:

```sh
curl http://localhost:3210/version
```

Busca una respuesta JSON con la versión del release. `docker compose ps` debería mostrar ambos servicios como `healthy`. Si el backend está atrapado en un bucle de reinicios, `docker compose logs backend` es lo primero que hay que leer. Causas habituales: un `INSTANCE_SECRET` ausente o mal formado, una URL de Postgres inalcanzable desde el contenedor, o un conflicto de puertos.

### Paso 6: Configura el proxy inverso con Nginx

Para producción con dominios personalizados, pon Nginx delante de los tres puertos. Cada hostname corresponde exactamente a un puerto del backend, y cada variable de entorno apunta a un hostname.

<figure>
<svg viewBox="0 0 900 300" width="100%" role="img" aria-label="Mapa de dominios del proxy inverso: api.yourdomain.com hace proxy al puerto 3210 y lo referencian CONVEX_CLOUD_ORIGIN y NEXT_PUBLIC_DEPLOYMENT_URL, actions.yourdomain.com hace proxy al puerto 3211 y lo referencia CONVEX_SITE_ORIGIN, dashboard.yourdomain.com hace proxy al puerto 6791" font-family="system-ui, -apple-system, sans-serif">
  <defs>
    <marker id="cvxProxyArrow" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
      <path d="M0,0 L8,4 L0,8 Z" fill="#4F46E5" />
    </marker>
  </defs>
  <rect x="0" y="0" width="900" height="300" rx="12" fill="#F8FAFC" />
  <rect x="30" y="30" width="240" height="70" rx="10" fill="#EEF2FF" stroke="#4F46E5" />
  <text x="150" y="58" text-anchor="middle" font-size="13" font-weight="600" fill="#0F172A">api.yourdomain.com</text>
  <text x="150" y="80" text-anchor="middle" font-size="11" fill="#4F46E5">CONVEX_CLOUD_ORIGIN</text>
  <text x="150" y="95" text-anchor="middle" font-size="10" fill="#64748B">NEXT_PUBLIC_DEPLOYMENT_URL</text>
  <rect x="30" y="115" width="240" height="70" rx="10" fill="#EEF2FF" stroke="#4F46E5" />
  <text x="150" y="143" text-anchor="middle" font-size="13" font-weight="600" fill="#0F172A">actions.yourdomain.com</text>
  <text x="150" y="165" text-anchor="middle" font-size="11" fill="#4F46E5">CONVEX_SITE_ORIGIN</text>
  <text x="150" y="180" text-anchor="middle" font-size="10" fill="#64748B">acciones HTTP</text>
  <rect x="30" y="200" width="240" height="70" rx="10" fill="#EEF2FF" stroke="#4F46E5" />
  <text x="150" y="228" text-anchor="middle" font-size="13" font-weight="600" fill="#0F172A">dashboard.yourdomain.com</text>
  <text x="150" y="250" text-anchor="middle" font-size="11" fill="#4F46E5">acceso desde el navegador</text>
  <text x="150" y="265" text-anchor="middle" font-size="10" fill="#64748B">sin variable de entorno, solo DNS</text>
  <rect x="470" y="30" width="200" height="70" rx="10" fill="#ffffff" stroke="#CBD5E1" />
  <text x="570" y="58" text-anchor="middle" font-size="13" font-weight="600" fill="#0F172A">Nginx / Traefik</text>
  <text x="570" y="80" text-anchor="middle" font-size="11" fill="#64748B">TLS + upgrade WebSocket</text>
  <rect x="720" y="30" width="150" height="70" rx="10" fill="#ffffff" stroke="#4F46E5" stroke-width="2" />
  <text x="795" y="58" text-anchor="middle" font-size="13" font-weight="600" fill="#0F172A">backend :3210</text>
  <text x="795" y="80" text-anchor="middle" font-size="11" fill="#64748B">API + sincronización</text>
  <rect x="720" y="115" width="150" height="70" rx="10" fill="#ffffff" stroke="#4F46E5" stroke-width="2" />
  <text x="795" y="143" text-anchor="middle" font-size="13" font-weight="600" fill="#0F172A">actions :3211</text>
  <text x="795" y="165" text-anchor="middle" font-size="11" fill="#64748B">acciones HTTP</text>
  <rect x="720" y="200" width="150" height="70" rx="10" fill="#ffffff" stroke="#4F46E5" stroke-width="2" />
  <text x="795" y="228" text-anchor="middle" font-size="13" font-weight="600" fill="#0F172A">dashboard :6791</text>
  <text x="795" y="250" text-anchor="middle" font-size="11" fill="#64748B">dashboard de Convex</text>
  <line x1="270" y1="65" x2="470" y2="65" stroke="#4F46E5" stroke-width="2" marker-end="url(#cvxProxyArrow)" />
  <line x1="270" y1="150" x2="470" y2="120" stroke="#4F46E5" stroke-width="2" marker-end="url(#cvxProxyArrow)" />
  <line x1="270" y1="235" x2="470" y2="120" stroke="#4F46E5" stroke-width="2" marker-end="url(#cvxProxyArrow)" />
  <line x1="670" y1="65" x2="720" y2="65" stroke="#4F46E5" stroke-width="2" marker-end="url(#cvxProxyArrow)" />
  <line x1="670" y1="120" x2="720" y2="150" stroke="#4F46E5" stroke-width="2" marker-end="url(#cvxProxyArrow)" />
  <line x1="670" y1="120" x2="720" y2="235" stroke="#4F46E5" stroke-width="2" marker-end="url(#cvxProxyArrow)" />
</svg>
<figcaption class="mt-2 text-center text-sm text-gray-500 dark:text-gray-400">Un hostname por puerto. Apunta cada variable de entorno al hostname, nunca a 127.0.0.1, una vez que tengas una URL pública.</figcaption>
</figure>

```sh
sudo apt install nginx certbot python3-certbot-nginx -y
sudo nano /etc/nginx/sites-available/convex
```

Pega tres bloques server:

```nginx
# Backend API -> 3210
server {
    listen 80;
    server_name api.yourdomain.com;

    location / {
        proxy_pass http://localhost:3210;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        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;
    }
}

# HTTP actions -> 3211
server {
    listen 80;
    server_name actions.yourdomain.com;

    location / {
        proxy_pass http://localhost:3211;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        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;
    }
}

# Dashboard -> 6791
server {
    listen 80;
    server_name dashboard.yourdomain.com;

    location / {
        proxy_pass http://localhost:6791;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        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;
    }
}
```

Habilita el sitio y obtén los certificados para los tres hostnames:

```sh
sudo ln -s /etc/nginx/sites-available/convex /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d api.yourdomain.com -d actions.yourdomain.com -d dashboard.yourdomain.com
```

Actualiza `.env` con las URLs públicas y recrea el stack:

```sh
CONVEX_CLOUD_ORIGIN=https://api.yourdomain.com
CONVEX_SITE_ORIGIN=https://actions.yourdomain.com
NEXT_PUBLIC_DEPLOYMENT_URL=https://api.yourdomain.com
```

```sh
docker compose up -d
```

Fallos típicos en este paso: el dashboard carga pero los datos nunca llegan, lo que casi siempre significa que `NEXT_PUBLIC_DEPLOYMENT_URL` sigue apuntando a `127.0.0.1` y el navegador no puede alcanzarlo. Acciones HTTP que devuelven 404 suele significar que falta el vhost del 3211 o que `CONVEX_SITE_ORIGIN` apunta al hostname del 3210.

### Paso 7: Genera la clave de administrador

```sh
# Access backend container
docker compose exec backend /bin/sh

# Navigate to convex directory and generate admin key
cd convex
./generate_admin_key.sh

# Exit container
exit
```

<Notice type="warning" title="La clave de administrador da acceso total">
El script deriva la clave de `INSTANCE_NAME` + `INSTANCE_SECRET`. Mismo secreto, misma familia de claves. La clave otorga acceso total a la API de administración, así que trátala como una contraseña root y consulta la sección de seguridad antes de pegarla en cualquier sitio.
</Notice>

Guarda la clave de administrador de forma segura. La necesitarás para tus proyectos.

## Usa Convex en tus proyectos

Con tu backend de Convex autoalojado en marcha, conecta tus aplicaciones. Mantén la CLI al día: `npm install convex@latest` (1.47.0 a octubre de 2026). Las CLI antiguas fallan contra un backend autoalojado actual de formas confusas.

### Configura el entorno de tu proyecto

Añade estas dos variables de entorno al archivo `.env.local` de tu aplicación (no lo subas a git):

```sh
CONVEX_SELF_HOSTED_URL=https://api.convex.yourdomain.com
CONVEX_SELF_HOSTED_ADMIN_KEY=convex-self-hosted|<your-admin-key>
```

Sustituye la URL por la URL real de tu backend (el hostname que llega al 3210) y la clave de administrador por la que generaste antes. La CLI de Convex usa tu backend autoalojado cuando estas variables están definidas.

Prueba toda la cadena:

<ListCheck>

- `npx convex --version` muestra una CLI reciente
- `CONVEX_SELF_HOSTED_URL` incluye el esquema (`https://`) y la clave de administrador es la cadena completa
- `npx convex deploy` sube tus funciones sin errores
- El dashboard muestra las funciones desplegadas y los datos que escribiste

</ListCheck>

Si los comandos `npx convex` fallan, comprueba primero la versión de la CLI, luego confirma que la URL tiene esquema y que la clave no está truncada. Con Postgres en el backend, `docker compose logs backend` debería mostrar `Connected to Postgres` tras el arranque, y debes ejecutar `npx convex deploy` una vez después de cambiar de base de datos para que las funciones lleguen a la nueva.

¿Quieres construir algo sobre esto? [Crea una app en tiempo real con Astro y Convex](/astro-convex-realtime-app/) recorre la parte del frontend, y [despliega Astro + Convex en Vercel para producción](/astro-convex-vercel-deployment/) cubre la vía del frontend alojado.

## Configuración avanzada

### Usar PostgreSQL externo (Neon, Supabase, AWS RDS)

Para PostgreSQL gestionado:

1. Crea una base de datos llamada `convex_self_hosted`
2. Obtén la cadena de conexión (sin nombre de base de datos ni parámetros de consulta)
3. Actualiza el entorno:

```sh
POSTGRES_URL=postgresql://user:pass@ep-xxx.aws.neon.tech:5432
DO_NOT_REQUIRE_SSL=0
```

Una URL de Postgres gestionado tiene esta pinta: `postgresql://user:pass@ep-xxx.aws.neon.tech:5432` para Neon o `postgresql://user:pass@db.xxxxxx.us-east-1.rds.amazonaws.com:5432` para RDS. No añadas `/convex_self_hosted` ni `?sslmode=...`. Convex deriva el nombre de la base de datos de `INSTANCE_NAME` y abre su propia conexión.

<Notice type="warning" title="Se requiere la misma región">
Coloca tu backend de Convex en la misma región que tu base de datos PostgreSQL. La latencia entre regiones se refleja directamente en la latencia de las mutaciones.
</Notice>

El ángulo del coste: el nivel gratuito de Neon es suficiente para empezar, y empaquetar `postgres:17-alpine` en el mismo VPS no cuesta nada extra. Si empaquetas Postgres, las copias de seguridad son tu problema. Si quieres un contenedor Postgres separado que puedas inspeccionar, [ejecuta PostgreSQL y pgAdmin en Docker](/deploy-pgvector-pgadmin-docker/).

### Usar MySQL (y PlanetScale)

Convex soporta MySQL desde febrero de 2025. Define la variable de entorno `MYSQL_URL`:

```sh
MYSQL_URL=mysql://user:password@aws.connect.psdb.cloud
```

La base de datos debe llamarse `convex_self_hosted`, la misma regla que con PostgreSQL. PlanetScale es el ejemplo documentado de MySQL gestionado; AWS RDS o cualquier servidor MySQL 8+ también funciona. Se aplica el mismo consejo de región: mantén la base de datos cerca del backend de Convex.

### Monitorización con Prometheus

El backend autoalojado expone un endpoint de métricas compatible con Prometheus en `/metrics`. El compose oficial lo trae deshabilitado, así que define esto para exponerlo:

```sh
DISABLE_METRICS_ENDPOINT=false
```

Verifica la recolección:

```sh
curl http://localhost:3210/metrics | head
```

Si obtienes un 404, la variable sigue en `true`. El endpoint te da contadores de peticiones, histogramas de latencia y tasas de error para Grafana o cualquier stack compatible con Prometheus. Para monitorización a nivel de host en paralelo, [monitoriza tu servidor con Beszel y Uptime Kuma](/beszel-uptime-kuma/).

### Ajustar la concurrencia y los timeouts de las acciones

El backend expone variables de entorno para controlar cuántas consultas, mutaciones y acciones se ejecutan al mismo tiempo:

```sh
APPLICATION_MAX_CONCURRENT_QUERIES=16
APPLICATION_MAX_CONCURRENT_MUTATIONS=16
APPLICATION_MAX_CONCURRENT_V8_ACTIONS=16
APPLICATION_MAX_CONCURRENT_NODE_ACTIONS=16
```

El valor por defecto es 16 para cada una, que coincide con el límite de consultas concurrentes del nivel gratuito de la nube. Auméntalos si tu servidor tiene CPU y memoria de sobra.

Los timeouts de las acciones se dividen por runtime:

```sh
V8_ACTION_USER_TIMEOUT_SECS=1800    # V8 actions, default 1800 (30 min)
NODE_ACTION_USER_TIMEOUT_SECS=600   # Node actions, default 600 (10 min)
HTTP_SERVER_TIMEOUT_SECONDS=300     # HTTP server, default 300 (5 min)
```

Si tus acciones agotan el tiempo de espera, sube el parámetro correspondiente. No dejes la antigua `ACTIONS_USER_TIMEOUT_SECS` en tu compose. Fue eliminada y dividida en el par de arriba.

### Configurar el almacenamiento S3

Para almacenamiento de archivos en producción, descarga los blobs a S3 en lugar del volumen local:

```yaml
services:
  backend:
    environment:
      # ... other vars ...
      - AWS_REGION=us-east-1
      - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
      - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
      - AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}
      - S3_STORAGE_EXPORTS_BUCKET=convex-snapshot-exports
      - S3_STORAGE_SNAPSHOT_IMPORTS_BUCKET=convex-snapshot-imports
      - S3_STORAGE_MODULES_BUCKET=convex-modules
      - S3_STORAGE_FILES_BUCKET=convex-user-files
      - S3_STORAGE_SEARCH_BUCKET=convex-search-indexes
```

Crea los buckets en AWS S3 o usa almacenamiento compatible con S3 como Cloudflare R2:

```sh
S3_ENDPOINT_URL=https://<account-id>.r2.cloudflarestorage.com
AWS_ACCESS_KEY_ID=<your-r2-access-key>
AWS_SECRET_ACCESS_KEY=<your-r2-secret-key>
```

Para MinIO, DigitalOcean Spaces y otros almacenes compatibles con S3, los parámetros de compatibilidad importan: `AWS_S3_FORCE_PATH_STYLE=true` para el direccionamiento path-style, `AWS_S3_DISABLE_CHECKSUMS=true` y `AWS_S3_DISABLE_SSE=true` para almacenes que rechazan las cabeceras checksum y SSE más nuevas.

### Migración entre proveedores de almacenamiento

Si cambias de SQLite a PostgreSQL o modificas la configuración de S3:

```sh
# Export data from old backend
npx convex export --path backup.zip

# Deploy new backend with different storage
# ...

# Import data to new backend
npx convex import --replace-all backup.zip
```

Las exportaciones son instantáneas de un momento dado, así que haz la exportación después de que pare el tráfico si necesitas hasta la última escritura.

### Dominios personalizados para las acciones HTTP

Para servir acciones HTTP desde un dominio personalizado:

1. Configura el DNS de `actions.yourdomain.com` y haz proxy al puerto 3211 (ver la configuración de Nginx de arriba)
2. Configura el entorno del backend:

```sh
CONVEX_SITE_ORIGIN=https://actions.yourdomain.com
```

`CONVEX_SITE_ORIGIN` debe ser la URL que llega al puerto 3211, no al 3210. Es la configuración errónea más común de esta sección.

3. En tu frontend, sobrescribe la URL del sitio en tiempo de compilación si la generada es incorrecta:

```sh
CONVEX_SITE_URL=https://actions.yourdomain.com
```

### Notas sobre el dashboard y el beacon

Dos datos operativos menores que ahorran tickets de soporte:

1. El editor Monaco del dashboard se carga desde una CDN por defecto. En una red restringida o aislada define `NEXT_PUBLIC_LOAD_MONACO_INTERNALLY=true` para que el dashboard funcione sin acceso saliente a la CDN.
2. El beacon de telemetría es opcional. Define `DISABLE_BEACON=true` para apagarlo.

<Notice type="info" title="Qué envía el beacon">
Con el beacon habilitado, el backend envía un identificador de despliegue aleatorio, la versión de la migración de la base de datos, la revisión de git y el tiempo de actividad. Nada de datos, consultas ni esquema. El proyecto es explícito sobre esto en la sección Disclaimers del repositorio, que es el tipo de transparencia que quiero antes de dejarlo encendido o apagarlo.
</Notice>

### Referencia de variables de entorno clave

| Variable | Valor por defecto | Qué hace |
|---|---|---|
| `INSTANCE_NAME` | `convex-self-hosted` | Identificador de instancia; también deriva el nombre de la base de datos |
| `INSTANCE_SECRET` | (obligatoria) | Credencial raíz para generar claves de administrador |
| `CONVEX_CLOUD_ORIGIN` | `http://127.0.0.1:3210` | URL pública de la API del backend (puerto 3210) |
| `CONVEX_SITE_ORIGIN` | `http://127.0.0.1:3211` | URL pública de las acciones HTTP (puerto 3211) |
| `NEXT_PUBLIC_DEPLOYMENT_URL` | `http://127.0.0.1:3210` | URL que usa el navegador para llegar al backend |
| `POSTGRES_URL` | (vacía = SQLite) | Postgres externo, sin nombre de bd ni parámetros |
| `MYSQL_URL` | (vacía) | MySQL externo, la bd debe ser `convex_self_hosted` |
| `DO_NOT_REQUIRE_SSL` | `1` en el compose oficial | Pon `0` para exigir TLS a la base de datos |
| `V8_ACTION_USER_TIMEOUT_SECS` | `1800` | Timeout para acciones V8 |
| `NODE_ACTION_USER_TIMEOUT_SECS` | `600` | Timeout para acciones Node |
| `HTTP_SERVER_TIMEOUT_SECONDS` | `300` | Timeout de peticiones del servidor HTTP |
| `APPLICATION_MAX_CONCURRENT_QUERIES` | `16` | Límite de consultas concurrentes |
| `APPLICATION_MAX_CONCURRENT_MUTATIONS` | `16` | Límite de mutaciones concurrentes |
| `APPLICATION_MAX_CONCURRENT_V8_ACTIONS` | `16` | Límite de acciones V8 concurrentes |
| `APPLICATION_MAX_CONCURRENT_NODE_ACTIONS` | `16` | Límite de acciones Node concurrentes |
| `DISABLE_METRICS_ENDPOINT` | `true` | Pon `false` para exponer `/metrics` |
| `DISABLE_BEACON` | `false` en la mayoría de los compose | Pon `true` para desactivar la telemetría |
| `DOCUMENT_RETENTION_DELAY` | `172800` (2 días) en el compose oficial | Cuánto tiempo se conservan las versiones antiguas de los documentos |
| `PORT` / `SITE_PROXY_PORT` / `DASHBOARD_PORT` | `3210` / `3211` / `6791` | Sustituciones de puertos del lado del host |
| `NEXT_PUBLIC_LOAD_MONACO_INTERNALLY` | (vacía) | Pon `true` para un dashboard sin acceso a CDN |

## Mantenimiento y copias de seguridad

### Copias de seguridad periódicas

**Para despliegues con Dokploy**, configura copias de seguridad automatizadas desde la interfaz de Dokploy o sigue la [guía de copias de seguridad de Dokploy](/dokploy-backups-cloudflare-r2/).

**Para Docker Compose con PostgreSQL**:

```sh
# Manual backup
docker compose exec postgres pg_dump -U postgres convex_self_hosted > backup-$(date +%Y%m%d).sql

# Restore backup
cat backup-20241124.sql | docker compose exec -T postgres psql -U postgres convex_self_hosted
```

**Script de copia de seguridad automatizada** (`backup.sh`):

```bash
#!/bin/bash
BACKUP_DIR="/backups/convex"
DATE=$(date +%Y%m%d-%H%M)
mkdir -p $BACKUP_DIR

docker compose exec -T postgres pg_dump -U postgres convex_self_hosted | gzip > $BACKUP_DIR/convex-$DATE.sql.gz

# Keep only last 7 days
find $BACKUP_DIR -name "convex-*.sql.gz" -mtime +7 -delete
```

Dale permisos de ejecución y añádelo al crontab:

```sh
chmod +x backup.sh
crontab -e
# Add: 0 2 * * * /path/to/backup.sh
```

**La copia de seguridad a nivel de aplicación** funciona tanto con SQLite como con Postgres y es lo que quieres para migraciones:

```sh
npx convex export --path backup.zip
npx convex env list --include-values > .env.convex
```

Conserva los dos tipos. El volcado SQL sirve para restaurar a nivel de infraestructura en el mismo stack. `backup.zip` más `.env.convex` restaura en cualquier instancia, incluida una máquina SQLite sencilla o Convex Cloud. Sube ambos a S3 o R2 fuera del sitio (unos 1-5 $/mes) para que un VPS muerto no sea un proyecto muerto.

### Actualizar Convex

No hagas simplemente `pull` y cruces los dedos. La vía oficial de actualización es primero la copia de seguridad.

**Antes que nada:**

```sh
npx convex export --path backup.zip
npx convex env list --include-values > .env.convex
```

<Notice type="warning" title="Fija ambas imágenes al mismo SHA">
No se garantiza que versiones distintas de `convex-backend` y `convex-dashboard` sean compatibles. Las imágenes se etiquetan con `latest` y con SHAs de commit completos de 40 caracteres. Los releases se llaman `precompiled-YYYY-MM-DD-<shortsha>`, por ejemplo `precompiled-2026-10-10-72ce13c` en la [página de releases](https://github.com/convex/convex-backend/releases). Para producción:

```yaml
image: ghcr.io/convex/convex-backend:72ce13c...   # full commit SHA
image: ghcr.io/convex/convex-dashboard:72ce13c... # SAME SHA
```
</Notice>

**Actualización en el lugar** (caso normal):

```sh
# Pin both images to the same new SHA, then
docker compose pull && docker compose up -d

# Watch migrations finish
docker compose logs -f backend
# wait for: Executing Migration N/N. MigrationComplete(N+1)
```

No cortes el tráfico ni des por terminada la actualización hasta ver la línea `MigrationComplete`. Si el backend está en medio de una migración, déjalo terminar.

**Actualización por exportación/importación** (saltos de versión mayores):

1. Detén el tráfico externo
2. `npx convex export`
3. `npx convex env list --include-values > .env.convex`
4. Actualiza las imágenes (ambas al mismo SHA)
5. `npx convex import --replace-all`
6. `npx convex env set --force --from-file .env.convex`
7. Restaura el tráfico

Las exportaciones son de un momento dado. Tu exportación final debe ocurrir después de que cese la carga.

**Rollback:** restaura `backup.zip` con `npx convex import --replace-all` sobre las imágenes fijadas anteriormente. Con Postgres, restaurar el volcado SQL previo a la actualización también funciona. Si aún no se ejecutó ninguna migración, volver al SHA fijado anterior en el compose y ejecutar `docker compose up -d` es suficiente.

**Vía Dokploy:** redespliega desde la UI del servicio. Consulta [cómo actualizar stacks de Docker Compose en Dokploy](/dokploy-update-docker-compose/) para los detalles de actualizaciones por etiqueta frente a SHA fijado.

Lista de verificación de la actualización:

<ListCheck>

- `backup.zip` previo a la actualización exportado y verificado
- `.env.convex` guardado
- Ambas imágenes fijadas al mismo SHA nuevo
- Línea `MigrationComplete` vista en el log tras el arranque
- Prueba de humo superada (dashboard, una consulta, una mutación)

</ListCheck>

### Borrar datos / reiniciar una instancia

<Notice type="error" title="Esto destruye datos">
Haz primero una copia de seguridad. Comprueba dos veces que `CONVEX_SELF_HOSTED_URL` apunta al despliegue que quieres borrar, no a producción.
</Notice>

No existe intencionadamente un comando de reset destructivo. Borrar los datos significa importar una instantánea vacía en cada tabla:

```sh
# List tables
npx convex data

# Clear one table
npx convex import --table $tableName --replace --format jsonLines /dev/null -y
```

Repite eso para los nombres de tabla que devuelve `npx convex data`. Para una reconstrucción completa, destruir el volumen o la base de datos Postgres y volver a ejecutar `npx convex deploy` suele ser más limpio que borrar tabla por tabla.

## Buenas prácticas de seguridad

<ListCheck>

- **INSTANCE_SECRET es la credencial raíz**: perderlo significa no poder crear nuevas claves de administrador; rotarlo invalida todas las claves de administrador existentes. Guárdalo en tu gestor de contraseñas y en tus copias de seguridad. Consulta [secretos de Docker Compose y alternativas seguras](/docker-compose-secrets/) para mejores patrones que un `.env` en texto plano
- **La clave de administrador es acceso total a la API de administración**: trátala como una contraseña root, no la acotes, y nunca la incluyas en código del frontend
- **Solo HTTPS**: TLS en todos los hostnames públicos
- **Cierra los puertos del backend**: una vez que el proxy funciona, solo 80 y 443 deberían estar abiertos. Bloquea 3210, 3211 y 6791 en el firewall
- **Contraseñas de base de datos fuertes**: contraseñas complejas para PostgreSQL y MySQL, incluso en una red Docker privada
- **Actualizaciones regulares**: mantén Convex, Docker y los paquetes del sistema actualizados con el procedimiento de actualización con copia previa de arriba
- **Cifrado de copias de seguridad**: cifra las copias de seguridad de la base de datos y las exportaciones en reposo, sobre todo si acaban en S3
- **Los archivos de entorno fuera de git**: `.env` y `.env.local` nunca se suben al repositorio
- **Rota las claves de administrador de forma deliberada**: solo cuando puedas actualizar el `.env.local` de cada app en la misma ventana
- **Aislamiento de red**: usa redes de Docker para aislar los servicios de contenedores no relacionados en la máquina
- **Monitoriza logs y puertos**: configura monitorización de logs y alertas sobre la superficie expuesta

</ListCheck>

Rotar `INSTANCE_SECRET` es una operación de mantenimiento, no una tarea casual. Todas las claves de administrador derivadas del secreto antiguo se rompen, así que regenera con `generate_admin_key.sh` y actualiza el `.env.local` de cada app en la misma ventana. Para la parte del firewall del host y la detección de intrusiones, [protege tu VPS con CrowdSec](/crowdsec-secure-server/).

## Solución de problemas comunes

| Síntoma | Causa probable | Solución |
|---|---|---|
| `docker pull ghcr.io/get-convex/...` denegado o no encontrado | El namespace de imágenes se movió | Usa `ghcr.io/convex/convex-backend` y `ghcr.io/convex/convex-dashboard` |
| El dashboard carga pero las consultas se cuelgan | `NEXT_PUBLIC_DEPLOYMENT_URL` apunta a `127.0.0.1` | Pon la URL que el navegador pueda alcanzar (tu hostname `api.`) |
| Bucle de reinicios del backend | `INSTANCE_SECRET` incorrecto, Postgres inalcanzable o conflicto de puertos | `docker compose logs backend`; comprueba `ss -tlnp \| grep 3210` |
| Acciones HTTP 404 | No hay vhost para el puerto 3211, o `CONVEX_SITE_ORIGIN` apunta al 3210 | Añade el bloque server de `actions.` y apunta `CONVEX_SITE_ORIGIN` a él |
| No aparece `Connected to Postgres` en los logs | Forma incorrecta de `POSTGRES_URL`, nombre de bd incorrecto o requisito de SSL | Sin nombre de bd ni parámetros; bd llamada `convex_self_hosted`; `DO_NOT_REQUIRE_SSL=1` para Postgres local sin SSL |
| Las acciones agotan el tiempo antes de tiempo | Parámetros de timeout antiguos o por defecto | Define `V8_ACTION_USER_TIMEOUT_SECS`, `NODE_ACTION_USER_TIMEOUT_SECS`, `HTTP_SERVER_TIMEOUT_SECONDS` |
| `/metrics` devuelve 404 | El endpoint de métricas está deshabilitado por defecto | Define `DISABLE_METRICS_ENDPOINT=false` |
| El editor del dashboard no carga sin conexión | Monaco se carga desde una CDN | Define `NEXT_PUBLIC_LOAD_MONACO_INTERNALLY=true` |

Si nada de esto encaja, el canal `#self-hosted` del [Discord de Convex](https://discord.gg/convex) está activo y merece la pena preguntar ahí.

## Conclusión

Autoalojar Convex te da control sobre tu infraestructura de backend en tiempo real manteniendo la experiencia de desarrollador de Convex. Ya sea con Dokploy o con Docker Compose, puedes tener un despliegue de Convex listo para producción en una sesión. Convex se encarga de las consultas en tiempo real, las funciones serverless, el almacenamiento de archivos y la búsqueda. El autoalojamiento añade control de la infraestructura a esas funciones.

Convex liberó su backend en noviembre de 2025 y levantó una Serie B de 57M $ liderada por Insight Partners en agosto de 2026. El proyecto se desarrolla activamente, y la versión autoalojada sigue al código de la nube con pocos días de diferencia. Ese es el argumento de frescura del autoalojamiento: no estás apostando por un proyecto secundario.

Para aplicaciones pequeñas, SQLite funciona bien con recursos mínimos. Cuando necesites escalar, pasa a PostgreSQL o MySQL, fija los SHAs de tus imágenes y mantén las copias de seguridad fuera del sitio.

### Próximos pasos

<ListCheck>

- Lee la [documentación de Convex](https://docs.convex.dev/) para funciones avanzadas
- Configura copias de seguridad automatizadas con la [guía de copias de seguridad de Dokploy](/dokploy-backups-cloudflare-r2/)
- Construye sobre ello con [el tutorial en tiempo real de Astro y Convex](/astro-convex-realtime-app/)
- Luego [despliega Astro + Convex en Vercel](/astro-convex-vercel-deployment/) si el frontend va alojado
- Únete al canal `#self-hosted` del [Discord de Convex](https://discord.gg/convex) para ayuda
- Mueve el almacenamiento de archivos a S3 antes de que se llene el disco (ver Configurar el almacenamiento S3)
- Vigila el disco, la memoria y `/metrics` desde el primer día, no después del primer incidente

</ListCheck>

<Button link="https://go.bitdoze.com/convex" text="Saber más sobre Convex" />

¿Preguntas sobre autoalojar Convex? Deja un comentario abajo.

## Preguntas frecuentes

<Accordion label="¿Debería usar SQLite o PostgreSQL?" group="faq" expanded="true">
**SQLite** funciona bien para:

- Desarrollo y pruebas
- Aplicaciones pequeñas y medianas (menos de 10k peticiones/día)
- Despliegues en un solo servidor
- Prototipos

**PostgreSQL** es mejor cuando:

- Tienes cargas de producción con mucho tráfico
- Quieres funciones de base de datos gestionada y replicación
- Usas servicios gestionados de bases de datos (Neon, Supabase, AWS RDS)

Empieza con SQLite y pasa a PostgreSQL cuando necesites más fiabilidad. El flujo de exportación/importación hace que ese cambio sea aburrido, que es lo que quieres.
</Accordion>

<Accordion label="¿Puedo migrar de Convex en la nube a autoalojado?" group="faq">
Sí. El proceso es exportar e importar:

1. Exporta los datos de la nube: `npx convex export --prod`
2. Configura el backend autoalojado
3. Importa los datos: `npx convex import --replace-all`
4. Actualiza las variables de entorno en tu frontend
5. Vuelve a desplegar las funciones: `npx convex deploy`

El código de tu aplicación no cambia. Solo cambian la URL del backend y la clave de administrador.
</Accordion>

<Accordion label="¿Cuánto cuesta autoalojar Convex?" group="faq">
**Costes mensuales** (ejemplo):

- VPS con 4 GB de RAM (clase Hetzner): 5-10 $/mes
- PostgreSQL en Neon: nivel gratuito, o `postgres:17-alpine` empaquetado en la misma máquina por 0 $
- Dominio: unos 1 $/mes
- Almacenamiento S3 (opcional): unos 1-5 $/mes

**Total**: 10-30 $/mes todo incluido.

Compáralo con Convex Professional a 25 $ por desarrollador al mes. Para un operador en solitario, autoalojar gana en coste si tu tiempo es gratis. Para un equipo, el producto gestionado suele ser más barato que las horas de soporte. Las cifras de una prueba de carga real están en [nuestras benchmarks de Convex autoalojado frente a la nube](/convex-self-hosted-benchmark/).
</Accordion>

<Accordion label="¿Puedo usar Convex autoalojado en producción?" group="faq">
Sí. Asegúrate de:

- Usar PostgreSQL o MySQL en lugar de SQLite
- Configurar copias de seguridad automatizadas (volcado SQL más `npx convex export`)
- Configurar la monitorización
- Usar SSL/TLS
- Fijar las imágenes del backend y del dashboard al mismo SHA
- Usar el procedimiento de actualización con copia previa de la sección Mantenimiento
- Ejecutar en un entorno de alojamiento fiable

Las pautas de producción de este artículo cubren el resto.
</Accordion>

<Accordion label="¿Soporta Convex autoalojado todas las funciones de la nube?" group="faq">
Convex autoalojado soporta todas las funciones del nivel gratuito del producto en la nube: consultas y mutaciones en tiempo real, funciones serverless, almacenamiento de archivos, funciones programadas, búsqueda de texto completo, búsqueda vectorial y acciones HTTP. El producto en la nube está optimizado para escala. Tú te encargas de la gestión de la infraestructura: copias de seguridad, escalado y mantenimiento.
</Accordion>

<Accordion label="¿Cómo escalo Convex autoalojado?" group="faq">
Escala verticalmente:

- Aumenta los recursos del VPS (CPU/RAM)
- Pasa a una instancia de PostgreSQL gestionado más grande
- Descarga los archivos a S3

No hay una historia oficial de múltiples instancias para el backend autoalojado. Es un único servicio con estado, así que no pongas varios backends detrás de un balanceador de carga. Ejecuta una máquina más grande y deja que Postgres haga el trabajo pesado. Si se te queda pequeño un nodo, el producto gestionado es el siguiente paso.
</Accordion>

<Accordion label="¿Puedo usar Convex Auth con autoalojado?" group="faq">
Convex Auth funciona con despliegues autoalojados. Sigue las [instrucciones de configuración manual](https://labs.convex.dev/auth/setup/manual) en la documentación de Convex Auth. La configuración automática de la CLI aún no soporta autoalojado.
</Accordion>

<Accordion label="¿Qué pasa si mi backend se cae?" group="faq">
Con una configuración adecuada:

- Docker reinicia los contenedores caídos
- Los datos de SQLite persisten en el volumen, los de Postgres en el suyo
- Las suscripciones en tiempo real se reconectan automáticamente
- Las operaciones recientes pueden necesitar reintento por parte de los clientes

Para alta disponibilidad:

- Monitoriza con [Beszel y Uptime Kuma](/beszel-uptime-kuma/)
- Mantén PostgreSQL en un servicio gestionado con replicación si lo necesitas
- Mantén copias de seguridad automatizadas y una ruta de restauración documentada

Un solo VPS es un único punto de fallo. Presupuesta para eso en lugar de fingir lo contrario.
</Accordion>