Go through this list before real users depend on an install. Each item links to the guide that explains it.
- Managed or operator-run datastores. Set
database.mode,redis.modeandclickhouse.modetoexternaland use replicated services with backups (external-databases.md). - Credentials backed up outside the cluster: the whole chart Secret,
not just the encryption key, because the built-in PostgreSQL's password
is tied to its volume (backup-restore.md).
Or provide them from your secret manager with
appwrite.secrets.existingSecretand each credential'ssecretName(security.md). - Backups of the database and files, with a tested restore (backup-restore.md).
- A ReadWriteMany volume for
/storage, so API and worker pods can spread across nodes. With S3 the volume can be small, but it's still mounted, and a ReadWriteOnce claim keeps those pods on one node (storage.md).
- TLS at the ingress or Gateway, with
appwrite.forceHttps: true(the default) (networking.md). - Wildcard certificates for
*.functions.<domain>and*.sites.<domain>if you use Functions or Sites. They need a DNS-01 issuer (networking.md). -
appwrite.domainTargetsset to your load balancer, so custom domains verify (networking.md). - Body size and timeouts on the controller suit uploads (30 MB by default), realtime WebSockets and synchronous executions (networking.md).
- Client IPs reach Appwrite in
X-Forwarded-Forand can't be spoofed (networking.md).
- Resources on every component, sized from real usage.
_APP_CPU_NUMfollows the CPU limit, or the request when there's no limit; setcpuNumwhere that isn't what the pod really gets (architecture.md). - Replicas and autoscaling for the API, console and realtime
(
replicaCount,autoscaling.enabled). - PHP memory limit raised for a realtime that holds thousands of
connections:
appwrite.config._APP_MEMORY_LIMIT(512M per PHP process,-1for none) applies to every Appwrite pod, so raiserealtime.resources.limits.memorywith it. - Function and site specifications capped with
appwrite.config._APP_COMPUTE_CPUSand_APP_COMPUTE_MEMORY, if you run Functions and Sites. Without a cap, Appwrite offers up to 8 CPUs and 8 GB per function, and builds are sized the same way (executor.md). - Connection budget: every Appwrite pod at peak, counting HPA
maximums, fits in the database's
max_connections(external-databases.md). Workers size their pools frommaxCoroutines, not fromconnectionsMax(architecture.md). - Worker topology:
separateorgroupedwhen one queue (usage, builds) shouldn't slow down the others (architecture.md). - Redis memory big enough that it never fills up. The built-in Redis
evicts least-recently-used keys when it reaches
redis.builtin.maxmemory(512 MB;allkeys-lru, as in Appwrite's Docker install). A queue is a key, so an evicted queue loses its jobs.
- Node drains planned for. Only the API, console and realtime get PodDisruptionBudgets, and only when more than one replica can run. Workers, tasks, geo, the executor and the built-in datastores have none, so nothing limits how many of them a drain evicts at once. Drain one node at a time (architecture.md).
- Functions and Sites (on by default): the executor on nodes of its
own (executor.md), with replicas and
resources for the executions you expect
(executor.md); or
compute.enabled: falseif you don't use them, which removes the privileged executor pod. - Object storage with redundancy: your own S3 store (storage.md), or backups of the built-in store's volume, which is a single replica (storage.md).
-
appwrite.logFormat: json(the default) shipped to your log pipeline. - Error tracking: set
_APP_LOGGING_CONFIGto a Sentry DSN throughappwrite.secretConfig(integrations.md). - Monitoring and alerts for the signals below.
- SMTP configured (
smtp), so verification and password emails go out (integrations.md). - Console sign-up restricted:
appwrite.console.whitelistRoot(the default) lets only the first account sign up;appwrite.console.whitelistEmailsandwhitelistIPsnarrow it further. - NetworkPolicies (
networkPolicy.enabled), with the entry points limited to your ingress controller (networkPolicy.ingress.from) (security.md). With Functions and Sites, restrict who can read pods and Jobs in the build namespace (security.md). - Upgrades rehearsed on a copy of production (upgrading.md).
Appwrite 2.6.2 exposes no Prometheus metrics. You have its logs, its health API, and the metrics of Kubernetes and your datastores.
| Watch | How | Alert when |
|---|---|---|
| Pod restarts | kubectl -n appwrite get pods, or kube_pod_container_status_restarts_total from kube-state-metrics |
Restarts keep rising. Workers and tasks have no probes: Kubernetes restarts them only when they exit, and a hung one shows up only in its queue. |
| Queue backlog | Length of each queue in Redis (below) | A queue keeps growing: its worker is behind, hung or missing. |
| Failed jobs | /v1/health/queue/failed/<queue> |
The count rises. |
| Datastores, as the API sees them | /v1/health/db, /v1/health/cache, /v1/health/pubsub, /v1/health/usage, /v1/health/storage |
A check fails. The API's probes don't test the datastores. |
| Redis memory | redis-cli info memory and info stats |
used_memory nears maxmemory, or evicted_keys rises. |
| Volume usage | kubelet_volume_stats_used_bytes and kubelet_volume_stats_capacity_bytes |
/storage or a datastore volume passes 80%. |
| Database connections | pg_stat_activity against max_connections |
Connections near the limit (sizing). |
| Certificate expiry | /v1/health/certificate?domain=<domain>, or cert-manager's metrics |
validTo is less than two weeks away. |
Every endpoint except /v1/health/version needs an API key. Appwrite grants
the health.read scope to every project API key, so create one in any
project in the console:
curl -s https://appwrite.example.com/v1/health/db \
-H "X-Appwrite-Project: <project-id>" \
-H "X-Appwrite-Key: <api-key>"A passing check answers 200; a failing one returns an error status and a
message.
| Endpoint | Checks |
|---|---|
/v1/health |
The API answers. |
/v1/health/db |
The console and project databases answer a ping. |
/v1/health/cache, /v1/health/pubsub |
Redis, as cache and as pub/sub. |
/v1/health/usage |
ClickHouse. |
/v1/health/storage |
A test file can be written to and read from the storage device (volume or S3). |
/v1/health/storage/local |
The local storage directories are readable and writable. |
/v1/health/queue/failed/<queue> |
Failed jobs in a queue, by its Redis name such as v1-webhooks or v1-builds. Answers 503 at or above ?threshold= (5000 by default). |
/v1/health/certificate?domain=<domain> |
The certificate served on port 443 of a public domain; validTo is a Unix timestamp. Fails only once it has expired. |
The checks run on whichever API pod answers, so they show what that pod can reach.
Each queue is a Redis list, utopia-queue.queue.<name>; an empty queue has
no key. Print the length of every queue that has jobs waiting:
kubectl -n appwrite exec appwrite-redis-0 -- sh -c \
'for q in $(redis-cli --scan --pattern "utopia-queue.queue.*"); do echo "$q $(redis-cli llen "$q")"; done'With an external Redis, run the loop with redis-cli against it. Queues
that grow without ever draining usually mean no worker consumes them: check
the warning the install notes print
(architecture.md).
For the built-in datastores:
kubectl -n appwrite exec appwrite-redis-0 -- redis-cli info memory | grep -E '^(used_memory_human|maxmemory_human)'
kubectl -n appwrite exec appwrite-redis-0 -- redis-cli info stats | grep '^evicted_keys'
kubectl -n appwrite exec appwrite-postgresql-0 -- \
sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SELECT count(*) FROM pg_stat_activity" -c "SHOW max_connections"'The chart can run exporters next to the built-in datastores and create Prometheus Operator objects for them. Nothing is scraped by default.
database:
builtin:
metrics:
enabled: true # postgres_exporter sidecar, port 9187
redis:
builtin:
metrics:
enabled: true # redis_exporter sidecar, port 9121
clickhouse:
builtin:
metrics:
enabled: true # ClickHouse's own endpoint, port 9363, no sidecar
metrics:
serviceMonitor:
enabled: true # one ServiceMonitor per datastore with metrics
labels:
release: prometheus # whatever your Prometheus selects on
prometheusRule:
enabled: true
groups:
- name: appwrite-datastores
rules:
- alert: AppwriteRedisDown
expr: redis_up == 0
for: 5m
- alert: AppwritePostgresConnectionsHigh
expr: sum(pg_stat_activity_count) / max(pg_settings_max_connections) > 0.8
for: 10m
networkPolicy:
metrics:
from: # with networkPolicy.enabled, who may scrape
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoringEach datastore's Service gets a metrics port. serviceMonitor and
prometheusRule need the Prometheus Operator's CRDs; without them, leave
them off and point your scraper at the Services. With networkPolicy.enabled,
the datastores only accept Appwrite pods of the release, plus the sources in
networkPolicy.metrics.from on the metrics ports.
External datastores are outside the chart: use your provider's metrics, or run exporters yourself.
examples/production.yaml puts this list
together: CloudNativePG, an external Redis and ClickHouse, S3, Secrets you
manage, an autoscaled API, grouped workers, a ReadWriteMany volume and
NetworkPolicies. Layer examples/aws.yaml or
examples/external-services.yaml on
top of it to use other datastores, and
examples/functions-existing-secrets.yaml
for Functions and Sites.