Self-host AppFlowy on a VPS
AppFlowy-Cloud is archived, so self-hosting now means the commercial repo. Deploy its Docker stack with TLS, new .env secrets, restic backups and upgrades.
What changed for AppFlowy self-hosting in 2026
To self-host AppFlowy on a VPS today, you deploy the AppFlowy-SelfHost-Commercial repository with Docker Compose. The old AppFlowy-Cloud repository is no longer the place to start. GitHub shows AppFlowy-Cloud as archived by its owner on 11 September 2026, and it is now read-only. Its README says the repository "is no longer used to build or distribute the latest AppFlowy self-hosted server", and it sends self-hosters to AppFlowy-SelfHost-Commercial.
The change matters for more than the clone URL. The archived AppFlowy-Cloud repository carries the AGPL-3.0 licence. The repository you deploy from now does not. Its README says: "This repository is distributed under the AppFlowy Self-Hosted Commercial License." That is a proprietary licence, so you run the vendor's prebuilt images under the vendor's terms.
This guide was checked against that repository on 27 October 2026. The README release notes name v0.19.11 as the latest release. Docker Hub already carries newer images, which the upgrade section explains.
What the AppFlowy free tier gives one person or a small team
The SelfHost-Commercial README lists a Free Tier with these items:
- One user seat per instance.
- Access to the AppFlowy web app through your own domain.
- Up to 3 guest editors, added to selected pages, with view, comment or edit permission.
- Version history and the Publish Pages feature.
- Unlimited workspaces.
For one person, this is enough: the one seat is you. For a small team, it does not stretch far. A second full member needs a paid self-hosted plan. Anyone else can join only as a guest on the pages you share, and at most three people can do that. The README says you "can start with the Free Tier and upgrade the same deployment to a paid self-hosted plan". For prices, it points to the Admin Panel of your own deployment and to the AppFlowy pricing page. This guide does not quote self-hosted prices, because the public pricing page did not show them in a form that could be checked.
Read the licence file before you use the free tier for work. SELF_HOST_LICENSE_AGREEMENT.md in the repository reads as a paid licence. It says "This license is granted on a per-server (per-machine) basis" and "You agree to pay the annual license fee as specified by Licensor." It limits use to "your internal business purposes", and it says "You shall not copy, modify, distribute, sell, lease, sublicense, or transfer the Software". The agreement does not mention a free tier, seats or guests. The README does. The two documents do not match, and this guide cannot settle that for you. If your use depends on it, ask AppFlowy in writing.
The README and the deployment docs describe no licence key or activation step for the free tier. The release notes do mention seat enforcement. After the first start, open the Admin Panel and check which plan your instance reports.
If a proprietary licence rules AppFlowy out, the guide to self-hosted Notion alternatives compares the other options. Self-hosting AFFiNE on a VPS covers another block-based workspace you run yourself.
What the AppFlowy compose file starts
The repository's docker-compose.yml defines twelve services. Eleven start by default. The twelfth, appflowy_backup, starts only when you turn on the backup profile. The roles below come from the routes in docker/nginx/nginx.conf and the comments in deploy.env. The repository has no separate page that describes each service.
nginx: the reverse proxy, and the only service that publishes host ports (80 and 443). It also terminates TLS (transport layer security).postgres: the database, from thepgvector/pgvector:pg16image. Data lives in thepostgres_datavolume.redis: job queues, with no data volume. The docs say Redis queues are not durable.minio: S3-compatible object storage for uploaded files, in theminio_datavolume. The default bucket isappflowy.gotrue: authentication. nginx sends/gotrue/to it on port 9999.appflowy_cloud: the API and the realtime websocket. nginx sends/api,/ws,/ai/and/scimto it on port 8000.admin_frontend: the admin console at/console, on port 3000.appflowy_web: the web app at/.appflowy_worker: background jobs.appflowy_search: search on port 4002, with its index in thekeyword_index_datavolume.ai: AI features on port 5001. They stay off with the template's empty AI provider settings.appflowy_backup: the optional built-in backup service.
Postgres, Redis and MinIO publish no host ports, so they are reachable only on the compose network. The bundled nginx does proxy two admin paths, though. /minio/ goes to the MinIO web console, and /pgadmin/ goes to a pgadmin host. The current compose file has no pgadmin service, so that second path has nothing behind it. The MinIO console path is live on your public domain. That is one more reason to replace the default minioadmin keys. If you never use the console, you can also delete the location /minio/ block from docker/nginx/nginx.conf.
How much RAM and disk a small VPS needs
AppFlowy's docs/HARDWARE_SIZING.md starts at 100 peak online users. For that tier on a single server, it gives 4 vCPU and 16 GiB of RAM for the whole host, MinIO included. The document calls every figure a planning estimate. It asks for SSD-backed storage for PostgreSQL and Search, and it gives no profile below 100 users.
So there is no vendor figure for one person on a 2 or 4 GB VPS. Plan for what the stack is: eleven containers, including PostgreSQL with pgvector, Redis, MinIO and a search index. After the first start, run sudo docker stats --no-stream and size the server from what you see. Disk use grows with your attachments in MinIO and with the database. Leave room for the local database dumps that the backup section creates. The images are built for AMD64 and ARM64, so an ARM VPS works too.
Install Docker and clone the repository
The docs require Docker Compose 2.30 or newer. If Docker is not installed yet, the Docker Compose basics guide for a VPS walks through it. Then check the version:
docker compose version --shortThe output should be 2.30.0 or higher. Next, clone the repository and create your .env from the template:
sudo git clone https://github.com/AppFlowy-IO/AppFlowy-SelfHost-Commercial /opt/appflowy
cd /opt/appflowy
sudo cp deploy.env .env
sudo chmod 600 .envThe directory name matters. Compose takes the project name from it, so your volumes become appflowy_postgres_data, appflowy_minio_data and appflowy_keyword_index_data. Run every docker compose command from /opt/appflowy. The reason is that .env also sets COMPOSE_FILE, which decides which compose files load. For more on that variable, see how Docker Compose combines multiple files.
The .env settings to change before first start
Change the secrets before the first docker compose up. The reason is the Postgres image. Its docs say its variables "will only have an effect if you start the container with a data directory that is empty", and "any pre-existing database will be left untouched on container startup." If you start once with POSTGRES_PASSWORD=password and change it later, the database keeps the old password. Every AppFlowy service then fails to log in.
Generate three random strings by running this three times:
openssl rand -hex 24Use hex because deploy.env puts the Postgres password inside connection URLs, for example APPFLOWY_DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}. A password that holds @ or / breaks that URL. Hex uses only 0-9 and a-f.
Open the file with sudo nano .env and set these lines:
FQDN=notes.example.com
SCHEME=https
WS_SCHEME=wss
POSTGRES_PASSWORD=<hex string 1>
GOTRUE_JWT_SECRET=<hex string 2>
GOTRUE_ADMIN_EMAIL=you@example.com
GOTRUE_ADMIN_PASSWORD=<a long password you will type>
AWS_ACCESS_KEY=appflowy-storage
AWS_SECRET=<hex string 3>
PGADMIN_DEFAULT_PASSWORD=<anything except password>The template defaults are FQDN=localhost, SCHEME=http, WS_SCHEME=ws, POSTGRES_PASSWORD=password, GOTRUE_ADMIN_EMAIL=admin@example.com, GOTRUE_ADMIN_PASSWORD=password, GOTRUE_JWT_SECRET=hello456, and minioadmin for both MinIO keys. GOTRUE_JWT_SECRET signs login tokens, so anyone who knows hello456 can forge them.
You do not need to edit the public URLs by hand. In deploy.env they are built from the three values above: APPFLOWY_BASE_URL=${SCHEME}://${FQDN}, APPFLOWY_WEBSOCKET_BASE_URL=${WS_SCHEME}://${FQDN}/ws/v2, API_EXTERNAL_URL=${APPFLOWY_BASE_URL}/gotrue, and APPFLOWY_S3_PRESIGNED_URL_ENDPOINT=${APPFLOWY_BASE_URL}/minio-api. Check your edits:
sudo grep -nE '^(FQDN|SCHEME|WS_SCHEME|POSTGRES_PASSWORD|GOTRUE_JWT_SECRET|GOTRUE_ADMIN_PASSWORD|AWS_ACCESS_KEY|AWS_SECRET)=' .envNo line in that output should still read localhost, password, hello456 or minioadmin.
Two sign-up settings also matter. GOTRUE_MAILER_AUTOCONFIRM=true confirms new accounts without an email, which is why the stack works with no SMTP (simple mail transfer protocol) server. GOTRUE_DISABLE_SIGNUP=false lets anyone who reaches your domain create an account. The free tier has one seat, so you will close sign-up after your own account exists. The start section shows how.
How to add TLS to the AppFlowy nginx
The bundled nginx terminates TLS itself. Its config listens on 443 ssl and reads ssl_certificate /etc/nginx/ssl/certificate.crt. The compose file mounts those files from ./docker/nginx/ssl/certificate.crt and ./docker/nginx/ssl/private_key.key. The repository ships a development certificate there, and the docs say to replace it for HTTPS. The simplest path is a Let's Encrypt certificate from certbot, issued before the first start while port 80 is still free.
Point a DNS A record for notes.example.com at the VPS. Open ports 80 and 443 in the VPS firewall and in your provider's network firewall. Then issue the certificate:
sudo apt update && sudo apt install -y certbot
sudo certbot certonly --standalone -d notes.example.comcertbot saves the files under /etc/letsencrypt/live/notes.example.com/. Copy them into the paths nginx reads. Use fullchain.pem, not cert.pem, so clients also get the intermediate certificate:
sudo cp /etc/letsencrypt/live/notes.example.com/fullchain.pem /opt/appflowy/docker/nginx/ssl/certificate.crt
sudo cp /etc/letsencrypt/live/notes.example.com/privkey.pem /opt/appflowy/docker/nginx/ssl/private_key.key
sudo chmod 600 /opt/appflowy/docker/nginx/ssl/private_key.keyRenewal needs port 80, which nginx holds once the stack runs. certbot runs every executable file in /etc/letsencrypt/renewal-hooks/pre, deploy and post as pre, deploy and post hooks. Use them to stop nginx, copy the new files, and start nginx again:
sudo tee /etc/letsencrypt/renewal-hooks/pre/appflowy-nginx-stop.sh >/dev/null <<'EOF'
#!/bin/sh
cd /opt/appflowy && docker compose stop nginx
EOF
sudo tee /etc/letsencrypt/renewal-hooks/deploy/appflowy-copy-cert.sh >/dev/null <<'EOF'
#!/bin/sh
cp /etc/letsencrypt/live/notes.example.com/fullchain.pem /opt/appflowy/docker/nginx/ssl/certificate.crt
cp /etc/letsencrypt/live/notes.example.com/privkey.pem /opt/appflowy/docker/nginx/ssl/private_key.key
EOF
sudo tee /etc/letsencrypt/renewal-hooks/post/appflowy-nginx-start.sh >/dev/null <<'EOF'
#!/bin/sh
cd /opt/appflowy && docker compose start nginx
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/*/appflowy-*.shThe cost is downtime while nginx is stopped at each renewal. The certbot docs say a --dry-run does not run deploy hooks by default, so a dry run does not test the copy step. After the first real renewal, check the date nginx serves by connecting to the TLS endpoint: openssl s_client -connect notes.example.com:443 -servername notes.example.com </dev/null 2>/dev/null | openssl x509 -enddate -noout.
If the VPS already runs Caddy or Traefik for other sites, the bundled nginx cannot also own ports 80 and 443. You would then move it to another port, or replace it, and forward every route nginx.conf defines: /gotrue/, /api, /ws, /minio-api/, /console, /scim, /ai/ and /. The /ws route needs websocket upgrade headers, and the bundled config gives it proxy_read_timeout 86400s. Uploads need large bodies: the bundled config allows 2G on /api/import and sets no limit on /minio-api/. The repository has no guide for this setup, so you would own it. The comparison of nginx, Caddy and Traefik as a reverse proxy covers the trade-offs. On a box that only runs AppFlowy, keep the bundled nginx.
Start AppFlowy and log in
cd /opt/appflowy
sudo docker compose up -d
sudo docker compose psThe first start pulls about a dozen images. After a minute or two, every service should show Up. A service whose status keeps going back to Restarting has failed. Start with its logs: sudo docker compose logs --tail 50 appflowy_cloud, with the service name you need. Container logs do not always explain a restart: applications can write to files instead of standard output, and logging-driver settings can prevent useful output.
Check that nginx serves your real certificate:
curl -I https://notes.example.com/consoleYou should get an HTTP status line back. If curl reports SSL certificate problem, read the error details and inspect the served certificate; an untrusted issuer or a hostname mismatch also causes verification to fail. If nginx is still serving the development certificate, copy the files again and run sudo docker compose restart nginx.
Open https://notes.example.com/console in a browser and sign in with GOTRUE_ADMIN_EMAIL and GOTRUE_ADMIN_PASSWORD. The web app itself is at https://notes.example.com/. Create your own account there. Then close sign-up, so that a stranger cannot take your only seat. That risk is our reading of a one-seat tier with open sign-up, not a vendor statement.
sudo sed -i 's/^GOTRUE_DISABLE_SIGNUP=false/GOTRUE_DISABLE_SIGNUP=true/' .env
sudo docker compose up -d gotrueThe docs apply changes to GOTRUE_* settings the same way, by recreating the gotrue service alone.
Point the desktop and mobile apps at your server
The web app needs no setup: it is your domain. AppFlowy's Local Host Deployment guide places the desktop server settings at the bottom of the login page. Set the base URL to https://notes.example.com. For mobile, look in the app's settings for the option to use your own server, and enter https://notes.example.com.
Test your phone before you depend on it. An issue opened on 22 October 2026 (issue #40 in the SelfHost-Commercial repository) reports that the current iOS client from the App Store cannot finish sign-in against server 0.19.10. The client connects to /ws/v1, but the server only serves /ws/v2, so the request gets a 404 and the app shows error 1017. Sign-in from the web app worked. This is one user's report, and Android was not tested. On 27 October 2026 the issue was still open with no maintainer reply. You can see the server side of the mismatch in deploy.env, which builds the websocket URL with /ws/v2.
Back up the database and object storage with restic
AppFlowy has an optional built-in backup. Set APPFLOWY_BACKUP_PROFILE=backup in .env and run sudo docker compose up -d. You then manage backups under Tools, then Backup, in the admin console. Two limits matter. The backups go to a private bucket, appflowy-backups, inside the same MinIO that holds your data, so losing the VPS loses them too. And the docs say: "Configuration, deployment secrets, and external encryption keys remain part of your separate recovery procedure."
So you still need a copy off the server. The plan below is ours, not the vendor's. The AppFlowy docs give no restic or pg_dump commands. If you do not have a restic repository yet, setting up restic backups on a VPS covers creating one and its password file. Set the two paths in the script below to match yours.
What to save:
- A
pg_dumpof thepostgresdatabase. GoTrue's accounts live in the same database, in theauthschema (GOTRUE_DATABASE_URLends in?search_path=auth), so one dump covers both. - The
appflowy_minio_datavolume, which holds your files. - The
appflowy_keyword_index_datavolume. .envanddocker/nginx/ssl/, because the vendor docs leave configuration and secrets to you.
A dump is safer than copying postgres_data while Postgres writes to it. restic can back up a command's output directly with --stdin-from-command, but that option arrived in restic 0.17.0, and Ubuntu 24.04 ships restic 0.16.4. Writing the dump to a file first works on both.
sudo apt install -y restic
sudo install -d -m 700 /var/backups/appflowy
sudo tee /usr/local/sbin/appflowy-backup >/dev/null <<'EOF'
#!/bin/sh
set -eu
export RESTIC_REPOSITORY=sftp:backup@203.0.113.20:/srv/restic/appflowy
export RESTIC_PASSWORD_FILE=/root/.restic-appflowy-pass
cd /opt/appflowy
docker compose exec -T postgres pg_dump -U postgres -d postgres -Fc > /var/backups/appflowy/postgres.dump
trap 'docker compose start minio' EXIT
docker compose stop minio
restic backup /var/backups/appflowy \
/var/lib/docker/volumes/appflowy_minio_data/_data \
/var/lib/docker/volumes/appflowy_keyword_index_data/_data \
/opt/appflowy/.env /opt/appflowy/docker/nginx/ssl
EOF
sudo chmod 700 /usr/local/sbin/appflowy-backupThe script stops MinIO for the length of the copy, so no file is captured half-written. Uploads fail while MinIO is stopped. The trap line starts MinIO again even when restic fails. Confirm the volume names with sudo docker volume ls first, because a different clone directory gives a different prefix.
Run it once by hand, then check the result:
sudo /usr/local/sbin/appflowy-backup
ls -lh /var/backups/appflowy/postgres.dump
sudo RESTIC_PASSWORD_FILE=/root/.restic-appflowy-pass restic -r sftp:backup@203.0.113.20:/srv/restic/appflowy snapshotsThe dump should not be zero bytes, and snapshots should list a new entry. Schedule the script nightly with one line in /etc/cron.d/appflowy-backup:
15 3 * * * root /usr/local/sbin/appflowy-backupTo restore, start a fresh install with the same .env. Load the dump with sudo docker compose exec -T postgres pg_restore -U postgres -d postgres --clean --if-exists < postgres.dump, and put the MinIO files back while MinIO is stopped. Test this once on a spare VPS. A backup you have never restored is untested.
How to upgrade AppFlowy without losing data
Eight of the nine AppFlowy images in the compose file default to the latest tag through a version variable, for example appflowyinc/appflowy_cloud:${APPFLOWY_CLOUD_VERSION:-latest}. The exception is minio, which the compose file pins to appflowyinc/minio:RELEASE.2025-09-07T16-13-09Z with no version variable. So for the other eight, latest means whatever the vendor pushed most recently, and the README can fall behind it. On 27 October 2026 the README named v0.19.11 as the latest release. On Docker Hub, appflowyinc/appflowy_cloud:latest and appflowyinc/appflowy_worker:latest were already 0.20.0, pushed on 26 October 2026. The images also use separate version numbers: on 28 October 2026, appflowy_web:latest was at 0.19.29 and gotrue:latest at 0.18.11. A fresh install today runs the 0.20.0 server, which the README release notes do not describe yet.
The vendor's upgrade rules are short. Keep the same Compose project name, which here means keep the clone in /opt/appflowy. A new directory name gives new, empty volumes. Never run docker compose down -v as an upgrade step, because it deletes the data volumes. Upgrade Cloud, Worker, Search, GoTrue, Admin and Backup as a compatible set, and let database migrations finish first. Some releases need extra steps. v0.19.2 needed a coordinated rollout. v0.18.5 needed APPFLOWY_WORKER_DATABASE_MAX_CONNECTIONS to allow at least 3 connections if you had changed it. Read the release notes in the README before each upgrade.
The docs do not give a single upgrade command. This sequence uses standard Compose commands:
sudo /usr/local/sbin/appflowy-backup
cd /opt/appflowy
sudo git pull
sudo diff deploy.env .env
sudo docker compose pull
sudo docker compose up -d
sudo docker compose logs -f appflowy_cloudThe diff shows variables that the new template added and your .env lacks. If git pull stops with Your local changes to the following files would be overwritten by merge, the files it names are usually your certificate files or an edited nginx.conf. Move them aside, pull, and copy them back. To choose when upgrades happen, set the version variables in .env, such as APPFLOWY_CLOUD_VERSION and GOTRUE_VERSION. Pin all eight images that have a variable at once, with tags you read from Docker Hub, because each image has its own version number. minio has no variable to set, because the compose file already pins its tag.
Failure modes, with the strings you will see
Every service fails to reach the database. sudo docker compose logs postgres shows:
FATAL: password authentication failed for user "postgres"You changed POSTGRES_PASSWORD after the first start, and the database still holds the old one. Put the old value back in .env. Or change the password inside Postgres to match the new value with sudo docker compose exec postgres psql -U postgres -c "ALTER USER postgres PASSWORD 'new-hex-value';", then run sudo docker compose up -d.
The browser shows a certificate warning. Check the served certificate and the warning details; a warning alone does not identify the bundled development certificate. If nginx is serving that certificate, copy fullchain.pem and privkey.pem into docker/nginx/ssl/ and restart nginx.
nginx will not start. Another program holds port 80 or 443, often Apache or a second nginx on the host. sudo ss -ltnp | grep -E ':(80|443) ' shows which process. Stop it, or move AppFlowy behind it as the TLS section describes.
Your workspace is empty after a move. sudo docker volume ls shows two postgres_data volumes with different prefixes. The stack started under a new project name and created empty volumes. Go back to the old directory, or set COMPOSE_PROJECT_NAME in .env to the old prefix.
The phone app fails with error 1017. This matches the open /ws/v1 report in the apps section. Use the web app on the phone until AppFlowy ships a fix.
FAQ
Is self-hosted AppFlowy still open source?
The old AppFlowy-Cloud repository is AGPL-3.0, but GitHub shows it archived on 11 September 2026. Its README sends self-hosters to AppFlowy-SelfHost-Commercial. That repository says it "is distributed under the AppFlowy Self-Hosted Commercial License", which is a proprietary licence. You run the vendor's prebuilt Docker images under those terms.
How many users does the AppFlowy self-hosted free tier allow?
The SelfHost-Commercial README lists one user seat per instance, plus up to 3 guest editors on selected pages. One person fits. A second full team member needs a paid self-hosted plan. The licence agreement in the same repository does not mention the free tier at all, so read both documents before you rely on it for work.
How much RAM does a self-hosted AppFlowy server need?
AppFlowy's sizing document starts at 100 peak online users. For that tier it gives 4 vCPU and 16 GiB of RAM on one server, MinIO included. It calls these planning estimates and gives no smaller profile. For a single user, start the eleven-container stack, run sudo docker stats --no-stream, and size the VPS from what you measure.
Why can my iPhone not sign in to my self-hosted AppFlowy server?
An open GitHub issue from 22 October 2026 reports that the current iOS client connects to /ws/v1, while server 0.19.10 serves only /ws/v2. The request gets a 404, and the app shows error 1017. In the same report, sign-in through the web app at your domain worked.