← Blog

Auto-hospedar AFFiNE, una alternativa de código abierto a Notion

Railyard

17 de agosto de 2026 · 7 min de lectura

Sigue esto en tu propio servidor. Empieza gratis, sin tarjeta

AFFiNE es un espacio de trabajo de código abierto que combina un editor de documentos al estilo Notion, una pizarra infinita y una base de datos ligera en una sola app, con tu contenido quedando en un servidor que tú controlas en lugar de la nube de un proveedor. También es un auto-hospedaje genuinamente no trivial: una base de datos Postgres con la extensión pgvector, Redis, y un trabajo de migración de una sola vez que tiene que terminar antes de que la app misma pueda arrancar. Al final de esta guía tendrás todo eso corriendo en tu propio servidor, detrás de HTTPS, con copias de seguridad y una actualización repetible.

Lo que necesitas

  • Un servidor con Docker y el plugin de Compose. Un Hetzner CX23 (2 vCPU, 4 GB de RAM, 40 GB NVMe, 5,99 €/mes sin IVA) corre AFFiNE cómodamente para un espacio de trabajo personal o de un equipo pequeño.
  • Un dominio o subdominio apuntado a la dirección IP del servidor.
  • Acceso SSH al servidor.
  • Unos diez minutos, la mayoría esperando el DNS y la primera ejecución de la migración.

Paso 1: apuntar el DNS al servidor y abrir el firewall

Crea un registro A para tu subdominio apuntando a la IP pública del servidor, y luego asegura el servidor dejando pasar solo lo que necesitas:

ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw --force enable

Paso 2: generar una contraseña de Postgres

openssl rand -hex 24
mkdir -p /opt/affine
cat > /opt/affine/.env <<'EOF'
POSTGRES_PASSWORD=paste-the-generated-value-here
EOF
chmod 600 /opt/affine/.env

Paso 3: escribir el archivo Compose

Esta es la parte que la mayoría de las guías de auto-hospedaje de AFFiNE se equivocan: el archivo Compose oficial corre la migración de la base de datos como su propio servicio de vida corta, no como un paso dentro del arranque de la app principal, y el contenedor de la app tiene que esperar a que ese trabajo realmente termine, no solo a que arranque, antes de iniciar.

services:
  affine:
    image: ghcr.io/toeverything/affine:0.26.7
    restart: unless-stopped
    depends_on:
      redis:
        condition: service_healthy
      db:
        condition: service_healthy
      affine-migration:
        condition: service_completed_successfully
    environment:
      DATABASE_URL: postgresql://affine:${POSTGRES_PASSWORD}@db:5432/affine
      REDIS_SERVER_HOST: redis
      AFFINE_SERVER_EXTERNAL_URL: https://notes.yourdomain.com
      AFFINE_INDEXER_ENABLED: "false"
    volumes:
      - affine_storage:/root/.affine/storage
      - affine_config:/root/.affine/config
    expose:
      - "3010"

  affine-migration:
    image: ghcr.io/toeverything/affine:0.26.7
    command: ["node", "./scripts/self-host-predeploy.js"]
    environment:
      DATABASE_URL: postgresql://affine:${POSTGRES_PASSWORD}@db:5432/affine
      REDIS_SERVER_HOST: redis
      AFFINE_INDEXER_ENABLED: "false"
    volumes:
      - affine_storage:/root/.affine/storage
      - affine_config:/root/.affine/config
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  db:
    image: pgvector/pgvector:0.8.7-pg16
    restart: unless-stopped
    environment:
      POSTGRES_USER: affine
      POSTGRES_DB: affine
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - affine_db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U affine -d affine"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7.2.5-alpine
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - affine_redis_data:/data

  caddy:
    image: caddy:2
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - affine

volumes:
  affine_storage:
  affine_config:
  affine_db_data:
  affine_redis_data:
  caddy_data:
  caddy_config:

