SSD Nodes Learn 🎉 VPS desde $4.99/mês
Guias Matt ConnorPor Matt Connor

Como hospedar o AFFiNE com Docker Compose

Veja como executar o AFFiNE em um VPS com Docker Compose: quatro containers, tags fixas, dados, backups e o que 2 GB de RAM realmente suportam.

O que você obtém ao alojar o AFFiNE por conta própria

A utilização autogerida do AFFiNE fornece um espaço de trabalho ao estilo do Notion num servidor sob o seu controlo, executado como quatro containers: a aplicação, um job de migração executado uma vez, Postgres e Redis. A colaboração em tempo real está incluída, até aos 10 utilizadores que um espaço de trabalho autogerido recebe por predefinição. A instalação consiste num ficheiro compose e num ficheiro de configuração JSON. É necessário decidir as tags das imagens, a disposição do disco, o limite de memória e o proxy colocado à frente da aplicação.

O AFFiNE mantém um editor de documentos e um canvas infinito no mesmo espaço de trabalho. Assim, uma página pode ser lida como um documento ou expandida como um quadro branco. Se ainda está a decidir o que executar, leia primeiro a comparação de alternativas autogeridas ao Notion. Este guia parte do princípio de que a escolha já foi feita e aborda a execução correta do AFFiNE, em vez de o comparar novamente.

Tudo aqui foi verificado com base na documentação de alojamento próprio do AFFiNE e nos ficheiros de release publicados em 8 de agosto de 2026. A release estável mais recente nessa data era a 0.27.3, publicada em 23 de julho de 2026.

O que os quatro containers fazem de facto

affine é o servidor e o cliente Web na mesma imagem. Ele escuta na porta 3010.

affine_migration é uma tarefa executada uma única vez que executa node ./scripts/self-host-predeploy.js, aplica as migrações da base de dados e termina. A aplicação declara condition: service_completed_successfully nessa tarefa, portanto uma migração que termine com um estado diferente de zero significa que affine nunca é iniciado. Quando a interface Web não fica disponível, o log dessa tarefa é o primeiro que deve consultar.

postgres armazena os seus documentos, utilizadores, áreas de trabalho e permissões. A imagem fornecida é pgvector/pgvector:pg16, que corresponde a Postgres 16 normal com a extensão pgvector compilada. O pgvector adiciona ao Postgres um tipo de coluna vector, o formato numérico usado para armazenar embeddings, permitindo pesquisar texto pelo significado.

redis é uma dependência obrigatória: tanto o servidor como a tarefa de migração aguardam a verificação de saúde antes de arrancar. Observe que o ficheiro compose fornecido não atribui ao Redis um volume. Nada no Redis sobrevive a um docker compose down, o que indica claramente que não armazena conteúdo seu e não precisa de cópia de segurança.

Por que a imagem do Postgres é pgvector e não o postgres padrão

O requisito vem do esquema do AFFiNE, não de uma preferência. Em schema.prisma, a fonte de dados declara extensions = [pgvector(map: "vector")], e quatro tabelas têm uma coluna embedding com o tipo vector(1024). O job de migração cria essas tabelas independentemente de as funcionalidades de IA serem ativadas, por isso a extensão já tem de existir na base de dados antes de a migração terminar. Se substituir por postgres:16, a extensão desaparece, a migração não consegue criar essas colunas e o servidor fica à espera de um job que falhou.

O AFFiNE passou para a imagem pgvector na versão 0.21. Numa instalação mais antiga, editar a linha da imagem não é suficiente para concluir a atualização. Leia a página de atualização na documentação do AFFiNE para instalação própria antes de fazer o pull.

Há mais um detalhe sobre essa tag. pg16 significa Postgres 16, e uma versão principal do Postgres não é um número que possa simplesmente aumentar. Se a alterar para pg17 usando um diretório de dados existente, o Postgres recusa-se a arrancar e apresenta uma linha como The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17 em docker compose logs postgres. A mudança de versão principal exige um dump e um restore para um diretório de dados novo.

