Skip to content

Repository files navigation

Butler

Your friendly PHP dev stack helper — a bash CLI that wraps Docker Compose to manage local PHP development stacks. Runs nginx-proxy as a shared reverse-proxy so multiple sites can be served under .test domains simultaneously. Templates, presets, and built-in commands are PHP-oriented; other stacks can be managed but are not first-class.

Prerequisites

mise is the only required dependency. It manages all other tools (just, shellcheck, shfmt, bats, lefthook).

curl https://mise.run | sh

Then add mise activation to your shell config (~/.zshrc, ~/.bashrc):

eval "$(mise activate zsh)"   # or bash

Installation

mise install       # install toolchain
just install       # symlink butler to /usr/local/bin, install autocomplete and git hooks

Copy .env.dist to .env and set your paths:

cp .env.dist .env
BUTLER_SITES_DIR=~/Sites/          # where Docker Compose site configs live
BUTLER_PROJECTS_DIR=~/Projects/    # where Git repos live
BUTLER_REQUIRED_CONTEXT=false      # if true, site add requires a context subdirectory
BUTLER_TLD=test                    # local domain TLD (sites served at <name>.test)
NGROK_AUTHTOKEN=changeme           # ngrok token for butler proxy
MYSQL_PASSWORD=secret              # shared MySQL password

Usage

butler COMMAND [SITE] [ARGS...]

If SITE is omitted from site commands, butler infers the site from your current working directory.

Management

Command Description
butler site add Add a new site
butler site list List all sites
butler site clone [-c CONTEXT] [SITE] <REPO> Clone a repo and link it to an existing site
butler site add --multi Add a new multi-project site
butler site convert [SITE] Convert single-project site to multi-project
butler site cd Change directory to a site
butler site status [SITE] Show status of site(s)
butler site link [SITE] Link or repair app symlinks; omit SITE to process all sites
butler dns install Configure dnsmasq for automatic *.test resolution
butler dns uninstall Remove butler DNS configuration
butler dns status Show DNS configuration and resolution state

Site Commands

Command Description
butler up [SITE] Start container stack
butler down [SITE] Stop container stack
butler restart [SITE] Restart container stack
butler exec [SITE] [PROJECT] <cmd> Execute command on app container
butler shell [SITE] [PROJECT] Open a shell in the app container
butler php [SITE] <args> Run php on the container
butler composer [SITE] [PROJECT] <args> Run composer on the container
butler run [SITE] <script> Run a custom script
butler scripts [SITE] List available scripts for the current site
butler proxy [SITE] Proxy site through ngrok

Other Commands

Command Description
butler templates List available site templates
butler mysql Open a MySQL shell
butler ftp Run an ephemeral FTP server
butler sftp Run an ephemeral SFTP server

Local DNS

Butler can configure dnsmasq to resolve all *.<BUTLER_TLD> domains to 127.0.0.1 automatically — no manual /etc/hosts entries needed after butler site add.

butler dns install     # configure dnsmasq for *.test resolution
butler dns uninstall   # remove butler DNS configuration
butler dns status      # show configuration and resolution state

Install dnsmasq first if it is not already present (brew install dnsmasq, sudo pacman -S dnsmasq, sudo apt install dnsmasq, etc.), then run butler dns install.

Platform Routing mechanism
macOS /etc/resolver/<tld> pointing to dnsmasq on port 5300
Linux + systemd-resolved /etc/systemd/resolved.conf.d/butler.conf routing ~<tld> to dnsmasq
Linux + NetworkManager /etc/NetworkManager/dnsmasq.d/butler.conf
WSL2 Requires systemd enabled — butler dns install will guide you if it is not

Without DNS configured, add entries to /etc/hosts manually:

127.0.0.1 mysite.test

Domain Watcher

Butler includes a watcher service that boots a site automatically when you browse to its .test domain — no need to butler up first.

When nginx-proxy receives a request for a domain with no running container, it falls through to the watcher. The watcher reads the Host header, maps it to a site directory in BUTLER_SITES_DIR, runs docker compose up -d in the background, and returns a page that auto-refreshes in five seconds once the stack is ready.

The watcher starts automatically alongside nginx-proxy the first time you run any docker-compose command.

Two-Directory Model

Butler maintains two separate directory trees:

  • Sites (BUTLER_SITES_DIR) — Docker Compose configs, one per site. Each contains docker-compose.yml, optional hooks/ (lifecycle hooks sourced before the matching compose command), optional scripts/ (user-facing commands exposed via butler run), and an app symlink (single-project) or app/ directory of named symlinks (multi-project) pointing to the project(s).
  • Projects (BUTLER_PROJECTS_DIR) — Git repository clones. Optionally nested under context subdirs (e.g. Projects/work/mysite) when BUTLER_REQUIRED_CONTEXT=true.

Butler resolves the current site by scanning app symlinks in BUTLER_SITES_DIR — so running butler shell from anywhere inside a project directory just works.

Single-project sites

A site's app is a symlink to one project directory. The project name is inferred from the current working directory, or overridden in site.env:

# Sites/mysite/site.env
BUTLER_PROJECT=custom-name           # override inferred project name
BUTLER_PROJECT_DIR=/custom/path      # override resolved project directory
BUTLER_APP_CONTAINER=php             # container name for exec/shell/composer

Use .env in the site directory for local overrides (passwords, domain overrides, extra compose files) that should not be committed.

# Sites/mysite/.env
BUTLER_COMPOSE_FILES=docker-compose.xdebug.yml   # colon-separated extra compose files

BUTLER_COMPOSE_FILES accepts relative paths (resolved against the site directory) or absolute paths. Useful for layering Xdebug, local volume mounts, or other per-developer overrides on top of the committed docker-compose.yml.

Multi-project sites

A multi-project site has app/ as a directory of named symlinks, one per project. Declare the projects in site.env:

# Sites/mysite/site.env
BUTLER_PROJECTS=project-a,project-b

Butler automatically exports BUTLER_PROJECT_<NAME> for each project, which docker-compose.yml can use for domains — the same way single-project sites use $BUTLER_PROJECT:

services:
  project-a:
    environment:
      VIRTUAL_HOST: "${BUTLER_PROJECT_PROJECT_A}.${BUTLER_TLD:-test}"
  project-b:
    environment:
      VIRTUAL_HOST: "${BUTLER_PROJECT_PROJECT_B}.${BUTLER_TLD:-test}"

Override a domain without changing docker-compose.yml by setting the var in .env:

# Sites/mysite/.env
BUTLER_PROJECT_PROJECT_A=other-domain   # serves as other-domain.test instead

For exec, shell, and composer, butler selects the container automatically when run from inside a project directory. From outside, pass the project name as an argument or choose from the picker:

butler shell mysite project-a        # explicit
butler shell mysite                  # fzf/numbered picker

Development

just fmt     # format all shell scripts with shfmt
just lint    # lint all shell scripts with shellcheck
just test    # run bats test suite

Formatting and linting are enforced automatically on every commit via lefthook.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages