Engineering teams at growth-stage companies routinely rebuild the same backend primitives — user authentication, file storage, database queries, and real-time subscriptions — for every new project. Appwrite eliminates that repetition. It is an open-source backend-as-a-service (BaaS) platform that ships pre-built APIs for authentication (50+ OAuth providers), NoSQL databases with schema enforcement, file storage, serverless functions, messaging, and real-time events — all behind a single consistent REST, GraphQL, and SDK-based interface. Running it yourself means your data never leaves your infrastructure, licensing costs drop to zero, and you retain full control over retention and compliance policy.
This guide walks through a production-ready deployment of Appwrite using Docker Compose for container orchestration and Caddy as the TLS-terminating reverse proxy on Ubuntu 22.04 LTS. By the end you will have a publicly accessible Appwrite Console and API endpoint with automatic HTTPS, persistent data volumes, systemd-supervised containers, and the operational hygiene expected of a real production workload.
Architecture and flow overview
Appwrite ships as a multi-container stack. The core components are the Appwrite application server, MariaDB for relational storage, Redis for session caching and job queuing, InfluxDB for usage metrics, and several internal worker processes for functions, webhooks, deletes, builds, and certificate management. The official Docker Compose bundle also includes an internal Traefik reverse proxy by default — for this guide you will bypass that component entirely and use Caddy instead, giving you a simpler TLS layer that integrates cleanly with the rest of your server's reverse proxy setup.
Traffic flow: client browser or mobile SDK connects to Caddy on port 443, Caddy terminates TLS and forwards HTTP requests to the Appwrite container on a localhost port (9000). Caddy obtains and auto-renews Let's Encrypt certificates via ACME HTTP-01 challenge. All Appwrite containers communicate internally over a Docker bridge network. Persistent data lives in named Docker volumes mounted to MariaDB, Redis, InfluxDB, and the Appwrite uploads directory. The volumes survive container restarts and image upgrades without data loss, provided you never run docker compose down -v.
Prerequisites
- Ubuntu 22.04 LTS server with root or sudo access
- Minimum 2 vCPU and 4 GB RAM (Appwrite's bundled stack is resource-intensive; 8 GB recommended for production workloads with file uploads or serverless functions)
- Docker Engine 24+ and Docker Compose Plugin installed
- Caddy 2.7+ installed and configured as a systemd service
- A domain name (e.g.,
appwrite.yourdomain.com) with an A record pointing to your server's public IP, propagated before you start Caddy - UFW or equivalent firewall allowing inbound ports 22, 80, and 443 only
Step-by-step deployment
1. Install Docker Engine and Compose Plugin
sudo apt update && sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo systemctl enable --now docker
2. Install Caddy from the official repository
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install -y caddy
sudo systemctl enable --now caddy
3. Download the Appwrite Docker Compose bundle
mkdir -p /opt/appwrite && cd /opt/appwrite
curl -o docker-compose.yml https://appwrite.io/install/compose
curl -o .env https://appwrite.io/install/env
4. Configure the Appwrite environment file
Open /opt/appwrite/.env and update the required variables. At minimum set the following:
_APP_ENV=production
_APP_DOMAIN=appwrite.yourdomain.com
_APP_DOMAIN_TARGET=appwrite.yourdomain.com
_APP_DOMAIN_FUNCTIONS=functions.yourdomain.com
[email protected]
[email protected]
_APP_DB_PASS=changeme_strong_db_password
_APP_DB_ROOT_PASS=changeme_strong_root_password
_APP_OPENSSL_KEY_V1= # see step below
Generate a 256-bit secret key for _APP_OPENSSL_KEY_V1 and add it to your .env:
openssl rand -hex 32
# Paste the 64-character output as the value of _APP_OPENSSL_KEY_V1
5. Expose Appwrite on a localhost port and remove internal Traefik
Edit /opt/appwrite/docker-compose.yml. Locate the appwrite service block and replace its ports with a single loopback binding. Remove or comment out the traefik service entirely:
services:
appwrite:
ports:
- "127.0.0.1:9000:80"
# Remove/comment out the traefik service block
6. Configure Caddy as the TLS reverse proxy
appwrite.yourdomain.com {
reverse_proxy 127.0.0.1:9000 {
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-Host {host}
header_up X-Real-IP {remote_host}
}
encode gzip
log {
output file /var/log/caddy/appwrite-access.log
format console
}
}
7. Start the Appwrite stack
cd /opt/appwrite
docker compose up -d
# Monitor startup — Appwrite initializes its database schema on first boot (60-90 seconds)
docker compose logs -f appwrite
8. Reload Caddy and verify TLS
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
curl -sv https://appwrite.yourdomain.com/v1/health 2>&1 | grep -E "(SSL|200|Connected)"
Configuration and secrets handling
The _APP_OPENSSL_KEY_V1 value is the master encryption key for all data stored in MariaDB — OAuth access tokens, API keys, and user sessions are all encrypted with this key at rest. Losing it makes that data permanently unrecoverable. The moment your stack is running, back this key up to an off-server secrets store:
chmod 600 /opt/appwrite/.env
# Archive encrypted to your secrets manager or a GPG-encrypted backup
gpg --symmetric --cipher-algo AES256 /opt/appwrite/.env
# Transfer the encrypted archive off-server immediately
Never rotate _APP_OPENSSL_KEY_V1 after first deployment without a formal key migration procedure — there is no built-in key rotation wizard as of Appwrite 1.6. If you spin up a staging environment, generate a completely separate key and a fresh database; do not reuse production keys in non-production contexts. SMTP credentials for transactional email (password reset, verification links, magic links) should be sourced from a dedicated sending account (SendGrid, Postmark, or SES) and set via the _APP_SMTP_* variables in .env before the first user signs up.
Verification
# Confirm all containers are healthy
cd /opt/appwrite && docker compose ps
# Query the Appwrite health API
curl -s https://appwrite.yourdomain.com/v1/health | python3 -m json.tool
# Confirm MariaDB and Redis are reachable from the Appwrite container
docker compose exec appwrite appwrite doctor
The /v1/health endpoint returns a JSON object with service statuses. A healthy response shows "status": "pass" for every dependency. Open the Console at https://appwrite.yourdomain.com in a browser to create your root admin account. Set up your first Project, enable the authentication methods you need (Email/Password is the minimum), and generate an API key for your application under API Keys in Project Settings.
Common issues and fixes
Container restarts immediately after startup: Run docker compose logs appwrite. The most common cause is a missing or malformed _APP_OPENSSL_KEY_V1. The value must be exactly 64 hexadecimal characters (32 bytes). Run openssl rand -hex 32 and set the value before starting again.
502 Bad Gateway from Caddy: Appwrite takes 60–90 seconds to initialize its database schema on first boot. Wait for the log line HTTP server started before testing the health endpoint. If the 502 persists, verify that port 9000 is listening: ss -tlnp | grep 9000.
Caddy fails to obtain a certificate: Your DNS A record must resolve to your server's public IP before Caddy can complete the ACME HTTP-01 challenge. Confirm resolution with dig +short appwrite.yourdomain.com. If you exceeded Let's Encrypt rate limits during testing, add acme_ca https://acme-staging-v02.api.letsencrypt.org/directory to the Caddyfile block temporarily.
Appwrite Console returns internal server error after login: Run database migrations: docker compose exec appwrite migrate. This is required after any version upgrade and occasionally after a crash recovery if the schema state is inconsistent.
File uploads fail or return 413: Check _APP_STORAGE_LIMIT in .env (default is 30 MB, value is in bytes). Caddy has no built-in request body size limit, but your application server or SDK may enforce one. Increase the storage limit and restart the stack.
Functions fail to execute or time out immediately: The appwrite-worker-functions and openruntimes-executor containers must be running. The executor spawns isolated runtime containers (Node.js, Python, PHP, etc.) on demand, which requires Docker socket access. Verify the executor has /var/run/docker.sock mounted. Check _APP_DOMAIN_FUNCTIONS is set to a valid hostname the executor can resolve.
FAQ
Is Appwrite a complete replacement for Firebase?
Appwrite covers Firebase's core developer-facing features: authentication (email/password, OAuth, magic links, phone), NoSQL document databases with indexes and queries, file storage with image transformations, serverless functions, real-time event subscriptions, and team management. It does not include Firebase's managed machine learning APIs (Vision, NLP) or Firebase Analytics. For teams prioritizing data sovereignty and avoiding vendor lock-in to Google infrastructure, Appwrite is a practical self-hosted alternative with comparable SDK coverage for web, mobile (iOS, Android, Flutter), and server-side runtimes.
Can I run Appwrite on a 1 GB VPS?
No. Appwrite's bundled stack — MariaDB, Redis, InfluxDB, Telegraf, multiple worker containers — consumes roughly 1.5–2 GB RAM at idle with no active projects. A 1 GB VPS will trigger the OOM killer during startup. The practical minimum is 2 vCPU and 4 GB RAM; 8 GB is recommended if you run serverless functions or handle significant file upload throughput.
How do I back up an Appwrite instance?
Back up three components: the .env file (secrets and config), the MariaDB data volume, and the Appwrite uploads volume. For MariaDB: docker compose exec mariadb sh -c 'mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" appwrite' > backup-$(date +%F).sql. For volumes, stop the stack and use docker run --rm --volumes-from appwrite_mariadb_1 -v $(pwd):/backup ubuntu tar czf /backup/mariadb-vol.tar.gz /var/lib/mysql. Automate this to run nightly and ship to an S3-compatible store (MinIO or AWS S3).
What happens if I lose the encryption key?
Data encrypted with _APP_OPENSSL_KEY_V1 — which includes stored OAuth provider tokens, API key secrets, and active user sessions — becomes permanently unrecoverable without the key. New accounts can still be created after a key reset, but existing encrypted records cannot be decrypted. Treat this key with the same urgency as a production database password: store it in HashiCorp Vault, AWS Secrets Manager, Vaultwarden, or an equivalent secrets management system the moment your instance starts.
How do I upgrade Appwrite to a newer version?
Pull new images and run migrations: cd /opt/appwrite && docker compose pull && docker compose down && docker compose up -d && docker compose exec appwrite migrate. Always read the Appwrite GitHub release notes before upgrading — major versions (e.g., 1.5 to 1.6) may include breaking schema changes that the migration command handles but that require a database backup before proceeding. Never skip versions when upgrading across multiple major releases.
Can multiple teams or products share one Appwrite instance?
Yes. Appwrite uses Projects as fully isolated tenant namespaces. Each project has its own authentication settings, database collections, storage buckets, function runtimes, API keys, OAuth providers, and team memberships. Console-level admin accounts can manage all projects. Project-scoped API keys have no access to other projects. This model works well for small-to-medium organizations running multiple internal tools or products on a single Appwrite instance — one server, multiple isolated backends.
How do I enable OAuth login (Google, GitHub, etc.)?
In the Appwrite Console, open your Project → Auth → Settings → OAuth2 Providers. Enable the provider (e.g., Google), enter your OAuth client ID and secret from the provider's developer console, and save. The redirect URI to register in Google Cloud Console is https://appwrite.yourdomain.com/v1/account/sessions/oauth2/callback/google/<PROJECT_ID>. Replace the provider slug and project ID as shown in the Console. Appwrite handles the OAuth2 PKCE flow and session token exchange automatically — no custom backend code required.
Internal links
- Production Guide: Deploy Authentik with Docker Compose + Caddy — Layer enterprise SSO, LDAP user federation, and SAML identity provider support on top of your Appwrite deployment.
- Production Guide: Deploy MinIO with Docker Compose + Caddy — Connect Appwrite's file storage backend to a self-hosted S3-compatible object store for scalable and durable file handling.
- Production Guide: Deploy Vaultwarden with Docker Compose + Caddy — Store your Appwrite OpenSSL master key, database passwords, and SMTP credentials in a self-hosted secrets manager.
Talk to us
If you want this deployed with hardened access controls, monitoring standards, and production runbooks tailored to your environment, our team can help end-to-end.