Skip to content

Compose-aware updates: treat a Compose project as an update unit #78

Description

@Turbootzz

Problem

freshdock currently updates containers individually. Docker Compose projects, however, contain relationships between containers that make an isolated update unsafe. Two failure modes I have hit in production:

1. One-shot init/migrate containers are skipped.

A typical stack looks like this:

yaml
migrate:
  image: app:latest
  command: ./bin/run-migrate.sh
  restart: "no"

web:
image: app:latest
depends_on:
migrate:
condition: service_completed_successfully

migrate sits in exited state after a successful run. freshdock pulls a new app:latest and recreates web, but leaves the exited migrate container untouched. Result: new application code against an old database schema.

Observed on a GlitchTip stack — the app came up "healthy" but every login failed with column ..._organizationsocialapp.is_public does not exist, and the ingest endpoint threw IndexError: tuple index out of range on a raw SQL row unpack. Nothing crashed, so no health-based guard would have caught it.

2. Dependent containers are not restarted.

Already tracked separately for network_mode: container:X (see the gluetun/qBittorrent issue), but it is the same root cause: freshdock has no notion of a container group. depends_on is the Compose-native version of the same relationship.

Why this is tractable

Compose already writes the entire dependency graph into container labels, so there is no YAML to parse and no need to locate the source file:

Label | Contains -- | -- com.docker.compose.project | project name — groups containers com.docker.compose.service | service name within the project com.docker.compose.depends_on | e.g. migrate:service_completed_successfully:true com.docker.compose.container-number | replica index com.docker.compose.project.config_files | path(s) to the compose file(s) com.docker.compose.project.working_dir | project directory

Note that docker ps must be listed with all: true for one-shot containers to show up at all — that is likely the concrete bug behind failure mode 1.

Proposed behaviour

When an image update applies to a container that carries com.docker.compose.project:

  1. Collect all containers in that project (including stopped/exited ones) that reference the updated image.
  2. Build a DAG from com.docker.compose.depends_on across the whole project.
  3. Stop dependents, recreate the updated containers, then start in topological order.
  4. Honour the dependency conditions:
    • service_completed_successfully — run and block until exit code 0. On a non-zero exit, abort the rollout, leave dependents stopped, and report.
    • service_healthy — wait for the healthcheck to pass, with a timeout.
    • service_started — proceed immediately.
  5. Log per-project rather than per-container, so the operator sees one coherent rollout.

Simpler alternative

Detect the Compose project and shell out to docker compose --project-directory <working_dir> up -d after pulling. Far less code, and Compose does the ordering natively. Downsides: requires the compose binary and filesystem access to the config files, which is not available when freshdock runs inside a container against the socket only. Might be worth supporting as an opt-in mode (freshdock.mode=compose) alongside the native implementation.

Related: a notify-only mode

Orthogonal but relevant. Even with correct ordering, an application update can introduce a breaking change that does not crash — a config key that has been renamed, a removed entrypoint script, a changed default port. The GlitchTip case above is exactly that: the container was healthy and the feature was broken.

For stateful applications, "tell me an update exists, do not apply it" is often the behaviour you actually want. Something like:

yaml
labels:
  - "freshdock.enable=true"
  - "freshdock.mode=notify"

This gives users a middle ground between full auto-update and removing the label entirely, which is currently the only workaround.

Suggested scope

The one-shot container fix (all: true when listing, plus honouring service_completed_successfully) is small and closes the sharpest edge on its own. Full DAG ordering can follow as a second PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions