How to Install Appwrite on Ubuntu 24.04 VPS: Open-Source Backend as a Service
Appwrite is the leading open-source Backend-as-a-Service platform, offering a batteries-included alternative to Firebase and Supabase that you can run on your own infrastructure. This tutorial walks you through a full production install on an Ubuntu 24.04 VPS, from the one-line Docker bootstrapper to a hardened deployment with Traefik auto-TLS, a custom domain, your first project, databases, cloud functions, auth providers, storage buckets, webhooks, and SDK integration.
Skip the manual work? Deploy a pre-tuned backend VPS on our CloudCore Professional plan with Docker and Traefik already configured, so you can focus on shipping your app.
Table of Contents
What is Appwrite?
Appwrite is an open-source backend server that bundles authentication, databases, storage, serverless functions, realtime subscriptions, messaging, and a CDN-style file API behind a single REST, GraphQL, and WebSocket interface. It runs as a set of Docker containers orchestrated by Docker Compose, with Traefik handling ingress and TLS, MariaDB storing metadata, Redis caching hot data, and InfluxDB plus Telegraf powering usage metrics. Every feature you would normally wire up from scratch (password hashing, JWT signing, OAuth dance handling, S3-compatible storage, cron workers, email queues) ships ready to use.
Under the hood, Appwrite is built in PHP with Swoole for the API layer, and each runtime language for cloud functions is its own sandboxed Docker image. You interact with it through an admin console (a React-based web UI at https://your-domain.com/console), through server SDKs for Node.js, Python, PHP, Go, Ruby, Deno, Swift, Kotlin, Dart, and .NET, or through client SDKs for Web, Flutter, iOS, Android, and React Native. The same APIs power your mobile apps, single-page apps, backend services, and serverless functions without you writing boilerplate auth or CRUD code.
Typical use cases include full-stack web apps that need user accounts plus a document database, Flutter and React Native mobile apps that want Firebase-like realtime sync, marketing teams that need a file upload and transformation API, and SaaS founders who want to ship an MVP in a weekend without managing half a dozen cloud services. Appwrite scales from a single-node VPS install (perfect for MVPs and internal tools) up to multi-node Kubernetes deployments serving millions of users.
Why Self-Host Appwrite vs Firebase or Supabase Cloud?
The BaaS market is crowded, and each option makes different tradeoffs. Running Appwrite on your own VPS buys you specific advantages over Firebase, Supabase Cloud, and even Appwrite Cloud:
- Flat, predictable cost. Firebase Firestore reads, writes, bandwidth, storage, and Cloud Functions invocations are all metered. A successful launch can surprise you with a four-figure bill. A VPS costs the same whether you serve 100 users or 100,000 — you only upgrade hardware when you actually outgrow it.
- Complete data residency and GDPR control. Your users' emails, password hashes, files, and database rows live on one server in the region you chose, not spread across Google's or AWS's global footprint. You sign your own DPA because you are the processor.
- No vendor lock-in. Firebase Firestore's query model and security rules don't port anywhere. Appwrite's data layer is standard SQL (MariaDB) underneath, and everything is open source under BSD-3. If you ever want to leave, you take your whole stack with you.
- Unlimited functions, projects, and users. Appwrite Cloud's free tier caps projects, function executions, bandwidth, and storage. Self-hosted, the only cap is your hardware.
- Run next to your app. If your frontend and your Appwrite instance live in the same data center, every auth check and database read is a sub-millisecond local network call instead of a cross-continent round trip.
- Custom runtimes and binaries. Want a function that calls
ffmpeg, shells out to a Python ML model, or uses a headless Chromium? Self-hosted Appwrite lets you extend runtimes and mount volumes. Cloud BaaS providers don't. - Open source all the way down. The entire codebase is on GitHub. You can patch bugs, audit security, and contribute features — something no proprietary BaaS permits.
Cost and Capability Comparison
| Feature | Firebase (Blaze) | Supabase Cloud (Pro) | Appwrite Cloud (Pro) | Self-Hosted Appwrite (VPS) |
|---|---|---|---|---|
| Monthly base cost | Pay-as-you-go | $25/mo | $15/mo | EUR 19.99/mo (flat) |
| Database reads | $0.06 / 100K | 8 GB incl. | 3.5M executions | Unlimited |
| Auth users | 50K free, then metered | 100K MAU | Unlimited | Unlimited |
| Storage | $0.026 / GB | 100 GB | 150 GB | Disk-limited |
| Bandwidth | $0.12 / GB | 250 GB | 300 GB | Unmetered on most VPS |
| Function invocations | Metered | Metered | 3.5M | Unlimited |
| Data residency control | Regional only | Regional | Regional | Single server, you pick |
| Custom runtimes | No | Edge functions only | Fixed list | Full Docker, any runtime |
| Source-available | No | Yes | Yes | Yes (BSD-3) |
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access.
- SSH access to the server.
- A registered domain (for example,
appwrite.yourdomain.com) with the ability to edit DNS records. - At least 4 GB of RAM, 2 vCPU, and 30 GB of disk — Appwrite runs 12+ containers including MariaDB, Redis, InfluxDB, and per-runtime function pools.
- Ports 80 and 443 open on your server firewall and at the provider level.
Recommended Plan: CloudCore Professional>
For a comfortable Appwrite deployment with headroom for function runtimes and growth, we recommend the CloudCore Professional plan:>
- 6 vCPU cores
- 12 GB RAM
- 100 GB NVMe SSD
- Unmetered bandwidth
- EUR 19.99/month>
This gives you the resources to run Appwrite alongside your app containers, a staging environment, or additional services like a Redis cache or background workers.
If you need a Docker walkthrough first, read our How to Install Docker on Ubuntu guide before continuing.
Connect via SSH:
ssh root@your-server-ipStep 1: Prepare the Ubuntu 24.04 VPS
Update the package index and upgrade installed packages so you have current security patches:
sudo apt update && sudo apt upgrade -yInstall a few utilities the rest of this guide uses:
sudo apt install -y curl ca-certificates gnupg ufw gitConfigure the firewall to allow SSH, HTTP, and HTTPS:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo ufw statusExpected output:
Status: active
To Action From -- ------ ---- OpenSSH ALLOW Anywhere 80/tcp ALLOW Anywhere 443/tcp ALLOW Anywhere
Set the server's hostname (cosmetic, but helps with logs):
sudo hostnamectl set-hostname appwriteIf the kernel was upgraded, reboot once and reconnect:
sudo rebootStep 2: Install Docker and Docker Compose
Appwrite is distributed as a Docker Compose stack, so Docker Engine and the Compose plugin are the only runtime dependencies. Install them from Docker's official apt repository:
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpgecho "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu noble stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/nullsudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginEnable and start Docker, then add your user to the docker group so you don't need sudo for every command:
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp dockerVerify the install:
docker --version
docker compose versionExpected output:
Docker version 27.3.1, build ce12230
Docker Compose version v2.29.7For a complete Docker walkthrough including rootless mode, log rotation, and storage drivers, see How to Install Docker on Ubuntu.
Step 3: Point Your Domain at the Server
Appwrite ships with Traefik, which will automatically request a Let's Encrypt certificate when a request arrives on your chosen hostname. That only works if DNS already resolves to this server, so do this before the install.
At your DNS provider (Cloudflare, Route 53, Namecheap, etc.), add an A record:
| Type | Name | Value | TTL |
|---|---|---|---|
| A | appwrite | your-server-ip | 300 |
| Type | Name | Value | TTL |
|---|---|---|---|
| A | *.appwrite | your-server-ip | 300 |
Verify propagation before continuing:
dig +short appwrite.yourdomain.comYou should see the server's public IP returned. If not, wait 1-5 minutes and retry.
Step 4: Run the Appwrite Installer
Appwrite publishes a one-line installer script that pulls the latest stable release, generates secrets, writes a docker-compose.yml, and starts the stack. Run it from the directory where you want the install to live (the default is /usr/src/code/appwrite):
docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:1.6.0The installer will prompt you for a few values:
? Enter your Appwrite HTTP port: (80)
? Enter your Appwrite HTTPS port: (443)
? Enter your Appwrite hostname: appwrite.yourdomain.com
? Enter a DNS A record hostname to serve as a CNAME for your custom domains:
appwrite.yourdomain.comAccept the defaults for ports, set the hostname to the FQDN you pointed at the server in Step 3, and use the same value for the CNAME target.
Expected final output:
Appwrite installed successfullyThe installer created a directory tree under ./appwrite/ on your host:
appwrite/
.env
docker-compose.ymlIt also pulled and started these containers (check with docker ps):
appwrite-traefik— ingress reverse proxy with Let's Encryptappwrite— main API serverappwrite-realtime— WebSocket serverappwrite-worker-*— background workers for mails, webhooks, functions, etc.appwrite-mariadb— metadata and application data storeappwrite-redis— cache and queueappwrite-influxdb— usage metricsappwrite-telegraf— metrics collectoropenruntimes-executor— sandboxed function runner
cd appwriteStep 5: Configure the .env File
The generated .env file is the single source of truth for all Appwrite settings. Open it in your editor:
nano .envThe keys you will almost always want to review or change are:
# --- Hostname & URLs ---
_APP_DOMAIN=appwrite.yourdomain.com
_APP_DOMAIN_TARGET=appwrite.yourdomain.com
_APP_DOMAIN_FUNCTIONS=functions.yourdomain.com--- Console account & email ---
[email protected]
_APP_CONSOLE_WHITELIST_IPS=
_APP_SYSTEM_EMAIL_NAME=Appwrite
[email protected]--- TLS ---
_APP_OPTIONS_FORCE_HTTPS=enabled--- SMTP (required for password resets & team invites) ---
_APP_SMTP_HOST=smtp.resend.com
_APP_SMTP_PORT=587
_APP_SMTP_SECURE=tls
_APP_SMTP_USERNAME=resend
_APP_SMTP_PASSWORD=re_xxxxxxxxxxxxxxxxxxxxxxxxxxxx--- Storage ---
_APP_STORAGE_LIMIT=30000000
_APP_STORAGE_DEVICE=local--- Functions ---
_APP_FUNCTIONS_SIZE_LIMIT=30000000
_APP_FUNCTIONS_TIMEOUT=900
_APP_FUNCTIONS_BUILD_TIMEOUT=900
_APP_FUNCTIONS_CPUS=1
_APP_FUNCTIONS_MEMORY=512--- Security ---
_APP_OPENSSL_KEY_V1=<auto-generated-32-byte-random-string>Key variables explained:
_APP_DOMAINand_APP_DOMAIN_TARGETtell Traefik which hostname to serve and which hostname to use as the CNAME target when users attach custom domains to their projects._APP_DOMAIN_FUNCTIONSis the domain under which deployed functions expose HTTP endpoints (for example64abc.functions.yourdomain.com). Point a wildcard A record at your server if you plan to use HTTP-triggered functions._APP_CONSOLE_WHITELIST_EMAILSrestricts who can sign up for an admin account at/console. Set this to your email before the first login, otherwise anyone who finds your URL can register as an admin._APP_SMTP_*powers transactional mail (password resets, verification, team invites). Resend, Mailgun, Postmark, Amazon SES, and any standard SMTP service all work._APP_OPTIONS_FORCE_HTTPS=enabledmakes Traefik redirect all HTTP traffic to HTTPS. Keep this on in production._APP_OPENSSL_KEY_V1encrypts sensitive values in the database. The installer generates a random value — never change this after your first boot or existing encrypted data will be unrecoverable. Back it up alongside your database.
docker compose up -dOnly the containers affected by env changes will restart.
Step 6: Enable Traefik Auto TLS with Let's Encrypt
Appwrite's default docker-compose.yml already includes Traefik configured for Let's Encrypt HTTP-01 challenges. The first request to your hostname over HTTPS will trigger certificate issuance automatically — there is nothing to configure beyond having DNS pointed correctly.
Verify Traefik picked up the hostname:
docker logs appwrite-traefik 2>&1 | grep -i acme | tail -20You should see lines like:
"Starting ACME client" providerName=letsencrypt.acme
"Trying to challenge certificate" domains=["appwrite.yourdomain.com"]
"Register..." acmeCA=https://acme-v02.api.letsencrypt.org/directory
"Certificates obtained" domains=["appwrite.yourdomain.com"]Open your browser and navigate to:
https://appwrite.yourdomain.comYou should see the Appwrite login screen with a valid Let's Encrypt certificate. If the certificate is self-signed or you get an ERR_CERT error, the HTTP-01 challenge failed — see the Troubleshooting section below.
Hardening Traefik
For production, edit the Traefik service block in docker-compose.yml to disable the dashboard and force strict TLS. Find the traefik: service and ensure these command flags are present:
services:
traefik:
command:
- --api.dashboard=false
- --providers.docker=true
- --providers.docker.exposedByDefault=false
- --entrypoints.web.address=:80
- --entrypoints.web.http.redirections.entrypoint.to=websecure
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --entrypoints.websecure.address=:443
- --certificatesresolvers.letsencrypt.acme.email=admin@yourdomain.com
- --certificatesresolvers.letsencrypt.acme.storage=/var/lib/traefik/acme.json
- --certificatesresolvers.letsencrypt.acme.httpchallenge=true
- --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=webReplace [email protected] with your own address — Let's Encrypt uses it for expiry notifications. Restart Traefik:
docker compose up -d traefikStep 7: Create the Admin Console Account
Visit https://appwrite.yourdomain.com/console/register and fill in:
- Name — your display name
- Email — must be in
_APP_CONSOLE_WHITELIST_EMAILS - Password — minimum 8 characters, ideally stored in a password manager
Immediately after signup, update the whitelist to either just your email or a comma-separated list of your team's emails. Leaving it empty (the default) allows anyone to register.
Then, enable multi-factor authentication on your admin account:
Step 8: Create Your First Project
Appwrite is multi-tenant by design: a single instance hosts any number of independent projects, each with its own databases, auth, storage, and API keys.
From the console:
my-saas-app).You land on the project overview. The sidebar shows Auth, Databases, Functions, Messaging, Storage, and Settings.
Add a Platform
A "platform" in Appwrite terminology is a client app that will talk to your API. The hostname on that platform must be registered or CORS will block requests.
app.yourdomain.com for production, localhost for development).Appwrite will show you the endpoint URL (https://appwrite.yourdomain.com/v1) and the project ID — you'll need both in Step 14.
Create a Server API Key
For server-side operations (a Node.js backend, a deploy script, a migration), create an API key:
backend-server).databases.read, databases.write, users.read).Copy the secret immediately — it is shown once and then hashed.
Step 9: Set Up Databases and Collections
Appwrite's database layer gives you schema-defined document collections backed by MariaDB. Attributes are typed (string, integer, float, boolean, datetime, enum, IP, URL, email, relationship), and every collection has per-document and per-role permissions.
From the project:
main and click Create.posts and click Create.Add attributes to the collection:
title, size 255, mark as Required.content, size 65535, required.authorId, size 50, required.publishedAt, optional.status, elements draft,published,archived, required, default draft.Add an index so queries are fast:
idx_status_published, type Key, attributes status ASC, publishedAt DESC.Set permissions:
any (for a public blog) or users (for logged-in only)
- Create -> users
- Update, Delete -> leave empty and use per-document permissions
You now have a production-ready document collection. Compare this to Firestore, where schema is implicit and indexes must be defined in a firestore.indexes.json file you can't see in the console — Appwrite shows everything upfront.
For a SQL-first approach with an identical BaaS feature set, see our How to Install Supabase on Ubuntu guide.
Step 10: Configure Authentication Providers
Appwrite ships with 30+ built-in auth providers. By default, email/password and anonymous login are enabled. Add social login and magic links from the Auth section.
Email and Password
Already on by default. Tweak settings at Auth -> Security:
- Password history — prevent users from reusing their last N passwords.
- Password dictionary — block the top 10,000 breached passwords.
- Session length — defaults to 1 year; shorten for high-security apps.
- Session limit — cap concurrent sessions per user.
OAuth 2 Providers
Under Auth -> Settings -> OAuth2 providers, you can enable any of:
- Google, Apple, Microsoft, GitHub, GitLab, Bitbucket
- Discord, Slack, Facebook, Twitch, Spotify
- Amazon, Auth0, Okta, LinkedIn, Zoom
- Plus 15+ more
https://appwrite.yourdomain.com/v1/account/sessions/oauth2/callback/google/[PROJECT_ID] to Authorized redirect URIs.Magic URL and OTP
Enable under Auth -> Settings:
- Magic URL login — sends a one-time login link via email.
- Email OTP — sends a 6-digit code instead of a link.
- Phone OTP — sends SMS codes via Twilio, MSG91, or Vonage (configure the SMS provider at Messaging -> Providers).
Multi-Factor Authentication
Turn on Auth -> Security -> MFA to let end users enroll a TOTP authenticator app in addition to their primary login method.
All of these work out of the box through the client SDKs without any extra code beyond calling the relevant method (account.createEmailPasswordSession, account.createOAuth2Session, etc.).
Step 11: Create Storage Buckets
Appwrite Storage is an S3-compatible file API with built-in image transformation, antivirus scanning, and encryption at rest.
uploads._APP_STORAGE_LIMIT in your .env.
- Allowed file extensions — leave empty for any, or restrict (jpg,jpeg,png,webp,pdf).
- Compression — gzip for text-heavy files, none for already-compressed media.
- Encryption — on by default, encrypts files with the _APP_OPENSSL_KEY_V1 secret.
- Antivirus — scans uploaded files with ClamAV (enable _APP_ANTIVIRUS=enabled in .env first).
users (logged-in users can upload)
- Read -> any (public bucket) or users (private bucket)
Every uploaded file gets a unique ID and can be fetched at:
https://appwrite.yourdomain.com/v1/storage/buckets/uploads/files/FILE_ID/view?project=PROJECT_IDFor images, Appwrite exposes a preview endpoint with on-the-fly transformations:
https://appwrite.yourdomain.com/v1/storage/buckets/uploads/files/FILE_ID/preview?width=400&height=400&gravity=center&quality=80&output=webp&project=PROJECT_IDThis means no more wiring up Sharp or ImageMagick in your app.
Using S3 or External Storage
To store files on S3, DigitalOcean Spaces, Backblaze B2, or Wasabi instead of local disk, set in .env:
_APP_STORAGE_DEVICE=s3
_APP_STORAGE_S3_ACCESS_KEY=AKIA...
_APP_STORAGE_S3_SECRET=...
_APP_STORAGE_S3_REGION=us-east-1
_APP_STORAGE_S3_BUCKET=my-appwrite-bucketRestart: docker compose up -d.
Step 12: Deploy Your First Function
Appwrite Functions run your code in response to HTTP requests, CRON schedules, or platform events (user signup, document update, file upload, etc.). Each function runs in a sandboxed container using OpenRuntimes.
Supported Runtimes
As of Appwrite 1.6, OpenRuntimes supports:
- Node.js 16, 18, 19, 20, 21, 22
- Python 3.8 through 3.12
- PHP 8.0, 8.1, 8.2, 8.3
- Ruby 3.0, 3.1, 3.2, 3.3
- Deno 1.35, 1.40, 1.46
- Dart 2.15 through 3.3
- Bun 1.0, 1.1
- Go 1.23
- Java 8, 11, 17, 18, 21
- Kotlin 1.6 through 1.9
- Swift 5.5 through 5.10
- .NET 6.0, 7.0, 8.0
- C++ 17, 20
Create a Function
hello-world.src/main.js.Deploy via the CLI
Install the Appwrite CLI on your laptop:
npm install -g appwrite-cliLog in and initialize a project:
appwrite login
appwrite init projectWhen prompted, choose your self-hosted endpoint https://appwrite.yourdomain.com/v1 and your project ID.
Pull the function scaffold:
appwrite init functionPick hello-world. This creates ./functions/hello-world/src/main.js:
export default async ({ req, res, log, error }) => { log(Received ${req.method} request to ${req.path});if (req.method === 'GET') { return res.json({ message: 'Hello from Appwrite Functions!', timestamp: new Date().toISOString(), }); }
return res.json({ error: 'Method not allowed' }, 405); };
Deploy:
appwrite push functionThe CLI uploads your code, Appwrite builds it in a container, and within 30-60 seconds the function is live. Test it:
curl https://appwrite.yourdomain.com/v1/functions/FUNCTION_ID/executions \
-H "X-Appwrite-Project: PROJECT_ID" \
-H "X-Appwrite-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"async": false, "path": "/", "method": "GET"}'Event Triggers and Schedules
In the function's Settings tab:
- Events — attach triggers like
users..create(runs when any user is created) ordatabases.main.collections.posts.documents..create(runs on new posts). - Schedule — CRON expression (
0 /6for every 6 hours). - Timeout — up to
_APP_FUNCTIONS_TIMEOUTseconds (default 900). - Execute access — who can call the function (
any,users, specific user IDs, specific teams).
Step 13: Configure Webhooks
Webhooks let external services react to Appwrite events — trigger a Slack alert on signup, POST to a Stripe customer endpoint on user creation, or kick off a Zapier flow on new documents.
slack-signup-notifier.https://hooks.slack.com/services/...).users.*.create — fires on every new user.
- users..sessions..create — fires on every login.
- databases.main.collections.posts.documents.*.create — fires on new posts.
X-Appwrite-Webhook-Signature HMAC header so you can verify authenticity.Every matching event will POST a JSON payload to your URL with the full resource object. Appwrite retries failed webhooks with exponential backoff up to 3 attempts.
Verifying Signatures (Node.js)
import crypto from 'crypto';
function verifyAppwriteSignature(signingSecret, url, payload, signature) { const computed = crypto .createHmac('sha1', signingSecret) .update(url + JSON.stringify(payload)) .digest('base64'); return crypto.timingSafeEqual( Buffer.from(computed), Buffer.from(signature) ); }
Step 14: Integrate an SDK
With the backend running, wire up your frontend. Here are the most common SDKs.
Web (JavaScript/TypeScript)
npm install appwriteimport { Client, Account, Databases, Storage, ID } from 'appwrite';const client = new Client() .setEndpoint('https://appwrite.yourdomain.com/v1') .setProject('PROJECT_ID');
const account = new Account(client); const databases = new Databases(client); const storage = new Storage(client);
// Sign up await account.create(ID.unique(), '[email protected]', 'securePassword123', 'Jane Doe');
// Log in await account.createEmailPasswordSession('[email protected]', 'securePassword123');
// Create a document await databases.createDocument( 'main', 'posts', ID.unique(), { title: 'Hello Appwrite', content: 'My first post!', authorId: (await account.get()).$id, status: 'published', publishedAt: new Date().toISOString(), } );
// Upload a file const file = document.getElementById('fileInput').files[0]; await storage.createFile('uploads', ID.unique(), file);
Node.js (Server)
npm install node-appwriteimport { Client, Databases, Users } from 'node-appwrite';const client = new Client() .setEndpoint('https://appwrite.yourdomain.com/v1') .setProject('PROJECT_ID') .setKey('YOUR_SERVER_API_KEY');
const databases = new Databases(client); const users = new Users(client);
const allUsers = await users.list(); console.log(Total users: ${allUsers.total});
Flutter
dependencies:
appwrite: ^13.0.0final client = Client() .setEndpoint('https://appwrite.yourdomain.com/v1') .setProject('PROJECT_ID');
final account = Account(client); await account.createEmailPasswordSession( email: '[email protected]', password: 'securePassword123', );
Realtime Subscriptions
Every SDK supports realtime updates over WebSockets. Subscribe to any event stream:
client.subscribe('databases.main.collections.posts.documents', (response) => {
console.log('New event:', response.events, response.payload);
});Post-Install: Backups and Upgrades
Daily Automated Backup
Create a backup script at /usr/local/bin/appwrite-backup.sh:
#!/bin/bash
set -e
BACKUP_DIR=/var/backups/appwrite
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
mkdir -p "$BACKUP_DIR"Dump MariaDB
docker exec appwrite-mariadb \
sh -c 'exec mysqldump --all-databases -uroot -p"$MARIADB_ROOT_PASSWORD"' \
| gzip > "$BACKUP_DIR/mariadb-$TIMESTAMP.sql.gz"Archive uploads and function builds
tar czf "$BACKUP_DIR/volumes-$TIMESTAMP.tar.gz" \
/var/lib/docker/volumes/appwrite_appwrite-uploads \
/var/lib/docker/volumes/appwrite_appwrite-functions \
/var/lib/docker/volumes/appwrite_appwrite-buildsRetain 14 days
find "$BACKUP_DIR" -type f -mtime +14 -deletesudo chmod +x /usr/local/bin/appwrite-backup.sh
sudo crontab -eAdd:
0 3 * /usr/local/bin/appwrite-backup.sh >> /var/log/appwrite-backup.log 2>&1Upgrading Appwrite
Appwrite follows semantic versioning. To upgrade from the current minor release to the next:
cd /root/appwrite
docker compose down
docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/:/usr/src/code/appwrite:rw \
--entrypoint="upgrade" \
appwrite/appwrite:1.7.0
docker compose up -dThe upgrade script preserves your .env and runs any schema migrations. Always back up MariaDB before a major version bump.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
| Certificate is self-signed / NET::ERR_CERT_AUTHORITY_INVALID | Let's Encrypt HTTP-01 challenge failed | Check DNS resolves to your server: dig +short appwrite.yourdomain.com. Check docker logs appwrite-traefik for ACME errors. Ensure ports 80 and 443 are open at both the VPS firewall and provider. |
| Console loads but can't create account | _APP_CONSOLE_WHITELIST_EMAILS doesn't include your address | Edit .env, add your email, run docker compose up -d. |
| Password reset emails never arrive | SMTP not configured or credentials wrong | Set _APP_SMTP_* variables. Test with docker exec appwrite sh -c 'curl -v smtp://$_APP_SMTP_HOST:$_APP_SMTP_PORT'. |
Function build failed: connection timeout | Executor can't reach the internet for package install | Check that Docker has DNS configured: add "dns": ["1.1.1.1","8.8.8.8"] to /etc/docker/daemon.json, then sudo systemctl restart docker. |
| 502 Bad Gateway on API calls | Main appwrite container crashed | docker logs appwrite to see the PHP error, usually an invalid .env value. Fix and docker compose up -d. |
| Realtime subscriptions disconnect instantly | Reverse proxy in front stripping WebSocket headers | If you run Cloudflare or another proxy, enable WebSocket support. Direct Traefik handles this automatically. |
Disk quota exceeded on uploads | Appwrite volumes filled the disk | df -h; clean old function builds with Functions -> each function -> Deployments -> delete old, or mount a larger volume. |
| Can't SSH after firewall setup | UFW blocked your own IP | Log in via the VPS provider's web console, run sudo ufw allow from YOUR.IP.ADDRESS.HERE. |
Viewing Logs
Stream a container's logs live:
docker compose logs -f appwrite
docker compose logs -f appwrite-realtime
docker compose logs -f traefikTail last 100 lines across all services:
docker compose logs --tail=100FAQ
How much RAM does Appwrite really need?
The minimum to run is about 2 GB, but that will feel sluggish once you deploy a couple of functions. For a production setup running 5-10 functions with a few thousand active users, 8 GB is comfortable and 12 GB (our CloudCore Professional spec) gives you room to add a staging environment, a separate Redis, or host your app containers on the same box. MariaDB is the memory hog — tune its innodb_buffer_pool_size down if you need to fit in 4 GB.
Can I run Appwrite behind Cloudflare?
Yes, with one caveat: WebSocket subscriptions require Cloudflare's WebSocket support (on by default on paid plans, enabled via Network toggle on free). Use the Flexible or Full (strict) SSL mode — Full (strict) is recommended since Appwrite already has a valid Let's Encrypt cert from Traefik. If you use HTTP-01 challenges, set the DNS record to DNS only (grey cloud) during the first cert issuance, then you can proxy it. For wildcard certificates, switch to Traefik's DNS-01 challenge with a Cloudflare API token.
How does Appwrite compare to Supabase and PocketBase?
Appwrite is document-database-first (MariaDB under the hood, but exposed as collections), comes with 30+ OAuth providers, realtime, functions in 14+ languages, and a polished admin console. Best for: apps that want Firebase's feel with full self-hosting and a broad runtime story.
Supabase is PostgreSQL-first — you write real SQL, use row-level security, and can talk to the database directly. Edge Functions run on Deno only. Best for: teams that want raw SQL power and relational data.
PocketBase is a single Go binary with SQLite, weighing in at ~20 MB, with auth, a realtime API, and a nice admin UI. Best for: small apps, internal tools, and embedded scenarios where you don't want Docker at all.
If you want SQL and relational joins, pick Supabase. If you want the smallest footprint, pick PocketBase. If you want the broadest feature set out of the box, Appwrite wins.
Is Appwrite production-ready for real apps?
Yes. Appwrite is used in production by companies including Apple (internally), German railway operators, Y Combinator-backed startups, and thousands of indie SaaS teams. The 1.x release line has been stable for over two years, with quarterly minor releases and near-weekly patch releases. For five- and six-figure user counts, a single VPS with 8-12 GB RAM handles it comfortably. Above that, you move MariaDB and Redis to dedicated nodes and scale the API servers horizontally.
How do I migrate from Firebase to Appwrite?
Appwrite provides official migration tooling for Firebase, Supabase, and NHost. From the console, go to Project Settings -> Migrations -> Create migration. Paste your Firebase service account JSON, select which resources to import (auth users, Firestore collections, Storage files), and Appwrite will pull them into matching Appwrite primitives. Functions are not auto-migrated — you'll need to rewrite Cloud Functions as Appwrite Functions, but the surface area is similar enough that it's usually a few hours of work per function.
Do I need a separate queue system like RabbitMQ or BullMQ?
No. Appwrite uses Redis internally for all asynchronous workloads — function invocations, webhook deliveries, outbound emails, and event fan-out all go through the built-in queue. You get retries, backoff, and dead-letter handling for free. Only add an external queue if you have app-level background jobs that don't map to Appwrite functions or webhooks.
Next Steps
You now have a production Appwrite instance with TLS, a custom domain, auth, databases, storage, functions, and webhooks. Here's what to build next:
- Set up monitoring with Uptime Kuma — point it at
https://appwrite.yourdomain.com/v1/healthand at each of your function HTTP endpoints for sub-minute outage alerts. - Add object storage offsite — switch
_APP_STORAGE_DEVICEtos3with a provider like Backblaze B2 for cheap durable storage and automatic geo-redundancy. - Wire up CI/CD for functions — GitHub Actions + the Appwrite CLI deploys functions on every merge to
main. Commitappwrite.jsonto your repo so your schema is versioned. - Enable per-tenant custom domains — Appwrite supports user-attached domains, perfect for SaaS with white-label URLs. Configure
_APP_DOMAIN_TARGETand point customer CNAMEs at your instance. - Compare alternatives — read our Supabase install guide or PocketBase install guide to see which BaaS fits your stack best.
- Read the official self-hosting docs — the Appwrite self-hosting reference covers scaling, Kubernetes, and enterprise features in depth.
Skip the Manual Install — Get a Pre-Tuned Backend VPS>
Our CloudCore Professional plan gives you 6 vCPU, 12 GB RAM, and 100 GB NVMe — the sweet spot for running Appwrite comfortably alongside your app. Deploy in 60 seconds with Docker and Traefik already installed.>
- Docker Engine + Compose pre-installed
- UFW firewall pre-configured for 80/443
- Let's Encrypt tooling ready
- systemd, log rotation, and automatic security updates enabled
- EUR 19.99/month, unmetered bandwidth>
Deploy Your Backend VPS Now