A release goes live only once it has booted
Every deploy builds beside the running release, checks it can boot, switches over in one move, and puts the previous release straight back if the site stops answering.
Nothing being served is touched until the new release is ready
Each deploy fetches into a new directory under releases/, links the
shared .env and storage, and runs your deploy script there as the
site's own user. Then it boots the release: the autoloader, and for Laravel the whole framework. Only
then does current move, and PHP-FPM reloads.
- A release that cannot boot never goes live, with no downtime at all
- A real request to the site afterwards; if it fails, the previous release goes straight back
- Roll back by hand to any release still on disk
Risky migrations, read before they run
Before the deploy script runs, the pending migrations are read as text. An index on an existing table, a column added with a default, a changed or dropped column, a new foreign key: each is listed with the live size of the table it touches, and raised when that table is large.
- Optionally hold a deploy you start by hand until somebody has looked
- It never runs a migration to find out, and never guesses how long a lock will take
The repository says what it needs
A shipways.yaml beside composer.json declares
the PHP version, extensions, the deploy script, the scheduler, Reverb, Inertia SSR or Octane, daemons and
cron entries. Every deploy reads it from the release it just fetched and makes the site match, so the code
and the configuration it needs ship in the same pull request.
- The panel drafts one from what the site runs now, and can open the pull request
- Pull requests get the plan as a check, before anything merges
- A deploy never removes anything; removals wait for a person
version: 1
php: "8.4"
extensions: [redis, imagick]
deploy: |
composer install --no-dev --optimize-autoloader
php artisan migrate --force
npm ci && npm run build
scheduler: true
reverb: true
daemons:
- name: horizon
command: php artisan horizon
stop_wait_seconds: 90
scheduled_jobs:
- name: prune
command: php artisan model:prune
expression: "0 3 * * *"
Sixteen kinds of site, each served the way it expects
The kind decides the nginx, the PHP pool or the Node process, and the default deploy script.
Everything else a site needs to stay up
GitHub, through a token or the GitHub App, GitLab on gitlab.com or your own instance, and Bitbucket Cloud. Each site gets its own read-only deploy key.
By hand, on push, from a deploy hook for CI, from the API, or from your assistant over MCP.
The .env lives on the server, not in Shipways. Every save keeps the previous version, with who and when.
Each name shows where it points right now, with the exact record to add when it does not point here.
Let's Encrypt, renewed from 30 days out and alerted at 14, or one of your own.
Reverb, Inertia SSR and Octane on Swoole, each one switch, with the nginx and daemons written for you.
Served by nginx rather than the app, with a bypass link for you while it is up.
Composer and npm logins for Nova, Spark or a private registry, written to every server.
To another server, with one short freeze for the last copy, and a way back until you finish.
One machine, free, for as long as you like
No card and no end date. Connect a spare box and watch it provision; everything it builds carries into whichever plan you move to.