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
- Open
http://your-server-ipin your browser - Create your admin account — this becomes the root user
- Create your first project from the console dashboard
- Generate an API key under Settings → API Keys for your application
- Install the Appwrite SDK in your app (
npm install appwritefor JavaScript)
Configuration
| Setting | Environment Variable | Default | Notes |
|---|---|---|---|
| Domain | _APP_DOMAIN | localhost | Your public domain for the API |
| HTTPS enforcement | _APP_OPTIONS_FORCE_HTTPS | disabled | Enable behind a reverse proxy with SSL |
| Max upload size | _APP_STORAGE_LIMIT | 30 MB | In bytes (30000000 = 30 MB) |
| Function timeout | _APP_FUNCTIONS_TIMEOUT | 900s | Max serverless function execution time |
| Abuse protection | _APP_OPTIONS_ABUSE | enabled | Rate limiting on API endpoints |
| Max function containers | _APP_FUNCTIONS_CONTAINERS | 10 | Concurrent function execution slots |
| Available runtimes | _APP_FUNCTIONS_RUNTIMES | Multiple | Comma-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 filesappwrite-config— SSL certificates and Traefik configurationappwrite-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
| Resource | Minimum | Recommended |
|---|---|---|
| RAM | 4 GB | 8 GB |
| CPU | 2 cores | 4 cores |
| Disk | 10 GB | 50 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).
Related
Get self-hosting tips in your inbox
Get the Docker Compose configs, hardware picks, and setup shortcuts we don't put in articles. Weekly. No spam.
Comments