Skip to content

VPS Docker Architecture

A multi-stack Docker architecture describes a setup where several groups of services (stacks) coexist on the same server, each defined by its own docker-compose.yaml file. Each stack is autonomous but can communicate with the others through a shared Docker network.

This infrastructure runs on a Hostinger KVM 4 VPS:

  • 16 GB RAM / 4 vCPU / Debian 13
  • 7 interconnected Docker stacks
  • 1 reverse proxy (Caddy) as the HTTP entry point
  • 1 dedicated SSH tunnel as the non-HTTP entry point (for TimeTrackr)
StackServicesFunctionRAM order of magnitude
security-stackCaddy, CrowdSec, TrivyReverse proxy + WAF + CVE scanner~500 MB
n8n-stackN8N main + 5 workers, PostgreSQL, RedisQueue-mode automation~2 GB
ai-stackQdrant, CLI Ollama, Claude Redis, MCP Gateway, N8N MCPAI, vectors, MCP~4 GB
odoo-stackOdoo 18, PostgreSQLERP (DB guig_db)~1.5 GB
monitoring-stackPrometheus, Grafana, Alertmanager, OTEL Collector, exportersObservability + telemetry~1.5 GB
notify-stackDIUN, ntfyNotifications + update detection~200 MB
timetrackr-stackPostgreSQL 17 (dedicated SSH tunnel)Time-tracking buffer~150 MB

Isolated networks

webproxy network (shared)

Entry points

HTTPS / HTTP3

SSH key + ForceCommand

forward 5433

Internet

TimeTrackr.exe · user workstation

Caddy · :80, :443, :443/udp

sshd · :22 · restricted timetrackr-tunnel

security-stack · CrowdSec, Trivy

n8n-stack · N8N main + 5 workers

ai-stack · CLI Ollama, MCP Gateway

odoo-stack · :8069, DB guig_db

monitoring-stack · Grafana, Prometheus

notify-stack · ntfy

mcp-backend · gateway ↔ n8n-mcp

ai-internal · Qdrant, Claude Redis

timetrackr-internal · PG 17

Three families of Docker networks coexist:

  • webproxy — shared bridge, the routing point for Caddy.
  • Per-stack internal networks (n8n-internal, odoo-internal, monitoring, ai-internal) — intra-stack communication.
  • Isolated networks (mcp-backend, timetrackr-internal, crowdsec-internal) — reduced surfaces for sensitive components (MCP gateway, TimeTrackr database, CrowdSec API).

Why Docker Compose rather than Kubernetes?

Section titled “Why Docker Compose rather than Kubernetes?”
  1. Operational simplicity — Kubernetes introduces etcd, control plane, ingress controllers, all unsuitable for a single server. Docker Compose starts a stack with one command.

  2. Resource constraints — The Kubernetes overhead (2–4 GB RAM for the control plane) would consume 15–25% of the VPS. Docker Compose has no such overhead.

  3. No need for horizontal scaling — No autoscaling, no multi-node distribution. Vertical scaling (more RAM/CPU) is enough.

  4. Learning curve — Compose is mastered, Kubernetes would be a disproportionate investment for this use case.

Problems solved by the multi-stack approach

Section titled “Problems solved by the multi-stack approach”
ProblemSolution
Failure isolationA failing stack does not impact the others
Independent deploymentUpdate a service without restarting the whole system
Diverging versionsEach stack manages its own dependencies (PG 15 for N8N/Odoo, PG 17 for TimeTrackr)
Security through segmentationMinimal surfaces: MCP backend isolated, TimeTrackr database off webproxy
ObservabilityDedicated stack scraping all the others

stacks_vps/
├── security-stack/ # Caddy + CrowdSec + Trivy
├── n8n-stack/ # N8N in queue mode, 5 workers
├── ai-stack/ # Qdrant + CLI Ollama + MCP Gateway + N8N MCP
├── odoo-stack/ # Odoo 18 ERP (DB guig_db)
├── monitoring-stack/ # Prometheus + Grafana + OTEL
├── notify-stack/ # DIUN + ntfy
├── timetrackr-stack/ # PostgreSQL 17 + SSH tunnel (off webproxy)
├── workflows/ # N8N workflow documentation
├── docs-external/ # 📦 Submodule: external CLI docs
├── n8n-exports/ # 📦 Submodule: N8N workflow exports
├── blog/ # 📦 Submodule: this blog (Astro)
└── scripts/ # deploy-all.sh, backup-databases.sh, ...

All HTTP stacks plug into the external webproxy network:

networks:
webproxy:
external: true

This bridge network allows Caddy to reach any container by its name (n8n:5678, odoo:8069, grafana:3000, ntfy:80, …).

Exception: timetrackr-stack does not touch webproxy. Its PostgreSQL database is exposed only on 127.0.0.1:5433, and access goes through a dedicated SSH user (timetrackr-tunnel) with ForceCommand /bin/false and PermitOpen localhost:5433. See TimeTrackr Stack for the details.

Fenêtre de terminal
# Create the shared network (once)
docker network create webproxy
# Start every stack
./scripts/deploy-all.sh start
# Status
./scripts/deploy-all.sh status
# Logs of a stack
./scripts/deploy-all.sh logs n8n-stack
# Restart
./scripts/deploy-all.sh restart odoo-stack
# Pull + restart
./scripts/deploy-all.sh pull
  1. security-stack — The reverse proxy must be ready before the others to terminate TLS handshakes.
  2. monitoring-stack — To capture scrape history from the moment the others boot.
  3. ai-stack, n8n-stack, odoo-stack, notify-stack — In any order.
  4. timetrackr-stack — Independent (does not need webproxy).

The scripts/backup-databases.sh script (daily cron at 03:00) produces:

  • pg_dump of the N8N database (n8n).
  • pg_dump of the Odoo database (guig_db) + filestore archive.
  • pg_dump of the TimeTrackr database (timetrackr_db).
  • Backup of the N8N_ENCRYPTION_KEY (without it, credentials are unrecoverable).

Local rotation (7 days) and GDrive push (30 days) via rclone. Failure → alert to the Notification Hub.


LimitImpactMitigation
Single serverPhysical SPOFDaily GDrive backups + monitoring + alerts
No native HADowntime on pull/restartPlanned maintenance windows, self-restart pattern for n8n-stack
Vertical scaling onlyKVM 4 hardware ceilingMigration to KVM 8 or multi-node possible
AI stack is the most resource-hungry~4 GB out of 16 availableShort-lived CLI subprocesses, no local model

If I have to replicate this setup for another customer:

  • The stacks become templates, packaged as Kubernetes namespaces (or Compose templates).
  • Helm charts for per-customer variations (DB names, secrets, domains).

If AI demands explode:

  • The ai-stack becomes externalisable to a cloud GPU (the Ollama-compatible API interface makes the swap easy).
  • Keep Qdrant local for RAG latency, push CLI Ollama to a dedicated host.

If 16 GB becomes insufficient:

  • KVM 8 upgrade (32 GB) — the simplest solution.
  • Or move monitoring + notify to a second server, the webproxy can be extended as a multi-host overlay.

If SSH sovereignty needs to be reinforced:

  • Migration of the TimeTrackr tunnel to WireGuard (wg.guigpap.com).
  • Allows opening other private services (Postgres admin, internal Redis) without multiplying SSH bouncers.

  • Glossary — Definitions of technical terms