A Go CLI that wraps Docker Compose to create, manage, and destroy Frappe benches with a single command. Supports both development and production modes. No YAML to write, no Docker flags to memorize.
- Docker with the Compose plugin (
docker compose) - Go 1.21+ (only needed to build from source)
curl -fsSL https://raw.githubusercontent.com/nasroykh/foxmayn_frappe_manager/main/install.sh | shDetects OS and architecture, downloads the latest release binary, verifies the SHA256 checksum, and installs to /usr/local/bin (or ~/.local/bin if the former is not writable).
powershell -ExecutionPolicy Bypass -Command "irm https://raw.githubusercontent.com/nasroykh/foxmayn_frappe_manager/main/install.ps1 | iex"Installs to %LOCALAPPDATA%\Programs\ffm and adds it to your user PATH automatically. No admin rights required.
go install github.com/nasroykh/foxmayn_frappe_manager/cmd/ffm@latestgit clone https://github.com/nasroykh/foxmayn_frappe_manager
cd foxmayn_frappe_manager
make installDevelopment (--mode dev) |
Production (--mode prod) |
|
|---|---|---|
| Purpose | Local dev with Claude Code, ffc, hot-reload | VPS deployment |
| Containers | 4 (frappe + db + redis×2) | 7 (gunicorn + socketio + workers + scheduler + db + redis×2) |
| Image | Full dev tools (zsh, starship, Go, ffc, Claude Code) | Minimal (no dev tools) |
| Database | MariaDB 11.8 or PostgreSQL 18 (experimental) | MariaDB 11.8 or PostgreSQL 18 (experimental) |
| Site name | <name>.localhost |
Your public domain |
| SSL | Via shared Traefik proxy | Let's Encrypt (or --no-ssl for external Caddy/Nginx) |
| Dev server | bench start (honcho) |
Services run via compose command: |
# Create a new bench (interactive form: mode, version + apps)
ffm create mybench
# Open the site
open http://localhost:8000 # or whatever port was allocated
# Login: administrator / admin
# Enable domain routing (mybench.localhost)
ffm proxy start
# Shell into the bench container (zsh inside frappe-bench/)
ffm shell
# Stop / start / restart
ffm stop
ffm start
ffm restart
# Tear it all down
ffm deleteMost commands accept an optional bench name. If omitted, ffm resolves it automatically: if your working directory is inside ~/frappe/<name>/, that bench is selected silently. Otherwise an interactive picker appears.
# Requires: public DNS A record for your domain pointing to this server
# Requires: ports 80 and 443 open on the firewall
ffm create mysite \
--mode prod \
--domain erp.example.com \
--admin-password StrongPassword \
--acme-email admin@example.com
# Access https://erp.example.com — Let's Encrypt cert is provisioned automaticallyWith existing Caddy/Nginx on 80/443:
# --no-ssl: skip Traefik on 443, expose ports directly for Caddy to proxy
ffm create mysite --mode prod --domain erp.example.com --no-ssl --admin-password StrongPassword
# Configure Frappe to know the browser connects via Caddy's HTTPS
ffm set-proxy mysite --host erp.example.com # sets socketio_port 443, use_ssl 1, host_name
ffm restart mysite # apply to all services
# Get a ready-to-paste Caddy snippet (uses actual allocated ports)
ffm set-proxy mysite --host erp.example.com --print-caddyOptional browser UI for managing benches (same operations as the CLI), modeled after the kbls admin console.
# Foreground (blocks; Ctrl+C to stop)
ffm dashboard start --admin-password 'choose-a-strong-password'
# Background daemon
ffm dashboard start -d --admin-password 'choose-a-strong-password'
# Management
ffm dashboard status
ffm dashboard stop
ffm dashboard logs -f- Default URL: http://127.0.0.1:8787/admin (HTTP Basic auth)
- Bind on LAN:
ffm dashboard start --listen 0.0.0.0:8787 --admin-password '…'— use TLS via a reverse proxy in production - Long operations (
create,recreate) run as background jobs with live log streaming - Interactive shell: use
ffm shell <name>in the terminal (dashboard supports one-shotexeconly)
Settings are stored in ~/.config/ffm/dashboard.json (mode 0600 when a password is set).
Creates and starts a new Frappe bench end-to-end. When run interactively (no flags), a form asks for mode first, then the relevant options.
Steps performed:
- Allocates a free host port pair (web: 8000+, socketio: 9000+)
- Writes
docker-compose.ymlandDockerfileto~/frappe/<name>/ - Builds the Docker image — installs zsh, zinit, starship, Go, ffc, pnpm, and Claude Code; pre-fetches 60 Frappe Claude skills to
/opt/. Cached after first build. - Runs
bench init— clones Frappe, installs Python/Node deps, copies skills intofrappe-bench/.agents/skills/and.claude/skills/ - Starts 4 containers with
workspace/bind-mounted at/workspace - Configures
common_site_config.json, creates site, enables developer mode - Installs any
--apps - Starts the dev server (
nohup bench start) - Generates Frappe API keys and writes
~/.config/ffc/config.yamlfor ffc
Steps performed:
1–5. Same as dev (minimal image, no dev tools, no devcontainer written)
6. Configures common_site_config.json, creates site (skip developer mode)
7. Installs any --apps
8. Builds production assets (bench build)
9. Sets host_name to https://<domain> (or http:// with --no-ssl)
On failure, all steps auto-rollback (containers torn down, directory removed).
Flags:
--mode string Bench mode: dev or prod (default "dev")
--domain string Public domain for production (required with --mode prod)
--no-ssl Skip Let's Encrypt — use when Caddy/Nginx already handles TLS
--acme-email string Email for Let's Encrypt (required on first prod+SSL bench;
saved to ~/.config/ffm/.acme_email for subsequent benches)
--frappe-branch string Frappe branch to initialise (default "version-15")
--frappe-repo string Custom Frappe repo URL with optional @branch suffix
(e.g. https://github.com/your-org/frappe.git@main).
Defaults to the official frappe/frappe repo.
--apps stringArray Apps to install (see formats below)
--admin-password string Frappe site admin password (default "admin"; required for prod)
--db-type string Database engine: mariadb or postgres (default "mariadb")
--db-password string Database root password (default "ffm123456")
--github-token string GitHub PAT for private HTTPS repos
--proxy-port int Dev reverse proxy: set socketio_port (e.g. 443 or 80)
--proxy-host string Dev reverse proxy: set per-site host_name
--verbose Stream full Docker and bench init output
ffm create mybench --apps erpnext --apps hrms
ffm create mybench --apps erpnext@version-16
ffm create mybench --apps git@github.com:myorg/myapp.git
ffm create mybench --apps "git@github.com:myorg/myapp.git@main"
ffm create mybench --apps https://github.com/myorg/myapp
ffm create mybench --apps "https://github.com/myorg/myapp@develop" --github-token ghp_xxxWhen SSH_AUTH_SOCK is set, the SSH agent is automatically forwarded into the container so SSH-URL private repos work without a token.
By default bench init clones the official frappe/frappe. Use --frappe-repo to point at a fork or mirror, with an optional @branch suffix that overrides --frappe-branch for the Frappe checkout (apps still use --frappe-branch). For a private repo, pass --github-token — the credentials are injected into the bench init container.
# Public fork on a custom branch
ffm create mybench --frappe-repo https://github.com/your-org/frappe.git@main
# Private fork (HTTPS) with a token
ffm create mybench \
--frappe-repo https://github.com/your-org/frappe.git@main \
--github-token ghp_xxx
# Private fork over SSH (uses forwarded SSH agent, no token needed)
ffm create mybench --frappe-repo "git@github.com:your-org/frappe.git@main"Both --frappe-repo and --github-token are also available in the interactive ffm create form (the token field is masked).
Lists all managed benches with their live status, mode (dev/prod), DB engine (maria/pg), port, domain URL, and Frappe branch.
Shows per-container status, credentials, ports, and URLs. Prod benches show the domain URL instead of localhost.
Starts a stopped bench. Dev: also reinstalls skills if missing and relaunches bench start. Prod: docker compose up -d only (services run via compose command:).
Stops all containers. Data is preserved.
Stops then starts a bench in one step.
Flags:
--rebuild Rebuild the Docker image before starting
--rebuild rewrites the Dockerfile from the current template (mode-aware) and runs docker compose build. Useful after an ffm upgrade adds new tools or template changes.
Opens an interactive shell inside the frappe container:
- Dev:
zshwith zinit, autosuggestions, syntax-highlighting, starship - Prod:
bash
Use --exec to run a single command non-interactively:
ffm shell mybench --exec "bench list-apps"
ffm shell mybench --exec "bench --site mybench.localhost migrate"
ffm shell myprod --exec "bench build"
ffm shell myprod --exec "bench --site erp.example.com migrate"Flags:
--service string Container to exec into (default "frappe")
--exec string Run a command non-interactively
Every dev bench includes .devcontainer/devcontainer.json.
# Option A: native host editing
code ~/frappe/mybench/workspace
# Option B: open inside the container (integrated terminal)
code ~/frappe/mybench
# → VS Code prompts "Reopen in Container"Both options work simultaneously — same bind-mounted files.
Streams container logs. Omit [service] to tail all containers.
Flags:
-f, --follow Follow log output (default true)
Manages the shared Traefik container.
ffm proxy start # start Traefik
ffm proxy stop # stop Traefik
ffm proxy status # show status + dashboard URL- Dev benches: routes
<name>.localhoston port 80 - Prod benches with SSL: also routes on port 443 with Let's Encrypt (added on first prod bench creation)
WSL2 note: Add .localhost entries to C:\Windows\System32\drivers\etc\hosts:
127.0.0.1 mybench.localhost
Configures a bench (dev or prod) for an external reverse proxy (Caddy, Nginx, etc.). Sets socketio_port, use_ssl, host_name, and socketio_frappe_url inside the Frappe container.
- Dev: dev server restarts automatically
- Prod: prints a reminder to run
ffm restart <name>to apply changes to all services
# HTTPS proxy on port 443 (default)
ffm set-proxy mybench --host frappe.example.com
ffm set-proxy myprod --host erp.example.com
# HTTP proxy on port 80
ffm set-proxy mybench --port 80 --host frappe.example.com
# Reset to direct-access defaults
# dev → socketio_port <allocated>, use_ssl 0, host_name http://<name>.localhost, socketio_frappe_url http://127.0.0.1:8000
# prod → socketio_port 443, use_ssl 1, host_name https://<domain>, socketio_frappe_url http://frappe:8000
ffm set-proxy mybench --reset
ffm set-proxy myprod --reset
# Print a ready-to-paste config snippet
ffm set-proxy mybench --host frappe.example.com --print-caddy
ffm set-proxy myprod --host erp.example.com --print-nginxGenerates Frappe API keys and writes ~/.config/ffc/config.yaml inside the bench container. Dev benches only. Run if ffc setup failed during ffm create or to regenerate keys.
Writes the bench's database, file attachments and configuration into one portable archive.
ffm backup # the bench in the current directory
ffm backup mybench
ffm backup mybench --out ~/archives
ffm backup mybench --no-files --label "before the v16 upgrade"Flags: --out <path> Directory to write into, or an explicit path ending in .tar
--label <text> Short note recorded in the archive
--no-files Database only, no attachments
--skip-space-check Do not check free disk space first
The archive holds Frappe's own database dump, the site's public and private files, the site and bench configuration, and each installed app's git commit. It does not hold the app source, the Python virtualenv or the built assets — those are rebuilt at restore time, which is what keeps a full ERPNext bench's backup under a megabyte instead of ~2 GB, and what lets it restore onto a different machine, architecture or host user.
A stopped bench is started for the backup and stopped again afterwards.
The archive contains the database root password, the Administrator password and the site's encryption key in plain text. It is written
0600inside a0700directory. ffm warns when the filesystem cannot enforce that — notably a Windows drive mounted into WSL2.
Rebuilds a bench from an archive. Always creates a new bench; it never writes into an existing one, so a failed restore leaves the machine as it found it.
ffm restore ~/frappe/_backups/mybench/mybench_20260826T090000Z.ffm.tar
ffm restore mybench_20260826T090000Z.ffm.tar staging # under a different name
ffm restore mybench_20260826T090000Z.ffm.tar --dry-run # validate onlyFlags: --dry-run Validate the archive and print the plan
--no-files Restore the database only
--pin-apps Check apps out at the archived commits, not branch head
--allow-missing-encryption-key Restore without the site key (Password fields will not decrypt)
--encryption-key <key> Key for a GPG-encrypted dump
--domain <domain> Override the production domain
--no-ssl / --acme-email <addr> TLS handling for a production restore
--reallocate-ports Always take a fresh port pair
--web-port / --socketio-port Explicit host ports
--admin-password <pw> Set a new Administrator password
--github-token <token> Token for private app clones
--skip-migrate Skip `bench migrate` after restoring
--keep-on-failure Leave a failed restore in place for diagnosis
--skip-space-check Do not check free disk space first
The bench is provisioned by the same pipeline as ffm create — same image, apps, mode, ports
and proxy wiring — and the archived data is restored into it. Ports are reused when they are
free, so URLs stay stable on a fresh machine.
What a restore cannot bring back, and says so when it happens: uncommitted changes in an app's
working tree, the VPS tunnel (its token lives in this host's tunnel.json, not the archive),
the ffc API secret (Frappe mints a new one on every request), and absolute URLs stored inside
the database when the site is renamed.
Stops and removes all containers, volumes, and the bench directory.
Aliases: rm, remove
Flags: --force Skip confirmation prompt
Checks GitHub for the latest release and replaces the running binary in place.
ffm update # check and update
ffm update --check # only check
ffm update --yes # skip confirmationUpdate availability is checked silently in the background on every command (24 h cache).
Prints the build version, commit hash, and build date.
~/frappe/
<bench-name>/
docker-compose.yml # generated per bench (dev: 4 services, prod: 7 services)
Dockerfile # dev: full tools image; prod: minimal image
workspace/ # bind-mounted at /workspace in container
frappe-bench/
.agents/skills/ # dev only: 60 Frappe Claude skills + ffc skill
.claude/skills/ # dev only: same skills for Claude Code
.devcontainer/ # dev only
devcontainer.json # VS Code dev container config
_backups/
<bench-name>/
<bench>_<UTC timestamp>.ffm.tar # `ffm backup` archives (0600, in a 0700 directory)
~/.config/ffm/
benches.json # state file tracking all managed benches
.update_check.json # cached latest release tag (refreshed every 24 h)
.acme_email # saved Let's Encrypt email (auto-used on subsequent prod benches)
Dev (4 containers):
| Service | Image | Purpose |
|---|---|---|
frappe |
Built locally (dev image) | App server + bench start (honcho) + all dev tools |
mariadb or postgres |
mariadb:11.8 / postgres:18 |
Database (selected via --db-type) |
redis-cache |
redis:alpine |
Cache |
redis-queue |
redis:alpine |
Background job queue |
Prod (7 containers):
| Service | Image | Purpose |
|---|---|---|
frappe |
Built locally (minimal image) | Gunicorn (bench serve --port 8000) |
socketio |
same | Node SocketIO server |
worker-long |
same | Long background jobs |
worker-short |
same | Short background jobs |
scheduler |
same | Scheduled tasks (bench schedule) |
mariadb or postgres |
mariadb:11.8 / postgres:18 |
Database with healthcheck (selected via --db-type) |
redis-cache |
redis:alpine |
Cache |
redis-queue |
redis:alpine |
Job queue |
A single Traefik container (ffm-proxy) is shared across all benches:
| Container | Image | Ports |
|---|---|---|
ffm-proxy |
traefik:3 |
0.0.0.0:80 (HTTP), 0.0.0.0:443 (HTTPS, when a prod bench uses SSL), 127.0.0.1:8080 (dashboard) |
Configured entirely via CLI flags — no config file on disk. Uses --restart=unless-stopped.
| Variable | Default | Description |
|---|---|---|
FFM_BENCHES_DIR |
~/frappe |
Where bench directories are stored |
FFM_CONFIG_DIR |
~/.config/ffm |
Where the state file is stored |
FFM_BACKUPS_DIR |
~/frappe/_backups |
Where ffm backup archives are written |
make # tidy + build + install (default)
make ship # same as above explicitly
make build # → ./bin/ffm
make install # → $GOPATH/bin/ffm
make vet # go vet
make fmt # gofmt
make tidy # go mod tidy
make clean # remove binary