Send an image to a server
$ grim deploy production --tag=v1.4.2
The image is built and sits in GHCR under a tag. Nothing on the server has changed yet. grim deploy is the step that changes it: it tells one environment of the project to pull that tag and restart on it.
Reach for it on its own when build and rollout are separate decisions. The tag that passed on staging yesterday goes to production today, unchanged. Or the last release turned out badly and you want the previous one back, which is the same command with --rollback. When you build and ship in one breath, grim release does both.
Usage
$ grim deploy --tag=v1.4.2 # production, asks before it starts
$ grim deploy staging --tag=v1.4.2 --force # staging, no question
$ grim deploy production --rollback # back to the tag before the current oneRun it anywhere inside the project. The project must have a home on the server already, made by grim server:add-project, and your SSH key must be accepted for the deploy user there. --tag is required; deploy never guesses one.
Arguments and options
Deploy project to server
Usage
grim deploy [-t|--tag TAG] [--rollback] [--force] [--] [<environment>]Arguments
| Argument | Description |
|---|---|
environment |
Target environment (production/staging) (optional, default: production) |
Options
| Option | Description |
|---|---|
-t, --tag=TAG |
Image tag to deploy |
--rollback |
Rollback to previous version |
--force |
Skip confirmation prompt |
What it actually does
It asks Deploy shop@v1.4.2 to 203.0.113.10 (production)? first, unless --force. Then six phases, each printed with its result.
- Syncs the config.
docker-compose.ymlis rendered again fromgrim.jsonand uploaded to the project's directory on the server, together withdocker/nginx-production.conf. The image name, the domain, the extra domains registered for the project, and the Horizon and Reverb services all come from this render. - Pulls the image for the tag.
- Restarts the containers.
docker compose down, thenup -d. The volume holdingpublic/is removed in between, so the new image's assets replace the old ones. Uploaded files live in another volume and stay. - Waits for the app container to report healthy, for up to a minute and a half.
- Runs the deploy hooks. The commands under
deploy.post-deployingrim.json, in order; a project made by grim hasphp artisan migrate --forcethere. A hook that fails prints a warning and does not stop the deploy. Then the app is verified withphp artisan about, retried untilhealth-timeoutruns out. On success the tag is appended to.deploy-historyon the server. - Cleans up. Older images of the project are removed from the server and dangling ones pruned.
Between 5 and 6 comes the summary: tag, URL, duration, and the server's memory, load and disk.
Where host, path and domain come from
Every setting is looked up for the environment first, then for the project as a whole. Path and domain are the exception: they name one stack, so the top-level deploy.path and deploy.domain belong to production and no other environment inherits them.
| Setting | grim.json |
Then | Last resort |
|---|---|---|---|
| Host | deploy.environments.<env>.host, then deploy.host |
The registration in ~/.grim/servers.json |
None: No deploy host configured. |
| Path | deploy.environments.<env>.path; for production also deploy.path |
The registration | /opt/<project>, for staging /opt/<project>-staging |
| Domain | deploy.environments.<env>.domain; for production also deploy.domain |
The registration | None, and the deploy stops |
There is no default domain. A project without one is refused before anything is uploaded: shop has no domain, and there is no default one. Set deploy.domain, or deploy.environments.<env>.domain, in grim.json, or register the project with grim server:add-project shop --domain=shop.example.com. Give each environment its own domain. The domain becomes the routing rule of that stack, and two stacks must not claim one hostname.
Pitfalls
The automatic rollback covers one failure only
When the verification in phase 5 fails, the server is put back on the last tag in .deploy-history and the command exits with an error. When the app container never turns healthy in phase 4, the deploy stops there, prints the last lines of the app log, and rolls nothing back. The new containers are left as they are. Read the log, then run grim deploy --rollback yourself or deploy a fixed tag.
Two rollbacks bring you back
--rollback takes the tag before the current one from .deploy-history and records itself there as a new entry. Run it a second time and the tag "before the current one" is the release you were escaping from. To go further back than one step, deploy the tag you want by name. A rollback asks no question and runs no hooks, so migrations are not reversed. It also keeps the volume that holds public/, which a normal deploy replaces. The older image then serves the compiled assets of the newer release. When the fault is in the front end, deploy the older tag by name instead.
The image is not there
Image not found. Build it first: means the server could not pull that tag. Either it was never pushed, or it was pushed under another namespace than the one in the rendered compose file. grim build explains how the name is chosen. unauthorized in the pull output is a different matter: the server has no GHCR credentials that can read the namespace, and grim server:ghcr-login is the fix.
A large image on a slow line
The remote run is cut off after deploy.pull-timeout plus deploy.health-timeout plus a minute of slack: 900 and 60 seconds by default. Raise deploy.pull-timeout in grim.json when the first pull to a new server needs more.
The first deploy of a project creates an admin account, admin@<domain>, with a random password. The password is printed once, in the summary of that deploy, and kept nowhere in plain text. Copy it before the terminal scrolls away.
After it finishes
✓ Deployed shop@v1.4.2 to 203.0.113.10 is the last line. Open the URL from the summary. If the app came up but behaves wrongly, the way back is one command and does not need the old tag's name.
$ grim deploy production --rollbackThe PHP-FPM pool the app runs with is not part of the image. It is a file on the server, written by grim tune.
Related spells
grim build
Forge the production image
Build the project's production Docker image, tag it with the next version, and push it to GHCR when asked.
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 tune
Size the PHP-FPM pool
Generate the PHP-FPM pool config for production, from grim.json or from the CPU and memory of the server itself.
grim server:add-project
Give a project a home
Prepare one environment of a project on a production server, with its database, nginx vhost, certificate, compose stack and .env.
grim server:ghcr-login
Let a server pull images
Log a server's deploy user in to the GitHub Container Registry, so the server can pull the project's private images.
Recipes that use it
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.
Roll back a deploy
Read the deploy history, put the server back on the previous tag, check the site, and deploy an older tag by name when one step back is not enough.