Architecture¶
Frappe Manager's service architecture: how containers, networks, volumes, and directories work together.
Overview¶
FM uses a two-tier Docker architecture:
- Global services — Shared infrastructure (MariaDB database, nginx reverse proxy)
- Per-bench services — Isolated environments (Frappe app, workers, Redis, nginx)
This design allows multiple benches to coexist on one machine while sharing database and proxy resources.
Quick navigation
Jump to: Global Services · Per-Bench Services · Networks · Volumes · File Layout
Service Architecture Diagram¶
%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#fff3e0','primaryTextColor':'#333','primaryBorderColor':'#f57c00','lineColor':'#666','secondaryColor':'#e3f2fd','tertiaryColor':'#ffebee','fontSize':'16px'}}}%%
flowchart TB
Internet([🌍 Internet])
Internet -->|Port 80/443| Proxy
subgraph global[" 🌐 GLOBAL SERVICES "]
Proxy[nginx-proxy<br/>Ports 80, 443<br/>Routes by VIRTUAL_HOST] -..-o DB[(MariaDB<br/>Port 3306<br/>Shared database)]
end
Proxy -->|Domain routing| BN
subgraph bench[" 📦 BENCH SERVICES (per bench) "]
BN[nginx<br/>Static files + proxy]
BN -->|HTTP| BF
BF[frappe<br/>━━━━━━━━━━━━━━━━<br/>Dev: Werkzeug single-thread<br/>Prod: Gunicorn multi-worker]
BF -->|WebSocket| BS[socketio<br/>Real-time events]
BF -->|Cache| RC[redis-cache<br/>Sessions + data]
BF -->|Enqueue jobs| RQ[redis-queue<br/>Job queues]
RQ -->|Pull jobs| W1[short-worker<br/>Quick tasks]
RQ -->|Pull jobs| W2[long-worker<br/>Slow tasks]
RQ -->|Scheduled| W3[schedule<br/>Cron jobs]
end
BF -.->|SQL queries| DB
classDef globalStyle fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#000
classDef benchStyle fill:#fff3e0,stroke:#f57c00,stroke-width:2px,color:#000
classDef dbStyle fill:#ffebee,stroke:#c62828,stroke-width:2px,color:#000
class global globalStyle
class bench benchStyle
class DB dbStyle
Key concepts:
- Global services (blue) are shared across all benches — one MariaDB instance, one nginx-proxy
- nginx-proxy routes requests by domain using
VIRTUAL_HOSTenvironment variable - Bench services (orange) are created per bench — each bench has its own isolated set of containers
- Dev vs Prod difference: Only the frappe container differs:
- Dev: Werkzeug single-threaded server +
bench watchhot-reload - Prod: Gunicorn multi-worker server (CPU×2+1 workers) + auto-restart
- Dev: Werkzeug single-threaded server +
- All benches share the same MariaDB database (red) but use separate databases within it
Traffic flow:
- Internet sends HTTP/HTTPS request to server
- global-nginx-proxy receives request, reads
Host:header - Routes to bench nginx via
VIRTUAL_HOSTenvironment variable match - Bench nginx serves static files, proxies dynamic requests to frappe
- Frappe processes request, queries MariaDB, uses Redis, enqueues background jobs
- Workers pull jobs from Redis queues and execute them
Workspace Layout¶
FM stores all data under a single root directory (default: ~/frappe/).
Relocate with environment variable
Set FRAPPE_MANAGER_HOME before any FM command to use a custom location:
Directory Tree¶
~/frappe/
│
├── 📄 fm_config.toml # (1)
│
├── 📁 logs/
│ └── fm.log # (2)
│
├── 📁 backups/
│ └── migrations/
│ └── 12-Apr-26--14-30-45/ # (3)
│ ├── mybench/
│ │ ├── bench_config.toml
│ │ ├── docker-compose.yml
│ │ └── database.sql.gz # (4)
│ └── fm_config.toml
│
├── 📁 archived-sites/ # (5)
│
├── 📁 services/ # (6)
│ ├── docker-compose.yml
│ ├── global-db/
│ │ └── data/ # (7)
│ └── nginx-proxy/
│ ├── ssl/
│ │ └── acmesh/
│ │ ├── .acme.sh/ # (8)
│ │ └── certs/
│ │ └── example.com/
│ │ ├── fullchain.pem
│ │ └── example.com.key
│ ├── certs/ # (9)
│ │ ├── example.com.crt → ../ssl/acmesh/certs/example.com/fullchain.pem
│ │ └── example.com.key → ../ssl/acmesh/certs/example.com/example.com.key
│ ├── vhostd/ # (10)
│ │ └── example.com
│ └── conf.d/ # (11)
│ └── standalone-project
│
└── 📁 sites/ # (12)
├── mybench.localhost/
│ ├── 📄 bench_config.toml # (13)
│ ├── docker-compose.yml # (14)
│ ├── docker-compose.workers.yml
│ ├── docker-compose.admin-tools.yml
│ ├── logs/
│ └── workspace/
│ └── frappe-bench/ # (15)
│ ├── apps/ # (16)
│ │ ├── frappe/
│ │ ├── erpnext/
│ │ └── custom_app/
│ ├── sites/ # (17)
│ │ ├── apps.txt
│ │ ├── common_site_config.json
│ │ ├── currentsite.txt
│ │ └── mybench.localhost/
│ │ └── site_config.json
│ ├── logs/ # (18)
│ │ ├── web.log
│ │ ├── web.error.log
│ │ ├── web.dev.log
│ │ ├── worker.log
│ │ └── schedule.log
│ ├── config/
│ │ └── supervisor.conf
│ └── env/ # (19)
│
└── prod.example.com/
└── ... (same structure)
- Global configuration — Machine-wide FM settings (ngrok tokens, DNS credentials, logging level)
- CLI operation log — All FM command output, auto-rotated at 10MB
- Migration backup timestamp — Each migration session gets unique timestamp
- Database dump — Gzipped MariaDB SQL dump (restore with
bench restore) - Archived benches — Failed bench creation attempts or archived migrations
- Global services — Shared MariaDB and nginx-proxy containers
- MariaDB data — Linux only (macOS uses Docker volume
fm-global-db-data) - acme.sh installation — SSL certificate automation tool
- Certificate symlinks — nginx-proxy reads from here (points to real certs in
ssl/acmesh/certs/) - HTTPS redirect configs — Per-domain nginx redirects (HTTP → HTTPS)
- Standalone nginx blocks — Custom configs for non-FM Docker projects
- All benches — Each subdirectory is a bench
- Bench configuration — Environment, SSL, upload limits, restart policy
- Docker Compose files — Multi-file compose setup (core + workers + admin tools)
- Frappe workspace — Standard Frappe bench directory layout
- Installed apps — Frappe app source code (frappe, erpnext, custom apps)
- Site files — Frappe site configuration and data
- Application logs — Frappe/ERPNext runtime logs (split by environment)
- Python virtualenv — Isolated Python packages for this bench
Do not directly edit workspace files
Files under workspace/frappe-bench/ are managed by Frappe/ERPNext. Use bench commands inside the container instead of editing directly.
Global Services¶
Shared infrastructure started once by FM, remain running across all benches.
global-db¶
Image: mariadb:10.6
Ports: 3306 (Docker networks only, not exposed to host)
Purpose: Shared MariaDB database server for all benches
Each bench gets its own database in this shared instance. Database name format: fm_<benchname>_<random>.
Platform-specific storage
Linux: Bind-mounted from ~/frappe/services/global-db/data/ (direct filesystem access)
macOS: Named Docker volume fm-global-db-data (avoids macOS bind-mount slowness)
Credentials:
- Root password: Auto-generated on first start
- Stored in:
services/docker-compose.yml(environment variableMYSQL_ROOT_PASSWORD) - Per-bench users: Auto-created with database-specific privileges
Management:
# View database list
fm services db-console
> SHOW DATABASES;
# Access specific bench database
fm shell mybench -c "bench mariadb"
global-nginx-proxy¶
Image: jwilder/nginx-proxy:1.6
Ports: 80 (HTTP), 443 (HTTPS) — Exposed to host
Purpose: Reverse proxy routing traffic to benches based on hostname
Routes incoming HTTP/HTTPS requests to bench containers using virtual host headers (VIRTUAL_HOST env var).
How routing works
Request arrives at mybench.localhost → nginx-proxy reads Host: header → Routes to container with VIRTUAL_HOST=mybench.localhost
SSL Certificate handling:
- Reads certs from
~/frappe/services/nginx-proxy/certs/ - Expects:
<domain>.crtand<domain>.key(symlinks to acme.sh storage) - Auto-enables HTTPS when cert files present
Configuration directories:
vhostd/— Per-domain HTTPS redirect configs (created byfm ssl add)conf.d/— Custom nginx server blocks (standalone mode)
See also: SSL guide, fm ssl commands
Per-Bench Services¶
Each bench runs its own isolated service stack. Container names prefixed with fm-<benchname>-.
Service lifecycle
All bench services start/stop together with fm start/fm stop. Restart policy controls auto-recovery (see restart_policy config).
Purpose: Reverse proxy that routes traffic to benches based on Host header
Automatically detects running benches via Docker labels (VIRTUAL_HOST, VIRTUAL_PORT) and generates nginx configuration dynamically.
SSL certificates are stored under services/nginx-proxy/ssl/ and automatically picked up via symlinks in certs/.
Per-bench services¶
Each bench is a separate Docker Compose project. Services are isolated per bench.
Core services (always present)¶
| Service | Image | Description | Ports |
|---|---|---|---|
frappe |
ghcr.io/rtcamp/frappe-manager-frappe:<tag> |
Frappe application (Gunicorn or dev server) | 80 (internal) |
nginx |
ghcr.io/rtcamp/frappe-manager-nginx:<tag> |
Per-bench nginx (static files, proxy to frappe) | 80 (internal) |
socketio |
ghcr.io/rtcamp/frappe-manager-frappe:<tag> |
Socket.IO server for real-time features | 9000 (internal) |
schedule |
ghcr.io/rtcamp/frappe-manager-frappe:<tag> |
Frappe scheduler (cron-like background tasks) | — |
redis-cache |
redis:8-alpine |
Redis for caching | 6379 (internal) |
redis-queue |
redis:8-alpine |
Redis for RQ job queue | 6379 (internal) |
Worker services (separate compose file)¶
| Service | Image | Description | Replicas |
|---|---|---|---|
short-worker |
ghcr.io/rtcamp/frappe-manager-frappe:<tag> |
Handles short and default queues |
Configurable (default: 1) |
long-worker |
ghcr.io/rtcamp/frappe-manager-frappe:<tag> |
Handles long, default, short queues (fallback) |
Configurable (default: 1) |
| Custom workers | ghcr.io/rtcamp/frappe-manager-frappe:<tag> |
App-defined queues from hooks.py |
Per app config |
See Workers & Background Jobs for details.
Admin tools (optional, separate compose file)¶
| Service | Image | Description | Access URL |
|---|---|---|---|
mailpit |
axllent/mailpit:v1.22 |
Email testing (catches all outgoing mail) | http://<bench>.localhost/mailpit |
adminer |
adminer:4 |
Database web UI | http://<bench>.localhost/adminer |
Enabled by default in dev environment. Toggle with fm update <bench> --admin-tools enable.
Container naming¶
Pattern: fm__<bench-name>__<service>
Dots in bench names are replaced with double underscores.
Examples:
- Bench mybench → fm__mybench__frappe, fm__mybench__nginx
- Bench mybench.localhost → fm__mybench__localhost__frappe
Docker networks¶
| Network | Scope | Purpose |
|---|---|---|
fm-global-frontend-network |
Global (external) | Connects per-bench nginx to global-nginx-proxy |
fm-global-backend-network |
Global (external) | Connects per-bench services to global-db |
fm__<bench>__site-network |
Per-bench (internal) | Connects all services within a single bench |
Network isolation: Benches cannot communicate with each other directly. They only share access to global-db and global-nginx-proxy.
Docker volumes¶
Global volumes¶
| Volume | Purpose |
|---|---|
fm-global-db-data |
MariaDB data (macOS only; Linux uses bind-mount) |
fm-sockets |
Unix sockets for supervisorctl communication via fmx |
Per-bench volumes¶
| Volume | Purpose |
|---|---|
fm__<bench>__mailpit-data |
Mailpit email database |
| Workspace bind-mount | ~/frappe/sites/<bench>/workspace/ mounted into containers at /workspace |
The workspace directory is shared across all bench containers (frappe, workers, schedule, socketio). Changes to app code are visible immediately without restart (in dev environment with hot-reload).
Compose file structure¶
Each bench uses multiple compose files layered together:
| File | Services | When loaded |
|---|---|---|
docker-compose.yml |
frappe, nginx, socketio, schedule, redis-cache, redis-queue | Always |
docker-compose.workers.yml |
short-worker, long-worker, custom workers | Always |
docker-compose.admin-tools.yml |
mailpit, adminer | Only when admin_tools = true |
FM uses Docker Compose's --file flag to layer these files when starting the bench:
When admin tools are enabled:
docker compose -f docker-compose.yml -f docker-compose.workers.yml -f docker-compose.admin-tools.yml up
Image tags¶
Frappe Manager images use version-based tags:
Pattern: ghcr.io/rtcamp/frappe-manager-<service>:<frappe-version>-<fm-version>
Examples:
- ghcr.io/rtcamp/frappe-manager-frappe:v15-0.19.0 — Frappe v15, FM 0.19.0
- ghcr.io/rtcamp/frappe-manager-nginx:v15-0.19.0
Check current images:
Logging architecture¶
| Log type | Location | Rotation |
|---|---|---|
| FM CLI logs | ~/frappe/logs/fm.log |
Automatic (10 files, 10MB each) |
| Frappe app logs | ~/frappe/sites/<bench>/workspace/frappe-bench/logs/ |
Manual (via Frappe) |
| Container stdout/stderr | Docker daemon | Via Docker log driver |
Access container logs with:
See Logs & Debugging for details.
Port allocation¶
Global services:
- 80 (HTTP) — global-nginx-proxy
- 443 (HTTPS) — global-nginx-proxy
- 3306 (MariaDB) — global-db (not exposed to host)
Per-bench services:
- 80 (internal) — bench nginx (routed via global-nginx-proxy)
- 8025 (internal) — Mailpit web UI (routed via bench nginx)
- 8080 (internal) — Adminer web UI (routed via bench nginx)
All per-bench services are exposed only to Docker networks, not to the host. Traffic reaches benches via global-nginx-proxy on ports 80/443.
Process architecture inside containers¶
frappe container¶
Development mode:
supervisord
├── bench serve (Werkzeug dev server, port 80)
└── bench watch (asset hot-reload watcher)
Production mode:
Worker count <N> defaults to (CPU count × 2) + 1.
Worker containers¶
Each worker container runs a single RQ worker process handling its assigned queues.
schedule, socketio containers¶
Each runs a single dedicated Frappe process.
See also¶
- Configuration Files —
fm_config.tomlandbench_config.tomlreference - Workers & Background Jobs — worker queue configuration
- Environments — dev vs prod architecture differences