Skip to content

Architecture

Frappe Manager's service architecture: how containers, networks, volumes, and directories work together.

Overview

FM uses a two-tier Docker architecture:

  1. Global services — Shared infrastructure (MariaDB database, nginx reverse proxy)
  2. 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_HOST environment 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 watch hot-reload
    • Prod: Gunicorn multi-worker server (CPU×2+1 workers) + auto-restart
  • All benches share the same MariaDB database (red) but use separate databases within it

Traffic flow:

  1. Internet sends HTTP/HTTPS request to server
  2. global-nginx-proxy receives request, reads Host: header
  3. Routes to bench nginx via VIRTUAL_HOST environment variable match
  4. Bench nginx serves static files, proxies dynamic requests to frappe
  5. Frappe processes request, queries MariaDB, uses Redis, enqueues background jobs
  6. 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:

export FRAPPE_MANAGER_HOME=/srv/frappe

Directory Tree

~/frappe/ — Frappe Manager Root
~/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)
  1. Global configuration — Machine-wide FM settings (ngrok tokens, DNS credentials, logging level)
  2. CLI operation log — All FM command output, auto-rotated at 10MB
  3. Migration backup timestamp — Each migration session gets unique timestamp
  4. Database dump — Gzipped MariaDB SQL dump (restore with bench restore)
  5. Archived benches — Failed bench creation attempts or archived migrations
  6. Global services — Shared MariaDB and nginx-proxy containers
  7. MariaDB data — Linux only (macOS uses Docker volume fm-global-db-data)
  8. acme.sh installation — SSL certificate automation tool
  9. Certificate symlinks — nginx-proxy reads from here (points to real certs in ssl/acmesh/certs/)
  10. HTTPS redirect configs — Per-domain nginx redirects (HTTP → HTTPS)
  11. Standalone nginx blocks — Custom configs for non-FM Docker projects
  12. All benches — Each subdirectory is a bench
  13. Bench configuration — Environment, SSL, upload limits, restart policy
  14. Docker Compose files — Multi-file compose setup (core + workers + admin tools)
  15. Frappe workspace — Standard Frappe bench directory layout
  16. Installed apps — Frappe app source code (frappe, erpnext, custom apps)
  17. Site files — Frappe site configuration and data
  18. Application logs — Frappe/ERPNext runtime logs (split by environment)
  19. 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 variable MYSQL_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>.crt and <domain>.key (symlinks to acme.sh storage)
  • Auto-enables HTTPS when cert files present

Configuration directories:

  • vhostd/ — Per-domain HTTPS redirect configs (created by fm 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 mybenchfm__mybench__frappe, fm__mybench__nginx - Bench mybench.localhostfm__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:

docker compose -f docker-compose.yml -f docker-compose.workers.yml up

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:

fm info <bench>

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:

fm logs <bench> --service <service-name>

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:

supervisord
└── gunicorn -w <N> frappe.app:application (port 80)

Worker count <N> defaults to (CPU count × 2) + 1.

Worker containers

supervisord
└── rq worker <queue-names>

Each worker container runs a single RQ worker process handling its assigned queues.

schedule, socketio containers

supervisord
└── bench schedule (or bench serve-socketio)

Each runs a single dedicated Frappe process.


See also