Skip to content

Lifecycle hooks

Run a migration, or any other task, before your service starts. The service starts only when the task succeeds.

compose.staging.yaml
services:
  web:
    image: my-app:local
    command: [python, manage.py, runserver, "0.0.0.0:8000"]
    secrets:
      - DB_PASSWORD # (1)!
    healthcheck:
      test: [CMD, curl, --fail, http://localhost:8000/healthz]
      interval: 10s
    pre_start:
      - command: [python, manage.py, migrate, --noinput] # (2)!
        environment:
          DB_PASSWORD: ${DB_PASSWORD:?Missing DB_PASSWORD} # (3)!
  worker:
    image: my-app:local
    command: [python, worker.py]
    depends_on:
      web:
        condition: service_healthy # (4)!

secrets:
  DB_PASSWORD: # (5)!
    file: secrets/staging/web/DB_PASSWORD.txt
  1. The service reads the secret as a file, as in Secrets.
  2. A command that ends. It runs with the image and the settings of the service.
  3. The task receives the same secret as a variable. The variable is named like the secret.
  4. The worker waits for the health check of web to pass.
  5. A secret named like the variable that the task reads.

Generated on every release. You never write these files or see them.

The task runs first, as a Job, with the secret from the cluster.

apiVersion: batch/v1
kind: Job
metadata:
  annotations:
    argocd.argoproj.io/hook: Sync # (1)!
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation # (2)!
    argocd.argoproj.io/sync-wave: '0' # (3)!
  labels:
    com.docker.compose.project: my-app
    com.docker.compose.service: web
  name: web-init-1
  namespace: my-app
spec:
  backoffLimit: 0 # (4)!
  template:
    metadata:
      labels:
        com.docker.compose.project: my-app
        com.docker.compose.service: web
        compose.blackstorm.dev/role: pre-start # (5)!
        com.docker.compose.network.default: 'true'
    spec:
      containers:
      - args:
        - python
        - manage.py
        - migrate
        - --noinput
        env:
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef: # (6)!
              key: value
              name: db-password
        image: my-app:local
        imagePullPolicy: IfNotPresent
        name: web
      restartPolicy: Never # (7)!
  1. Part of every release: the task runs each time a release is deployed.
  2. The run of the previous release is removed when a new one starts.
  3. The order. Lower numbers go first: the task at 0, then web at 1, then worker at 2.
  4. No retries. If the task fails, the release stops here.
  5. Marks the containers of the task. The address of web never sends them traffic.
  6. The value comes from the Secret of the cluster, never from a file in the release.
  7. When the command ends, the container is not started again.

Then web, and then worker. The numbers give the order.

apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    argocd.argoproj.io/sync-wave: '1' # (1)!
  labels:
    com.docker.compose.project: my-app
    com.docker.compose.service: web
  name: web
  namespace: my-app
  1. After the task succeeded.
apiVersion: apps/v1
kind: Deployment
metadata:
  annotations:
    argocd.argoproj.io/sync-wave: '2' # (1)!
  labels:
    com.docker.compose.project: my-app
    com.docker.compose.service: worker
  name: worker
  namespace: my-app
  1. After web is ready, which is what depends_on asked for.

With two copies, it still runs once

A task runs once for the service, not once for each copy. With deploy.replicas: 2, the migration runs one time, and then the two copies start.

A migration runs before the service on your machine and as a Job before the new version in the cluster

Fields

Field Required Default Values
pre_start[].command Yes A command that ends, as a list
pre_start[].environment No Variables for the task. ${NAME:?message} takes the value of the secret NAME
pre_start[].image No The image of the service Another image for the task
pre_start[].working_dir No The one of the service Where the command starts
depends_on.<service>.condition No service_healthy waits for the health check of that service. service_started waits the same

What you get

On your machine

Behavior Detail
Before the service Each task runs in its own container, in order, before the service starts
Once per service Not once per copy. Starting or scaling the service again does not repeat a task that succeeded
A failure stops the start The service does not start, and neither do the services that wait for it
The secret as a variable The task reads DB_PASSWORD from its environment. The service keeps reading its file

On the cluster

Behavior Detail
Before the new version Each task runs as a Job before the service is updated
A failure keeps the old version The new version is not applied. The copies that were running keep running
It can run again A later release runs the tasks again. Write them so that running twice is harmless
No traffic The containers of a task never receive requests
Order A service starts after the tasks and services it depends on

Rules

Rule Detail
The variable is named like the secret ${DB_PASSWORD} takes the value of the secret declared as DB_PASSWORD
A secret is the whole value ${DB_PASSWORD} cannot be part of a longer value, such as a connection URL
Only permanent services A scheduled job cannot declare pre_start
Once per service per_replica: true is rejected
No other user user and privileged in a task are rejected
A dependency has a health check service_healthy needs a healthcheck on the service you wait for
No waiting for a task service_completed_successfully is rejected. Use pre_start on the service that needs the task
No cycles Two services cannot wait for each other