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] [--skip-monitoring] [--] [<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 |
--skip-monitoring |
Do not register the tenant with the Monitoring HQ after the deploy |
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 with the three sizing filesdocker/php-fpm-production.conf,docker/php-production.inianddocker/nginx-production.conf, rendered from theperformanceblock (seegrim tune). The image name, the domain, the extra domains registered for the project, the Horizon and Reverb services and the container limits all come from this render. So does the tag: it is written into the file as the default of every image line (${IMAGE_TAG:-v1.0.3}), and anIMAGE_TAGline an older grim left in the.envon the server is removed, so adocker compose uprun by hand on the server stays on the deployed tag. Nothing in.envsays which tag runs; the compose file does. After the stack is up, nginx is reloaded so a changed config takes effect without a recreate. Inside the stack nginx reaches PHP-FPM over a unix socket in a volume the two containers share (php-socket, mounted at/run/php), not over TCP; a stack registered before that gets the volume and the new configs with its next deploy, anddocker compose up -drecreates both containers for it. - 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 is named and fails 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.
Afterwards two things reach the portal, neither of which can fail the deploy that has already happened.
The tenant registers with the Monitoring HQ, once per environment. The first deploy asks the portal for the registration secret with your GitHub token (GET /api/registry/monitoring-secret, answered only to a wizard with package access), runs php artisan monitoring:register in the app container with it, and writes the moment into grim.json as deploy.environments.<environment>.monitoring.registered_at. Every later deploy sees that key and skips the round trip. The secret is the portal's and stays there: not in grim.json, not in the tenant's .env, not on your terminal. From then on the tenant reports to the HQ every five minutes (MONITORING_MODE=active in the .env written by grim server:add-project; a stack from before that gets the two lines with grim env:set). --skip-monitoring leaves all of this out; a portal that is not an HQ, a missing token or a failed monitoring:register is one line of explanation and nothing more. Commit the changed grim.json.
The deploy sends a heartbeat telling the portal what is now running — project, environment, modules, active themes, core version, PHP, host and URL. A heartbeat that does not arrive is printed and changes nothing.
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.
A failed hook stops the deploy where it stands
The new image is running and healthy by the time the hooks run, so a hook that fails rolls nothing back — rolling a serving image back over a migration that did not apply trades a mistake for an outage. The deploy prints which hooks failed, leaves with an error, and the summary box is not shown. Fix it forward — grim remote:artisan <command> production runs a single command against the deployed stack — and the next deploy is an ordinary one.
A migration that says the table is already there
Base table or view already exists from php artisan migrate --force on an instance that skipped a few trains: a migration file renamed between trains carries the same table under a new name, and the new name has no row in migrations. grim migrate:mark-applied writes those rows without running anything; then deploy again.
$ grim migrate:mark-applied --all-existing --dry-run --remote=production # what would be marked
$ grim migrate:mark-applied --all-existing --remote=production # mark itTwo rollbacks bring you back
--rollback takes the tag before the current one from .deploy-history, rewrites the default in docker-compose.yml to it and records itself in the history 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 production stack
Render the PHP-FPM pool, php.ini and nginx config the production stack mounts, from a profile in 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.