Roll back a deploy
Going back is one command and needs no tag, which is why it is worth knowing exactly what it leaves alone. It swaps the image. It does not reverse migrations, and it keeps the built assets of the release you are leaving.
Before you start
A release went out and the site is worse for it. The project was deployed at least twice, so there is a tag to return to. The first deploy has nothing behind it, as First deploy to a fresh VPS says.
You are in the project directory, and your SSH key is accepted for the deploy user on the server.
$ ssh deploy@203.0.113.10 trueDecide one thing before you start: did the bad release bring a migration. A rollback leaves the database as the new release made it. A migration that only added a table or a nullable column does no harm to the old code. One that dropped or renamed something does, and then step two comes first.
The ritual, in order
Five steps. The third is the rollback itself. The second and the fifth are optional, and the second has to come before the third when you need it at all.
Read the history
Every successful deploy and every rollback appends a line to
.deploy-historyin the project's directory on the server.grim remote:execreads it without an SSH session of your own.$ grim remote:exec cat .deploy-history 2026-03-02T10:14:07+00:00 v1.4.1 2026-03-09T16:40:52+00:00 v1.4.2 2026-03-10T09:03:31+00:00 v1.4.3The last line is what runs now. The line above it is where the rollback will take you.
grim remote:exectakes no environment and reaches the one registered first, which is production in the usual case. For staging, runssh deploy@203.0.113.10 cat /opt/shop-staging/.deploy-history.Reverse the migration while the new code is still there
Optional. Do it only when the old code cannot live with the new schema. The
down()methods are in the new image, so after the rollback there is nothing left that could run them. Ask first which batch is the last one, withgrim remote:artisan.$ grim remote:artisan migrate:status $ grim remote:artisan migrate:rollback --forcemigrate:rollbackdrops whatever the last batch created, with the rows customers wrote there since the release. There is no confirmation and no undo. When in doubt, leave the schema alone and ship a fix forward instead.Roll back
grim deploywith--rollbacktakes the tag before the current one from the history, pulls it and starts the containers on it. It asks no question and runs no hooks.$ grim deploy production --rollback Rolling back to previous version... Rolling back to v1.4.2 … Rollback complete: v1.4.2The rollback writes itself into the history as
v1.4.2 (rollback). That has a consequence. Run the command a second time and the tag before the current one isv1.4.3, the release you were escaping from. Two rollbacks bring you back.Check the site
The rollback waits for nothing and verifies nothing, so look yourself. Ask the healthcheck and read what the app logged while it started, with
grim logs.$ curl -I https://shop.example.com/api/v1/healthcheck $ grim logs app --remote=productionLook at the pages in a browser too. The volume that holds
public/is replaced only by a full deploy, so the site still serves the CSS and JavaScript built forv1.4.3. When the fault was in the PHP code, that is fine. When it was in the front end, go on to step five.Deploy an older tag by name
Optional. This is the way to go further back than one step, and the way to get everything of an older release, assets included. It is an ordinary deploy of a tag that still sits in GHCR.
$ grim deploy production --tag=v1.4.1 Deploy shop@v1.4.1 to 203.0.113.10 (production)?It asks, replaces the
public/volume, waits for the app to turn healthy and runs the hooks.php artisan migrate --forcefinds nothing to do, because the older image carries no migration the database has not seen.✓ Deployed shop@v1.4.1 to 203.0.113.10is the last line, and the history gets a plainv1.4.1entry.
What you have now
Production runs the older image. On the server, .deploy-history tells the story in order, and its last line is the truth about what runs.
2026-03-10T09:03:31+00:00 v1.4.3
2026-03-10T09:21:12+00:00 v1.4.2 (rollback)The bad image is still in GHCR under its tag. Nothing deletes it and nothing will deploy it again by itself. The next grim release takes the next patch number, v1.4.4, and that is where the fix goes. The database is as the newest migration left it, unless you ran step two.
When it does not work
“No previous version found in .deploy-history”
There is no history file in that directory. Either the project never finished a deploy, or the path is wrong. For staging the directory is /opt/shop-staging, and the environment goes on the command line: grim deploy staging --rollback.
The rollback ran and nothing changed
The history has one line. The tag before the current one is then the current one, and the server was restarted on the image it already had. There is nothing older on this server. Deploy a tag by name as in step five, or fix forward.
The deploy rolled itself back
A deploy that fails its last check prints Rolling back to v1.4.2 by itself and ends with an error. That covers one failure only: the app started and php artisan about kept failing. The server is back on the last good tag and the history is unchanged. Read the log, fix the cause and release again.
The app container never turned healthy and nothing rolled back
App container failed to become healthy stops the deploy, prints the last lines of the app log and leaves the new containers as they are. The automatic rollback does not cover this case. Run step three yourself.
“Image not found. Build it first:”
The tag you named in step five is not in GHCR under the image name grim.json gives now. Check the spelling against the history. When docker.owner changed since that release, the old tags live under the old name.
“Rollback failed.”
The pull or the restart failed on the server, and the lines above the message say which. unauthorized means the server's GHCR login no longer works. Give it a fresh token with grim server:ghcr-login --host=vps1 and roll back again.
Next recipe
Change config on a live app
Set one value in a server's .env, confirm the app reads it, change many values through a pull and a push, rotate a secret the whole team shares, and apply an edit on a stack you run yourself.
Read itSpells used here
grim deploy
Send an image to a server
Roll a pushed image tag out to one environment over SSH, verify the app answers, and fall back to the previous tag when it does not.
grim release
Ship in one pass
Test, build, push and deploy the project to an environment with a single command and a single tag.
grim remote:exec
Run a command on the server
Run one shell command on the server, in the deployed project's directory, without opening an SSH session yourself.
grim remote:artisan
Run artisan on the server
Run one artisan command inside the app container of the deployed project, from your own terminal.
grim logs
Read the logs
Show what the containers have been printing, for one service or all of them, on your machine or on a deployed server.
Related recipes
Staging to production, one image
Register a staging environment next to production, name both in grim.json, release to staging, look at it, then send the same tag to production without building again.
First deploy to a fresh VPS
Take a bare Ubuntu or Debian server and a project that runs on your machine, and end with the project live on its own domain over HTTPS.