Skip to content

rtCamp/frappe-deployer

Repository files navigation

fmd — Frappe Manager Deployer

Zero-downtime Frappe deployments with atomic releases and rollback capability.

Features

  • 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

Install

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

Quick Start

# 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

Key Commands

# 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.toml

App format: org/repo:ref or org/repo:ref:subdir/path (for monorepos)

Configuration

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 = 22

See example-config.toml for complete schema with all hooks and options.

Deploy Modes

ModeUse CaseBuild LocationDeploy Target
pullStandard deployOn-serverSame server
shipRemote deployLocal machineRemote server via SSH/rsync

GitHub Action

Deploy automatically on push using the rtcamp/frappe-deployer composite action. Two strategies:

StrategyBuild locationWhen to use
pullRemote serverServer has enough CPU/RAM to build; simpler setup
shipCI runner (Docker)Offload build from server; cross-arch builds

pull

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).

ship

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 = 22
name: 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 }}

Hooks

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.

Setting up 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

Directory Structure

~/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/

Maintenance Mode

  • 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

Remote Workers

# 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.100

Frappe Cloud Integration

Sync 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.

CI/CD Workflow

# 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.

Troubleshooting

Private repo access:

export GITHUB_TOKEN=ghp_xxx
fmd deploy pull --config site.toml

Symlink app not found:

[[apps]]
repo = "my-org/monorepo"
ref = "main"
subdir_path = "apps/my-app"  # Path within repo
symlink = true               # Symlink instead of copy

FC 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 running

Verbose logs:

fmd -v deploy pull <site>  # Global -v flag before subcommand

See docs/troubleshooting.md for more.

Documentation

**Full documentation site**: https://rtcamp.github.io/fmd/

Quick links:

License

MIT

About

No description or website provided.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors