DockerSelf-HostingAI Coding Agents

AFFiNE with Docker: a self-hosted Notion and Miro alternative (Linux, Windows, Mac)

Before you start

  • 20 min read

Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#

AFFiNE is an open-source workspace that merges documents, a free-form whiteboard and databases in one app. You can run its server yourself with Docker Compose and keep your notes on your own machine.

AFFiNE official logo

Logo: the official AFFiNE project (affine.pro)

The project calls AFFiNE "a privacy-focused, local-first, open-source, and ready-to-use alternative for Notion & Miro". It suits a person or small team that wants wiki pages, boards and a free-form canvas without a cloud account. Know three things first: the project says it is in beta, the server code has its own licence (see "The licence, honestly"), and a self-hosted workspace gets at most 10 seats.

WhatValue (official docs, read November 2026)
Current versionAFFiNE 0.27.4, released August 2026 (latest stable on GitHub)
Image to pinghcr.io/toeverything/affine:0.27.4; the tag stable pointed at the same image when we checked
Time neededAbout 20 minutes for the Compose route (our estimate, not measured)
RAM and diskDocs: 2 GB RAM for basic use, 4 GB for large docs, 4 CPU cores recommended, about 1.5 GB disk for the server
PortTCP 3010 (the web app, the API and sync)
LicenceMostly MIT; the server (packages/backend) has its own licence, read it below
Official docsdocs.affine.pro/self-host-affine and the repository

At a glance:

  • What runs: the AFFiNE server, a one-time migration job, Postgres (with pgvector) and Redis.
  • Where data lives: Postgres holds users and documents; uploads go to a folder; settings go in config/config.json.
  • Configuration changed in 0.27: settings now live in config/config.json and the Admin Panel, not in .env. Old tutorials are wrong for this.
  • Seats: up to 10 per workspace; more needs a Team Plan or an Installable License.
  • AI: bring your own key (BYOK) from OpenAI, Anthropic, Gemini or FAL.
  • Upgrades are the risky part: 0.27.0 and 0.27.4 broke some upgrades. Back up first and pin the version.

How it fits together#

Your browser or app talks to one AFFiNE server container. That server uses Postgres for the data, Redis for cache, background jobs and sync, and a folder for uploaded files. An HTTPS proxy in front is optional on one machine and a must on a server.

Top-to-bottom diagram: your browser or AFFiNE app connects over HTTPS to an optional Caddy or Traefik proxy on port 443, which passes requests to the AFFiNE server on port 3010, which reads and writes Postgres with pgvector, Redis and the uploaded files folder

Drawn for this guide from the official Compose file and docs. The dashed HTTPS route is optional.

Before you start#

You need Docker with Compose v2. The docs treat Docker Compose as the supported route and name no operating-system limits. The images are built for Linux on amd64, arm64 and arm/v7.

SystemWhat to useNotes
LinuxDocker Engine with the Compose pluginThe best choice for a server others use
WindowsDocker Desktop (Linux containers)For trying it on your own PC; untested by us
MacDocker DesktopSame; Apple silicon uses the arm64 image

Check Docker on every system:

Run in terminalbash
docker --version
docker compose version

Checklist:

  • [ ] Docker and Docker Compose v2 installed (the check above works)
  • [ ] 2 GB RAM free (4 GB for long documents) and about 1.5 GB of disk
  • [ ] Port 3010 free on the machine (ss -ltn | grep 3010 on Linux shows nothing)
  • [ ] A backup plan (see "Updates and backups" below)
  • [ ] For the HTTPS section only: a domain name with a DNS record pointing at the server, and ports 80 and 443 free

Tested by DIY Lab Hub: not yet. The commands come from the official Compose file, config example and docs read in November 2026. We did not run them.

Step 1: Create the folders#

We keep everything under one folder, as all DIY Lab Hub guides do. The official Compose file expects ./data/postgres, ./data/storage and ./config next to it.

Linux#

Run in terminalbash
mkdir -p ~/docker_volumes/affine/{data,config,backups}
cd ~/docker_volumes/affine

Windows (PowerShell)#

Codepowershell
New-Item -ItemType Directory -Force "$HOME\docker_volumes\affine\data", "$HOME\docker_volumes\affine\config", "$HOME\docker_volumes\affine\backups" | Out-Null
Set-Location "$HOME\docker_volumes\affine"

Mac (Terminal)#

