Self-Hosting Appwrite with Docker Compose

If you need a backend for your web or mobile app but don’t want to hand your data to Firebase or Supabase’s cloud, Appwrite gives you the full backend-as-a-service stack — authentication, databases, storage, serverless functions, and real-time messaging — running entirely on your own hardware.

Quick Verdict

Appwrite is the most complete self-hosted BaaS available. It replaces Firebase’s core features (auth, Firestore, storage, cloud functions) with an open-source stack you control. The trade-off: it runs 20+ containers and needs at least 4 GB of RAM. If you want a lighter alternative, check PocketBase — but you’ll sacrifice features for simplicity.

Use Cases

  • Mobile/web app backends — REST and GraphQL APIs, SDKs for Flutter, React Native, web, and more
  • Replacing Firebase — auth, database, storage, functions, and real-time in one platform
  • Internal tool backends — pair with ToolJet or Appsmith for a full self-hosted stack
  • Multi-tenant SaaS — built-in team management, project isolation, and API key scoping

Prerequisites

  • A Linux server (Ubuntu 22.04+ recommended)
  • Docker and Docker Compose installed (guide)
  • 4 GB of RAM minimum (8 GB recommended)
  • 10 GB of free disk space (plus storage for uploads)
  • A domain name for production use

Docker Compose Configuration

Appwrite’s official deployment uses 20+ containers (workers, schedulers, and supporting services) all running the same appwrite/appwrite image with different entrypoints. The official installer at appwrite.io/install generates the full docker-compose.yml automatically.

For a manual deployment, create a docker-compose.yml:

# Appwrite self-hosted — simplified core services
# For the full 20+ container deployment, use: docker run -it --rm \
#   --volume /var/run/docker.sock:/var/run/docker.sock \
#   appwrite/appwrite:1.8.1 install