De quanta CPU e RAM precisa uma instalação self-hosted do AFFiNE

A página de requisitos do AFFiNE pede pelo menos 4 núcleos de CPU e 2 GB de RAM. A memória necessária aumenta para 4 GB quando os documentos ultrapassam 10,000 palavras. A mesma página explica para onde vai a memória: o sistema de sincronização e a fusão de documentos. Também apresenta um valor que convém memorizar: a fusão de um documento com 10,000 modificações pode atingir um pico de 1 GB.

Agora compare isso com um plano de 2 GB usado por duas pessoas para escrever. A média é suficiente. O Postgres e o processo Node ficam abaixo do limite, com alguma margem. O pico é o problema. Uma única fusão grande pode pedir 1 GB além de toda a memória já residente. Num servidor com 2 GB e sem swap, o kernel responde a esse pedido com o OOM (out-of-memory) killer, que termina o processo maior: o servidor AFFiNE.

O seu colega não vê uma mensagem de erro. Vê a página recarregar, porque restart: unless-stopped repõe o container em funcionamento em poucos segundos. Não faça suposições. Confirme:

docker inspect affine_server --format '{{.State.OOMKilled}} {{.RestartCount}}'
sudo dmesg -T | grep -i -E 'out of memory|killed process'

true no primeiro comando, ou uma linha Killed process que identifique node no segundo, significa que ficou sem memória, e não que encontrou um bug. Corrija o problema dos dois lados. Comece por adicionar swap, para que um pico torne o sistema lento em vez de o terminar:

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 -h

free -h deve agora indicar um total de 2.0Gi de swap. A swap não torna o AFFiNE rápido, nem é essa a sua finalidade. Transforma um pico de um segundo num atraso, em vez de resultar num container terminado. O outro lado da correção é impedir que o Postgres aumente a sua cache até ocupar o espaço de que a aplicação precisa durante a fusão. É para isso que servem os limites de memória num serviço Compose.

O armazenamento é muito mais fácil de prever. Estes são os valores publicados pelo AFFiNE na mesma página:

ChartPublished AFFiNE storage figures, August 2026
The data behind this chart
[
  {
    "label": "Server install",
    "gb": 1.5
  },
  {
    "label": "Postgres per 1,000 docs",
    "gb": 0.1
  },
  {
    "label": "Blob store per 1,000 uploads",
    "gb": 10
  }
]

A instalação do servidor ocupa 1.5 GB. Mil documentos com aproximadamente mil palavras cada acrescentam 0.1 GB de dados do Postgres, o que é praticamente nada. Mil ficheiros carregados acrescentam 10 GB, que representam todo o impacto relevante. Estes valores são estimativas publicadas para planeamento, e não medições de uma instância em execução. Considere-os uma indicação da proporção, não uma garantia. A proporção é o que importa: a base de dados permanece pequena, e os ficheiros carregados determinam o espaço em disco.

Escreva o ficheiro Compose manualmente, com as tags fixadas

A instalação documentada descarrega um ficheiro pronto com curl -L -o docker-compose.yml https://github.com/toeverything/AFFiNE/releases/latest/download/docker-compose.yml. Isso funciona. Há um detalhe que deve conhecer antes de depender dele: em 8 August 2026, o ficheiro anexado à release 0.27.3 ainda lê os caminhos de um ficheiro .env, usando ${UPLOAD_LOCATION}, ${CONFIG_LOCATION} e ${DB_DATA_LOCATION}, enquanto a página de referência da documentação mostra um layout mais recente, que mantém tudo em ./data e não precisa de .env. As duas opções são válidas. Escrever o ficheiro manualmente elimina a dúvida, e terá de o editar de qualquer forma para fixar as imagens e definir uma palavra-passe para a base de dados.

mkdir -p ~/affine/config ~/affine/data
cd ~/affine
printf 'DB_PASSWORD=%s\n' "$(openssl rand -hex 24)" > .env
chmod 600 .env

O Compose lê automaticamente .env a partir do diretório do projeto e substitui ${DB_PASSWORD} por si. Assim, a palavra-passe nunca aparece no ficheiro que colaria num tópico de suporte. Vale a pena manter este hábito em todas as stacks que executar. A explicação está em manter os segredos fora do ficheiro Compose.

Agora escreva ~/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-stopped

Há quatro diferenças em relação ao ficheiro disponibilizado pelo upstream, e cada uma tem uma razão.

  • 127.0.0.1:3010:3010 publica a porta apenas no endereço de loopback. Assim, nada fora do servidor pode aceder ao AFFiNE até decidir como o fazer. O '3010:3010' do upstream associa-se a todas as interfaces. Na maioria das imagens VPS, isso inclui a interface pública.
  • POSTGRES_HOST_AUTH_METHOD: trust foi removido e foi definida uma palavra-passe. A autenticação de confiança aceita qualquer ligação a essa base de dados como o utilizador affine, sem palavra-passe. Isto está limitado à rede privada do Compose. É aceitável até ao dia em que associa outro contentor a essa rede ou publica a porta 5432 durante a investigação de um problema.
  • redis:8-alpine substitui um redis sem versão, que resolve para latest. Em August 2026, isso corresponde ao Redis 8. A fixação mantém a versão principal que testou e impede que o Redis 9 seja instalado durante uma docker compose pull não relacionada.
  • pgvector/pgvector:pg16 permanece exatamente como definido pelo upstream, pelo motivo explicado acima.

POSTGRES_PASSWORD só é lido quando o Postgres cria o diretório de dados pela primeira vez. Numa instância que já exista, defina a palavra-passe com docker compose exec postgres psql -U affine -c "ALTER USER affine WITH PASSWORD 'yourpassword'" e atualize depois DATABASE_URL para corresponder.

A configuração fica em config/config.json

O AFFiNE lê as suas definições de config/config.json, que é o diretório montado em /root/.affine/config. Nada cria esse ficheiro automaticamente, por isso crie-o antes do primeiro arranque. Abra ~/affine/config/config.json num editor e introduza este conteúdo, substituindo o domínio de exemplo pelo seu:

{
  "$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 tem de ser o endereço que os utilizadores realmente abrem num navegador. O AFFiNE cria ligações de partilha e convites para áreas de trabalho a partir desse valor. Se permanecer definido como http://localhost:3010, um convite enviado aponta o destinatário para a própria máquina dele e falha nesse local. Defina o endereço HTTPS público antes do primeiro arranque, para que o ficheiro e o painel de administração nunca apresentem valores diferentes.

copilot controla as funcionalidades de IA. copilot.byok.enabled é a opção que permite usar uma chave própria. Assim, o proprietário de uma área de trabalho pode introduzir a chave do seu fornecedor de modelos nas definições da área de trabalho. Alojar o AFFiNE não inclui uma subscrição de IA. Deixe ambos os valores como false se não precisar dessa funcionalidade.

Inicie a stack:

docker compose up -d
docker compose ps

docker compose ps deve apresentar affine_postgres e affine_redis como saudáveis, affine_server como em execução e affine_migration_job com o estado exited (0). Qualquer outro código de saída no job de migração é o problema que deve investigar. O respetivo log identifica o passo em que o processo parou:

docker compose logs affine_migration

Fixe a imagem antes de esquecer

stable é uma tag mutável. O fluxo de lançamento do AFFiNE aponta várias tags para cada build estável, e duas são relevantes aqui: stable, que é redefinida em cada lançamento, e stable- seguida do hash curto do git, que não é. Se permanecer em stable, um docker compose pull daqui a seis meses obtém uma imagem diferente e executa as migrações na sua base de dados num momento que não escolheu. Fixe a imagem exata que testou:

docker compose pull
docker image inspect ghcr.io/toeverything/affine:stable --format '{{index .RepoDigests 0}}'

Isto imprime uma linha semelhante a ghcr.io/toeverything/affine@sha256: seguida de um hash longo. Cole a string completa na linha image: de ambos affine e affine_migration. Os dois valores têm de coincidir sempre, porque representam a mesma imagem a desempenhar dois papéis. Uma diferença significa migrar a base de dados para um esquema e disponibilizá-la com outro. A atualização passa então a ser uma alteração deliberada, e não uma surpresa: altere o digest, faça uma cópia de segurança, docker compose pull, docker compose up -d.

Crie a conta de administrador antes de qualquer outra pessoa

Abra /admin numa instância nova e o AFFiNE encaminha-o para uma página de criação de conta, porque o servidor ainda não tem administrador. Esse fluxo não utiliza código de convite nem token de configuração. A primeira pessoa a carregar essa página torna-se o administrador do seu servidor, por isso a porta deve permanecer fechada até concluir o registo.

É por isso que o ficheiro compose acima associa o serviço a 127.0.0.1. Aceda-lhe através de um túnel SSH a partir da sua própria máquina:

ssh -L 3010:127.0.0.1:3010 you@your-server-ip

Deixe o túnel em execução e abra http://127.0.0.1:3010/admin no navegador local. Registe-se e inicie sessão; em seguida, feche o túnel. Só depois é seguro disponibilizar a instância num nome público.

Onde o AFFiNE armazena os seus dados

Três caminhos contêm todos os dados e ficam dentro do diretório que criou.

  • ./data/postgres é o diretório de dados do Postgres: documentos, utilizadores, workspaces e permissões.
  • ./data/storage é montado em /root/.affine/storage no contentor e contém todos os ficheiros carregados.
  • ./config é montado em /root/.affine/config e contém config.json.

O upstream usa bind mounts neste caso, em vez de named volumes, e essa escolha é deliberada: pode compactar e copiar estes caminhos com comandos comuns, sem pedir ao Docker que indique onde os colocou. A desvantagem é que a propriedade dos ficheiros no host passa a ser da sua responsabilidade. Esse é o compromisso explicado em bind mounts e named volumes.

Como fazer backup do AFFiNE

É necessário fazer backup de duas coisas, e cada uma é copiada de uma forma diferente. A base de dados é um servidor ativo, por isso copiar os ficheiros enquanto está em execução produz uma cópia corrompida. Em vez disso, faça um dump:

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/

O dump é executado dentro do contentor através do socket local, por isso não pede a palavra-passe. Verifique o tamanho na saída de ls. Um ficheiro com algumas centenas de bytes significa que o dump falhou, embora a shell tenha criado o ficheiro. Este é o tipo de falha que só é descoberto seis meses depois. -T também é importante: sem essa opção, o Compose pode alocar um terminal e corromper o fluxo binário.

Os ficheiros carregados são ficheiros normais, por isso compacte-os com tar:

tar czf backup/storage-$(date +%F).tgz -C data storage
cp config/config.json backup/config-$(date +%F).json

Mantenha config.json manualmente no backup. A documentação do AFFiNE ainda indica que a exportação da configuração a partir do painel de administração não está implementada, verificado em August 2026, por isso o ficheiro no disco é a única cópia das suas definições. Copie os três ficheiros para fora do servidor. Um backup no mesmo disco que o elemento que protege não é um backup.

Restauração e uma armadilha nas etapas publicadas

Leia as etapas oficiais de restauração antes de precisar delas e leia-as com atenção. Conforme publicadas em agosto de 2026, elas copiam um ficheiro chamado affine.backup para o contentor e depois restauram a partir de ./pg.backup. Estes são dois nomes diferentes. Além disso, removem um diretório ./postgres, enquanto o ficheiro compose atual mantém os dados em ./data/postgres. Siga os caminhos que realmente utilizou, e não os caminhos do exemplo. Esta é a sequência para a estrutura usada neste guia:

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 -d

Observe o mv, e não um rm. Restaurar sobre uma base de dados da qual não guardou uma cópia é uma forma de um comando incorreto causar perda total de dados. Mover o diretório antigo para outro local não tem qualquer custo. Restaure também os uploads com tar xzf backup/storage-2026-08-08.tgz -C data, caso contrário todos os documentos serão apresentados com anexos quebrados. Depois, inicie sessão e abra um documento que contenha uma imagem. Esse é o teste. Uma restauração que não abriu num browser é um ficheiro, não uma cópia de segurança.

Colocando o AFFiNE atrás de um proxy que já executa

O AFFiNE usa WebSocket, e isso não é opcional. A documentação é clara: o WebSocket é a base do sistema de sincronização e colaboração do AFFiNE. Por isso, um proxy que não atualiza essas ligações deixa o workspace num estado em que as edições deixam de ser sincronizadas silenciosamente. A página carrega, o login funciona, e uma edição feita num navegador nunca chega ao outro. Nas ferramentas de desenvolvimento do navegador, abra o separador Network e filtre por WS. Uma ligação que abre e fecha repetidamente indica que o proxy não está a encaminhar o upgrade.

Se já executa o Traefik para outros contentores, o AFFiNE é adicionado como um serviço normal. Elimine o bloco ports: do serviço affine e adicione:

    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'

Na parte inferior do ficheiro, junto de services:, adicione:

networks:
  proxy:
    external: true

O nome do resolver de certificados tem de corresponder ao definido na configuração do Traefik, e loadbalancer.server.port é a porta do contentor 3010, nunca uma porta do host. O Traefik encaminha ligações WebSocket sem configuração adicional. Não é necessário adicionar mais nada. A execução de várias aplicações atrás de uma única instância está descrita em um único Traefik à frente de várias aplicações.

No nginx, é necessário solicitar explicitamente o upgrade:

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 tem o valor predefinido de 1 MB no nginx. Sem essa linha, todos os uploads maiores do que uma fotografia pequena falham com o estado 413 e nada aparece nos logs do AFFiNE, porque o pedido nunca chegou ao serviço. O Caddy precisa de uma linha, reverse_proxy http://127.0.0.1:3010, e trata diretamente dos certificados e dos upgrades WebSocket.

O que a instalação self-hosted não oferece

Seja honesto consigo antes de migrar uma equipa.

A colaboração em tempo real está disponível e é a funcionalidade a que se referem todas as recomendações de dimensionamento, porque a documentação do próprio AFFiNE atribui o consumo de memória ao sistema de sincronização e à união de documentos. A edição offline é o motivo pelo qual muitas pessoas procuram uma ferramenta local-first, e a aplicação para desktop pode adicionar o seu servidor self-hosted à lista de espaços de trabalho e iniciar sessão nele. Teste o comportamento offline exato de que a sua equipa depende antes de se comprometer: edite na aplicação para desktop com a rede desligada, volte a ligar-se e verifique o resultado num segundo dispositivo. As listas de funcionalidades não são provas, e esta também não é.

A pesquisa de texto completo no servidor está desativada no ficheiro compose fornecido, onde AFFINE_INDEXER_ENABLED=false está definido no servidor e no job de migração. Ativá-la requer adicionar um contentor Manticore Search, que será um quinto serviço e consumirá mais memória. Num servidor com 2 GB, essa é a alteração que faz ultrapassar o limite. A pesquisa dentro do cliente continua a funcionar no espaço de trabalho que está aberto.

Há dois limites que deve conhecer antes de convidar pessoas. Um espaço de trabalho self-hosted pode ter no máximo 10 lugares, e ultrapassar esse limite requer uma licença Team da AFFiNE. O armazenamento ilimitado de blobs e o tamanho ilimitado dos blobs para instâncias self-hosted são descritos na documentação como funcionalidades previstas, mas ainda não totalmente implementadas, segundo a verificação feita em agosto de 2026. Nenhum destes limites é relevante para uma utilização doméstica ou uma equipa pequena. Ambos são relevantes se estava a planear migrar quarenta pessoas.

Atualizações

Leia primeiro as notas da versão, sobretudo para uma atualização de versão menor, como de 0.26 para 0.27, porque podem ser introduzidas alterações incompatíveis. Faça uma cópia de segurança da base de dados e do diretório de armazenamento antes de alterar qualquer coisa, porque o job de migração altera o esquema na inicialização seguinte e não existe forma de desfazer essa alteração. Depois, altere o digest fixado, execute docker compose pull seguido de docker compose up -d e monitorize docker compose logs -f affine_migration até terminar sem erros. docker image prune limpa as camadas antigas depois disso. Uma nota histórica para quem usa uma instalação muito antiga: a partir da versão 0.23.0, o nome da imagem mudou de affine-graphql para affine. Por isso, um ficheiro compose anterior a essa versão precisa de ter as linhas da imagem reescritas antes de um pull conseguir encontrar a imagem.

FAQ

Por que o contêiner AFFiNE nunca inicia?

O serviço affine declara condition: service_completed_successfully no job affine_migration. Por isso, se a migração terminar com um estado diferente de 0, o servidor nunca é iniciado e não aparece qualquer interface web. Execute docker compose logs affine_migration para ver em que passo o processo parou. A causa mais comum num ficheiro compose editado manualmente é usar uma imagem postgres padrão em vez de pgvector/pgvector:pg16, porque o esquema do AFFiNE declara a extensão pgvector e cria tabelas com colunas vector(1024) que o Postgres normal não consegue criar.

De quanta RAM precisa o AFFiNE autoalojado?

A página de requisitos do AFFiNE pede pelo menos 4 núcleos de CPU e 2 GB de RAM. O requisito sobe para 4 GB quando os documentos ultrapassam 10,000 palavras. A página também indica que a fusão de um documento com 10,000 alterações pode atingir 1 GB. Num servidor com 2 GB, esse pico é o que causa o problema, não a carga em repouso: o mecanismo de falta de memória do kernel termina o processo do AFFiNE e restart: unless-stopped inicia-o novamente. Por isso, os utilizadores veem a página recarregar em vez de receberem um erro. Confirme com docker inspect affine_server --format '{{.State.OOMKilled}}' e sudo dmesg -T | grep -i 'out of memory'. Depois, adicione um ficheiro swap de 2 GB para que um pico abrande o sistema em vez de o terminar.

Onde é que o AFFiNE guarda os meus dados e do que devo fazer cópias de segurança?

Três caminhos dentro do diretório compose contêm tudo: ./data/postgres para a base de dados, ./data/storage para os ficheiros carregados e ./config para config.json. Faça uma cópia de segurança da base de dados com docker compose exec -T postgres pg_dump --format c --username affine affine > affine.dump em vez de copiar os ficheiros, porque não é seguro copiar um Postgres em execução. Crie um arquivo tar de ./data/storage para os carregamentos. Guarde manualmente uma cópia de config.json, porque a exportação da configuração a partir do painel de administração aparece como ainda não implementada em agosto de 2026.

A colaboração em tempo real funciona num AFFiNE autoalojado?

Sim. Não é necessário ativar nada para isso. O único requisito é o reverse proxy, porque a sincronização usa ligações WebSocket. No nginx, isso significa proxy_http_version 1.1, além dos cabeçalhos Upgrade e Connection: upgrade. O Traefik e o Caddy encaminham essas ligações sem configuração adicional. Quando o proxy não faz o upgrade das ligações, o sintoma é um espaço de trabalho que carrega e permite iniciar sessão normalmente, mas as alterações feitas num navegador nunca aparecem noutro.

Posso executar o AFFiNE com uma imagem Postgres padrão?

Não. O schema.prisma do AFFiNE declara extensions = [pgvector(map: "vector")] e define quatro tabelas com uma coluna embedding do tipo vector(1024). O job de migração cria essas tabelas mesmo quando as funcionalidades de IA estão desativadas. Use pgvector/pgvector:pg16, que é o Postgres 16 com essa extensão compilada. Se, em vez disso, apontar o AFFiNE para um servidor Postgres externo, instale nele o pgvector e crie a extensão na base de dados de destino antes de executar a migração.