Same commands as Linux:

Run in terminalbash
mkdir -p ~/docker_volumes/affine/{data,config,backups}
cd ~/docker_volumes/affine

Check it worked. ls (Linux, Mac) or dir (Windows) shows the folders data, config and backups.

Tip: The docs ask you to fix the data and config locations before the first start and not to move them once the instance holds data.

Step 2: Write the Compose file#

This is the official docker-compose.yml from the v0.27.4 release with three changes of ours: AFFiNE is pinned to 0.27.4 (the official file says stable), Redis to redis:7 (the docs call Redis 6.x or 7.x "fully tested"; the official file leaves it unpinned), and the web port is published on 127.0.0.1 only (the official file uses every interface).

Linux#

Run in terminalbash
cd ~/docker_volumes/affine
cat > docker-compose.yml << 'EOF'
name: affine
services:
  affine:
    image: ghcr.io/toeverything/affine:0.27.4
    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@postgres:5432/affine
      - AFFINE_INDEXER_ENABLED=false
    restart: unless-stopped

  affine_migration:
    image: ghcr.io/toeverything/affine:0.27.4
    container_name: affine_migration_job
    volumes:
      - ./data/storage:/root/.affine/storage
      - ./config:/root/.affine/config
    command: ['sh', '-c', 'node ./scripts/self-host-predeploy.js']
    environment:
      - REDIS_SERVER_HOST=redis
      - DATABASE_URL=postgresql://affine@postgres:5432/affine
      - AFFINE_INDEXER_ENABLED=false
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  redis:
    image: redis:7
    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_DB: affine
      POSTGRES_INITDB_ARGS: '--data-checksums'
      POSTGRES_HOST_AUTH_METHOD: trust
    healthcheck:
      test: ['CMD', 'pg_isready', '-U', 'affine', '-d', 'affine']
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
EOF

Windows (PowerShell)#

Open the file in Notepad, paste the same YAML as in the Linux block, and save:

Run in terminalpowershell
notepad docker-compose.yml

Mac (Terminal)#

Same command as Linux: run the cat > docker-compose.yml << 'EOF' block above in ~/docker_volumes/affine.

Check it worked. Ask Compose to read the file; it prints the merged configuration and no error:

Run in terminalbash
docker compose config --quiet && echo "compose file OK"

On Windows, docker compose config --quiet; if ($?) { "compose file OK" } does the same.

Tip: The official file runs Postgres with POSTGRES_HOST_AUTH_METHOD: trust, which means no password. That is safe only because Postgres publishes no port. Never add a ports: line to the postgres service.

Step 3: Write the configuration file#

Since 0.27 the server reads config/config.json. The one setting you must get right is server.externalUrl: it is the exact address people type into the browser.

Linux#

Run in terminalbash
cd ~/docker_volumes/affine
cat > config/config.json << 'EOF'
{
  "$schema": "https://github.com/toeverything/affine/releases/latest/download/config.schema.json",
  "server": {
    "name": "AFFiNE Self-hosted",
    "externalUrl": "http://localhost:3010"
  },
  "copilot": {
    "enabled": true,
    "byok": {
      "enabled": true
    }
  }
}
EOF
chmod 600 config/config.json

Windows (PowerShell)#

Run in terminalpowershell
notepad config\config.json

Paste the same JSON as in the Linux block and save.

Mac (Terminal)#

Same commands as Linux.

Check it worked. The file is valid JSON. On Linux or Mac with Python installed: python3 -m json.tool config/config.json. On Windows: Get-Content config\config.json -Raw | ConvertFrom-Json | Out-Null. No output means it parsed.

Tip: localhost:3010 works only if you open exactly http://localhost:3010. http://127.0.0.1:3010 or a LAN address gets blocked, because the server checks the browser's origin against externalUrl (issue #15552, open).

Step 4: Start AFFiNE#

Linux#

Run in terminalbash
cd ~/docker_volumes/affine
docker compose up -d
docker compose ps -a
docker compose logs --tail=200 affine affine_migration

Windows (PowerShell)#

Codepowershell
Set-Location "$HOME\docker_volumes\affine"
docker compose up -d
docker compose ps -a
docker compose logs --tail=200 affine affine_migration

Mac (Terminal)#

Same commands as Linux.

