Skip to main content
Shipways deploys by building a new release beside the running one and then moving a symlink. Nothing the site is currently serving is touched until the new release has been built and checked, so a deploy that fails leaves the previous one running.

The layout on disk

Nginx serves through current, never a release by name. That is what makes going live a rename rather than a rewrite of the vhost and a reload of the web server on every deploy.

The steps

1

Fetch the code

A fresh checkout of the branch into a new release directory.
2

Record the commit

Read from the release rather than from whatever triggered the deploy, so the history reflects the code that actually ran even when the branch moved in between.
3

Link the shared files

storage and .env are replaced with links into shared, along with any shared paths you named.
4

Prepare the environment file

On the first deploy only. See environment.
5

Check the pending migrations

Read, never run. See migration review.
6

Run the deploy script

Yours. See the deploy script.
7

Check the release can boot

The autoloader is required and, for a Laravel application, the framework is booted in full. A missing extension, an unreadable .env, a broken service provider or a package left out of the install all fail here — while the site is still serving the previous release and nothing is at risk.
8

Make the release live

The symlink is moved, atomically, and FPM is reloaded so PHP stops serving the old release out of its resolved-path cache.
9

Check the site responds

A real HTTP request. See below.
10

Prune old releases

Down to the number you chose to keep.

The two checks

They sit either side of the switch on purpose. The boot check catches a release that cannot start, with no downtime at all — the old release is still serving. The HTTP check catches a release that boots on the command line and still fails a real request. It is sent to loopback with the site’s Host header, so it works before DNS points anywhere. Anything below 500 counts as alive: a 404 or a redirect means the application is running and answering, which is what is being asked. If the HTTP check fails, the previous release goes straight back rather than waiting for somebody to notice the site is down. The deployment is recorded as rolled back, which says more than a plain failure. If it fails and there is no earlier release to put back, the deploy fails and the site stays as it is. A site under maintenance answers 503 to everybody. The check carries the bypass header so that a deploy into a site that is deliberately down does not roll itself back for that reason alone.

Migration review

Neither check above helps with a migration. The deploy script runs php artisan migrate on the way past, and a single ALTER TABLE on a large table can hold a lock for minutes: the release boots, the HTTP check passes, and the site is down anyway. Putting the previous release back would not help either — the lock is in the database, not in the code. So before the deploy script runs, Shipways reads the migrations the release is about to run and reports the ones doing something known to lock: Beside each one is the size of the table at the moment of the deploy, asked of the engine while the release is being checked. The same statement is nothing on a thousand rows and an outage on fifty million, and the size is what tells them apart.
There is no estimate of how long a lock will last, and there will not be. It depends on the engine, its version, how much of the table is already in the buffer pool and what else is hitting it — a figure invented from outside the machine would be planned around, be badly wrong once, and take the credibility of the rest with it.

What it does not tell you

It reads the migration files as text and looks for a known list of patterns. It does not run them, and migrate --pretend is deliberately not used: pretend still executes the migration’s own PHP. That means it will miss things. A schema change written in raw SQL, built from a macro, or generated at runtime is not read, and is reported as unread. A report with nothing in it means nothing was recognised — it is never a statement that a migration is without risk. It also reports statements inside conditionals as though they will run, because deciding otherwise would mean evaluating the condition.

Holding a deploy

Off by default. Every deploy reports what it found and carries on. Turn on Hold for migration review on a site and a deploy you started stops before its migrations run when something is raised, leaving the previous release serving. Read the report on the deployment page and press Deploy anyway to go ahead; that starts a fresh deploy carrying your answer about that commit, so if the branch moved in the meantime it is held again. A push, a deploy hook and a rollback are never held, whatever the setting. A pipeline that silently stops is a release everybody thinks went out and did not, which is worse than the lock it was warning about. A finding is raised, rather than only listed, when the table has more than 250,000 rows — or when its size could not be measured, because a table nobody has a number for is not a small one. Set SHIPWAYS_LARGE_TABLE_ROWS to move that line.

Triggering a deploy

Manually, from the site. On push, from the Deploy on push setting. Shipways registers a webhook with your source control account. If it could not register one — no connected account, or a token without the access — the panel gives you the address to add yourself rather than pretending it worked. From CI, with a deploy hook. A rollback puts an earlier release back. Only releases still on disk are offered.

The deploy key

Each site gets its own SSH key pair. The public half goes on the repository as a read-only deploy key; it reaches that one repository and nothing else. If you have connected a source control account, Shipways installs it for you. Otherwise copy it and add it yourself. Rotate issues a new pair. The old one stops working immediately, so a key that leaked is dealt with in one action — but a key you installed by hand needs installing again.

The deploy hook

An address CI can POST to in order to deploy the site’s branch. Nothing signs it, so the address is the credential. Anyone holding it can deploy this site. Keep it where you keep your other secrets, and replace it if it ends up somewhere it should not.

Releases and rollbacks

Releases to keep decides how many stay on disk after a deploy. Each one is another copy of the application, vendor and built assets included, so this is a disk-space decision as much as a safety one. Pruned releases stop being offered for rollback, because the directory is no longer there. The holding page a site starts life serving is never pruned. It is the fallback that is always available however many deployments come and go.

When a deploy fails

The step that failed keeps its output. The common ones: No vendor/autoload.php at the boot check — the deploy script did not install dependencies, or the root directory is pointing somewhere without a composer.json. The boot check fails on configuration — usually a value missing from the environment file. The HTTP check fails but the boot check passed — the application starts and cannot serve. Assets not built is the most common cause; check the site’s error log.