Notifications
Doco-CD can be configured to send notifications with Apprise to various services when a deployment is started, finished, or failed and on reconciliation events. You can find a list of all supported services and platforms in the Apprise documentation.
For that, specify the required settings in the app environment section and add an Apprise container to your docker-compose.yml file.
Settings¶
| Key | Type | Description | Default value |
|---|---|---|---|
APPRISE_API_URL | string | The URL of the Apprise API to send notifications to (e.g. http://apprise:8000/notify) | |
APPRISE_NOTIFY_URLS | string | A comma-separated list of Apprise-URLs to send notifications to the supported services/platforms (e.g. pover://{user_key}@{token},mailto://{user}:{password}@{domain}) | |
APPRISE_NOTIFY_URLS_FILE | string | Path to the file inside the container containing the Apprise-URLs (see APPRISE_NOTIFY_URLS). Mutually exclusive with APPRISE_NOTIFY_URLS. | |
APPRISE_NOTIFY_LEVEL | string | The minimum level of notifications to send. Possible values: info, success, warning, failure | success |
APPRISE_NOTIFY_REPEAT_INTERVAL | duration | How long an unchanged failure notification is suppressed before it is sent again as a reminder (see Repeated failures). 0 sends every failure. | 1h |
APPRISE_NOTIFY_BODY_TEMPLATE | string | Optional Go text/template rendering the notification body (see Custom notification body). Empty uses the built-in format. | |
APPRISE_NOTIFY_BODY_TEMPLATE_FILE | string | Path to a file inside the container containing the template (see APPRISE_NOTIFY_BODY_TEMPLATE). Mutually exclusive with APPRISE_NOTIFY_BODY_TEMPLATE. |
A request to the Apprise API fails if Apprise does not answer within 30 seconds, so an unresponsive Apprise server cannot hold up deployments.
Release announcements¶
Doco-CD checks for new releases once at startup. Set VERSION_CHECK_ENABLED to false to skip the GitHub release lookup, new-version warning, and Apprise release announcement.
Example docker-compose.yml¶
Adjust your docker-compose.yml file to include the Apprise service and the necessary environment variables for the app:
services:
app:
container_name: doco-cd
# add the code below to your existing docker-compose.yml file
depends_on:
apprise:
condition: service_healthy
environment:
APPRISE_API_URL: http://apprise:8000/notify
APPRISE_NOTIFY_LEVEL: success
APPRISE_NOTIFY_URLS: "pover://{user_key}@{token},mailto://{user}:{password}@{domain}"
apprise:
image: caronc/apprise:latest
restart: unless-stopped
ports:
- "8000:8000"
environment:
TZ: Europe/Berlin
APPRISE_WORKER_COUNT: 1
healthcheck:
test: [ "CMD-SHELL", "curl -fsS http://localhost:8000/status >/dev/null || exit 1" ]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
Repeated failures¶
A failure repeats as often as its trigger. A poll job runs on its interval and a broken deployment fails the same way every run, so one fault produces one message per poll until somebody fixes it - an expired registry token can turn into hundreds of identical notifications overnight.
Failure notifications are therefore de-duplicated per stack:
- The first failure of a stack is sent immediately.
- While the failure stays the same, further notifications are skipped and repeated only once per
APPRISE_NOTIFY_REPEAT_INTERVAL, so a long outage stays visible without flooding. - A different error on the same stack is sent right away - it is new information.
- A successful notification for that stack (e.g.
Deployment completed) clears the suppression, so it also acts as the recovery signal and the next failure is sent immediately. - Set
APPRISE_NOTIFY_REPEAT_INTERVALto0to send every failure, unchanged or not.
Stacks are tracked separately per repository, target and Docker context, and the state is in memory: a restarted daemon sends the next failure again.
For the same reason a failed deployment notifies once, not twice. The stack reports its own failure, and the poll job or webhook that received the same error no longer repeats it under its own title. Logs and metrics are unchanged.
Metadata fields¶
When a notification is sent, the following metadata fields are included in the notification body:
| Field name | Description | Example |
|---|---|---|
duration | Time the deployment took (omitted when no deployment ran, e.g. for reconciliation notifications) | 12.483s |
job_id | Unique ID of the deployment job that triggered the notification (not included for reconciliation notifications) | |
repository | Repository name | github.com/my/repo |
revision | Branch/tag and Commit SHA that was deployed | main (abc123), v1.0.0 (def456) |
stack | Project/Stack name | my-stack |
Custom notification body¶
By default the notification body is the message followed by the metadata fields as key: value lines. Set APPRISE_NOTIFY_BODY_TEMPLATE (or APPRISE_NOTIFY_BODY_TEMPLATE_FILE) to a Go text/template to render the body yourself. This is useful to drop noisy fields, add a host label, or produce a one-liner when several stacks report into one channel.
The template is validated at startup: a syntax error or a reference to an unknown field stops doco-cd from starting. The title (emoji + optional [R] marker + title text) is not affected by the template.
Don't swallow failure reasons
The template replaces the default body for every notification level, and on failure the error text is only carried by {{ .Message }}. A template that never references it produces failure notifications with no failure reason at all (the title only says Deployment failed; the details then live only in the logs). On success, .Message is largely redundant with the title, so compact templates should include it guarded by level:
The following fields are available:
| Field | Description |
|---|---|
.Level | info, success, warning or failure |
.Emoji | Level emoji (ℹ️/✅/⚠️/❌) |
.Title | Title text, e.g. Deployment completed |
.Message | Core message |
.IsReconciliation | true when triggered by a reconciliation event |
.Repository | Repository name |
.Stack | Project/Stack name |
.Target | Custom webhook/poll target (empty for the default target) |
.Context | Docker context the stack is deployed to (empty for the default context) |
.Revision | Branch/tag and commit SHA |
.JobID | Deployment job ID (empty for reconciliation events) |
.ReconciliationEvent | Reconciliation event that triggered the action |
.TraceID | Reconciliation trace ID |
.AffectedActorKind | container or service |
.AffectedActorID | Affected container/service ID |
.AffectedActorName | Affected container/service name |
.Commits | Commits that changed this stack since the last deploy (see Commit changelog) |
.Duration | Time the deployment (or destroy) took, from job start to the notification, e.g. 12.483s. Zero where no deployment ran (reconciliation restarts, scheduled jobs); hide it with {{if .Duration}}...{{end}} |
.ChangedServices | Sorted names of the services changed by this deploy: force-recreated ones (changed mounted/referenced files or a changed auto-discovery label) plus services whose image digest changed in the registry (detected with force_image_pull). Empty when the whole stack is (re)deployed for other reasons, e.g. a compose config change (including image tag changes in the compose file), state drift or force_recreate |
{{ .DefaultBody }} renders the built-in body (message + metadata), so you can extend the default format instead of replacing it, e.g. {{ .DefaultBody }}\nhost: my-vm.
One-line body
environment:
APPRISE_NOTIFY_BODY_TEMPLATE: "{{.Emoji}} {{if .Target}}{{.Target}}/{{end}}{{.Stack}} - {{.Message}} ({{.Revision}})"
renders e.g. ✅ prod-vm/app - Successfully deployed stack app (main (abc123)) for target prod-vm, or ✅ app - Successfully deployed stack app (main (abc123)) without a custom target.
Commit changelog¶
.Commits is the list of commits that got deployed since the previously deployed commit, newest first. It is only populated on successful deploy notifications of Git sources; on the first deploy, on failures and for OCI sources it stays empty (a range over it just renders nothing). Each entry has:
| Field | Description |
|---|---|
.Hash | Full commit SHA |
.ShortHash | Shortened commit SHA |
.Subject | First line of the commit message |
.Author | Commit author name |
Printing an entry directly ({{ . }}) gives shortHash subject.
Body with changelog
Only commits that changed a file of the stack are listed, like git log -- <paths>. The paths are the ones the compose project resolves to: the compose files, configs:, secrets:, env_file:, bind mount sources and build contexts, plus the deploy config file (.doco-cd.yml or .doco-cd.<target>.yml) that declares the stack. A repository that holds several stacks therefore gets a changelog per stack instead of everything that happened in the repository. Where the stack resolves to no path inside the repository, or covers the repository root, all commits of the range are listed.
The deploy config counts as a whole file, so a commit that changes one document of it is listed by every stack of that file that deploys afterwards.
Merged branches are handled like git log does: the commits of a merged pull request are listed, the merge commit itself only when it changed the stack's files compared to every parent, e.g. a resolved conflict.
A few changes cannot be attributed to a stack and are therefore not listed:
- Files pulled in with
include:orextends: file:, because Compose resolves them away and the loaded project no longer names them. - Services of a profile that is not active, their files do not belong to the deployed stack.
- Stacks that live in a Git submodule, a submodule bump only changes the submodule path in the parent repository.
A build context matches everything below it, so a second stack placed inside the build context of another one shows up in both changelogs.
Up to 50 commits are listed, counted after that filtering. A walk reads at most 5000 commits, so a stack that was not touched for a very long time gets a truncated list and a warning in the log. After a rebase or force-push the list starts from the point where the histories diverged.
The changelog is best-effort: with a small GIT_CLONE_DEPTH the previously deployed commit may sit beyond the shallow boundary, in which case the list is truncated or omitted. It never blocks the notification.
Reconciliation notifications¶
If a notification was triggered by reconciliation, the title gets a short [R] marker.
Example notification titles
- Regular deploy notification title:
✅ Deployment completed - Reconciliation notification title:
✅ [R] Deployment completed
Reconciliation notifications also include a reconciliation: block in the body metadata.
Metadata fields¶
| Field name | Description |
|---|---|
event | reconciliation event that triggered the action |
container_id | affected container name |
container_name | affected container name |
trace_id | reconciliation trace ID for log correlation |
| Field name | Description |
|---|---|
event | reconciliation event that triggered the action |
service_id | affected service name |
service_name | affected service name |
trace_id | reconciliation trace ID for log correlation |