Check it worked. The docs' expected result: the one-time migration container finishes successfully, while AFFiNE, Postgres and Redis keep running. In docker compose ps -a that means affine_migration_job shows Exited (0) and the other three show running or healthy. The first start downloads the images and runs migrations, so give it a few minutes. The docs publish no expected log text; read the log for errors.

Tip: If affine_server does not start, read the affine_migration_job log first; the server waits for it.

Step 5: Create the admin account#

Linux#

Open http://localhost:3010 in a browser on the same machine. The docs say the first visit sends you to the admin sign-up page (/admin), where you register the administrator and log in. On a remote server, use an SSH tunnel and then open the same address:

Run in terminalbash
ssh -N -L 3010:127.0.0.1:3010 <your-user>@<your-server-address>

Windows (PowerShell)#

Open http://localhost:3010 in your browser. For a remote server, run the same tunnel in PowerShell and keep the window open:

Run in terminalpowershell
ssh -N -L 3010:127.0.0.1:3010 <your-user>@<your-server-address>

Mac (Terminal)#

Same as Linux, including the ssh -N -L tunnel command.

Optional: install the desktop app from the download page, add your server in the workspace list at the top left, and log in. Server 0.27 needs clients 0.26 or newer; clients 0.27 need servers 0.27 (release notes).

Check it worked. You can log in and create a doc. http://localhost:3010/admin/settings opens the Admin Panel where server settings can be edited.

Tip: Store the admin password in a password manager now. If the admin sign-up page ever appears again, you are looking at an empty database (see Troubleshooting).

Put it behind HTTPS (with or without Traefik)#

Plain http://localhost:3010 is fine on your own computer. For other people, put HTTPS in front on a name such as https://affine.<your-domain>. This section is for a server with a domain; local-only users can skip it. Both routes need a DNS record pointing at the server. AFFiNE sync uses websockets and the docs say your proxy must allow them; Traefik and Caddy do by default.

After either route, change server.externalUrl in config/config.json to the final HTTPS address and run docker compose up -d to recreate the containers.

With Traefik#

On a server that already runs Traefik v3 with the Docker provider, add labels to the affine service and drop its host port.

This assumes an external Docker network proxy and an entrypoint https; use your own names. Create the network once if missing:

Run in terminalbash
docker network create proxy

Change the affine service: remove ports:, add the proxy network next to default (Postgres and Redis stay on it), and add the labels.

File contentyaml
services:
  affine:
    image: ghcr.io/toeverything/affine:0.27.4
    networks:
      - default
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=proxy"
      - "traefik.http.routers.affine.rule=Host(`affine.<your-domain>`)"
      - "traefik.http.routers.affine.entrypoints=https"
      - "traefik.http.routers.affine.tls=true"
      - "traefik.http.services.affine.loadbalancer.server.port=3010"
    # keep depends_on, volumes, environment and restart from Step 2

networks:
  proxy:
    external: true

Then set externalUrl to https://affine.<your-domain> and run docker compose up -d.

Check it worked. curl -I https://affine.<your-domain>/ returns an HTTP status line over HTTPS, and the page opens with a valid certificate.

Without Traefik: Caddy#

Choose this if you have no Traefik. Caddy gets and renews its own certificates.

Caddy gets Let's Encrypt certificates for a public domain and makes its own local certificate (tls internal) for names that exist only at home. Run it as one more service in the same Compose file, mounting the conf folder, not a single Caddyfile (Caddy's Docker docs warn against single-file mounts). Remove the ports: block of affine: Caddy reaches it by name on the Compose network.

Create the folder and the Caddyfile:

Run in terminalbash
mkdir -p ~/docker_volumes/affine/caddy
nano ~/docker_volumes/affine/caddy/Caddyfile

On Windows: New-Item -ItemType Directory -Force "$HOME\docker_volumes\affine\caddy" and notepad "$HOME\docker_volumes\affine\caddy\Caddyfile". For a public domain, the file is:

File contenttext
affine.example.com {
    reverse_proxy affine:3010
}

For a home-only name, use tls internal instead:

File contenttext
affine.home.arpa {
    tls internal
    reverse_proxy affine:3010
}

Add this service and these volumes to docker-compose.yml. The image caddy:2.11.8 was the latest Caddy tag on Docker Hub when we checked:

File contentyaml
  caddy:
    image: caddy:2.11.8
    container_name: affine_caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./caddy:/etc/caddy
      - caddy_data:/data
      - caddy_config:/config
    depends_on:
      - affine

