Skip to content

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 one

Run 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.

  1. Syncs the config. docker-compose.yml is rendered again from grim.json and uploaded to the project's directory on the server, together with docker/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.
  2. Pulls the image for the tag.
  3. Restarts the containers. docker compose down, then up -d. The volume holding public/ is removed in between, so the new image's assets replace the old ones. Uploaded files live in another volume and stay.
  4. Waits for the app container to report healthy, for up to a minute and a half.
  5. Runs the deploy hooks. The commands under deploy.post-deploy in grim.json, in order; a project made by grim has php artisan migrate --force there. A hook that fails prints a warning and does not stop the deploy. Then the app is verified with php artisan about, retried until health-timeout runs out. On success the tag is appended to .deploy-history on the server.
  6. 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 --rollback

The PHP-FPM pool the app runs with is not part of the image. It is a file on the server, written by grim tune.

Recipes that use it