Deploy Hooks
Run a command from your app's image at four points of a Git deployment: before and after the build, before and after the traffic switch.
What a deploy hook is
A deploy hook is a single command attached to an app and to one stage of its deployment. sh0 runs it in a throwaway container started from the app's image, records its output in the deployment log, and decides from its exit code whether the deployment continues.
Typical uses: a database migration before traffic reaches the new version (npm run migrate, php artisan migrate --force), a smoke test against the freshly built image, a cache warm-up after the switch.
Which apps run hooks
Only apps deployed from a Git repository run hooks. sh0 refuses to create a hook on any other app (archive upload with sh0 push, Docker image, pasted Dockerfile, Compose file, template), because the hook would never fire:
$ sh0 hooks my-site create --hook-type pre_build --command "node --version"
Error: Deploy hooks only run on git-based deploys; app 'my-site' is a 'upload' app, where hooks would never fire. Remove the hook or convert the app to a git deploy. (BAD_REQUEST)A Git app whose repository is deployed as a Compose stack does not run hooks either: the Compose path leaves the pipeline before the first hook stage.
The four stages
The stage decides when the hook runs, which image it runs in, what it receives, and what a failure does:
| Stage | When | Image and inputs | If it fails |
|---|---|---|---|
| pre_build | After the clone, before the image build | The image of the version currently deployed. No environment variables, no volumes. Skipped on the first deployment (no image exists yet). | The deployment fails. The previous version keeps serving traffic. |
| post_build | After the image build, before the new container starts | The new image, with the app's environment variables. No volumes. | The deployment fails. The previous version keeps serving traffic. |
| pre_deploy | After the new container passed its health check, before the route switches to it | The new image, with the environment variables and the app's volumes mounted. | The deployment fails, the new container is removed, the previous version keeps serving traffic. |
| post_deploy | After the route switch and the removal of the old container | The new image, with the environment variables and the volumes. | Logged as a failure; the deployment stays successful (the new version is already live). |
pre_build, post_build or pre_deploy hook always fails the deployment, a failing post_deploy hook never does.Creating a hook
In the dashboard, open the app, then the Hooks tab, and click Create Hook. Choose the Hook Type, enter the Command and, if needed, the Timeout (seconds) (300 by default).
From the CLI, the type is passed with --hook-type (pre_build, post_build, pre_deploy, post_deploy):
$ sh0 hooks my-api create --hook-type post_build --command "id"
Deploy hook created: 949d4097-4350-48c0-9c7c-6ca3c582a51a
Type: post_build
Command: id
Timeout: 300s
$ sh0 hooks my-api list
ID Type Command Timeout Enabled
----------------------------------------------------------------------------------------------------------
281e3d14-fdca-4478-8ae3-985d4a604562 post_deploy env | grep NODE 300 Yes
1b025181-05a1-4f8e-bd6b-3a26fcb69848 post_deploy printenv PORT 300 Yes
22ee2ab3-7fc4-4714-a05d-f1dd779514f3 pre_deploy echo second 300 Yes
6f1fa1be-b188-4cc0-b277-cf5acfbe0780 pre_deploy echo first 300 Yes
949d4097-4350-48c0-9c7c-6ca3c582a51a post_build id 300 Yes
b8303249-54fe-4358-90f8-a287390c219f pre_build node --version 300 Yessh0 hooks my-api delete <id> removes a hook. To pause one without deleting it, send PATCH /api/v1/deploy-hooks/<id> with enabled=false. The API also exposes GET and POST /api/v1/apps/<app-id>/deploy-hooks. Creating or changing a hook requires the developer role on the app.
Where a hook runs
Never on the server itself. Each hook gets a one-shot container started from the image named in the table above, with these properties:
- It runs as the unprivileged user
1000:1000, never as root. - The image's
ENTRYPOINTis cleared: the command runs as given, not as arguments tonodeornpm. - It is attached to the
sh0-netnetwork, so it can reach the app's databases by their internal host name. - No shell. The command is split on spaces and executed directly.
|,;,&,>,<, backticks and$(are rejected, and quotes are passed literally. For anything that needs a shell, put a script in your image and call it:sh scripts/migrate.sh. The full rules are those of cron jobs; a command is limited to 4,096 characters.
A post_build hook running id on a Node.js image shows the sandbox user:
[HOOKS] post_build hooks:
[OK] id (242ms, exit: Some(0))
stdout: uid=1000(node) gid=1000(node) groups=1000(node)Order and timeout
Hooks of the same stage run one after the other, most recently created first. If the order matters, create the hook that must run last first, or chain the steps in a single script.
Each hook has its own timeout, 300 seconds by default. A hook that exceeds it counts as a failure (Hook timed out after 5s in the log). Its container is not stopped: it keeps running until the command ends on its own, and stays on the server afterwards.
Reading the results
Results are appended to the deployment's build log, in the Deployments tab. Each hook gets one line with its status (OK, FAIL or SKIP), duration and exit code, followed by the first 10 lines of its standard output and standard error. Secrets are redacted from these lines. Real log of a first deployment with five hooks:
[HOOKS] pre_build hooks:
[SKIP] node --version (0ms, exit: None)
stderr: skipped: no app image available yet (hooks run in a container, never on the host)
[HOOKS] post_build hooks:
[OK] id (292ms, exit: Some(0))
stdout: uid=1000(node) gid=1000(node) groups=1000(node)
[HOOKS] pre_deploy hooks:
[OK] echo second (287ms, exit: Some(0))
stdout: second
[OK] echo first (295ms, exit: Some(0))
stdout: first
[HOOKS] post_deploy hooks:
[OK] printenv PORT (249ms, exit: Some(0))
stdout: 3001When a blocking hook fails, the log ends with the error and the cleanup of the new container. Here a pre_deploy hook exited with code 3, and the version already online kept running:
[STEP 5/6] Health check passed (previous step: 8.3s)
[ERROR] Deploy hook 'node -e process.exit(3)' failed with exit code Some(3):
[WARNING] Removed the container this failed deployment had started (/sh0-17b83e1c-my-api-6abc39e3). A failed deploy leaves nothing running.The output of the failing hook, and of the hooks of the same stage that ran before it, is not kept in that case: only the error line remains.
Limits
- A command containing a forbidden character is accepted when the hook is created, and rejected only when it runs. On a blocking stage the deployment then fails; on
post_deploythe hook disappears from the log without a trace. Check your command before saving it. - Hooks do not send notifications. sh0's alerts and their channels (email, Slack, Discord, Telegram, webhook) are covered in Monitoring & Metrics.
- A rollback of a Git app goes through the whole pipeline again, hooks included. See Blue-Green & Rollbacks for what a rollback actually deploys.