volumes:
  caddy_data:
  caddy_config:

The caddy service goes under services:; volumes: is top level. Then set externalUrl and run docker compose up -d.

For the home-only name, each device must trust Caddy's own root certificate. Copy it out of the container (run this on the machine that runs Docker), then import it on each device. These import commands follow Caddy's docs for each system:

Run in terminalbash
docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt ./root.crt
  • Linux: sudo cp root.crt /usr/local/share/ca-certificates/root.crt && sudo update-ca-certificates
  • Windows (PowerShell, as Administrator): certutil -addstore -f "ROOT" root.crt
  • Mac (Terminal): sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain root.crt

Caddy's docs add that Firefox and Chrome may need root.crt imported by hand. Point the name at the server's LAN address in your router's DNS or a hosts file.

Check it worked. curl -I https://affine.example.com/ returns an HTTP status line over HTTPS. For a home-only name, the browser shows a padlock only after the root is trusted on that device. See also our Caddy reverse proxy guide.

Tip: A certificate error usually means the name in the Caddyfile is not the name in DNS, or ports 80 and 443 do not reach the server.

What self-hosting changes#

The cloud version at app.affine.pro and your own server run the same app; the surroundings differ (official self-host docs).

TopicSelf-hosted (your server)
SeatsUp to 10 per workspace; more needs a Team Plan, or an Installable License for servers that cannot reach the licence service
File storageUnlimited quota and size per the docs; local folder by default, or S3-compatible storage or Cloudflare R2
AIBYOK only: each workspace owner adds a provider key under Settings, Integrations, AI BYOK
Meeting notes (macOS)The docs page says self-hosted is not supported; it may be older than the BYOK docs
LoginEmail codes need SMTP (Admin Panel, Notification); Google, GitHub and OIDC are under Admin Panel, OAuth
Who does the workYou: Postgres, Redis, backups, upgrades and HTTPS

The licence, honestly#

The repository's LICENSE says most content is MIT, but everything under packages/backend and packages/common/native uses the licence at packages/backend/server/LICENSE: the "AFFiNE Enterprise Edition (EE) license". Its text says the software may be used in production only if you have agreed to the AFFiNE Subscription Terms of Service or another agreement and have a valid Enterprise Edition subscription for the right number of seats. You may modify it, publish patches and use it for development and testing, but not copy, publish or sell it. Another paragraph puts parts distributed as the Community Edition, or served client-side, under MPL 2.0 and does not list which parts those are.

The self-host docs give every workspace up to 10 seats and sell larger teams a Team Plan. We cannot say how the two fit for your case, and we are not lawyers. For a personal server the docs' 10 seats are the clear route; for a company, read both files and ask the AFFiNE team first.

Security and testing#

Postgres runs without a password, so what protects your data is the network around it.

ControlHow
Postgres and RedisPublish no port for them. Only the affine container reaches them on the Compose network
Web port127.0.0.1:3010 until HTTPS works; then no host port at all
Admin accountA long unique password; the Admin Panel is at /admin
Sign-upThe docs mention people registering themselves; check Admin Panel, Settings before exposing the server
config/config.jsonchmod 600; keep it readable by trusted admins only (SMTP credentials)
config/private.keyCreated by the pre-deploy step; keep it backed up and private, since replacing it can invalidate signed data (docs)
BackupsThey hold every document and the key file; treat them as secret

Quick tests after each change (on Windows use curl.exe):

Run in terminalbash
cd ~/docker_volumes/affine
docker compose ps -a
docker compose logs --tail=100 affine
curl -I http://localhost:3010/

What leaves the machine: if you use AI, the text goes to the provider whose key you added; a Team licence is checked against app.affine.pro. The docs list optional Google Analytics settings but describe no default telemetry.

Common mistakes#

From the issue tracker (read November 2026), the docs' warnings and common AI answers.

