Skip to Content

Production Guide: Deploy Appwrite with Docker Compose + Caddy on Ubuntu

Self-host an open-source backend-as-a-service platform with authentication, databases, file storage, and serverless functions behind automatic HTTPS.

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

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.

Contact Us

Production Guide: Deploy Rocket.Chat with Docker Compose + Caddy + MongoDB on Ubuntu
Self-host the leading open-source team chat platform with automatic HTTPS, persistent storage, and production-grade operations.