Infomaniak Public Cloud
This page lays the Docker Compose production model onto Infomaniak Public Cloud — a sovereign, Swiss, OpenStack-based IaaS (Nova compute, Cinder block storage, Neutron networking, Swift/S3 object storage, Terraform/OpenTofu support). It’s a good fit for AnvilBase: the whole tested single-node stack runs unchanged, and the data-sovereignty story (GDPR + Swiss FADP, outside the US Cloud Act, ISO 27001, 100% Swiss) matches Built in Europe.
Scope & honesty. The topology below is the one AnvilBase is built and tested for (single-node Compose,
local = cloud). Thedeploy/infomaniak/Terraform module derives from our working load-test rig (so the OpenStack plumbing is proven), but it has not beenterraform apply-ed end-to-end against a production account — treat it as a starting point and walk the validation checklist. Every euro figure is an estimate — confirm in Infomaniak’s live calculator.
The shape
Internet (clients, SDKs) │ 443/80 floating IPv4 ────┤ (+ free IPv6) ▼ ┌─────────────────────────────────────────────────────────┐ │ 1× Public Cloud instance a8-ram32 (8 vCPU / 32 GB) │ │ region ch-gva (Geneva) │ │ │ │ Traefik (:80/:443, TLS) ── the ONLY public service │ │ │ │ │ ├─ public → control-plane / auth / realtime / │ │ │ storage / functions / /health │ │ └─ private → console + management API (overlay) │ │ │ │ control-plane · auth · realtime · webhooks · deno · │ │ imgproxy · valkey · console (stateless / cache) │ │ │ │ Postgres 15 ──► dedicated Cinder CEPH_1_perf2 volume │ │ (1000 IOPS / 400 MB/s) @ PGDATA │ └───────────────┬──────────────────────────┬──────────────┘ │ wal-g (WAL + basebackups) │ per-project buckets ▼ ▼ Infomaniak S3 (Swift gateway, s3.pub1.infomaniak.cloud)Why a single VM and not multiple VMs or Kubernetes: in our load test an
a8-ram32 running the full stack served a realistic multi-project workload
(writes to ~300–340 req/s, reads far higher, 0 errors); the binding constraint is
availability (one box is a SPOF), not throughput. You buy down availability
without leaving Compose — tested PITR plus an optional warm standby — long
before a fleet or an orchestrator is justified. See Scaling for the
growth path and Load Testing for the numbers and their caveats.
Kubernetes is experimental here (Kubernetes) — it forks dev
from prod and isn’t tested; don’t use it for production.
1. Provision the instance (Terraform / OpenStack)
Infomaniak Public Cloud speaks the standard OpenStack API, so Terraform uses
terraform-provider-openstack. Authenticate with an OpenStack application
credential (create one in the Infomaniak Manager → Public Cloud → API access),
and never commit it — pass it via environment or a gitignored *.tfvars.
# deploy/infomaniak/terraform.tfvars (gitignored — copy from terraform.tfvars.example)keypair_name = "anvilbase" # an OpenStack keypair you createdadmin_cidr = "YOUR.IP.ADDR/32" # SSH allow-list — lock this downregion = "dc3-a" # Genevaflavor = "a8-ram32-disk20-perf1" # 8 vCPU / 32 GBpg_volume_size = 100 # GB — dedicated perf2 Ceph volumepg_volume_type = "CEPH_1_perf2" # 1000 IOPS / 400 MB/s (request quota first)# App credential via env: export TF_VAR_app_cred_id=… TF_VAR_app_cred_secret=…The module creates: the instance, a dedicated CEPH_1_perf2 block volume for
Postgres, a private network + router, a hardened security group, and a
floating IPv4. It does not create secrets or run an unattended bootstrap
with credentials baked into user-data — you bring .env out of band (below).
perf2(1000 IOPS / 400 MB/s) is enabled by Infomaniak support request — raise the volume-type quota before you provision, or the volume create fails.
2. Storage: Postgres volume + object storage
Postgres data → a dedicated perf2 Cinder volume. The Compose stack keeps
Postgres in the postgres_data Docker named volume, so the clean layout is to
put Docker’s data root (/var/lib/docker) on the perf2 volume — then
postgres_data (plus the small cache/minio_data volumes) all sit on fast,
durable Ceph. The Terraform module’s cloud-init formats and mounts the volume
there before Docker starts. Never leave Docker’s data root on the instance
root disk — VM root disks are rate-limited to ~500 IOPS, and IOPS (not vCPU/RAM)
is the real ceiling for a many-project Postgres host. Ceph is 3-replica, so the
volume itself is durable; live retyping moves you to perf3/perf4 later without
a rebuild.
Object storage → Infomaniak S3 (swap the bundled MinIO out). This removes the
most failure-prone bulk state from the box and from your backup scope. The full
provider config — endpoint, EC2-credential minting, and the required
ANVILBASE_STORAGE_BUCKET_ENCRYPTION=false (Infomaniak’s Swift S3 gateway has no
put_bucket_encryption API) — is in
Object Storage Backend → Infomaniak.
This is the one deliberate crack in
local = cloud: local dev uses bundled MinIO, production uses Infomaniak S3. It’s a supportedMINIO_ENDPOINToverride and is how real deployments run — just know the storage backend differs between dev and prod.
3. Tune Postgres for the box (important)
The default pg-autotune sets shared_buffers to 25% of the Postgres
container memory limit, which defaults to 8g — so out of the box you get
shared_buffers ≈ 2 GB, not 8 GB, even on a 32 GB host. To use the RAM:
# .env — give Postgres real working memory on an a8-ram32ANVILBASE_POSTGRES_MEM_LIMIT=24g # raise the container ceilingANVILBASE_PG_SHARED_BUFFERS=8GB # or pin it explicitly (overrides autotune)Queue/write latency under load is dominated by synchronous_commit. Our
“good-to-go” queue numbers were measured with synchronous_commit=off on a perf1
volume; the safe default (on) trades latency for durability. For queue-heavy or
write-heavy workloads where a few-ms window of committed-but-unflushed
transactions on a hard crash is acceptable (most SaaS — not financial
writes), opt in explicitly:
ANVILBASE_PG_SYNCHRONOUS_COMMIT=off # faster writes; understand the durability tradeLeave it unset (=on) if every commit must survive a crash. perf2 gives more
IOPS headroom but does not, by itself, substitute for this setting on the
fsync-bound queue path.
4. Backups → Infomaniak S3
WAL archiving is on by default; point it (and the scheduled basebackups) at your Infomaniak S3 bucket and enable the scheduler profile:
ANVILBASE_WALG_ENABLED=trueWALG_S3_PREFIX=s3://anvilbase-wal/AWS_ENDPOINT=https://s3.pub1.infomaniak.cloud # dc3-a; s3.pub2 for dc4-aAWS_REGION=us-east-1 # harmless compat valueAWS_ACCESS_KEY_ID=<ec2 access> # the OpenStack EC2 credentialAWS_SECRET_ACCESS_KEY=<ec2 secret>AWS_S3_FORCE_PATH_STYLE=trueANVILBASE_BASEBACKUP_INTERVAL=86400 # daily full basebackupANVILBASE_BASEBACKUP_RETAIN_COUNT=7docker compose --profile wal-g-scheduler up -d wal-g-schedulerThis gives continuous WAL + scheduled basebackups (RPO seconds). Pair it with
periodic Cinder volume snapshots for fast whole-box rollback, and consider an
off-provider copy to Swiss Backup for 3-2-1. Procedures, restore drills, and
the encryption key (WALG_LIBSODIUM_KEY) are in
Backups & Restore and PITR.
5. Network & firewall
The dev docker-compose.yml publishes every internal port on 0.0.0.0 for
convenience. In production, only Traefik is public. Two layers:
-
Don’t publish internal ports. Bind Traefik to the real ports and keep everything else on the private Docker network:
.env TRAEFIK_HTTP_PORT=80TRAEFIK_HTTPS_PORT=443 -
Security group as the backstop — the deny-list that must hold even if a port is accidentally published:
Rule Port(s) Source ALLOW 80, 443 (Traefik) 0.0.0.0/0,::/0ALLOW 22 (SSH) admin_cidronlyDENY (everything else) postgres 39432 · postgres-replica 39433 · minio 39900/39901 · valkey 39637 · control-plane 39001 · auth 39002 · deno 8082 · webhooks 39003 · realtime 39040 · supavisor 39654 · console 39004 · traefik dashboard 39081 · mailpit 1025/8025 · prometheus 39090 · grafana 39091 — Watch
deno:8082— it’s the one binding with no${VAR}override and is published on0.0.0.0. It runs untrusted edge-function code; it must be firewalled.
Use one billed floating IPv4 for public 80/443 (keep IPv4 — not all SDK clients are IPv6-reachable) and attach the free IPv6 too. Internal traffic never leaves the box, so internal mTLS stays off on a single VM — you only need it when you span more than one host (Scaling).
6. TLS at the edge
The shipped docker/traefik/traefik.yml is HTTP-only (dev); production must add an
ACME resolver. Point your domain’s A/AAAA records at the floating IP, then add a
resolver and default it on the HTTPS entrypoint:
# docker/traefik/traefik.yml (production)certificatesResolvers: letsencrypt: acme: email: ops@your-domain storage: /etc/traefik/acme/acme.json httpChallenge: entryPoint: webentryPoints: websecure: address: ":443" http: tls: certResolver: letsencryptRedirect HTTP → HTTPS and disable the Traefik dashboard. See Network Security.
7. Access model — public data plane, private admin
The split that matters:
| Surface | Routes | Exposure |
|---|---|---|
| Data plane (must be public) | /auth, /realtime, /api/v1/... project endpoints, /functions, storage, /health | Public, behind Traefik. AuthZ = per-project API keys + RLS. |
| Admin plane (keep private) | the console (/), the platform/management endpoints, the Traefik dashboard | Overlay / private only |
To take the console off the public IP, add a second Traefik entrypoint bound
to a private/overlay interface and move the console router (and the /v1
management router) to it, leaving the data routers on websecure:
entryPoints: adminsecure: address: "100.x.y.z:443" # your overlay (or private) interface IP# docker/traefik/dynamic/routing.yml — admin routers bind to adminsecure onlyrouters: console: rule: "PathPrefix(`/`)" service: console entryPoints: [adminsecure]One honest nuance:
/api/v1carries both public project endpoints and platform/management endpoints, so you can’t cleanly path-split the whole prefix. The management endpoints are already Bearer-token gated by the control plane, so leaving them on the public entrypoint is safe-by-auth; move them behind the overlay too only if you also want network isolation.
How you reach the console in a browser
The console container is an nginx that serves the SPA and same-origin
reverse-proxies its API calls (/api/auth → auth, /api/* and /v1/* →
control-plane). So “console is private” means that whole origin lives on the
private entrypoint. Reach it two ways:
- Overlay (recommended). Run a mesh VPN and join your laptop:
self-hosted Netbird/Headscale or WireGuard keeps the admin path on Swiss
infrastructure (consistent with choosing Infomaniak); hosted Tailscale is fine
if you accept a US coordinator. Open
https://console.your-overlay-host. Because the console is same-origin, Better Auth cookies and CORS just work. ssh -Lport-forward (break-glass / solo operator). Tunnel to the console host and browse it locally. Zero third-party dependency; clunkier day-to-day.
Rules: never route the public API through Tailscale/an exit node (latency
- coordinator-dependent paths) — it stays on Traefik’s real floating IPv4. Always keep a firewall break-glass (a security-group rule you can toggle to allow SSH from your current IP) so an overlay outage can’t lock you out.
Console status (#69): the console SPA logs in via Better Auth but does not yet exchange that session for the Bearer token the management API requires, so management calls currently 401 — the console is not usable end-to-end yet. The access pattern above is what you’ll use; re-validate it when the console’s cookie→JWT bridge ships.
8. Growth path
Stay on Compose well past the first box. In order, as you actually need it:
- Durability / faster recovery: add one warm streaming standby (read-replica profile) + a drilled manual-promotion runbook. Minutes-RTO, not auto-failover.
- Connection pressure (
429 db_pool_exhausted): enable the Supavisor pooler profile and/or raisemax_connections. - Zero-downtime deploys / blast-radius isolation / write ceiling: split stateful/stateless across a few instances behind Octavia LBaaS, with a standalone shared Valkey and internal mTLS on.
The thresholds, the replicas × active_projects × pool_max < max_connections
math, and the multi-host gotchas are in Scaling.
Validate before you trust it
Before this carries real projects, confirm — don’t assume:
-
terraform applysucceeds and the perf2 volume backs Docker’s data root (df -h /var/lib/dockershows the Cinder volume, not the root disk). - Security group:
80/443reachable, everything else refused from outside (scan from off-box).deno:8082is closed. - TLS valid on your domain; HTTP redirects to HTTPS; dashboard not reachable.
- A throwaway project can create a bucket and upload/download to
Infomaniak S3 (proves the S3 +
BUCKET_ENCRYPTION=offconfig). -
wal-gis shipping WAL and a basebackup to S3, and a restore has been tested on a staging copy (PITR). - You can reach the console over the overlay (and via
ssh -Lbreak-glass). - Walk the full Production Checklist.
Next: Scaling · Production Checklist.