Why self-host Appwrite on a VPS
Appwrite brings together in a single platform everything an application needs on the server side: authentication with more than 30 OAuth methods, a document database with granular permissions, file storage with image transformation, serverless functions in several languages, and messaging. Its cloud version bills per usage and per resources; by self-hosting on a VPS, you deploy the whole thing via Docker and drive it from an elegant web console, with no cap.
It's particularly suited to Flutter, React Native, or web developers who want a ready-to-use backend with official SDKs, while keeping the data and logic on their own infrastructure. You control the functions, the API keys, and the permissions, and you scale at your own pace — without being subject to the quotas or price increases of a public cloud.
The concrete benefits of a self-hosted Appwrite
- All-in-one backend: Auth, Databases, Storage, Functions, and Messaging in a single console.
- Official SDKs for Flutter, React Native, Web, Android, iOS, Node, Python, and more.
- More than 30 built-in OAuth providers (Google, GitHub, Apple...) with no code.
- Self-hosted serverless functions: run your business logic without an external service.
- Granular permissions at the document and collection level, managed from the console.
- No billing per execution and no user quota: a fixed VPS plan.
- Data hosted on your own infrastructure: simplified GDPR compliance, no third-party cloud dependency.
- Built-in Realtime: subscribe to changes in documents, files, and session state via WebSocket.
Internal architecture: what Docker actually launches
Before installing, it helps to know what Appwrite orchestrates. The official installer generates a docker-compose.yml with at least twenty services:
- appwrite — the main API server (PHP, exposed on port 80/443 via Traefik).
- appwrite-console — the web administration console (React, served at the same domain on /).
- appwrite-realtime — the WebSocket service for real-time subscriptions.
- appwrite-worker-* — a series of workers for asynchronous tasks: sending e-mails, outbound webhooks, temporary file cleanup, etc.
- appwrite-executor — the serverless Functions execution runtime (Docker-in-Docker).
- mariadb — the internal relational database (users, projects, permission rules).
- redis — the cache and task queue (worker jobs, sessions, rate-limiting).
- traefik — the built-in reverse proxy that handles HTTPS routing and Let's Encrypt certificates.
For a development VPS, only the core node is strictly necessary: API, Console, MariaDB, Redis, and Traefik. The workers and executor are indispensable in production if you use Functions, webhooks, or e-mail sending. Count on at least 4 GB of RAM for this full stack: MariaDB and the background workers share the available memory, and each Function invocation starts its own ephemeral container.
Hardware and software prerequisites
For a development or test instance, 2 vCPU and 4 GB of RAM with 40 GB of SSD are enough. For production with active serverless functions and several client apps, aim for 4 vCPU, 8 GB of RAM, and 80 GB of SSD — each Function invocation starts its own ephemeral container, which puts pressure on disk and CPU.
On the software side: Ubuntu 22.04 or 24.04 LTS, Docker CE and Docker Compose v2 (Appwrite manages its own Traefik for routing and SSL), and a domain name pointing to the VPS IP — for example api.myapp.com. Ports 80 and 443 must be free and open in ufw for Traefik to obtain the Let's Encrypt certificates via the ACME challenge.
A note on storage. Functions generate intermediate Docker images that accumulate on the SSD. Without regular pruning, an instance running for several months can saturate its disk. Plan a weekly docker system prune --volumes -f cron task, and monitor free space with df -h.
Deploying self-hosted Appwrite with Docker
Prepare the VPS and open the ports
Install Docker CE and Docker Compose v2, then open the required ports:
ufw allow 80/tcp ufw allow 443/tcp ufw enablePoint your domain (e.g.,
api.myapp.com) to the VPS IP via an A record. Wait for DNS propagation before running the installer — Traefik needs the domain to resolve correctly in order to pass the ACME challenge.Run the official interactive installer
Run the installer with one command:
docker run -it --rm \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \ --entrypoint="install" \ appwrite/appwriteThe wizard asks for the main domain, HTTP port (80), HTTPS port (443), an e-mail for Let's Encrypt, and a few options. It generates a
docker-compose.ymland a.envfile with randomly generated secrets. Do not change these secrets by hand — modify them only via the environment variables in.env.Verify the domain and SSL configuration
After startup (
docker compose up -d), wait 30 to 60 seconds for Traefik to issue the certificate. Check:curl -I https://api.myapp.com/v1/healthThe response should be
HTTP/2 200with acontent-type: application/json. If Traefik does not yet have a certificate, you'll get a TLS error — this is normal during the first few seconds. If it persists, check that ports 80/443 are not filtered by an upstream firewall and that the domain resolves to the correct IP (dig api.myapp.com).Create the administrator account and the first project
Open
https://api.myapp.comin your browser. Appwrite invites you to create the root administrator account on first access — this account is the only one with access to platform settings. From the console, create a project, add a platform (Web, Flutter, Apple…) by declaring the allowed hostname, and retrieve the project ID. This information feeds the client-side SDK initialization:npm install appwriteimport { Client } from 'appwrite'; const client = new Client() .setEndpoint('https://api.myapp.com/v1') .setProject('<your-project-id>');Harden the production configuration
Open the
.envfile generated by the installer and adjust the following variables:-
_APP_OPTIONS_ABUSE=enabled— activates built-in per-IP rate-limiting.
-_APP_OPTIONS_FORCE_HTTPS=enabled— redirects all HTTP requests to HTTPS.
-_APP_SMTP_HOST/_APP_SMTP_PORT/_APP_SMTP_SECURE/_APP_SMTP_USERNAME/_APP_SMTP_PASSWORD— configure your SMTP relay for verification and password-reset e-mails.
-_APP_CONSOLE_WHITELIST_ROOT=disabledif you want to allow open registration to the console (not recommended in prod); leave it onenabledand fill in_APP_CONSOLE_WHITELIST_EMAILSwith the authorized addresses.Restart the services after modification:
docker compose up -d.Configure automated backups
Two Docker volumes hold all of Appwrite's persistent data:
-
appwrite-mariadb— the database (users, projects, documents, rules).
-appwrite-uploads— files uploaded by client apps.Schedule a daily dump:
docker exec appwrite-mariadb mysqldump \ -u appwrite -p"$MARIADB_ROOT_PASSWORD" appwrite \ > /backups/appwrite-$(date +%Y%m%d).sqlAlso archive the
appwrite-uploadsvolume (atarof the data or a volume snapshot depending on your infrastructure). Also back up_APP_OPENSSL_KEY_V1— Appwrite uses it to encrypt files and some sensitive data. Without this key, a database restore makes the encrypted content permanently unreadable.
Networking and Traefik: what happens behind the scenes
Traefik is Appwrite's only network entry point: it listens on ports 80 and 443, manages Let's Encrypt certificates, and routes requests to the correct internal service. This integration is transparent — you don't write any Traefik configuration, it's generated by the installer.
A few important points if you add an upstream reverse proxy (nginx, Cloudflare Tunnel…):
- Do not double the TLS. If your upstream proxy already terminates SSL and forwards in HTTP to Appwrite, disable the ACME challenge by setting _APP_OPTIONS_FORCE_HTTPS=disabled and leaving _APP_SYSTEM_SECURITY_EMAIL_ADDRESS= empty to prevent Traefik from trying to obtain a certificate it cannot validate.
- Forward the real IP. Add X-Forwarded-For and X-Real-IP in your upstream proxy, and enable header trust in Traefik (already configured by default in the Appwrite installer).
- Console ports. The web console and the API are served under the same domain: do not route port 9501 (internal Realtime) publicly — it is managed internally by Traefik.
If you place Appwrite behind Cloudflare in proxied mode, set the zone's SSL mode to Full (strict) — in Flexible mode, nginx redirects HTTP→HTTPS, which creates an infinite redirect loop between Cloudflare and Traefik.
Troubleshooting common issues
The console shows "Could not connect to the Appwrite API".
Check that the _APP_DOMAIN and _APP_DOMAIN_TARGET variables in .env exactly match the domain you are using. A misconfigured subdomain (with or without www) prevents Traefik from routing correctly. Run docker compose up -d after any .env modification.
Verification e-mails are not arriving.
Appwrite has no built-in SMTP relay — you must configure _APP_SMTP_* with an external service (Mailgun, Resend, your own server). Without SMTP configured, e-mails are queued and ignored. Check the logs of the appwrite-worker-mails worker: docker compose logs appwrite-worker-mails --tail 50.
Serverless Functions time out.
Each Function starts its own Docker container. On a VPS with 2 GB of RAM, the cold start often exceeds 10 seconds, which triggers the default timeout. Increase _APP_FUNCTIONS_TIMEOUT in .env and plan for at least 4 GB of RAM for production use. Also monitor disk space — runtime images stay on the SSD after execution.
The SSL certificate is not being generated.
Traefik needs ports 80 and 443 to respond from the outside in order to pass the ACME challenge. If an upstream firewall (your VPS provider's security rules) blocks these ports, the challenge fails silently. Test from an external machine: curl -I http://api.myapp.com should respond (even with a 302), not time out.
MariaDB starts but the console shows database errors.
If you have manually modified .env between restarts, check that _APP_DB_HOST, _APP_DB_USER, and _APP_DB_PASS match the values in the MariaDB container. A partial restart (a single service) without docker compose down can leave orphaned containers with old variables in memory.
Appwrite vs Supabase: which one to self-host?
Scroll the table
| Criterion | Appwrite | Supabase |
|---|---|---|
| Database | MariaDB, document model | Native PostgreSQL, full SQL |
| Approach | SDK- and mobile-app-oriented | SQL- and relational-app-oriented |
| Serverless functions | Native, multi-language built-in | Edge Functions (Deno) |
| Installation | Official interactive installer | Compose to configure manually |
| SSL / routing | Built-in Traefik, automatic | External reverse proxy to add |
| Vector / AI search | Not native | Native via pgvector |
| Realtime | Built-in WebSocket, multi-resource | Postgres CDC via Realtime |
| Ideal for | Flutter, React Native, mobile-first | Advanced SQL, RAG, relational |
| Administration console | Rich, guided console | Studio focused on SQL tables |
Updates and ongoing maintenance
To update Appwrite, re-run the installer with the new version or modify the image tag in docker-compose.yml and run docker compose pull && docker compose up -d. Appwrite applies schema migrations automatically when the API container starts — no manual command is needed.
Also schedule a weekly purge of orphaned Docker images to avoid SSD saturation:
docker system prune --volumes -fBack up _APP_OPENSSL_KEY_V1 in a separate vault (secrets manager, encrypted file outside the VPS). Without this key, a database restore makes encrypted files and some sensitive data permanently unreadable — it is the only point of no return in an Appwrite installation.
This template ships the core — Functions come later
The official Appwrite installer generates 25+ containers, including background workers and an execution runtime for serverless Functions and Sites. To keep the VPS footprint small, this template ships only the self-consistent core — the API server, MariaDB, Redis and Traefik — which fully powers Auth, Databases, Storage, Realtime and the Console. Serverless Functions, outbound webhooks and background e-mail need the extra worker and executor containers and are not enabled here. When your project needs them, grow to Appwrite's full official docker-compose stack and plan for 4 GB+ of RAM. For backups, snapshot the appwrite-mariadb (data) and appwrite-uploads (files) Docker volumes on a schedule.