services:
  traefik:
    image: traefik:2.11
    container_name: appwrite-traefik
    command:
      - --providers.docker=true
      - --providers.docker.exposedByDefault=false
      - --providers.docker.constraints=Label(`traefik.constraint`,`appwrite`)
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - appwrite-config:/storage/config:ro
      - appwrite-certificates:/storage/certificates:ro
    networks:
      - gateway
      - appwrite
    depends_on:
      - appwrite

  appwrite:
    image: appwrite/appwrite:1.8.1
    container_name: appwrite
    restart: unless-stopped
    networks:
      - appwrite
    labels:
      - traefik.enable=true
      - traefik.constraint=appwrite
      - traefik.http.routers.appwrite.rule=PathPrefix(`/`)
      - traefik.http.routers.appwrite.entrypoints=web
    volumes:
      - appwrite-uploads:/storage/uploads
      - appwrite-cache:/storage/cache
      - appwrite-config:/storage/config
      - appwrite-certificates:/storage/certificates
      - appwrite-functions:/storage/functions
      - appwrite-builds:/storage/builds
      - appwrite-sites:/storage/sites
      - appwrite-imports:/storage/imports
    depends_on:
      - mariadb
      - redis
    env_file:
      - .env

  appwrite-realtime:
    image: appwrite/appwrite:1.8.1
    container_name: appwrite-realtime
    entrypoint: realtime
    restart: unless-stopped
    networks:
      - appwrite
    labels:
      - traefik.enable=true
      - traefik.constraint=appwrite
      - traefik.http.routers.appwrite-realtime.rule=PathPrefix(`/v1/realtime`)
      - traefik.http.routers.appwrite-realtime.entrypoints=web
    depends_on:
      - mariadb
      - redis
    env_file:
      - .env

  appwrite-worker-databases:
    image: appwrite/appwrite:1.8.1
    container_name: appwrite-worker-databases
    entrypoint: worker-databases
    restart: unless-stopped
    networks:
      - appwrite
    depends_on:
      - mariadb
      - redis
    env_file:
      - .env

  appwrite-worker-deletes:
    image: appwrite/appwrite:1.8.1
    container_name: appwrite-worker-deletes
    entrypoint: worker-deletes
    restart: unless-stopped
    networks:
      - appwrite
    volumes:
      - appwrite-uploads:/storage/uploads
      - appwrite-cache:/storage/cache
      - appwrite-functions:/storage/functions
      - appwrite-sites:/storage/sites
      - appwrite-builds:/storage/builds
      - appwrite-certificates:/storage/certificates
    depends_on:
      - mariadb
      - redis
    env_file:
      - .env

  appwrite-worker-mails:
    image: appwrite/appwrite:1.8.1
    container_name: appwrite-worker-mails
    entrypoint: worker-mails
    restart: unless-stopped
    networks:
      - appwrite
    depends_on:
      - redis
    env_file:
      - .env

  appwrite-worker-functions:
    image: appwrite/appwrite:1.8.1
    container_name: appwrite-worker-functions
    entrypoint: worker-functions
    restart: unless-stopped
    networks:
      - appwrite
    volumes:
      - appwrite-functions:/storage/functions
      - appwrite-builds:/storage/builds
    depends_on:
      - redis
      - openruntimes-executor
    env_file:
      - .env

  appwrite-worker-builds:
    image: appwrite/appwrite:1.8.1
    container_name: appwrite-worker-builds
    entrypoint: worker-builds
    restart: unless-stopped
    networks:
      - appwrite
    volumes:
      - appwrite-functions:/storage/functions
      - appwrite-sites:/storage/sites
      - appwrite-builds:/storage/builds
      - appwrite-uploads:/storage/uploads
    depends_on:
      - redis
    env_file:
      - .env

  appwrite-maintenance:
    image: appwrite/appwrite:1.8.1
    container_name: appwrite-maintenance
    entrypoint: maintenance
    restart: unless-stopped
    networks:
      - appwrite
    depends_on:
      - redis
    env_file:
      - .env

  appwrite-schedule-functions:
    image: appwrite/appwrite:1.8.1
    container_name: appwrite-schedule-functions
    entrypoint: schedule-functions
    restart: unless-stopped
    networks:
      - appwrite
    depends_on:
      - mariadb
      - redis
    env_file:
      - .env

  appwrite-worker-messaging:
    image: appwrite/appwrite:1.8.1
    container_name: appwrite-worker-messaging
    entrypoint: worker-messaging
    restart: unless-stopped
    networks:
      - appwrite
    volumes:
      - appwrite-uploads:/storage/uploads
    depends_on:
      - redis
    env_file:
      - .env

  appwrite-console:
    image: appwrite/console:7.5.7
    container_name: appwrite-console
    restart: unless-stopped
    networks:
      - appwrite
    labels:
      - traefik.enable=true
      - traefik.constraint=appwrite
      - traefik.http.routers.appwrite-console.rule=PathPrefix(`/console`)
      - traefik.http.routers.appwrite-console.entrypoints=web

  openruntimes-executor:
    image: openruntimes/executor:0.7.22
    container_name: openruntimes-executor
    hostname: exc1
    restart: unless-stopped
    stop_signal: SIGINT
    networks:
      appwrite:
      runtimes:
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - appwrite-builds:/storage/builds
      - appwrite-functions:/storage/functions
      - appwrite-sites:/storage/sites
      - /tmp:/tmp
    environment:
      - OPR_EXECUTOR_INACTIVE_THRESHOLD=60
      - OPR_EXECUTOR_MAINTENANCE_INTERVAL=3600
      - OPR_EXECUTOR_NETWORK=runtimes
      - OPR_EXECUTOR_DOCKER_HUB_USERNAME=
      - OPR_EXECUTOR_DOCKER_HUB_PASSWORD=
      - OPR_EXECUTOR_ENV=${_APP_EXECUTOR_ENV}
      - OPR_EXECUTOR_SECRET=${_APP_EXECUTOR_SECRET}
      - OPR_EXECUTOR_RUNTIME_VERSIONS=v4
      - OPR_EXECUTOR_LOGGING_PROVIDER=${_APP_LOGGING_PROVIDER}
      - OPR_EXECUTOR_LOGGING_CONFIG=${_APP_LOGGING_CONFIG}
      - OPR_EXECUTOR_STORAGE_DEVICE=${_APP_STORAGE_DEVICE}

  mariadb:
    image: mariadb:10.11
    container_name: appwrite-mariadb
    restart: unless-stopped
    networks:
      - appwrite
    volumes:
      - appwrite-mariadb:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: ${_APP_DB_ROOT_PASS}
      MYSQL_DATABASE: ${_APP_DB_SCHEMA}
      MYSQL_USER: ${_APP_DB_USER}
      MYSQL_PASSWORD: ${_APP_DB_PASS}
    command: >
      --innodb-flush-method=fsync
      --innodb-use-native-aio=0

  redis:
    image: redis:7.2.4-alpine
    container_name: appwrite-redis
    restart: unless-stopped
    networks:
      - appwrite
    command: >
      redis-server
      --maxmemory 512mb
      --maxmemory-policy allkeys-lru
    volumes:
      - appwrite-redis:/data

volumes:
  appwrite-mariadb:
  appwrite-redis:
  appwrite-cache:
  appwrite-uploads:
  appwrite-imports:
  appwrite-certificates:
  appwrite-functions:
  appwrite-sites:
  appwrite-builds:
  appwrite-config:

networks:
  gateway:
  appwrite:
  runtimes:
    name: runtimes

Create a .env file alongside your docker-compose.yml:

# Core settings
_APP_ENV=production
_APP_LOCALE=en
_APP_DOMAIN=localhost                    # Change to your domain
_APP_DOMAIN_FUNCTIONS=functions.localhost # Change to functions.yourdomain.com
_APP_DOMAIN_SITES=sites.localhost         # Change to sites.yourdomain.com
_APP_DOMAIN_TARGET=localhost

# Security — CHANGE THESE
_APP_OPENSSL_KEY_V1=your-random-32-char-key-change-me
_APP_EXECUTOR_SECRET=another-random-secret-change-me

# Console
_APP_CONSOLE_WHITELIST_ROOT=enabled

# System email
_APP_SYSTEM_EMAIL_NAME=Appwrite
_APP_SYSTEM_EMAIL_ADDRESS=[email protected]
_APP_SYSTEM_TEAM_EMAIL=[email protected]
_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=[email protected]

# Database (MariaDB)
_APP_DB_HOST=mariadb
_APP_DB_PORT=3306
_APP_DB_SCHEMA=appwrite
_APP_DB_USER=appwrite                    # Change for production
_APP_DB_PASS=change-this-password        # CHANGE THIS
_APP_DB_ROOT_PASS=change-this-root-pass  # CHANGE THIS

# Cache (Redis)
_APP_REDIS_HOST=redis
_APP_REDIS_PORT=6379

# Functions runtime
_APP_EXECUTOR_HOST=http://exc1/v1
_APP_EXECUTOR_RUNTIME_NETWORK=runtimes
_APP_FUNCTIONS_RUNTIMES=node-21.0,python-3.12,php-8.3,ruby-3.3
_APP_FUNCTIONS_TIMEOUT=900
_APP_FUNCTIONS_CONTAINERS=10

# Storage
_APP_STORAGE_LIMIT=30000000
_APP_STORAGE_DEVICE=local

# Options
_APP_OPTIONS_ABUSE=enabled
_APP_OPTIONS_FORCE_HTTPS=disabled
_APP_USAGE_STATS=enabled

# DNS
_APP_DNS=8.8.8.8

# Maintenance
_APP_MAINTENANCE_INTERVAL=86400

# SMTP (optional — configure for email verification)
# _APP_SMTP_HOST=smtp.yourdomain.com
# _APP_SMTP_PORT=587
# _APP_SMTP_SECURE=tls
# _APP_SMTP_USERNAME=
# _APP_SMTP_PASSWORD=

Start the stack:

docker compose up -d

The first startup takes 2-3 minutes as MariaDB initializes and Appwrite runs migrations.

Initial Setup

  1. Open http://your-server-ip in your browser
  2. Create your admin account — this becomes the root user
  3. Create your first project from the console dashboard
  4. Generate an API key under Settings → API Keys for your application
  5. Install the Appwrite SDK in your app (npm install appwrite for JavaScript)

Configuration

SettingEnvironment VariableDefaultNotes
Domain_APP_DOMAINlocalhostYour public domain for the API
HTTPS enforcement_APP_OPTIONS_FORCE_HTTPSdisabledEnable behind a reverse proxy with SSL
Max upload size_APP_STORAGE_LIMIT30 MBIn bytes (30000000 = 30 MB)
Function timeout_APP_FUNCTIONS_TIMEOUT900sMax serverless function execution time
Abuse protection_APP_OPTIONS_ABUSEenabledRate limiting on API endpoints
Max function containers_APP_FUNCTIONS_CONTAINERS10Concurrent function execution slots
Available runtimes_APP_FUNCTIONS_RUNTIMESMultipleComma-separated list of runtime environments

Enabling SMTP

For email verification, password resets, and team invitations to work, configure SMTP:

_APP_SMTP_HOST=smtp.yourdomain.com
_APP_SMTP_PORT=587
_APP_SMTP_SECURE=tls
_APP_SMTP_USERNAME=your-smtp-user
_APP_SMTP_PASSWORD=your-smtp-password

Advanced Configuration

S3-Compatible Storage

Replace local file storage with S3, Backblaze B2, or any S3-compatible provider:

_APP_STORAGE_DEVICE=s3
_APP_STORAGE_S3_ACCESS_KEY=your-access-key
_APP_STORAGE_S3_SECRET=your-secret-key
_APP_STORAGE_S3_REGION=us-east-1
_APP_STORAGE_S3_BUCKET=appwrite-storage
_APP_STORAGE_S3_ENDPOINT=https://s3.amazonaws.com

ClamAV Antivirus Scanning

Add virus scanning for file uploads by adding a ClamAV container:

  clamav:
    image: clamav/clamav:1.4
    container_name: appwrite-clamav
    restart: unless-stopped
    networks:
      - appwrite
    volumes:
      - appwrite-uploads:/storage/uploads:ro