MistakeWhy it happensDo this instead
Swapping in the new Compose file on an old .env install0.27.4 moved Postgres to ./data/postgres, uploads to ./data/storage, and logs in as a passwordless affine role. Issue #15517 (closed): users and workspaces vanished and then role "affine" does not existFollow the docs' legacy .env checklist: back up, diff the files, keep the old paths or restore into the new layout. Never delete .env or old folders before it works
Upgrading without a backupIssue #15240 (closed): 0.26.7 to 0.27.0 left tables missing and Prisma stuck on P3009 for some users; a fixed 0.27 tag followedBack up Postgres and uploads first, pin the tag, read the release notes, read the affine_migration_job log
Opening AFFiNE at a different address than externalUrlThe CORS allow-list is built from server.externalUrl. Issue #15552 (open): every request from another origin is blockedUse one address and put it in externalUrl; recreate the containers after a change
Putting AI provider keys in config.json or .envOld tutorials did this. The docs now say keys belong to the workspaceAdd keys under Settings, Integrations, AI BYOK. Issue #15294 (closed) got the same answer
Mixing client and server versionsThe 0.27 release notes: clients 0.27 need servers 0.27, servers 0.27 need clients 0.26 or newerUpgrade server and apps together
Expecting the mobile apps to just work0.27 reports (iOS #15343, closed; Android #15299, closed as a duplicate) describe self-hosted sign-in failingTest web and desktop first; check the tracker before promising mobile access
Jumping to a beta tagIssue #15784 (open): a 0.27.5 beta failed migration with column "auth_epoch" does not existStay on the stable tag
AI assistants: telling you to put DB_PASSWORD, UPLOAD_LOCATION or AFFINE_REVISION in a .env fileThese were the old Compose variables. The docs say the current Compose file no longer reads themUse the official file, config/config.json and the Admin Panel
AI assistants: recommending stable or latest as the imageMany examples copy the official file; the docs say not to use latest as a blind upgrade pathPin 0.27.4 and change the tag yourself after reading the release notes

Tip: Before you copy any Compose file from a blog or an AI answer, compare its image tag, volume paths and database line with the official file at the release tag you pin.

Tip: Run docker compose logs --tail=200 affine affine_migration after every start or upgrade. Most failures print there.

Tip: Test your backup the day you set it up, not the day you need it. Reading the dump with pg_restore --list takes seconds.

Troubleshooting#

ProblemSolution
The browser asks you to create a new administrator after an upgradeThe new Compose file started an empty Postgres folder. Stop it, go back to the old files, take a backup, then migrate on purpose (docs, Upgrade)
FATAL: role "affine" does not existThe database was created with another user (old DB_USERNAME). Use the old Compose file to dump with the real role; do not create an affine role to silence it
Migration job fails or tables are missing (relation "mail_deliveries" does not exist, Prisma P3009)Seen on 0.27.0 (#15240). Read the migration log; the reported fix was prisma migrate resolve --applied 20260618000000_backend_runtime_tables inside the container, plus the corrected 0.27 image
Blocked CORS request from origin: ... in the server log, buttons say "Connection failed"server.externalUrl does not match the address in the browser. Fix it and run docker compose up -d
"Failed to generate invitation link" on a new serverA commenter on #15372 (closed) found a log line saying new accounts cannot create invite links for 24 hours. This is a user report, not in the official docs. Wait a day, or invite by email with SMTP set
Sync icon spins foreverCheck the proxy passes websockets, then check client and server versions match the rule above
Logged out repeatedly on a desktop appReported on 0.27.x clients (#15654, #15590, both closed); update the client
Port 3010 already in useChange the left side of the mapping, for example 127.0.0.1:3011:3010, and use that address in externalUrl

Resetting a user's password#

Admin Panel, Accounts, the user's operations menu, "Reset Password": send the link to the user. The docs warn that Delete and Ban remove a user's data for good.

Known bugs (November 2026)#

  • #15784 (open): migration to 0.27.5-beta.2 fails. Use the stable tag.
  • #15552 (open): BYOK failures traced to externalUrl and probe settings.

Diagnose with docker compose ps -a, docker compose logs --tail=200 affine affine_migration and docker inspect affine_server.

Updates and backups#

Update. The docs' route is to download the target release's Compose file next to the old one, compare, then swap. For a pinned file like ours, change the tag in docker-compose.yml (both affine and affine_migration), after the backup below:

Run in terminalbash
cd ~/docker_volumes/affine
AFFINE_VERSION=v0.xx.x   # replace with the target release tag
curl -L -o docker-compose.yml.new "https://github.com/toeverything/affine/releases/download/${AFFINE_VERSION}/docker-compose.yml"
diff -u docker-compose.yml docker-compose.yml.new

Carry your changes into the new file, replace the old one, then run docker compose pull, docker compose up -d and the log command. The affine_migration service migrates the database on start. Then check login, workspaces, editing and uploads.

Back up. A complete backup is Postgres, the uploaded files and the configuration. The Postgres command is the docs' own; it reads the real user and database name from the container.

Linux#

Run in terminalbash
cd ~/docker_volumes/affine
mkdir -p backups
STAMP=$(date +%F)
docker exec affine_postgres sh -c 'pg_dump --format=custom --file=/tmp/affine.backup --username="$POSTGRES_USER" "$POSTGRES_DB"'
docker cp affine_postgres:/tmp/affine.backup "backups/affine-db-$STAMP.backup"
docker exec affine_postgres sh -c 'pg_restore --list /tmp/affine.backup > /dev/null' && echo "dump is readable"
tar -czf "backups/affine-files-$STAMP.tar.gz" data/storage config docker-compose.yml
chmod 600 backups/*

To run it every night at 03:00, save those lines as ~/docker_volumes/affine/backup.sh, run chmod +x backup.sh, and add 0 3 * * * $HOME/docker_volumes/affine/backup.sh with crontab -e. Copy the files in backups to another machine.

Windows (PowerShell)#

The $ characters are escaped so PowerShell does not expand them (our adaptation of the docs' command):

Codepowershell
Set-Location "$HOME\docker_volumes\affine"
New-Item -ItemType Directory -Force backups | Out-Null
$stamp = Get-Date -Format yyyy-MM-dd
docker exec affine_postgres sh -c "pg_dump --format=custom --file=/tmp/affine.backup --username=`$POSTGRES_USER `$POSTGRES_DB"
docker cp affine_postgres:/tmp/affine.backup "backups\affine-db-$stamp.backup"
docker exec affine_postgres sh -c "pg_restore --list /tmp/affine.backup > /dev/null"
tar -czf "backups\affine-files-$stamp.tar.gz" data/storage config docker-compose.yml

To schedule it, save those lines as backup.ps1 and run schtasks /Create /SC DAILY /ST 03:00 /TN "AFFiNE backup" /TR "powershell -NoProfile -File $HOME\docker_volumes\affine\backup.ps1".

Mac (Terminal)#

Same script as Linux. Schedule it with crontab -e (grant cron Full Disk Access if macOS blocks it) or a launchd job.

Restore. The docs' rule: stop AFFiNE, never restore over your only copy, and restore into a new, empty ./data/postgres folder while keeping the old one. Start only Postgres, copy the dump in, run pg_restore --format=custom --clean --if-exists --no-owner --no-privileges with the container's own user and database, then start everything so the migration job runs. A full restore test needs a spare machine or the live stack stopped; at least run the pg_restore --list check above after every backup.

Postgres major upgrades (16 to 17) are a backup and restore, not a tag change; the docs say so plainly.

Performance and resources#

The docs give these figures, from what the project sees on its own cloud:

  • RAM: 2 GB for basic use; 4 GB if some documents pass 10,000 words (merging a document with 10,000 edits peaks near 1 GB).
  • CPU: at least 4 cores recommended.
  • Disk: about 1.5 GB for the server, 100 MB of Postgres per 1,000 docs of 1,000 words, 10 GB of uploads per 1,000 files.
  • Prometheus: on by default, about 200 MB.
  • Image sizes (amd64, compressed, registry data): AFFiNE about 228 MB, pgvector/pgvector:pg16 about 156 MB, redis:7 about 44 MB.

We did not measure a running instance; run docker stats for your own numbers. Larger setups can use an external Postgres (DATABASE_URL), an external Redis (REDIS_SERVER_HOST) or S3-compatible file storage.

FAQ#

Is AFFiNE free to self-host? The app and the server image cost nothing, and up to 10 seats per workspace are included. The server code has its own licence; read the licence section above.

Is the project dead, archived or renamed? No. The repository is active, with a release in August 2026 and beta tags after it. The project itself says it is in beta.

Can I use my own Postgres or Redis? Yes. Set DATABASE_URL (Postgres is the only supported database) or the REDIS_SERVER_* variables, and remove the bundled service. Redis 6 or 7 is the tested range.

How does it compare with Outline or BookStack? Those are wiki tools; AFFiNE adds a whiteboard and databases. If you only need a wiki, see our Outline guide and BookStack guide.

Sources#

Checked against the official docs on 2026-11-05.