From f2f6fee89a354a76abfeef075b4e82e70b70c8e9 Mon Sep 17 00:00:00 2001 From: Pasha Sviderski Date: Thu, 9 Apr 2026 20:24:19 +1000 Subject: [PATCH] docs: new pre-deploy hooks page and x-pre_deploy extension in the reference page --- .../2-deploy-specific-machines.md | 2 +- .../1-deployments/3-deploy-global-services.md | 2 +- .../1-deployments/4-rolling-deployments.md | 1 + .../1-deployments/5-pre-deploy-hooks.md | 209 ++++++++++++++++++ .../1-support-matrix.md | 36 +++ .../2-image-tag-template.md | 2 +- 6 files changed, 249 insertions(+), 3 deletions(-) create mode 100644 website/docs/4-guides/1-deployments/5-pre-deploy-hooks.md diff --git a/website/docs/4-guides/1-deployments/2-deploy-specific-machines.md b/website/docs/4-guides/1-deployments/2-deploy-specific-machines.md index a89dd115..04a56a7c 100644 --- a/website/docs/4-guides/1-deployments/2-deploy-specific-machines.md +++ b/website/docs/4-guides/1-deployments/2-deploy-specific-machines.md @@ -78,7 +78,7 @@ See [Push local images to cluster machines](1-deploy-app.md#push-local-images-to ## See also -- [Deploy an app](1-deploy-app.md): Build and deploy from source code or prebuilt images +- [Deploy an app](1-deploy-app.md): Build and deploy from source code or pre-built images - [Deploy a global service](3-deploy-global-services.md): Deploy one service replica on each cluster machine - [Compose support matrix](../../8-compose-file-reference/1-support-matrix.md): Supported Compose features and Uncloud extensions diff --git a/website/docs/4-guides/1-deployments/3-deploy-global-services.md b/website/docs/4-guides/1-deployments/3-deploy-global-services.md index d7208c29..68e7e2ee 100644 --- a/website/docs/4-guides/1-deployments/3-deploy-global-services.md +++ b/website/docs/4-guides/1-deployments/3-deploy-global-services.md @@ -63,7 +63,7 @@ The default mode is `replicated`, where you specify the number of replicas. ## See also -- [Deploy an app](1-deploy-app.md): Deploy from source code or prebuilt images +- [Deploy an app](1-deploy-app.md): Deploy from source code or pre-built images - [Deploy to specific machines](2-deploy-specific-machines.md): Deploy services to specific machines in your cluster - [Compose Specification: deploy.mode](https://github.com/compose-spec/compose-spec/blob/main/deploy.md#mode): Compose specification for deployment modes diff --git a/website/docs/4-guides/1-deployments/4-rolling-deployments.md b/website/docs/4-guides/1-deployments/4-rolling-deployments.md index f9b420b5..968f53ca 100644 --- a/website/docs/4-guides/1-deployments/4-rolling-deployments.md +++ b/website/docs/4-guides/1-deployments/4-rolling-deployments.md @@ -168,6 +168,7 @@ configuration hasn't changed and only redeploy the remaining ones. ## See also +- [Pre-deploy hooks](5-pre-deploy-hooks.md): Run a command before deploying service containers - [Deploy an app](1-deploy-app.md): Build and deploy from source code or pre-built images - [Compose support matrix](../../8-compose-file-reference/1-support-matrix.md): Supported Compose features and Uncloud extensions diff --git a/website/docs/4-guides/1-deployments/5-pre-deploy-hooks.md b/website/docs/4-guides/1-deployments/5-pre-deploy-hooks.md new file mode 100644 index 00000000..d9727d04 --- /dev/null +++ b/website/docs/4-guides/1-deployments/5-pre-deploy-hooks.md @@ -0,0 +1,209 @@ +# Pre-deploy hooks + +Run a one-off command before deploying a service. + +Pre-deploy hooks are useful for **one-off tasks** such as: + +- Database schema and data migrations +- Uploading static assets to a CDN +- Cache invalidation +- Any setup task that needs to run **once** before new code goes live, not on every container startup + +## How it works + +When you run `uc deploy` for a service with a pre-deploy hook configured, the hook command runs after building and +pushing the new image (if [building from source](1-deploy-app.md#deploy-from-source-code)) but **before** rolling out +any new containers. + +`uc deploy` runs your hook command inside a new container and waits for it to finish or time out (**5 minutes** by +default). This container **inherits** most of the **service's configuration**, including the image, environment +variables, volumes, placement, and compute resources. + +If the command exits with code 0, the deployment continues with a normal [rolling update](4-rolling-deployments.md). If +the command fails or times out, the deployment stops immediately with an error. `uc deploy` will display the latest logs +from the hook container to help you diagnose the issue. + +The hook runs on one of the machines where the service will be deployed. Similar to service containers, hook containers +can reach other services over the network, connect to databases, and read or write shared volumes. Note that file system +changes do not persist after the hook finishes, except for changes written to shared volumes. + +## Usage + +Add the [`x-pre_deploy`](../../8-compose-file-reference/1-support-matrix.md#x-pre_deploy) extension to a service in your +Compose file. The only required attribute is `command`, which can be a string or a list of strings, just like the +service's [`command`](https://github.com/compose-spec/compose-spec/blob/main/05-services.md#command). + +```yaml title="compose.yaml" +services: + web: + build: . + x-pre_deploy: + command: python manage.py migrate +``` + +:::info note + +The `command` replaces the image's default command ([`CMD`](https://docs.docker.com/reference/dockerfile/#cmd)) but the +[`ENTRYPOINT`](https://docs.docker.com/reference/dockerfile/#entrypoint) still runs. If your image has an entrypoint, +the hook command is passed as arguments to it. You can override the entrypoint for the service using +[`entrypoint`](https://github.com/compose-spec/compose-spec/blob/main/05-services.md#entrypoint) which applies to both +hook and regular service containers. + +::: + +Since your command runs in the same image as the service, any tools or dependencies it needs must be installed in that +image. + +See [`x-pre_deploy`](../../8-compose-file-reference/1-support-matrix.md#x-pre_deploy) for all available attributes and +their defaults. + +### Database migrations + +The most common use case for pre-deploy hooks is running database migrations before deploying a new app version: + +```yaml title="compose.yaml" +services: + web: + build: . + environment: + DATABASE_URL: postgres://postgres:${DB_PASSWORD}@db:5432/postgres + x-ports: + - app.example.com:8000/https + x-pre_deploy: + # Apply Django migrations from the built image before deploying new app containers + command: python manage.py migrate + depends_on: + - db + + db: + image: postgres:18 + environment: + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - db-data:/var/lib/postgresql + +volumes: + db-data: +``` + +When you run `uc deploy`, the migration runs first inside a new container created with the same image and environment +variables as the `web` service. Only after it succeeds, the deployment starts replacing service containers with the new +image. + +### Running multiple commands + +If you need to run several tasks before deployment, you can wrap them in a single shell command: + +```yaml title="compose.yaml" +services: + web: + build: . + x-pre_deploy: + # Apply Django migrations, collect static files, and upload them to S3 bucket + command: sh -c "python manage.py migrate && python manage.py collectstatic --no-input" +``` + +For more complex scenarios, create a dedicated script and use it as the hook command: + + + + +```yaml +services: + web: + build: . + x-pre_deploy: + command: ./scripts/pre_deploy.sh +``` + + + + +```bash +#!/bin/bash +# Ensure the script exits immediately with a non-zero code if any command fails. +set -e + +python manage.py migrate +python manage.py collectstatic --no-input +python manage.py clear_cache +``` + + + + +Make sure the script is included in your service image and exits with a non-zero code on any command failure (`set -e`). + +### Custom environment and user + +The hook container inherits environment variables from the service. You can add hook-specific variables or override +existing ones with `environment`. Use `user` to run the command as a different user. + +For example, if your service runs as a non-root user but the hook needs root to fix file permissions on a shared volume: + +```yaml title="compose.yaml" +services: + web: + build: . + user: app + volumes: + - data:/data + x-pre_deploy: + command: chown -R app:app /data/uploads + user: root + +volumes: + data: +``` + +:::tip + +Uncloud automatically sets `UNCLOUD_HOOK_PRE_DEPLOY=true` in the hook container. You can check this variable in a shared +entrypoint script or your command to detect when it's running as a pre-deploy hook versus a regular service container. + +::: + +## Failure handling + +### Non-zero exit code + +When the hook command exits with a non-zero code, the deployment stops immediately. No service containers are created or +replaced. `uc deploy` prints the latest logs from the hook container to help you diagnose the issue. + +The failed hook container is not automatically removed, so you can inspect it with `uc inspect` and `uc ps`, and fetch +its full logs as part of the service logs: + +```shell +uc logs web +``` + +Fix the issue and run `uc deploy` again to retry. + +### Timeout + +If the hook doesn't finish within the timeout (default **5 minutes**), Uncloud kills the container and fails the +deployment. The stopped container is kept for inspection, same as with a non-zero exit code. + +You can increase the timeout for long-running tasks like large database migrations or data uploads: + +```yaml title="compose.yaml" +services: + web: + x-pre_deploy: + command: python manage.py migrate + timeout: 30m +``` + +### Idempotency + +Design your hook commands to be **idempotent** when possible. If a deployment fails after the hook succeeds (for +example, a new container crashes on startup) and you retry with `uc deploy`, the hook runs again. + +Most database migration tools handle this naturally since they track which migrations have already been applied. + +## See also + +- [`x-pre_deploy` reference](../../8-compose-file-reference/1-support-matrix.md#x-pre_deploy): All available attributes + and their defaults +- [Rolling deployments](4-rolling-deployments.md): How Uncloud updates containers with zero downtime +- [Deploy an app](1-deploy-app.md): Build and deploy from source code or pre-built images diff --git a/website/docs/8-compose-file-reference/1-support-matrix.md b/website/docs/8-compose-file-reference/1-support-matrix.md index 2aa32e62..bea34114 100644 --- a/website/docs/8-compose-file-reference/1-support-matrix.md +++ b/website/docs/8-compose-file-reference/1-support-matrix.md @@ -77,6 +77,7 @@ If you rely on a specific Compose feature that is not supported by Uncloud, plea | `x-caddy` | ✅ Uncloud-specific | Custom Caddy configuration | | `x-machines` | ✅ Uncloud-specific | Machine placement constraints | | `x-ports` | ✅ Uncloud-specific | Service port publishing | +| `x-pre_deploy` | ✅ Uncloud-specific | Pre-deploy hook command | [volume-drivers]: https://docs.docker.com/engine/storage/volumes/#use-a-volume-driver @@ -167,3 +168,38 @@ services: # Short syntax for a single machine # x-machines: machine-1 ``` + +### `x-pre_deploy` + +Configure a pre-deploy hook to run a one-off command in a separate container and wait for it to finish successfully +before rolling out service containers. It's useful for preparation tasks that need to run before every service +deployment, such as database migrations, static asset uploads, or cache invalidation. + +The hook container uses the service's image and inherits its environment variables, volumes, placement, and compute +resources. If the command fails or times out (5 minutes by default), the deployment stops. + +```yaml +services: + web: + build: . + x-pre_deploy: + command: python manage.py migrate + environment: + LOG_LEVEL: DEBUG + timeout: 10m +``` + +#### Attributes + +| Attribute | Type | Default | Description | +|---------------|-------------------------|-----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `command` | string / list | (required) | The command to run in the hook container (same format as the service's [`command`](https://github.com/compose-spec/compose-spec/blob/main/05-services.md#command)) | +| `environment` | map / list of KEY=VALUE | - | Additional env vars that override or extend the service's [`environment`](https://github.com/compose-spec/compose-spec/blob/main/05-services.md#environment) | +| `privileged` | bool | service's value | Override the service's [`privileged`](https://github.com/compose-spec/compose-spec/blob/main/05-services.md#privileged) mode | +| `timeout` | duration | `5m` | Max time to wait for the command to finish before killing it (e.g., `1m30s`, `30m`, `1h`) | +| `user` | string | service's value | Override the service's [`user`](https://github.com/compose-spec/compose-spec/blob/main/05-services.md#user) to run as (`user`, `UID`, `user:group`, or `UID:GID`) | + +The hook container also gets `UNCLOUD_HOOK_PRE_DEPLOY=true` environment variable set automatically. + +See [Pre-deploy hooks](../4-guides/1-deployments/5-pre-deploy-hooks.md) for more details, usage examples, and failure +handling. diff --git a/website/docs/8-compose-file-reference/2-image-tag-template.md b/website/docs/8-compose-file-reference/2-image-tag-template.md index d76356dd..f2540272 100644 --- a/website/docs/8-compose-file-reference/2-image-tag-template.md +++ b/website/docs/8-compose-file-reference/2-image-tag-template.md @@ -143,7 +143,7 @@ image: myapp:{{gitsha 7}}.${GITHUB_RUN_ID:-local} # GITHUB_RUN_ID not set → ## See also -- [Deploy an app](../4-guides/1-deployments/1-deploy-app.md): Deploy from source code or prebuilt images +- [Deploy an app](../4-guides/1-deployments/1-deploy-app.md): Deploy from source code or pre-built images - [Compose Build Specification](https://github.com/compose-spec/compose-spec/blob/main/build.md) - [Compose Specification: image](https://github.com/compose-spec/compose-spec/blob/main/spec.md#image) - [Go template documentation](https://pkg.go.dev/text/template)