Esta guía fija 0.26.7, la versión estable vigente al momento de escribir esto, en todos los contenedores de AFFiNE; pgvector/pgvector:0.8.7-pg16 es Postgres 16 con la extensión pgvector que AFFiNE necesita para sus propios embeddings. affine-config es su propio volumen separado de affine-storage a propósito: el script de predeploy de AFFiNE genera un par de llaves real en formato PEM para firma y cifrado en /root/.affine/config/private.key la primera vez que corre, no un secreto simple que tú definas, así que sin un volumen persistente para ese directorio, cada redespliegue generaría en silencio uno nuevo e invalidaría el anterior. AFFINE_INDEXER_ENABLED se deja en false aquí, igual que el valor por defecto de AFFiNE; actívalo cuando quieras búsqueda de texto completo entre documentos, no es necesario para usar la app.

Verifica que el archivo se analiza correctamente:

docker compose config

Paso 4: agregar Caddy y arrancar la pila

notes.yourdomain.com {
	reverse_proxy affine:3010
}
docker compose up -d

docker compose up arranca primero db y redis, espera ambos chequeos de salud, y luego corre affine-migration hasta que termina antes de que affine mismo pueda arrancar. Observa cómo sucede:

docker compose logs -f affine-migration

Deberías ver la salida de la migración terminando en una salida limpia, y luego:

docker compose ps
curl -I https://notes.yourdomain.com

Deberías ver affine-migration listado como Exited (0), cada otro servicio en Up o healthy, y el curl devolver HTTP/2 200.

Paso 5: crear tu espacio de trabajo

Visita https://notes.yourdomain.com en un navegador. Nada viene pre-cargado, así que crea tu primera cuenta y espacio de trabajo directamente desde la interfaz web. Desde ahí, invita a tus compañeros y empieza a escribir documentos o pizarras de la misma forma en que lo harías en un Notion o Miro gestionados.

Paso 6: respaldar Postgres y el volumen de configuración

Los documentos y el contenido de las pizarras viven en Postgres; el par de llaves de firma del Paso 3 vive en el volumen affine-config. Respalda ambos:

cat > /opt/affine/backup.sh <<'EOF'
#!/bin/sh
cd /opt/affine
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
docker compose exec -T db pg_dump -U affine affine | gzip > backups/db-$TIMESTAMP.sql.gz
docker run --rm -v affine_affine_config:/config -v "$PWD/backups":/backup alpine \
  tar czf /backup/config-$TIMESTAMP.tar.gz -C /config .
find backups -mtime +14 -delete
EOF
mkdir -p /opt/affine/backups
chmod +x /opt/affine/backup.sh
(crontab -l 2>/dev/null; echo "0 3 * * * /opt/affine/backup.sh") | crontab -

Ejecútalo una vez a mano y verifica que haya funcionado: ./backup.sh && ls -lh backups. Deberías ver tanto un archivo .sql.gz como un .tar.gz con la marca de tiempo de hoy. Copia ambos fuera del servidor, perder el volumen de configuración sin un respaldo significa que cada cliente tiene que volver a confiar en una nueva llave de firma en el siguiente reinicio, no solo una pérdida de datos.

Paso 7: actualizar

docker compose pull
docker compose up -d

Compose vuelve a correr affine-migration hasta el final antes de recrear affine mismo, el mismo orden de dependencias del Paso 4, así que cualquier cambio de esquema en la nueva versión se aplica automáticamente. Revisa docker compose logs -f affine-migration de nuevo para confirmar una salida limpia antes de asumir que la actualización quedó bien.

El atajo: hazlo con Railyard

Todo lo anterior, Postgres con pgvector, Redis, el trabajo de migración que tiene que completarse antes de que la app arranque, el volumen de configuración separado para la llave de firma, es también lo que configura por ti la plantilla de pila AFFiNE de Railyard. Conecta un servidor, y luego elige AFFiNE en el catálogo de plantillas: app.railyard.run/deploy?stack=affine. Railyard conecta automáticamente las cadenas de conexión de la base de datos y de Redis y corre el script de predeploy como un paso de lanzamiento antes de cada despliegue, no solo el primero, y emite el certificado HTTPS para tu dominio.

Consulta desplegar con un botón y bases de datos y copias de seguridad para ver cómo funcionan las plantillas y los volúmenes persistentes en la plataforma de forma más general.

Solución de problemas

El contenedor affine se reinicia en bucle y nunca sirve nada. Revisa docker compose ps, si affine-migration muestra algo distinto de Exited (0), la app principal está correctamente negándose a arrancar sobre datos malos en lugar de servir sobre un esquema no migrado; lee docker compose logs affine-migration para ver la falla real.

