Zero-downtime Frappe deployments with atomic releases and rollback capability.
- Atomic Releases: Timestamped releases with instant symlink-based switching
- Zero Downtime: Workers drain gracefully, maintenance mode during migrations only
- Rollback: Keep N previous releases, instant rollback on failure
- 2 Deploy Modes: pull (on-server), ship (remote) — dev to production
- Frappe Cloud Sync: Import apps, deps, and DB backups from FC
- Monorepo Support: Symlink subdirectory apps for efficient workspace management
pip install frappe-deployer # When available on PyPI
# Or from source
git clone <repo-url>
cd fmd
pip install -e .Requirements: Python 3.10+, Docker + Frappe Manager
# 1. Configure workspace (one-time setup)
fmd release configure site.localhost
# 2. Deploy Frappe + ERPNext
fmd deploy pull site.localhost \
--app frappe/frappe:version-15 \
--app frappe/erpnext:version-15 \
--maintenance-mode --backups
# 3. Verify deployment
fmd release list site.localhost
fmd info site.localhost# Deploy: Full automated deployment (configure → create → switch)
fmd deploy pull <site> # Minimal, uses config file
fmd deploy pull <site> --app frappe/frappe:version-15 --maintenance-mode
# Release: Manual control for CI/CD
fmd release configure <site> # One-time workspace setup
fmd release create <site> # Build new release (safe, no live changes)
fmd release switch <site> <rel> # Atomically activate release
fmd release list <site> # Show all releases
# Maintenance
fmd cleanup <site> -r 3 -b 5 -y # Keep 3 releases, 5 backups
fmd search-replace <site> "old.com" "new.com" --dry-run
# Remote deploy (build locally, deploy to remote server)
fmd deploy ship --config site.tomlApp format: org/repo:ref or org/repo:ref:subdir/path (for monorepos)
Create site.toml:
site_name = "site.localhost"
bench_name = "site" # Optional, defaults to site_name
github_token = "ghp_xxx" # For private repos
[[apps]]
repo = "frappe/frappe"
ref = "version-15"
[[apps]]
repo = "frappe/erpnext"
ref = "version-15"
[release]
releases_retain_limit = 7
symlink_subdir_apps = false # Auto-symlink monorepo apps
python_version = "3.11" # Pin Python version
use_fc_apps = false # Import app list from Frappe Cloud
use_fc_deps = false # Import Python version from FC
[switch]
migrate = true
migrate_timeout = 300
maintenance_mode = true
maintenance_mode_phases = ["migrate"] # Valid: "drain", "migrate"
backups = true
rollback = false
search_replace = true
# Worker draining
drain_workers = false
drain_workers_timeout = 300
skip_stale_workers = true
skip_stale_timeout = 15
worker_kill_timeout = 15
# Frappe Cloud integration
[fc]
api_key = "fc_xxx"
api_secret = "fc_xxx"
site_name = "mysite.frappe.cloud"
team_name = "my-team"
# Remote worker
[remote_worker]
server_ip = "192.168.1.100"
ssh_user = "frappe"
ssh_port = 22See example-config.toml for complete schema with all hooks and options.
| Mode | Use Case | Build Location | Deploy Target |
|---|---|---|---|
| pull | Standard deploy | On-server | Same server |
| ship | Remote deploy | Local machine | Remote server via SSH/rsync |
Deploy automatically on push using the rtcamp/frappe-deployer composite action. Two strategies:
| Strategy | Build location | When to use |
|---|---|---|
| pull | Remote server | Server has enough CPU/RAM to build; simpler setup |
| ship | CI runner (Docker) | Offload build from server; cross-arch builds |
name: Deploy (pull)
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via pull
uses: rtcamp/frappe-deployer@main
with:
command: pull
sitename: ${{ vars.SITE_NAME }}
config_path: .github/configs/site.toml
gh_token: ${{ secrets.GH_TOKEN }}
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh_server: ${{ secrets.SSH_SERVER }}
ssh_user: ${{ secrets.SSH_USER }}Required secrets/vars: GH_TOKEN, SSH_PRIVATE_KEY, SSH_SERVER, SSH_USER, SITE_NAME (var).
The CI runner builds the release locally in Docker, rsyncs it to the remote server, then SSHes in to switch. Requires a [ship] section in the config:
[ship]
host = "192.168.1.100"
ssh_user = "frappe"
ssh_port = 22name: Deploy (ship)
on:
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Deploy via ship
uses: rtcamp/frappe-deployer@main
with:
command: ship
config_path: .github/configs/site.toml
gh_token: ${{ secrets.GH_TOKEN }}
ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh_server: ${{ secrets.SSH_SERVER }}
ssh_user: ${{ secrets.SSH_USER }}Each [[apps]] entry supports 8 build-phase hooks; [switch] supports 4 restart-phase hooks:
[[apps]]
repo = "my-org/custom-app"
ref = "main"
before_bench_build = """
npm ci
npm run build:prod
"""
host_after_bench_build = """
curl -s -X POST "$WEBHOOK_URL" -d "app=$APP_NAME built"
"""
[switch]
host_before_restart = "echo 'switching to new release'"
after_restart = "bench --site $SITE_NAME clear-cache"Hooks prefixed host_ run on the host; others run inside the Docker container. Values are inline shell. See docs/github-action.md for the full hooks reference and app_env for injecting secrets.
Generate a dedicated deploy key:
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/deploy_key -N ""
cat ~/.ssh/deploy_key # → paste as SSH_PRIVATE_KEY secret
cat ~/.ssh/deploy_key.pub # → append to ~/.ssh/authorized_keys on remote server~/frappe/sites/<site>/
├── workspace/
│ ├── frappe-bench → release_YYYYMMDD_HHMMSS (symlink to current)
│ ├── deployment-data/ (persistent across releases)
│ │ ├── sites/ (DB, files)
│ │ ├── config/ (supervisor configs)
│ │ └── logs/
│ ├── release_YYYYMMDD_HHMMSS/ (each release is isolated)
│ │ ├── apps/
│ │ ├── env/ (release-scoped Python venv)
│ │ ├── .uv/ (UV package cache, per-release)
│ │ ├── .fnm/ (Node.js runtime, per-release)
│ │ ├── sites → ../deployment-data/sites (symlink)
│ │ └── .fmd.toml (config snapshot)
│ └── .cache/ (workspace-level caches)
└── deployment-backup/
└── release_YYYYMMDD_HHMMSS/
- Bypass tokens:
fmd maintenance enable <site>generates a cookie for dev access during maintenance - Phase control: Enable maintenance only during
"migrate"or"drain"phases, not full deploy - Nginx integration: Serves custom page, honors bypass cookie
# Enable remote worker (opens Redis/MariaDB ports in docker-compose)
fmd remote-worker enable <site> --rw-server 192.168.1.100 --force
# Sync release to remote worker
fmd remote-worker sync <site> --rw-server 192.168.1.100Sync apps, Python deps, or DB backups from Frappe Cloud:
[release]
use_fc_apps = true # FC commit hashes override local refs (preserves hooks/symlinks)
use_fc_deps = true # Auto-set python_version from FC
[switch]
use_fc_db = true # Download and restore latest FC backup at switch time
[fc]
api_key = "fc_xxx"
api_secret = "fc_xxx"
site_name = "mysite.frappe.cloud"
team_name = "my-team"Merge behavior: FC apps are merged with local [[apps]] by repo name; FC commit hash overrides local ref, but local hooks/symlink/subdir settings are preserved. Local-only apps are kept.
# 1. On CI runner: build release artifact
fmd release create --config prod.toml --mode image --build-dir /tmp/releases
cd /tmp/releases && tar -czf release.tar.gz release_*
# 2. Ship to production server
scp release.tar.gz prod:/tmp/
ssh prod "cd /path/to/workspace && tar -xzf /tmp/release.tar.gz"
# 3. Activate release on prod
ssh prod "fmd release switch --config prod.toml release_20250410_120000"Alternative: use fmd deploy ship --config site.toml for automated build-local/deploy-remote.
Private repo access:
export GITHUB_TOKEN=ghp_xxx
fmd deploy pull --config site.tomlSymlink app not found:
[[apps]]
repo = "my-org/monorepo"
ref = "main"
subdir_path = "apps/my-app" # Path within repo
symlink = true # Symlink instead of copyFC integration fails: Verify API credentials with curl -u fc_key:fc_secret https://frappecloud.com/api/method/press.api.bench.apps
Worker drain timeout: Increase timeouts if workers process long jobs:
[switch]
drain_workers_timeout = 600 # Wait 10 min for workers to finish
worker_kill_timeout = 30 # Force-kill after 30s if still runningVerbose logs:
fmd -v deploy pull <site> # Global -v flag before subcommandSee docs/troubleshooting.md for more.
**Full documentation site**: https://rtcamp.github.io/fmd/
Quick links:
- Quick Start — Deploy your first site in minutes
- Configuration Guide — Complete config reference
- Deploy Modes — Pull vs ship strategies
- GitHub Actions — CI/CD integration
- Command Reference — All commands with examples
- Troubleshooting — Common issues and solutions
- example-config.toml — Complete config schema with comments
MIT