How the current stack is wired: containers, networking, TLS, data flow.
Internet
│ :80 / :443
▼
┌──────────────────────────┐
│ traefik:v3 (v3.7.10) │ reverse proxy + TLS termination
│ dashboard @ traefik.* │ Let's Encrypt (HTTP-01 challenge)
└──────┬─────────┬─────────┘
│ │
Host `crm.*` Host `n8n.*`
│ │
┌──────▼──┐ ┌──▼─────┐
│ espocrm │ │ n8n │
│ :80 │ │ :5678 │
└────┬────┘ └───┬────┘
│ │
┌────▼────┐ ┌────▼─────┐
│ espocrm-│ │ postgres │
│ db │ │ :5432 │
│ mariadb │ └──────────┘
│ :3306 │
└─────────┘
(all on network: supplyshore_web)
Key facts:
80/443 are published.supplyshore_websupplyshore).espocrm-daemon runs scheduled jobs (cron-equivalent) for the CRM.| Service | Image | Container | Exposed port | DB backend | Purpose |
|---|---|---|---|---|---|
traefik |
traefik:v3 |
traefik | 80, 443 (host) | — | Reverse proxy, TLS |
espocrm |
espocrm/espocrm:latest |
espocrm | 80 (internal) | MariaDB | CRM |
espocrm-db |
mariadb:11 |
espocrm-db | 3306 (internal) | — | CRM database |
espocrm-daemon |
espocrm/espocrm:latest |
espocrm-daemon | — | — | Scheduled jobs |
n8n |
n8nio/n8n:latest |
n8n | 5678 (internal) | PostgreSQL | Workflow automation |
wiki |
ghcr.io/requarks/wiki:2 |
wiki | 3000 (internal) | PostgreSQL | Internal knowledge base |
postgres |
postgres:16-alpine |
postgres | 5432 (internal) | — | n8n + wiki database |
Configured entirely via CLI flags in docker-compose.yml:
--api.dashboard=true → enable dashboard web UI
--providers.docker=true → auto-discover containers via labels
--providers.docker.exposedbydefault=false
--entrypoints.web.address=:80
--entrypoints.websecure.address=:443
--certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}
--certificatesresolvers.letsencrypt.acme.storage=/certificates/acme.json
--certificatesresolvers.letsencrypt.acme.httpchallenge=true (HTTP-01 on :80)
Routes are declared with Docker labels on each service. The pattern is:
labels:
- "traefik.enable=true"
- "traefik.http.routers.<name>.rule=Host(`<sub>.${DOMAIN}`)"
- "traefik.http.routers.<name>.entrypoints=websecure"
- "traefik.http.routers.<name>.tls.certresolver=letsencrypt"
- "traefik.http.services.<name>.loadbalancer.server.port=<port>"
The dashboard router uses basic auth middleware:
- "traefik.http.routers.dashboard.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=${DASHBOARD_USER}:$$apr1$$..."
Note the $$ escaping for the $ in the bcrypt/apr1 hash inside Compose.
web (:80).traefik-cert volume at /certificates/acme.json.crm., n8n., traefik. (valid ≈ 90 days).espocrm/espocrm:latest (Apache/PHP), v10.0.3.ESPOCRM_ADMIN_USERNAME,ESPOCRM_ADMIN_PASSWORD, DB credentials).espocrm-data → /var/www/html/dataespocrm-custom → /var/www/html/customespocrm-custom-client → /var/www/html/client/customespocrm-daemon reuses the same image + volumes, runs docker-daemon.shespocrm being healthy.n8nio/n8n:latest (v2.33.5).DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=postgres
DB_POSTGRESDB_DATABASE=n8n
n8n-data volume.N8N_ENCRYPTION_KEY for encrypting credential data.espocrm-db)mariadb:11, volume espocrm-db:/var/lib/mysql.espocrm + user espocrm created at first init.healthcheck.sh --connect --innodb_initialized.postgres)postgres:16-alpine, volume twenty-db:/var/lib/postgresql/data.pg_isready.| Data | Where it lives |
|---|---|
| Traefik certs + ACME state | traefik-cert volume |
| CRM data (records, files) | espocrm-data / espocrm-custom / espocrm-custom-client volumes |
| CRM DB | espocrm-db volume (MariaDB) |
| n8n workflows/credentials | n8n-data volume |
| Wiki.js content (pages, uploads, settings) | PostgreSQL wiki database |
| n8n DB | twenty-db volume (PostgreSQL) |
All volumes are named and survive container recreation. They are not yet
backed up off-server — see troubleshooting.md → Backup plan.
80/443 are exposed to the internet.:ro) — required for label.env (not committed anywhere). .env is readable by root.erp DNS record and all ERPNext containers/volumes/images were fullyhistory.md).