Then set _APP_STORAGE_ANTIVIRUS=enabled and _APP_STORAGE_ANTIVIRUS_HOST=clamav in your .env.

Reverse Proxy

If you’re running Appwrite behind an external reverse proxy instead of its built-in Traefik, remove the Traefik service and expose Appwrite’s port directly. Alternatively, configure your proxy to forward to Traefik on ports 80/443.

For Nginx Proxy Manager, point your domain to http://appwrite-traefik:80. See Reverse Proxy Setup for full configuration.

Backup

Back up these volumes to preserve all Appwrite data:

  • appwrite-mariadb — database (most critical)
  • appwrite-uploads — user-uploaded files
  • appwrite-config — SSL certificates and Traefik configuration
  • appwrite-functions — deployed serverless functions
# Database backup
docker exec appwrite-mariadb mariadb-dump -u root -p"$ROOT_PASS" appwrite > appwrite-backup.sql

# Volume backup
docker run --rm -v appwrite-uploads:/data -v $(pwd):/backup alpine \
  tar czf /backup/appwrite-uploads.tar.gz -C /data .

See Backup Strategy for automated backup approaches.

Troubleshooting

Console shows “Network Error” after setup

Symptom: The Appwrite console loads but API calls fail with network errors. Fix: Set _APP_DOMAIN to your actual server IP or domain, not localhost. Restart all containers with docker compose down && docker compose up -d.

Functions fail to execute

Symptom: Serverless functions deploy but return errors when triggered. Fix: Verify the OpenRuntimes executor can access Docker by checking that /var/run/docker.sock is mounted. Ensure the runtimes network exists: docker network create runtimes.

MariaDB connection refused on startup

Symptom: Appwrite logs show “Connection refused” to MariaDB during first boot. Fix: MariaDB needs 30-60 seconds to initialize on first start. The containers will retry automatically. If it persists, check MariaDB logs: docker logs appwrite-mariadb.

High memory usage

Symptom: Server runs out of memory after deploying Appwrite. Fix: Reduce _APP_FUNCTIONS_CONTAINERS from 10 to 3-5. Set Redis max memory lower: change --maxmemory 512mb to 256mb in the Redis command. Consider disabling unused workers.

Email verification not sending

Symptom: Users sign up but never receive verification emails. Fix: SMTP must be configured. Without _APP_SMTP_HOST set, Appwrite silently skips email sending. Configure SMTP and restart: docker compose restart appwrite appwrite-worker-mails.

Resource Requirements

ResourceMinimumRecommended
RAM4 GB8 GB
CPU2 cores4 cores
Disk10 GB50 GB+ (depends on file uploads)
Containers~15 (core)~20 (full deployment)

Appwrite is a heavy deployment. The full stack runs 15-20+ containers. Idle memory usage is around 2-3 GB with all workers running. If you need something lighter, PocketBase runs as a single binary with ~50 MB RAM.

Verdict

If you’re building a mobile or web app and want full control over your backend, Appwrite is the best self-hosted option available. It matches Firebase feature-for-feature — auth, databases, storage, functions, real-time — and adds features Firebase doesn’t offer, like self-hosted deployment and no vendor lock-in. The cost is complexity: 15-20 containers, 4 GB minimum RAM, and a learning curve for the admin console. For solo developers who just need a database and auth, PocketBase is dramatically simpler. For teams building production apps who want Firebase without the Firebase bill, Appwrite wins.

FAQ

How does Appwrite compare to Firebase?

Appwrite covers the same core features — authentication (email, OAuth, phone), document databases, file storage, serverless functions, and real-time subscriptions. Firebase has deeper integrations with Google services (Analytics, Crashlytics, FCM). Appwrite gives you data ownership and no usage-based billing. See Self-Hosted Alternatives to Firebase for a full comparison.

Can Appwrite handle production traffic?

Yes. Appwrite’s architecture with separate worker containers for databases, functions, mail, and messaging lets you scale each component independently. For high traffic, run multiple instances of workers behind a load balancer.

Does Appwrite support GraphQL?

Yes. Appwrite 1.4+ includes a built-in GraphQL API alongside the REST API. Both expose the same functionality — databases, auth, storage, and functions.

What programming languages does Appwrite support for serverless functions?

Node.js, Python, PHP, Ruby, Dart, Swift, Kotlin, Java, C++, .NET, Deno, and Bun. You can add custom runtimes through the Open Runtimes project.

How do I upgrade Appwrite?

Update the image tag in your docker-compose.yml, pull the new images, and run docker compose up -d. Appwrite runs database migrations automatically on startup. Always run migrations even for patch releases (e.g., 1.8.0 to 1.8.1).

Comments