Skip to content

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). The deploy/infomaniak/ Terraform module derives from our working load-test rig (so the OpenStack plumbing is proven), but it has not been terraform 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 created
admin_cidr = "YOUR.IP.ADDR/32" # SSH allow-list — lock this down
region = "dc3-a" # Geneva
flavor = "a8-ram32-disk20-perf1" # 8 vCPU / 32 GB
pg_volume_size = 100 # GB — dedicated perf2 Ceph volume
pg_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 supported MINIO_ENDPOINT override 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:

Terminal window
# .env — give Postgres real working memory on an a8-ram32
ANVILBASE_POSTGRES_MEM_LIMIT=24g # raise the container ceiling
ANVILBASE_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:

Terminal window
ANVILBASE_PG_SYNCHRONOUS_COMMIT=off # faster writes; understand the durability trade

Leave 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:

.env
ANVILBASE_WALG_ENABLED=true
WALG_S3_PREFIX=s3://anvilbase-wal/
AWS_ENDPOINT=https://s3.pub1.infomaniak.cloud # dc3-a; s3.pub2 for dc4-a
AWS_REGION=us-east-1 # harmless compat value
AWS_ACCESS_KEY_ID=<ec2 access> # the OpenStack EC2 credential
AWS_SECRET_ACCESS_KEY=<ec2 secret>
AWS_S3_FORCE_PATH_STYLE=true
ANVILBASE_BASEBACKUP_INTERVAL=86400 # daily full basebackup
ANVILBASE_BASEBACKUP_RETAIN_COUNT=7
Terminal window
docker compose --profile wal-g-scheduler up -d wal-g-scheduler

This 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:

  1. Don’t publish internal ports. Bind Traefik to the real ports and keep everything else on the private Docker network:

    .env
    TRAEFIK_HTTP_PORT=80
    TRAEFIK_HTTPS_PORT=443
  2. Security group as the backstop — the deny-list that must hold even if a port is accidentally published:

    RulePort(s)Source
    ALLOW80, 443 (Traefik)0.0.0.0/0, ::/0
    ALLOW22 (SSH)admin_cidr only
    DENY (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 on 0.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: web
entryPoints:
websecure:
address: ":443"
http:
tls:
certResolver: letsencrypt

Redirect HTTP → HTTPS and disable the Traefik dashboard. See Network Security.

7. Access model — public data plane, private admin

The split that matters:

SurfaceRoutesExposure
Data plane (must be public)/auth, /realtime, /api/v1/... project endpoints, /functions, storage, /healthPublic, behind Traefik. AuthZ = per-project API keys + RLS.
Admin plane (keep private)the console (/), the platform/management endpoints, the Traefik dashboardOverlay / 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:

docker/traefik/traefik.yml
entryPoints:
adminsecure:
address: "100.x.y.z:443" # your overlay (or private) interface IP
# docker/traefik/dynamic/routing.yml — admin routers bind to adminsecure only
routers:
console:
rule: "PathPrefix(`/`)"
service: console
entryPoints: [adminsecure]

One honest nuance: /api/v1 carries 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:

  1. 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.
  2. ssh -L port-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 raise max_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 apply succeeds and the perf2 volume backs Docker’s data root (df -h /var/lib/docker shows the Cinder volume, not the root disk).
  • Security group: 80/443 reachable, everything else refused from outside (scan from off-box). deno:8082 is 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=off config).
  • wal-g is 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 -L break-glass).
  • Walk the full Production Checklist.

Next: Scaling · Production Checklist.