How to Deploy Appwrite on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Self-host Appwrite, the open-source backend-as-a-service, on a VPS using its official installer run unattended. Covers domain and certificates, locking down the console, backups and upgrades.
- A VPS with at least 2 vCPU / 4 GB RAM (the catalog minimum; Appwrite runs around thirty containers)
- A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
- A domain you can point at the server, with ports 80 and 443 free for Appwrite's own proxy
- Docker Engine + Compose installed (see the base guide below)
What Appwrite is
Appwrite is a BSD-3-Clause backend-as-a-service. It bundles authentication, databases, file storage, serverless functions, messaging, real-time subscriptions and static site hosting behind one API and a web console, with SDKs for web, mobile and server. Supabase and Firebase fill the same role. Supabase vs Appwrite and PocketBase vs Appwrite compare them, and Directus vs Appwrite covers the data-platform angle.
Appwrite is not a single container. The official installer writes a Docker Compose project with the API, the console, MongoDB, Redis, a Traefik proxy, an executor for functions, and a long list of workers and schedulers. On our test box it started 29 containers. Size the server for that and treat it as a dedicated machine.
Server sizing
The catalog's minimum for Appwrite is 4 GB RAM, with 2 vCPU. We don't have a formal idle measurement for it yet. As a rough check, shortly after install on our 8 GB test box (GCP e2-standard-2, Ubuntu 26.04), about 3.5 GB of the machine's memory was in use, OS included.
- 4 GB RAM / 2 vCPU: the floor. Fine for development and small projects.
- 8 GB RAM / 4 vCPU: comfortable. Function builds and executions run in extra containers and are the first thing to run out of memory.
- Disk: 40 GB or more. Images, the database, uploaded files and function and site builds all accumulate.
Give Appwrite ports 80 and 443 to itself. Its Traefik container binds them, obtains Let's Encrypt certificates for your domain, and routes custom domains for functions and sites. Running another reverse proxy on the same ports means reworking that setup, which this guide doesn't cover.
Prepare the server
This guide assumes Docker Engine and the Compose plugin are installed. If not, work through Docker & Compose on Ubuntu first. Open SSH and the two web ports:
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw --force enable
sudo ufw status verbose
Point an A record for appwrite.example.com at the server's public IP now,
so DNS has time to propagate before you configure the domain.
Paid link — we earn a commission if you shop through it.
Install Appwrite (official installer, unattended)
Appwrite installs through its own image. The container gets the Docker socket,
writes docker-compose.yml and .env into ./appwrite, generates the secrets
and starts the stack. By default it opens an interactive web installer. Passing
--interactive=N together with the ports skips that and uses the defaults,
which is easier to repeat:
cd ~
docker run --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.9.5 \
--interactive=N --http-port=80 --https-port=443
The first run pulls a lot of images and took about three minutes on our test
box. When it finishes, check that the stack is up. The main appwrite
container and MongoDB report health checks:
cd ~/appwrite
docker compose ps --format '{{.Name}}\t{{.Status}}'
Some scheduler and task containers restarted once during the first boot in our test and then stayed up. That is normal. Look for containers that keep restarting, not ones that restarted once.
The console answers on port 80 and redirects to /console/:
curl -s -o /dev/null -w '%{http_code} -> %{redirect_url}\n' http://localhost/
Everything the installer generated lives in ~/appwrite/.env: the database
passwords, _APP_OPENSSL_KEY_V1 (the key Appwrite encrypts data with) and the
executor secret. This file matters as much as the database. Without it, a
restored database is unreadable. The installer runs as root, so the files are
owned by root. Use sudo to edit them.
Domain and HTTPS
A fresh install thinks its domain is localhost. Point it at your real
hostname by editing the domain variables in .env. _APP_DOMAIN is the
console and API hostname, the _APP_DOMAIN_TARGET_* values are what custom
domains should point to, and _APP_SYSTEM_SECURITY_EMAIL_ADDRESS is the
contact address used for Let's Encrypt:
cd ~/appwrite
sudo sed -i \
-e 's/^_APP_DOMAIN=.*/_APP_DOMAIN="appwrite.example.com"/' \
-e 's/^_APP_DOMAIN_TARGET=.*/_APP_DOMAIN_TARGET="appwrite.example.com"/' \
-e 's/^_APP_DOMAIN_TARGET_CNAME=.*/_APP_DOMAIN_TARGET_CNAME="appwrite.example.com"/' \
-e 's/^_APP_DOMAIN_TARGET_A=.*/_APP_DOMAIN_TARGET_A="203.0.113.10"/' \
-e 's/^_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=.*/_APP_SYSTEM_SECURITY_EMAIL_ADDRESS="you@example.com"/' \
.env
docker compose up -d
Use your server's real IPv4 address for _APP_DOMAIN_TARGET_A. Set
_APP_DOMAIN_FUNCTIONS and _APP_DOMAIN_SITES too if you plan to serve
functions or sites from their own subdomains. Their defaults are
functions.localhost and sites.localhost.
Once https://appwrite.example.com/console/ loads with a valid certificate,
turn on HTTPS redirects. Set _APP_OPTIONS_FORCE_HTTPS and
_APP_OPTIONS_ROUTER_FORCE_HTTPS from disabled to enabled in .env, then
run docker compose up -d again. Do this after the certificate works. Forcing
HTTPS first locks you out of a console that can't yet serve it.
Securing the console
Open the console and create your account immediately. The installer sets
_APP_CONSOLE_WHITELIST_ROOT="enabled", which lets only the first user sign up
to the console. It is the instance's root account, so claim it before anyone
else finds the URL. After that:
- Restrict console sign-ups further if you need to, with
_APP_CONSOLE_WHITELIST_EMAILS(allowed addresses) or_APP_CONSOLE_WHITELIST_IPS. Both are empty by default. - Leave abuse protection on.
_APP_OPTIONS_ABUSEisenabledby default and rate-limits the API. - Configure SMTP (
_APP_SMTP_HOSTand related variables). Otherwise email verification, password recovery and team invitations won't send. - Treat API keys like passwords. Create one per server-side integration with only the scopes it needs. Never put a server API key in a client app. Client SDKs authenticate as users, not with keys.
- Protect
~/appwrite/.env. Back it up somewhere encrypted and keep it out of git.
Backups
Appwrite's state has three parts: the MongoDB database, the named
volumes (uploads, functions, builds, sites, certificates, config), and
.env. The installer names the volumes after the project, for example
appwrite_appwrite-uploads. Check with docker volume ls.
A database dump with the root credentials from .env:
cd ~/appwrite
mkdir -p ~/appwrite-backups
set -a; . ./.env; set +a
docker compose exec -T mongodb mongodump -u root -p "$_APP_DB_ROOT_PASS" \
--authenticationDatabase admin --archive --gzip > ~/appwrite-backups/mongo-$(date +%F).archive.gz
ls -lh ~/appwrite-backups/
Then the file volumes and the configuration:
cd ~/appwrite
for v in uploads functions builds sites certificates config; do
docker run --rm -v appwrite_appwrite-$v:/data:ro -v ~/appwrite-backups:/backup alpine \
tar czf /backup/$v-$(date +%F).tar.gz -C /data .
done
sudo cp .env docker-compose.yml ~/appwrite-backups/
sudo chown -R "$USER": ~/appwrite-backups
ls -lh ~/appwrite-backups/
Copy ~/appwrite-backups off the server. To restore, reinstall the same
Appwrite version, put back .env and the volumes, and load the archive with
mongorestore --archive --gzip inside the mongodb container.
Upgrades
Upstream's upgrade path is the installer again, run with the new version from
the directory that contains appwrite/. It detects the existing install and
keeps your .env. After that, run the data migration inside the new
container. Back up first, every time. Read the release notes too, because
Appwrite versions sometimes need specific upgrade steps.
cd ~
docker run --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="upgrade" \
appwrite/appwrite:<new-version> --interactive=N
cd ~/appwrite
docker compose exec appwrite migrate
Don't try to roll back by changing tags. The migration changes your data, so the way back is the backup you took first.
Troubleshooting
The installer finishes but the console never loads. Run
docker compose ps in ~/appwrite and read the logs of anything not running,
starting with docker compose logs appwrite --tail 100 and
docker compose logs mongodb --tail 100. On a 4 GB box, memory is the usual
cause. Check free -m.
No certificate for your domain. DNS isn't pointing at the server yet, port 80
is blocked, or _APP_DOMAIN is still localhost. Traefik obtains certificates
for the configured domain only. Read docker compose logs traefik and
docker compose logs appwrite-worker-certificates.
"Sign up is restricted" on the console. The root whitelist is doing its job: someone, probably you, already created the first account. Sign in with it, or adjust the whitelist variables.
Functions fail to build or run. Builds and executions run through the
executor with the Docker socket. Check docker compose logs openruntimes-executor,
and check free memory and disk. Both run out quickly on the smallest plans.
Verification + next steps
You're done when:
https://appwrite.example.com/console/loads over a valid certificate;- you have created the root account;
- the HTTPS-redirect options are enabled;
- a test project with one collection and one file survives
docker compose restart; - a dated MongoDB dump, the volume archives and
.envexist off the server.
From there, connect a client SDK to a project, or deploy a first function. For a lighter backend, see PocketBase on a VPS. For a Postgres-based one, see Supabase on a VPS. For ranked hosts, see Best VPS for self-hosting.