Zero-downtime releases
Files are synced into a fresh release directory while the old one keeps serving. Going live is one atomic symlink swap.
How a deploy runs
Zero-downtime, atomic releases for Composer-based projects. Inspired by Deployer and Capistrano — without the runtime.
Three commands to install, four to deploy.
brew tap ochorocho/shippy https://github.com/ochorocho/shippy
brew trust ochorocho/shippy
brew install shippygo install github.com/ochorocho/shippy@latestdocker run --rm ghcr.io/ochorocho/shippy:latest shippy --helpshippy 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 itNo 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.
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 hostsThe 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/releases/20260109203841current; the site goes liveA 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.
# .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# .gitlab-ci.yml
deploy:
image: ochorocho/shippy:latest
rules:
- if: $CI_COMMIT_BRANCH == "main"
script:
- shippy deploy productionshippy backup — a ZIP with a database dump and your shared files, with TYPO3 credentials auto-detected and cache tables excludedshippy gitlab:upload — push that archive to the GitLab package registry straight from a scheduled pipelineshippy rollback -l — list every release with its date, git commit and tag before you chooseshippy config show <host> — the fully resolved config for one host, templates expanded and per-host overrides appliedshippy unlock — clear a lock left behind by a crashed deployment