Skip to content

ShippyDeployments for PHP, in a single binary

Zero-downtime, atomic releases for Composer-based projects. Inspired by Deployer and Capistrano — without the runtime.

Shippy

Install and ship

Three commands to install, four to deploy.

bash
brew tap ochorocho/shippy https://github.com/ochorocho/shippy
brew trust ochorocho/shippy
brew install shippy
bash
go install github.com/ochorocho/shippy@latest
bash
docker run --rm ghcr.io/ochorocho/shippy:latest shippy --help
bash
shippy init                     # write .shippy.yaml from your composer.json
vim .shippy.yaml                # add hostname, remote_user, ssh_key, include
shippy config validate          # check syntax, fields and template variables
shippy deploy production --dry-run   # preview the file list, no connection
shippy deploy production        # ship it

One file describes the whole deployment

No scripting DSL, no plugins — a host, an allowlist of what ships, the paths that must survive, and the commands to run before the release goes live.

yaml
hosts:
  production:
    hostname: www.example.com
    remote_user: deploy
    deploy_path: /var/www/{{name}}   # {{name}} comes from composer.json
    rsync_src: ./
    ssh_key: ~/.ssh/id_rsa
    keep_releases: 10                # ten releases to roll back to
    include:                         # nothing ships unless it is listed here
      - public/
      - vendor/
      - config/
      - composer.json
      - composer.lock
    shared:                          # symlinked into every release
      - .env
      - var/log/
      - public/fileadmin/
      - public/uploads/

commands:
  - name: Clear TYPO3 cache
    run: ./{{config.bin-dir|vendor/bin}}/typo3 cache:flush

  - name: Database migrations
    run: ./{{config.bin-dir|vendor/bin}}/typo3 upgrade:run
    only: [production]               # scope a command to specific hosts

Full configuration guide →

What it looks like on the server

The familiar Deployer/Capistrano layout — point your web server at current/ and never touch it again.

/var/www/myproject/
├── current -> releases/20240109120000    # Symlink to latest release
├── releases/
│   ├── 20240109120000/                   # Current release
│   ├── 20240109110000/                   # Previous release
│   └── 20240109100000/                   # Older release
└── shared/
    ├── .env                              # Shared files
    ├── var/
    │   ├── log/
    │   └── session/
    └── public/
        ├── fileadmin/
        └── uploads/

Eight steps, and the site only moves at step seven

  1. Scan files — walks the source directory, applying the deny-by-default allowlist
  2. Connect to server — establishes the SSH connection
  3. Create release — a new timestamped directory, e.g. releases/20260109203841
  4. Sync files — transfers everything into that release
  5. Create symlinks — links the shared files and directories
  6. Execute commands — cache flush, migrations, warmup — inside the new release
  7. Activate release — atomically repoints current; the site goes live
  8. Cleanup — removes old releases, keeps the last N

A failure in steps 1–6 never reaches production: the broken release is simply never activated. A lock on the host keeps a second deployment from starting while one is in flight.

Read the full sequence →

Ships from your pipeline too

yaml
# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ochorocho/shippy-action@v0.0.1
        with:
          args: deploy production
yaml
# .gitlab-ci.yml
deploy:
  image: ochorocho/shippy:latest
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
  script:
    - shippy deploy production

CI/CD guide →

Also in the box

  • shippy backup — a ZIP with a database dump and your shared files, with TYPO3 credentials auto-detected and cache tables excluded
  • shippy gitlab:upload — push that archive to the GitLab package registry straight from a scheduled pipeline
  • shippy rollback -l — list every release with its date, git commit and tag before you choose
  • shippy config show <host> — the fully resolved config for one host, templates expanded and per-host overrides applied
  • shippy unlock — clear a lock left behind by a crashed deployment

Command reference →

Released under the MIT License.