Self-host do AFFiNE, uma alternativa open-source ao Notion
Siga isso no seu próprio servidor. Comece grátis, sem cartão
O AFFiNE é um workspace open-source que mistura um editor de documento estilo Notion, um quadro branco infinito e um banco de dados leve num app só, com o seu conteúdo ficando num servidor que você controla em vez da nuvem de um fornecedor. Também é um self-host genuinamente não trivial: um banco de dados Postgres com a extensão pgvector, Redis, e um job de migration de execução única que precisa terminar antes da própria aplicação ter permissão de iniciar. No final deste guia você vai ter tudo isso rodando no seu próprio servidor, atrás de HTTPS, com backups e um upgrade repetível.
O que você vai precisar
- Um servidor com Docker e o plugin Compose. Um Hetzner CX23 (2 vCPU, 4 GB de RAM, 40 GB NVMe, €5,99/mês sem IVA) roda o AFFiNE confortavelmente pra um workspace pessoal ou de equipe pequena.
- Um domínio ou subdomínio apontado pro endereço IP do servidor.
- Acesso SSH ao servidor.
- Cerca de dez minutos, a maior parte esperando pelo DNS e pela primeira execução de migration.
Passo 1: aponte o DNS pro servidor e abra o firewall
Crie um registro A pro seu subdomínio apontando pro IP público do servidor, depois trave a máquina só pro que você precisa:
ufw allow OpenSSH
ufw allow 80
ufw allow 443
ufw --force enable
Passo 2: gere uma senha 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
Passo 3: escreva o arquivo Compose
Essa é a parte que a maioria dos guias de self-host do AFFiNE erra: o arquivo Compose oficial roda a migration do banco de dados como o próprio serviço de curta duração, não como uma etapa dentro da inicialização da aplicação principal, e o container da aplicação tem que esperar esse job de fato terminar, não só iniciar, antes de subir.
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:
Este guia fixa 0.26.7, o release estável atual no momento em que isso foi escrito, em todo container do AFFiNE; pgvector/pgvector:0.8.7-pg16 é Postgres 16 com a extensão pgvector que o AFFiNE precisa pros próprios embeddings dele. O affine-config é um volume próprio separado do affine-storage de propósito: o script de predeploy do AFFiNE gera um par de chaves real de assinatura e criptografia em formato PEM em /root/.affine/config/private.key na primeira vez que roda, não um secret simples que você define você mesmo, então sem um volume persistente pra esse diretório, todo redeploy silenciosamente criaria um novo e invalidaria o último. AFFINE_INDEXER_ENABLED fica false aqui, correspondendo ao próprio padrão do AFFiNE; ative ele quando você quiser busca de texto completo entre documentos, não é necessário pra usar a aplicação.
Cheque se o arquivo é interpretado corretamente:
docker compose config
Passo 4: adicione o Caddy e inicie a stack
notes.yourdomain.com {
reverse_proxy affine:3010
}
docker compose up -d
docker compose up inicia db e redis primeiro, espera os dois healthchecks, depois roda o affine-migration até terminar antes do próprio affine ter permissão de iniciar. Observe isso acontecer:
docker compose logs -f affine-migration
Você deve ver a saída da migration terminando numa saída limpa, depois:
docker compose ps
curl -I https://notes.yourdomain.com
Você deve ver affine-migration listado como Exited (0), todo outro serviço Up ou healthy, e o curl retornar HTTP/2 200.
Passo 5: crie o seu workspace
Visite https://notes.yourdomain.com num navegador. Nada vem pré-populado, então crie a sua primeira conta e workspace direto pela interface web. A partir daí, convide colegas de equipe e comece a escrever documentos ou quadros brancos do mesmo jeito que você faria num Notion ou Miro hospedado.
Passo 6: faça backup do Postgres e do volume de configuração
Documentos e conteúdo de quadro branco moram no Postgres; o par de chaves de assinatura do Passo 3 mora no volume affine-config. Faça backup dos dois:
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 -
Rode manualmente uma vez e cheque se funcionou: ./backup.sh && ls -lh backups. Você deve ver tanto um arquivo .sql.gz quanto um .tar.gz com o timestamp de hoje. Copie os dois pra fora do servidor, perder o volume de configuração sem um backup significa que todo cliente precisa confiar de novo numa chave de assinatura nova no próximo restart, não só uma perda de dados.
Passo 7: faça o upgrade
docker compose pull
docker compose up -d
O Compose roda o affine-migration de novo até terminar antes de recriar o próprio affine, a mesma ordem de dependência do Passo 4, então qualquer mudança de schema na versão nova é aplicada automaticamente. Cheque docker compose logs -f affine-migration de novo por uma saída limpa antes de assumir que o upgrade funcionou.
O caminho mais rápido: faça isso com a Railyard
Tudo acima, Postgres com pgvector, Redis, o job de migration que precisa terminar antes da aplicação iniciar, o volume de configuração separado pra chave de assinatura, também é o que o template de stack AFFiNE da Railyard configura pra você. Conecte um servidor, depois escolha AFFiNE no catálogo de templates: app.railyard.run/deploy?stack=affine. A Railyard conecta as connection strings do banco de dados e do Redis automaticamente e roda o script de predeploy como uma etapa de release antes de todo deploy, não só o primeiro, e emite o certificado HTTPS pro seu domínio.
Veja deploy with a button e databases and backups pra como templates e volumes persistentes funcionam na plataforma de forma mais geral.
Resolução de problemas
O container affine reinicia em loop e nunca serve nada. Cheque docker compose ps, se affine-migration mostrar qualquer coisa diferente de Exited (0), a aplicação principal está corretamente se recusando a iniciar sobre dados ruins em vez de servir contra um schema não migrado; leia docker compose logs affine-migration pela falha real.
Todo cliente é deslogado depois de um restart, ou pede pra confiar de novo no servidor. O volume affine-config foi perdido ou recriado, que regenera a chave de assinatura. Confirme que o volume está listado em docker volume ls e não foi acidentalmente removido; restaurar o backup config-*.tar.gz do Passo 6 num volume novo é o único caminho de volta.
HTTPS não é emitido. docker compose logs caddy mostrando erros de ACME repetidos quase sempre significa que o registro de DNS do Passo 1 ainda não se propagou ou aponta pro IP errado; confirme com dig +short notes.yourdomain.com.
Conexão com o Postgres recusada. Confirme que DATABASE_URL usa db como hostname tanto no affine quanto no affine-migration, não localhost.
A busca não retorna resultados entre documentos. Isso é esperado com a configuração padrão; AFFINE_INDEXER_ENABLED é false por padrão, correspondendo ao próprio arquivo Compose de referência do AFFiNE, ative no Passo 3 se você precisar disso.
Perguntas frequentes
Eu posso pular o volume de configuração e só definir uma chave fixa eu mesmo? Não, o próprio script de predeploy do AFFiNE gera o par de chaves PEM real que ele precisa; não existe uma variável de ambiente documentada pra fornecer uma diretamente, que é exatamente por que o volume do Passo 3 é obrigatório em vez de opcional.
O AFFiNE precisa de um provedor de email externo? Não pra usar a aplicação no dia a dia; email importa se você quiser emails de convite ou fluxos de redefinição de senha, que o AFFiNE suporta configurar separadamente mas não são obrigatórios pra uma instalação funcional de workspace único.
A edição colaborativa em tempo real está incluída? Sim, várias pessoas editando o mesmo documento ou quadro branco ao mesmo tempo é uma funcionalidade central, não um complemento, apoiada pelo mesmo Postgres e Redis já rodando aqui.
Eu posso mover arquivos enviados pra um object storage em vez do volume local? O AFFiNE suporta um backend de armazenamento compatível com S3 pra anexos; o padrão aqui mantém tudo no affine_storage por simplicidade, vale revisitar quando os anexos crescerem.
O que acontece se eu perder o servidor sem um backup? Documentos, quadros brancos e a chave de assinatura todos se vão; não existe um caminho de recuperação pra dados que nunca foram salvos em outro lugar, que é por que o Passo 6 não é opcional na prática.
Uma instalação funcional do AFFiNE é quatro serviços que dependem um do outro numa ordem específica: Postgres e Redis primeiro, o job de migration depois, a aplicação por último. Depois que essa ordem está certa e os backups estão rodando, sobra muito pouco pra manter além de uma atualização de versão ocasional do Passo 7.
Por que escolhem o Railyard para isso
- Sua própria conta de cloud. Faça deploy no DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, ou em qualquer servidor Linux via SSH, cobrado direto pelo seu provedor, sem markup.
- Um preço fixo por equipe. O Free é grátis para sempre: um servidor mais um servidor de banco de dados, três apps, sem cartão. O Pro custa US$19 por mês para até cinco servidores e apps ilimitados. O Team custa US$49 por mês com servidores ilimitados. Sem cobrança por app, por deploy, por assento ou por gigabyte.
- Templates de um clique. Faça deploy de apps open source, como bancos de dados, analytics e ferramentas de dev, no seu próprio servidor em um clique.
- Bancos de dados gerenciados. Postgres, MySQL e Redis com backups programados, verificados por restauração, e recuperação point-in-time.
- Push-to-deploy. Faça deploy a partir do GitHub, GitLab ou da CLI, com HTTPS automático e releases sem downtime.