Docs/ Deployment/ Deploy Hooks

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:

Terminal
$ 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:

StageWhenImage and inputsIf it fails
pre_buildAfter the clone, before the image buildThe 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_buildAfter the image build, before the new container startsThe new image, with the app's environment variables. No volumes.The deployment fails. The previous version keeps serving traffic.
pre_deployAfter the new container passed its health check, before the route switches to itThe 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_deployAfter the route switch and the removal of the old containerThe new image, with the environment variables and the volumes.Logged as a failure; the deployment stays successful (the new version is already live).
Warning
There is no per-hook “fail on error” setting. The stage alone decides: a failing 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):

Terminal
$ 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      Yes

sh0 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 ENTRYPOINT is cleared: the command runs as given, not as arguments to node or npm.
  • It is attached to the sh0-net network, 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:

Build log
[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:

Build log
[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: 3001

When 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:

Build log
[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_deploy the 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.