Cómo alojar AFFiNE con Docker Compose en un VPS
Instala AFFiNE en un VPS con Docker Compose: conoce sus cuatro contenedores, fija las etiquetas de imagen, ubica los datos y evalúa qué permite 2 GB de RAM.
Qué se obtiene al alojar AFFiNE por cuenta propia
Alojar AFFiNE por cuenta propia proporciona un espacio de trabajo con un funcionamiento similar al de Notion en un servidor bajo su control. Se ejecuta mediante cuatro contenedores: la aplicación, un trabajo de migración de una sola ejecución, Postgres y Redis. La colaboración en tiempo real está incluida, con un límite de 10 usuarios, que es el valor predeterminado para un espacio de trabajo autohospedado. La instalación requiere un archivo compose y un archivo de configuración JSON. Los aspectos que requieren planificación son las etiquetas de imagen, la distribución del disco, el límite de memoria y el proxy que se coloca delante.
AFFiNE integra un editor de documentos y un lienzo infinito en el mismo espacio de trabajo. Por eso, una página puede leerse como un documento o ampliarse como una pizarra. Si todavía está decidiendo qué ejecutar, lea primero la comparación de alternativas autohospedadas a Notion. Esta guía parte de que la elección ya está hecha y explica cómo ejecutar AFFiNE correctamente, no vuelve a compararlo.
Todo lo incluido aquí se comprobó con la documentación de autohospedaje de AFFiNE y los archivos de lanzamiento publicados el 8 de agosto de 2026. La versión estable más reciente en esa fecha era la 0.27.3, publicada el 23 de julio de 2026. Las afirmaciones que incluyen una fecha posterior se volvieron a comprobar con sus fuentes primarias el 12 de octubre de 2026, cuando la versión estable más reciente era la 0.27.4, publicada el 18 de agosto de 2026.
Qué hacen realmente los cuatro contenedores
affine contiene el servidor y el cliente web en una sola imagen. Escucha en el puerto 3010.
affine_migration es un trabajo de una sola ejecución que ejecuta node ./scripts/self-host-predeploy.js, aplica las migraciones de la base de datos y termina. La aplicación declara condition: service_completed_successfully como dependencia de ese trabajo, por lo que una migración que termina con un estado distinto de cero significa que affine no se inicia. Si la interfaz web no se inicia, el registro de ese trabajo es lo primero que debe revisar.
postgres almacena sus documentos, usuarios, espacios de trabajo y permisos. La imagen proporcionada es pgvector/pgvector:pg16, que es Postgres 16 estándar con la extensión pgvector compilada. pgvector añade a Postgres un tipo de columna vector, la representación numérica utilizada para almacenar embeddings y permitir búsquedas de texto por significado.
redis es una dependencia estricta: tanto el servidor como el trabajo de migración esperan su comprobación de estado antes de iniciarse. Observe lo que el archivo compose proporcionado no asigna a Redis, que es un volumen. Nada de su contenido sobrevive a un docker compose down, lo que indica claramente que no contiene datos suyos y no necesita copias de seguridad.
Por qué la imagen de Postgres es pgvector y no el Postgres estándar
El requisito procede del esquema de AFFiNE, no de una preferencia. En v0.27.4, schema.prisma todavía declara extensions = [pgvector(map: "vector")], pero no contiene columnas de embeddings. Los cuatro modelos con columnas embedding de tipo vector(1024) que existían en v0.27.3 no están presentes en v0.27.4. Mantenga la imagen pgvector/pgvector:pg16 proporcionada: la extensión todavía está declarada y el postgres:16 estándar no la incluye.
La plantilla oficial de v0.27.4 usa la imagen pgvector; la plantilla de v0.21.0 todavía usaba postgres:16. En una instalación antigua, editar la línea de la imagen no completa la actualización. Lea la página de actualización de la documentación de autohospedaje de AFFiNE antes de descargar nada.
Hay otro aspecto relacionado con esa etiqueta. pg16 significa Postgres 16, y la versión mayor de Postgres no es un número que se pueda incrementar sin más. Si la cambia a pg17 sobre un directorio de datos existente, Postgres se niega a arrancar y muestra una línea como The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17 en docker compose logs postgres. Para cambiar de versión mayor se necesita uno de los tres métodos indicados en las notas de la versión de PostgreSQL 17: hacer un volcado y restaurarlo en un directorio de datos nuevo, usar pg_upgrade o utilizar la replicación lógica. En una instalación única de Compose, el volcado y la restauración suelen ser la opción más sencilla, porque pg_upgrade necesita tener en paralelo los binarios del servidor antiguo y del nuevo.
Cuánta CPU y RAM necesita AFFiNE autohospedado
La página de requisitos de AFFiNE pide al menos 4 núcleos de CPU y 2 GB de RAM, y eleva la memoria a 4 GB cuando los documentos superan las 10,000 palabras. La misma página indica dónde se usa la memoria: el sistema de sincronización y la combinación de documentos. También proporciona una cifra que conviene recordar: combinar un documento con 10,000 modificaciones puede alcanzar un pico de 1 GB.
Ahora compare esto con un plan de 2 GB usado por dos personas para escribir. El promedio no supone un problema. Postgres y el proceso de Node se mantienen por debajo del límite, con margen disponible. El problema es el pico. Una sola combinación grande puede elevar el uso de memoria del servidor hasta aproximadamente 1 GB. En un servidor de 2 GB sin swap, ese pico, sumado a Postgres y al resto de procesos residentes, puede agotar la memoria. Entonces el kernel activa el asesino de procesos por falta de memoria (OOM) para liberar memoria. El servidor de AFFiNE, como proceso más grande, es la víctima más probable, aunque la documentación de Docker deja claro que puede elegirse cualquier proceso, incluido Docker.
Cuando se termina el servidor de AFFiNE, su compañero no ve un error. Ve que la página se vuelve a cargar porque restart: unless-stopped vuelve a levantar el contenedor en cuestión de segundos. No lo suponga: confírmelo:
docker inspect affine_server --format '{{.State.OOMKilled}} {{.RestartCount}}'
sudo dmesg -T | grep -i -E 'out of memory|killed process'true en la salida del primer comando, o una línea Killed process que indique node en la salida del segundo, significa que se agotó la memoria y no que se encontró un error en el código. Corríjalo desde ambos extremos. Añada swap primero, para que el pico tenga un espacio temporal donde descargarse antes de que actúe el asesino OOM:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -hfree -h debería mostrar ahora un total de swap de 2.0Gi. La swap no hace que AFFiNE sea rápido, ni está pensada para eso. Proporciona un destino temporal para un pico breve, de modo que sea mucho más probable que el pico provoque un segundo de lentitud en lugar de matar el contenedor. Es un búfer, no una garantía. El otro extremo de la solución consiste en impedir que Postgres haga crecer su caché hasta ocupar el espacio que la aplicación necesita durante la combinación. Para eso sirven los límites de memoria en un servicio de Compose. Si los procesos siguen terminándose después de aplicar ambas medidas, el servidor necesita los 4 GB que solicita la página de requisitos de AFFiNE para documentos grandes.
El almacenamiento es mucho más fácil de prever. Estas son las cifras que AFFiNE publica en esa misma página:
Los datos de este gráfico
[
{
"label": "Server install",
"gb": 1.5
},
{
"label": "Postgres per 1,000 docs",
"gb": 0.1
},
{
"label": "Blob store per 1,000 uploads",
"gb": 10
}
]La instalación del servidor ocupa 1.5 GB. Mil documentos de aproximadamente mil palabras cada uno añaden 0.1 GB de datos de Postgres, una cantidad prácticamente insignificante. Mil archivos subidos añaden 10 GB, y eso representa todo el problema. Son cifras de planificación publicadas, no mediciones de una instancia en ejecución. Trátelas como una referencia de proporción, no como una garantía. Lo importante es la proporción: la base de datos sigue siendo pequeña y los archivos subidos son los que determinan el espacio en disco.
Escriba usted mismo el archivo compose, con las etiquetas fijadas
La instalación documentada descarga un archivo ya preparado con curl -L -o docker-compose.yml https://github.com/toeverything/AFFiNE/releases/latest/download/docker-compose.yml. Funciona. Hay un detalle que conviene conocer antes de depender de él: a 8 August 2026, el archivo incluido en la versión 0.27.3 todavía leía las rutas desde un archivo .env mediante ${UPLOAD_LOCATION}, ${CONFIG_LOCATION} y ${DB_DATA_LOCATION}, mientras que la página de referencia de la documentación mostraba una estructura más reciente que coloca el almacenamiento y los datos de PostgreSQL bajo ./data, mantiene la configuración bajo ./config y no necesita .env. La versión 0.27.4 trasladó el archivo incluido a esa estructura y, a 12 October 2026, ambos coinciden. Si instaló la versión anterior del archivo, las rutas todavía proceden de .env, y la página de actualización de AFFiNE advierte que sustituir el archivo compose sin trasladar esos valores puede iniciar Postgres con un directorio de datos vacío. Si escribe usted mismo el archivo, evita por completo esa incertidumbre. Además, de todos modos debe editarlo para fijar las imágenes y establecer una contraseña de base de datos.
mkdir -p ~/affine/config ~/affine/data
cd ~/affine
printf 'DB_PASSWORD=%s\n' "$(openssl rand -hex 24)" > .env
chmod 600 .envCompose lee .env por sí solo desde el directorio del proyecto y sustituye ${DB_PASSWORD} automáticamente, por lo que la contraseña nunca aparece en el archivo que pegaría en un hilo de soporte. Conviene mantener este hábito en todas las pilas que ejecute. El razonamiento se explica en mantener los secretos fuera del archivo compose.
Ahora escriba ~/affine/docker-compose.yml:
name: affine
services:
affine:
image: ghcr.io/toeverything/affine:stable
container_name: affine_server
ports:
- '127.0.0.1:3010:3010'
depends_on:
redis:
condition: service_healthy
postgres:
condition: service_healthy
affine_migration:
condition: service_completed_successfully
volumes:
- ./data/storage:/root/.affine/storage
- ./config:/root/.affine/config
environment:
- REDIS_SERVER_HOST=redis
- DATABASE_URL=postgresql://affine:${DB_PASSWORD}@postgres:5432/affine
- AFFINE_INDEXER_ENABLED=false
restart: unless-stopped
affine_migration:
image: ghcr.io/toeverything/affine:stable
container_name: affine_migration_job
command: ['sh', '-c', 'node ./scripts/self-host-predeploy.js']
volumes:
- ./data/storage:/root/.affine/storage
- ./config:/root/.affine/config
environment:
- REDIS_SERVER_HOST=redis
- DATABASE_URL=postgresql://affine:${DB_PASSWORD}@postgres:5432/affine
- AFFINE_INDEXER_ENABLED=false
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
redis:
image: redis:8-alpine
container_name: affine_redis
healthcheck:
test: ['CMD', 'redis-cli', '--raw', 'incr', 'ping']
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
postgres:
image: pgvector/pgvector:pg16
container_name: affine_postgres
volumes:
- ./data/postgres:/var/lib/postgresql/data
environment:
POSTGRES_USER: affine
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: affine
POSTGRES_INITDB_ARGS: '--data-checksums'
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'affine', '-d', 'affine']
interval: 10s
timeout: 5s
retries: 5
restart: unless-stoppedHay cuatro diferencias respecto al archivo que publica el proyecto original. Cada una tiene un motivo.
127.0.0.1:3010:3010publica el puerto únicamente en la dirección de loopback. Las versiones de Docker anteriores a 28.0.0 permiten que los hosts del mismo segmento L2 accedan a los puertos publicados en localhost. El enrutamiento directo configurado también puede exponer el puerto del contenedor aunque se publique mediante loopback. Use Docker 28.0.0 o una versión posterior y mantenga desactivado el enrutamiento directo durante la configuración. El archivo'3010:3010'del proyecto original enlaza con todas las interfaces. En la mayoría de las imágenes de VPS, eso incluye la interfaz pública.POSTGRES_HOST_AUTH_METHOD: trustdesaparece y se establece una contraseña en su lugar. La autenticación trust acepta cualquier conexión a esa base de datos como el usuarioaffinesin contraseña. Está limitada a la red privada de Compose, lo cual funciona hasta que añade otro contenedor a esa red o publica 5432 para depurar.redis:8-alpinesustituye a unredissin versión, que se resuelve comolatest. A August 2026, esa versión es Redis 8. La fijación conserva la versión principal que ha probado e impide que Redis 9 se instale durante unadocker compose pullno relacionada.pgvector/pgvector:pg16permanece exactamente como lo establece el proyecto original, por el motivo indicado anteriormente.
POSTGRES_PASSWORD sólo se lee cuando Postgres crea por primera vez su directorio de datos. En una instancia que ya existe, establezca la contraseña con docker compose exec postgres psql -U affine -c "ALTER USER affine WITH PASSWORD 'yourpassword'" y actualice después DATABASE_URL para que coincida.
La configuración se encuentra en config/config.json
AFFiNE lee su configuración desde config/config.json, que es el directorio montado en /root/.affine/config. Ningún componente crea ese archivo automáticamente, así que debe escribirlo antes del primer arranque. Abra ~/affine/config/config.json con un editor y asígnele este contenido. Sustituya el dominio de ejemplo por el suyo:
{
"$schema": "https://github.com/toeverything/affine/releases/latest/download/config.schema.json",
"server": {
"name": "Team workspace",
"externalUrl": "https://affine.example.com"
},
"copilot": {
"enabled": false,
"byok": {
"enabled": false
}
}
}server.externalUrl debe ser la dirección que los usuarios abren realmente en un navegador. AFFiNE genera los enlaces para compartir y las invitaciones a espacios de trabajo a partir de ese valor. Si se deja en http://localhost:3010, una invitación que envíe apunta al equipo del propio destinatario y falla allí. Asígnelo a la dirección HTTPS pública antes del primer arranque. De este modo, el archivo y el panel de administración siempre coinciden. Las opciones que cambie después en el panel de administración se guardan en la base de datos, no se escriben de nuevo en este archivo.
copilot controla las funciones de IA. copilot.byok.enabled activa la opción de usar una clave propia. Permite que el propietario de un espacio de trabajo pegue su propia clave del proveedor de modelos en la configuración del espacio de trabajo. El alojamiento autónomo de AFFiNE no incluye una suscripción de IA. Deje ambos valores en false si no desea usarla.
Inicie la pila:
docker compose up -d
docker compose ps --alldocker compose ps --all debe mostrar affine_postgres y affine_redis como healthy, affine_server como running y affine_migration_job con el estado exited (0). La opción --all es importante: sin ella, docker compose ps sólo muestra los contenedores en ejecución. Por tanto, un trabajo de migración que haya terminado correctamente no aparece. Cualquier otro código de salida del trabajo de migración requiere investigación. Su registro indica el paso en el que se detuvo:
docker compose logs affine_migrationFije la imagen antes de olvidarlo
stable es una etiqueta móvil. El flujo de publicación de AFFiNE apunta varias etiquetas a cada compilación estable, y aquí importan dos: stable, que se reasigna en cada publicación, y stable- seguido del hash corto de git, que no cambia. Si deja stable, un docker compose pull dentro de seis meses descargará una imagen diferente y ejecutará sus migraciones en la base de datos en un momento que usted no eligió. Fije la imagen exacta que probó:
docker compose pull
docker image inspect ghcr.io/toeverything/affine:stable --format '{{index .RepoDigests 0}}'Ese comando muestra una línea como ghcr.io/toeverything/affine@sha256: seguida de un hash largo. Pegue la cadena completa en la línea image: de ambos affine y affine_migration. Ambos deben coincidir siempre, porque son la misma imagen con dos funciones distintas. Si no coinciden, la base de datos se migra a un esquema y se sirve con otro. La actualización pasa a ser un cambio deliberado en lugar de una sorpresa: cambie el digest, haga una copia de seguridad, docker compose pull, docker compose up -d.
Cree la cuenta de administrador antes que cualquier otra persona
Abra /admin en una instancia nueva. AFFiNE le redirigirá a una página de creación de cuenta porque el servidor todavía no tiene un administrador. Ese flujo no usa ningún código de invitación ni token de configuración. La primera persona que complete el registro se convierte en el administrador del servidor. Por eso, el puerto debe permanecer cerrado hasta que termine el registro.
Por eso, el archivo compose anterior enlaza con 127.0.0.1. Acceda mediante un túnel SSH desde su propio equipo:
ssh -L 3010:127.0.0.1:3010 you@your-server-ipDeje el túnel en ejecución y abra http://127.0.0.1:3010/admin en el navegador local. Regístrese e inicie sesión. Después, cierre el túnel. Solo entonces será seguro publicar la instancia con un nombre accesible públicamente. La misma condición de carrera existe en otras aplicaciones autoalojadas. Es más problemática cuando el primer inicio de sesión crea una passkey vinculada al nombre de host. Por eso, TLS y el dominio definitivo deben estar configurados antes de crear la primera cuenta cuando autoaloja openGym.
Dónde guarda AFFiNE los datos
Tres rutas contienen todos los datos y están dentro del directorio que creó.
./data/postgreses el directorio de datos de Postgres: documentos, usuarios, espacios de trabajo, permisos y cualquier configuración del servidor guardada desde el panel de administración../data/storagese monta en/root/.affine/storagedentro del contenedor y contiene todos los archivos cargados../configse monta en/root/.affine/configy contieneconfig.jsonyprivate.keygenerados por el trabajo de migración.
El proyecto usa aquí montajes bind en lugar de volúmenes con nombre, y es una decisión deliberada: puede empaquetar y copiar estas rutas con comandos comunes, sin tener que preguntarle a Docker dónde las ubicó. La desventaja es que ahora usted debe gestionar la propiedad de los archivos en el host. Este es el compromiso que se explica en montajes bind y volúmenes con nombre.
Cómo hacer copias de seguridad de AFFiNE
Hay que hacer copias de seguridad de dos elementos, y cada uno requiere un método distinto. La base de datos es un servidor activo, por lo que copiar sus archivos mientras está en ejecución produce una copia dañada. En su lugar, genere un volcado:
mkdir -p ~/affine/backup
cd ~/affine
docker compose exec -T postgres pg_dump --format c --username affine affine \
> backup/affine-$(date +%F).dump
ls -lh backup/El volcado se ejecuta dentro del contenedor mediante su socket local, por lo que no solicita la contraseña. Compruebe el tamaño en la salida de ls. Un archivo de unos pocos cientos de bytes indica que el volcado falló, aunque el shell haya creado el archivo de todos modos. Este es el fallo que muchas personas descubren seis meses después. -T también es importante: sin esa opción, Compose puede asignar un terminal y dañar el flujo binario.
Los archivos subidos son archivos normales, por lo que puede archivarlos con tar:
tar czf backup/storage-$(date +%F).tgz -C data storage
tar czf backup/config-$(date +%F).tgz config docker-compose.yml .envIncluya todo el directorio config, el archivo de Compose y .env en la copia de seguridad, porque el volcado no contiene esos elementos. El trabajo de migración también crea config/private.key, por lo que copiar sólo config.json deja ese archivo fuera. La configuración guardada en el panel de administración se almacena en la base de datos, por lo que el volcado anterior ya la incluye. Además, desde el 12 de octubre de 2026, la página de copias de seguridad de AFFiNE permite exportar también desde el panel de administración la configuración del servidor modificada. Copie los tres archivos fuera del servidor. Una copia de seguridad almacenada en el mismo disco que los datos que protege no es una copia de seguridad. Esta combinación de un volcado de la base de datos y un archivo tar del directorio de archivos subidos es el patrón que debe repetir para cualquier otro contenedor con estado que ejecute. Es el mismo método que mantiene a salvo el historial de conversaciones y los archivos adjuntos cuando autoaloja Chatwoot como mesa de ayuda.
Restauración y una trampa en los pasos publicados
Lea los pasos oficiales de restauración antes de necesitarlos y revíselos con atención. En la versión publicada en agosto de 2026 se copiaba un archivo llamado affine.backup en el contenedor y después se restauraba desde ./pg.backup. Son dos nombres distintos. Además, se eliminaba un directorio ./postgres, aunque el archivo compose actual mantiene los datos en ./data/postgres. Desde el 12 de octubre de 2026, la página usa affine.backup en todos los pasos e indica que se conserve el directorio antiguo de Postgres hasta verificar la restauración. Por tanto, el problema ya está corregido en la documentación original. Sin embargo, una copia de los pasos guardada antes de esa fecha todavía lo contiene. En cualquier caso, use las rutas que haya utilizado realmente, no las de un fragmento de configuración. Esta es la secuencia para la estructura de este tutorial:
cd ~/affine
docker compose down
sudo mv data/postgres data/postgres.old
docker compose up -d postgres
docker compose cp backup/affine-2026-08-08.dump postgres:/tmp/affine.dump
docker compose exec postgres pg_restore --format c --username affine \
--dbname affine --verbose /tmp/affine.dump
docker compose up -dObserve que se usa mv y no rm. Restaurar sobre una base de datos cuya copia no ha conservado puede convertir un comando incorrecto en una pérdida total de datos. Apartar el directorio antiguo no cuesta nada. Restaure también los archivos subidos con tar xzf backup/storage-2026-08-08.tgz -C data. De lo contrario, todos los documentos se mostrarán con los adjuntos dañados. Después, inicie sesión y abra un documento que contenga una imagen. Esa es la prueba. Una restauración que no se ha abierto en un navegador es un archivo, no una copia de seguridad.
Poner AFFiNE detrás de un proxy existente
AFFiNE usa WebSocket y no es opcional. La documentación lo indica claramente: WebSocket es la base del sistema de sincronización y colaboración de AFFiNE. Por tanto, un proxy que no actualice esas conexiones permite acceder a un espacio de trabajo en el que la edición deja de sincronizarse silenciosamente. La página carga y el inicio de sesión funciona, pero una edición realizada en un navegador nunca llega al otro. En las herramientas de desarrollo del navegador, abra la pestaña Network y filtre por WS. Una conexión que se abre y se cierra repetidamente indica que el proxy no está pasando la actualización.
Si ya ejecuta Traefik para otros contenedores, AFFiNE se incorpora como un servicio normal. Elimine el bloque ports: del servicio affine y añada:
networks:
- default
- proxy
labels:
- 'traefik.enable=true'
- 'traefik.docker.network=proxy'
- 'traefik.http.routers.affine.rule=Host(`affine.example.com`)'
- 'traefik.http.routers.affine.entrypoints=websecure'
- 'traefik.http.routers.affine.tls.certresolver=letsencrypt'
- 'traefik.http.services.affine.loadbalancer.server.port=3010'Al final del archivo, junto a services:, añada:
networks:
proxy:
external: trueEl nombre del resolvedor de certificados debe coincidir con el definido en la configuración de Traefik. loadbalancer.server.port es el puerto 3010 del contenedor, nunca un puerto del host. Traefik gestiona las conexiones WebSocket sin configuración adicional, por lo que no es necesario añadir nada más. Si el resto de la pila ya está detrás de Authentik para el inicio de sesión único, un middleware de autenticación delegada en este router controlará el acceso de los navegadores a AFFiNE. Sin embargo, manténgalo desactivado hasta probar la aplicación de escritorio, que no tiene una sesión de navegador y simplemente dejará de sincronizarse. La ejecución de varias aplicaciones detrás de una misma instancia se explica en un solo Traefik delante de varias aplicaciones.
En nginx debe solicitar la actualización explícitamente:
location / {
proxy_pass http://127.0.0.1:3010;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
client_max_body_size 100m;
}client_max_body_size tiene un valor predeterminado de 1 MB en nginx. Sin esa línea, cualquier carga superior a una foto pequeña falla con un estado 413 y no aparece nada en los registros de AFFiNE, porque la petición nunca llegó al servicio. Caddy necesita una línea, reverse_proxy http://127.0.0.1:3010, y gestiona por sí mismo los certificados y las actualizaciones de WebSocket.
Qué no incluye la instalación autohospedada
Sea realista al respecto antes de migrar un equipo.
La colaboración en tiempo real está disponible, y es la función a la que se refiere toda la información sobre dimensionamiento, ya que la documentación de AFFiNE atribuye el uso de memoria al sistema de sincronización y a la combinación de documentos. La edición sin conexión es la razón por la que muchas personas quieren una herramienta local-first, y la aplicación de escritorio puede añadir el servidor autohospedado a su lista de espacios de trabajo e iniciar sesión en él. Pruebe el comportamiento exacto sin conexión del que depende su equipo antes de comprometerse: edite en la aplicación de escritorio con la red desconectada, vuelva a conectarse y compruebe el resultado en un segundo dispositivo. Las listas de funciones no son pruebas, y esta tampoco.
La búsqueda de texto completo en el servidor está desactivada en el archivo compose incluido, donde AFFINE_INDEXER_ENABLED=false está establecido en el servidor y en el trabajo de migración. Activarla implica añadir un contenedor de Manticore Search, que sería un quinto servicio y requeriría más memoria. En un equipo con 2 GB, ese cambio es el que hace que se supere el límite. La búsqueda dentro del cliente sigue funcionando en el espacio de trabajo abierto.
Conviene conocer dos límites antes de invitar a otras personas. Un espacio de trabajo autohospedado admite como máximo 10 usuarios, y superar esa cifra requiere una licencia Team de AFFiNE. La documentación describe el almacenamiento ilimitado de blobs y el tamaño ilimitado de blobs para instancias autohospedadas como funciones previstas, pero todavía no completamente implementadas, según la comprobación realizada en octubre de 2026. Ninguno de estos límites importa en un hogar o un equipo pequeño. Ambos importan si tenía previsto migrar a cuarenta personas.
Actualizaciones
Lea primero las notas de la versión, especialmente cuando actualice una versión menor, como de 0.26 a 0.27, porque puede incluir cambios incompatibles. Haga una copia de seguridad de la base de datos y del directorio de almacenamiento antes de modificar nada, porque el trabajo de migración modifica el esquema en el siguiente arranque y no se puede deshacer. Después, cambie el digest fijado, ejecute docker compose pull seguido de docker compose up -d y supervise docker compose logs -f affine_migration hasta que finalice correctamente. docker image prune elimina las capas antiguas después. Como referencia histórica para instalaciones muy antiguas: a partir de la versión 0.23.0, el nombre de la imagen cambió de affine-graphql a affine. Por tanto, un archivo de compose anterior a esa versión necesita que se reescriban sus líneas de imagen antes de que un pull encuentre la imagen. Además, a partir de la versión 0.27.4, la plantilla oficial de compose cambió las rutas de Postgres, de carga y de configuración, y modificó la conexión de base de datos predeterminada. Compare el archivo nuevo con el suyo en lugar de sobrescribir una instalación que funciona.
FAQ
¿Por qué el contenedor de AFFiNE nunca se inicia?
El servicio affine declara condition: service_completed_successfully en el trabajo affine_migration. Por eso, si la migración termina con un estado distinto de 0, el servidor nunca se inicia y no aparece ninguna interfaz web. Ejecute docker compose logs affine_migration para ver qué paso se detuvo. Compruebe que un archivo de Compose editado manualmente siga usando pgvector/pgvector:pg16: Postgres estándar no incluye la extensión pgvector que el esquema de AFFiNE todavía declara, aunque v0.27.4 ya no defina los cuatro modelos de embeddings presentes en v0.27.3.
¿Cuánta RAM necesita AFFiNE autoalojado?
La página de requisitos de AFFiNE solicita al menos 4 núcleos de CPU y 2 GB de RAM. La cantidad aumenta a 4 GB cuando los documentos superan las 10,000 palabras. También indica que combinar un documento con 10,000 modificaciones puede alcanzar un pico de 1 GB. En un servidor con 2 GB, ese pico es lo que provoca el problema, no la carga en reposo: el asesino de procesos por falta de memoria del kernel selecciona un proceso para detenerlo, probablemente el servidor de AFFiNE por ser el más grande, y restart: unless-stopped lo inicia de nuevo. Por eso los usuarios ven que la página se vuelve a cargar en lugar de mostrar un error. Confírmelo con docker inspect affine_server --format '{{.State.OOMKilled}}' y sudo dmesg -T | grep -i 'out of memory'. Después, añada un archivo de swap de 2 GB como margen para los picos breves y aumente la RAM a 4 GB si los procesos siguen terminando.
¿Dónde almacena AFFiNE mis datos y qué debo respaldar?
Tres rutas bajo el directorio de Compose contienen todo: ./data/postgres para la base de datos, ./data/storage para los archivos subidos y ./config para config.json y el private.key generado. Haga una copia de seguridad de la base de datos con docker compose exec -T postgres pg_dump --format c --username affine affine > affine.dump en lugar de copiar los archivos, porque no se puede copiar un Postgres en ejecución de forma segura. Ese volcado también contiene la configuración guardada en el panel de administración, ya que AFFiNE la almacena en la base de datos. Comprima ./data/storage con tar para los archivos subidos y conserve todo el directorio config, el archivo de Compose y .env, porque esos archivos no forman parte de la base de datos.
¿Funciona la colaboración en tiempo real en un AFFiNE autoalojado?
Sí, y no es necesario habilitar nada. El único requisito es el reverse proxy, porque la sincronización se ejecuta mediante conexiones WebSocket. En nginx, esto significa proxy_http_version 1.1, además de las cabeceras Upgrade y Connection: upgrade. Traefik y Caddy transfieren esas conexiones sin configuración adicional. El síntoma de un proxy que no las actualiza es un espacio de trabajo que carga y permite iniciar sesión normalmente, mientras que las ediciones realizadas en un navegador nunca aparecen en otro.
¿Puedo ejecutar AFFiNE con una imagen estándar de Postgres?
Use pgvector/pgvector:pg16, que es Postgres 16 con la extensión pgvector compilada. AFFiNE v0.27.4 todavía declara extensions = [pgvector(map: "vector")] en schema.prisma, pero ya no define los cuatro modelos con columnas de embeddings vector(1024) presentes en v0.27.3. Postgres estándar no incluye la extensión declarada. Si conecta AFFiNE a un servidor Postgres externo, instale pgvector en él y cree la extensión en la base de datos de destino antes de ejecutar la migración.