Pre- / Post-Deployment Scripts¶
In this documentation, we will cover how to run scripts or commands during the deployment and container lifecycle of your Docker Compose services.
Why can scripts not be run directly in doco-cd?
To keep the image small and secure doco-cd itself does not provide a shell environment for executing scripts. Instead, it relies on the underlying container runtime (e.g., Docker) to execute these scripts within the context of the deployed containers/compose services.
Available options to run scripts/commands during deployment or container lifecycle include:
Reconciliation for non-Swarm deployments follows classic Compose restart semantics
- Services with
restart: alwaysorrestart: unless-stoppedare expected to stay running. - Services with no explicit
restartpolicy are treated asrestart: "no". - Services with
restart: on-failuremay remain exited after success (exited with status code0), andrestart: "no"is treated as one-time behavior and is not reconciled back to running. - Events from Compose lifecycle-hook runner containers are ignored. These helpers are not workload replicas, so their events do not restart or redeploy the service.
- Restarts run the
pre_stopandpost_startlifecycle hooks of the service, likedocker compose restart.pre_starthooks do not run. - Restart events use the stop signal that is configured in the container image (
StopSignalin the image config); you can override this withreconciliation.restart_signal. If neither is available,SIGTERMis used. - To prevent endless restart loops caused by flappy health checks,
unhealthyrestarts are rate-limited viareconciliation.restart_limitandreconciliation.restart_window.
More information on the restart policies can be found in the Docker Compose specification.
Reconciliation in Docker Swarm
Docker Swarm manages some desired-state reconciliation by itself with Swarm service modes and deploy.restart_policy behavior. See Docker's documentation on desired state reconciliation.
Doco-CD's reconciliation for Swarm deployments only manages service updates and scaling, but not container restarts or health status.
Init Containers¶
Init containers are containers that run before the main application containers in your Docker Compose setup and complete their tasks before the main containers start. They are useful for performing setup tasks, such as initializing databases, running migrations, or preparing configuration files.
Some common use cases for init containers include:
- Database migrations: Running database migration scripts before the main application starts.
- Running shell scripts to generate configuration files or perform setup tasks.
- Preloading data into databases or caches.
We use the depends_on option with the condition: service_completed_successfully condition to ensure that the main application container waits for the init container to complete successfully before starting. The init container will run its specified commands and exit with a status code of 0 to indicate success, allowing the main application container to start afterward.
Recommended Restart Policy for One-Time Script Services
It is recommended to use restart: on-failure for one-time script services to allow them to remain stopped after successful completion, while still enabling automatic restarts in case of failures.
Example¶
x-common-env: &common-env # (4)!
MYVAR: world # We will use this variable in both init and app containers
services:
init:
image: busybox
restart: on-failure:3 # (3)!
environment:
<<: *common-env
entrypoint: "sh -c" # (5)!
volumes:
- ./web:/web
working_dir: /web
command: # (1)!
- |
echo Starting pre-deployment script
echo "Hello $${MYVAR}!" > /web/index.html
echo Finished pre-deployment script
exit 0 # Exit with code 0 to indicate success, not required if the last command already returns 0 but added here for clarity
app:
image: nginx
environment:
<<: *common-env
volumes:
- ./web:/usr/share/nginx/html:ro
ports:
- 8080:80
depends_on:
init:
condition: service_completed_successfully # (2)!
- Double dollar-sign (
$$) is required to use variables in the shell script, otherwise Docker Compose will try to resolve it as a variable in thedocker-compose.ymlfile instead of passing it to the container. - Wait for init container to complete/stop with exit code
0usingdepends_on - Using
restart: on-failure:3allows the init container to be retried up to 3 times in case of failure during script execution, while still allowing it to remain stopped after successful completion. To retry indefinitely, you can userestart: on-failurewithout a retry limit. See Restart Policy Documentation. - See Fragments and Extensions in the Compose File Reference for more details on how to use YAML anchors and aliases to share common configuration between services.
-
Use
sh -cas the entrypoint to run multiple commands in thecommandsection -
If you have a shell script in your repo for the init stuff, you can remove
entrypointand mount the script directly and run it via thecommandoption: -
If you need commands from the app container, try to use the same image as your app container. Many app images also come with a shell (sh, ash, bash)
Post-Init and Standalone One-Shot Services¶
Services referenced by depends_on.condition: service_completed_successfully are automatically treated as one-shot services. For a one-shot service that runs after another service or is not referenced by another service, add the cd.doco.deployment.one_shot: "true" label:
services:
app:
image: backend
healthcheck:
test: ["CMD", "curl", "--fail", "http://localhost:8080/health"]
interval: 2s
timeout: 1s
retries: 10
post-init: # Runs *after* the app service is healthy
image: backend
command: ["./create-initial-resources.sh"]
restart: on-failure:3
labels:
cd.doco.deployment.one_shot: "true"
depends_on:
app:
condition: service_healthy
Doco-CD waits for labeled one-shot services to exit. Exit code 0 completes the deployment successfully; a nonzero exit code fails the deployment. A one-shot service that does not finish before the deployment timeout also fails the deployment.
Troubleshooting¶
container exited (0)¶
If a service is expected to exit successfully but the deployment reports container <service-name> exited (0), make its lifecycle explicit:
- For a pre-init service, reference it from the dependent service with
condition: service_completed_successfully - For a post-init or standalone service, add the
cd.doco.deployment.one_shot: "true"label.
Doco-cd evaluates declared one-shot services by their exit code even if they finish before the first status check.
Related issue: #1115
Sidecar Containers¶
Sidecar containers are additional containers that run alongside your main application containers. They can be used to provide auxiliary services, such as background tasks, metrics collection, or log forwarding.
Some common use cases for sidecar containers include:
- Background tasks, e.g. cron jobs or scheduled tasks (See also Job Scheduling / Cron Jobs for the built-in scheduling feature)
- Metrics collection for monitoring tools like Prometheus
- Log forwarding to external systems
Example¶
volumes:
webdata:
services:
app:
image: nginx
ports:
- "8080:80"
volumes:
- webdata:/usr/share/nginx/html:ro
sidecar:
image: busybox
restart: always # (1)!
volumes:
- webdata:/webdata
depends_on:
- app
entrypoint: "sh -c"
command:
- |
while true; do
echo "Updating web content..."
echo "The current time is $(date)" > /webdata/index.html
sleep 60
done
- Using
restart: alwaysensures that the sidecar container continues to run and perform its tasks as long as the main application container is running.
Compose Lifecycle Hooks¶
Requires Docker Compose 2.30.0 or later
Docker Compose lifecycle hooks allow you to run commands/scripts at specific points in the container lifecycle, such as before or after starting (pre_start and post_start) or before stopping pre_stop a container.
When doco-cd runs lifecycle hooks¶
Deployments run the hooks through Docker Compose. doco-cd also stops, starts, and restarts containers of a deployed project outside of a deployment. For these operations, doco-cd runs the hooks of the deployed service definition with the same semantics as the matching Docker Compose command:
| Operation (Docker Standalone) | Like | pre_start | post_start | pre_stop |
|---|---|---|---|---|
Scheduled job start (restart mode) | docker compose start | ✅ | ✅ | - |
Scheduled job restart of a running container (restart mode) | docker compose restart | ❌ | ✅ | ✅ |
stop_services stop before a job | docker compose stop | - | - | ✅ |
stop_services start after a job | docker compose start | ✅ | ✅ | - |
| Reconciliation restart | docker compose restart | ❌ | ✅ | ✅ |
- Like Docker Compose,
pre_starthooks run only when no replica of the service is running. - A
pre_stophook is skipped if the container already stopped, for example after akillevent. - A failed
pre_stophook cancels the stop or restart of a scheduled job and ofstop_services. Reconciliation restarts the container anyway, because the restart is the recovery, and logs a warning. - After a failed
post_starthook, the container keeps running. A scheduled job execution fails. Reconciliation sends a warning notification instead of the success notification. - Reconciliation waits at most 2 minutes for each group of hooks, because a hanging hook must not block other events of the repository. A timeout stops waiting but does not terminate the hook process in the container. Loading the service definition, including waiting for source locks, has the same limit.
- doco-cd reloads the deployed service definition from the deployed revision to find the hooks. The load also resolves the external secrets of the deployment. If the definition cannot be loaded, for example for a container without doco-cd deployment labels, the operation continues without hooks.
- doco-cd records the hook phases of each service in the
cd.doco.deployment.lifecycle_hookscontainer label. For a service without hooks, doco-cd does not load the definition. Containers from doco-cd versions before this label do not have it, so doco-cd loads their definition until a deployment recreates them. - Swarm does not support lifecycle hooks.
Project actions of the REST API and MCP server
The start, stop, and restart project actions of the REST API and the MCP server do not run lifecycle hooks yet.
Example¶
Post Start Hook¶
This example demonstrates how to use the post_start hook to set up the correct volume permissions that the application needs.
How It Works:
- Volume Initialization: Docker creates the data volume with root ownership
- Container Starts: The container runs with user: 1001
- Permission Setup: Two post-start hooks execute sequentially:
- First hook changes ownership to user 1001
- Second hook sets appropriate read/write permissions
- Application Runs: The application can now access the volume with proper permissions
services:
app:
image: backend
user: "1001"
volumes:
- data:/data
post_start:
- command: ["chown", "-R", "1001:1001", "/data"]
user: root
- command: ["chmod", "-R", "755", "/data"]
user: root
volumes:
data:
driver: local
Pre Stop Hook¶
This example demonstrates how to use the pre_stop hook to run cleanup tasks before the container stops.
How It Works:
- Shutdown Initiated: Container receives shutdown signal (e.g., docker compose down)
- Pre-Stop Sequence: Hooks execute in order:
- Flush application cache
- Backup important data
- Notify monitoring system
- Container Stops: After hooks complete, container proceeds with shutdown or restart
services:
app:
image: backend
pre_stop:
- command: ["./scripts/flush_cache.sh"]
- command: ["./scripts/backup_data.sh"]
- command: ["curl", "-X", "POST", "http://monitoring.example.com/notify_shutdown"]
Further Reading¶
- More examples can be found in this guide: Managing Container Lifecycles with Docker Compose Lifecycle Hooks
- Using lifecycle hooks with Compose
- Docker Compose Post Start Hook Documentation
- Docker Compose Pre Stop Hook Documentation
- Reconciliation settings