Cada cliente se desconecta después de un reinicio, o pide volver a confiar en el servidor. El volumen affine-config se perdió o se volvió a crear, lo que regenera la llave de firma. Confirma que el volumen esté listado en docker volume ls y que no se haya eliminado por accidente con un prune; restaurar el respaldo config-*.tar.gz del Paso 6 en un volumen nuevo es la única forma de volver atrás.

El HTTPS no se emite. Que docker compose logs caddy muestre errores de ACME repetidos casi siempre significa que el registro DNS del Paso 1 no se ha propagado o apunta a la IP equivocada; confírmalo con dig +short notes.yourdomain.com.

Conexión a Postgres rechazada. Confirma que DATABASE_URL use db como nombre de host tanto en affine como en affine-migration, no localhost.

La búsqueda no devuelve resultados entre documentos. Esto es esperado con la configuración por defecto; AFFINE_INDEXER_ENABLED viene en false de fábrica, igual que el propio archivo Compose de referencia de AFFiNE, actívalo en el Paso 3 si lo necesitas.

Preguntas frecuentes

¿Puedo saltarme el volumen de configuración y simplemente definir una llave estática yo mismo? No, el propio script de predeploy de AFFiNE genera el par de llaves PEM real que necesita; no hay una variable de entorno documentada para suministrar una directamente, que es exactamente por qué el volumen del Paso 3 es obligatorio y no opcional.

¿AFFiNE necesita un proveedor de correo externo? No para usar la app en el día a día; el correo importa si quieres emails de invitación o flujos de restablecimiento de contraseña, que AFFiNE admite configurar por separado pero que no son necesarios para una instalación funcional de un solo espacio de trabajo.

¿Incluye edición colaborativa en tiempo real? Sí, varias personas editando el mismo documento o pizarra a la vez es una función central, no un complemento, respaldada por el mismo Postgres y Redis que ya corren aquí.

¿Puedo mover los archivos subidos a almacenamiento de objetos en lugar del volumen local? AFFiNE admite un backend de almacenamiento compatible con S3 para adjuntos; la configuración por defecto aquí mantiene todo en affine_storage por simplicidad, vale la pena revisarlo una vez que los adjuntos crezcan mucho.

¿Qué pasa si pierdo el servidor sin un respaldo? Los documentos, las pizarras y la llave de firma desaparecen todos; no hay forma de recuperar datos que nunca se respaldaron en otro lugar, por lo que el Paso 6 no es opcional en la práctica.

Una instalación de AFFiNE que funciona son cuatro servicios que dependen entre sí en un orden específico: Postgres y Redis primero, el trabajo de migración después, la app al final. Una vez que ese orden está bien y los respaldos están corriendo, queda muy poco por mantener más allá de la actualización ocasional de versión del Paso 7.

Por qué eligen Railyard para esto

  • Tu propia cuenta en la nube. Despliega en DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, o cualquier servidor Linux por SSH, facturado directamente por tu proveedor, sin márgenes adicionales.
  • Un precio fijo por equipo. El plan Free es gratis para siempre: un servidor más un servidor de base de datos, tres apps, sin tarjeta. Pro cuesta $19 al mes por hasta cinco servidores y apps ilimitadas. Team cuesta $49 al mes con servidores ilimitados. Sin cargos por app, por despliegue, por asiento ni por gigabyte.
  • Plantillas de un clic. Despliega apps de código abierto, como bases de datos, analítica y herramientas de desarrollo, en tu propio servidor con un clic.
  • Bases de datos administradas. Postgres, MySQL y Redis, con copias de seguridad programadas y verificadas por restauración, y recuperación a un punto en el tiempo.
  • Despliegue con git push. Despliega desde GitHub, GitLab o la CLI, con HTTPS automático y lanzamientos sin tiempo de inactividad.

Despliega AFFiNE en un clic

Elige AFFiNE en el catálogo de plantillas de Railyard y obtén Postgres, Redis y el paso de migración ya conectados.

Plan gratis, sin tarjeta, y 14 días de Pro para cada equipo nuevo. Un precio fijo por equipo.