Problem
deploy.sh currently calls restart.sh for every app in APPS (except
Watchtower-labeled ones):
forappin"${apps[@]}";do
...
"$(dirname "$0")/restart.sh""$app"donerestart.sh is down.sh + up.sh — an unconditional, forced stop+start
regardless of whether anything about that app actually changed. So every
deploy (core release, config change, or in principle a scheduled check)
restarts every non-Watchtower app, whether or not it needed to. There's no
way today to do a predictable, targeted update of one service without
bypassing the pipeline and SSHing in by hand.
Discussion summary
Explored several designs for distinguishing "full stack redeploy" from
"point update of one service," roughly in this order:
- A dedicated label (Watchtower-style) marking which apps auto-update vs
need explicit control. - No label — infer predictability from pin depth (floating tag vs exact
patch vs digest pin). - A per-app state file recording
{core_version, config_version, digest},
compared on every run to decide what to restart. - Realized this state file is unnecessary:
docker compose pull +
docker compose up -d is already idempotent per service — it compares
the freshly pulled image digest and the full resolved service config
(env vars, volumes, labels, etc.) against what the running container was
created from, and only recreates what actually changed. This also
naturally handles multi-container apps (compose files with 2-3 services)
at the right granularity, for free.
Proposed change
Swap deploy.sh's per-app step from restart.sh (forced down.sh +
up.sh) to plain up.sh (docker compose pull + docker compose up -d,
no forced down.sh). This alone makes the per-app step safe to call
unconditionally on any trigger — cron, PR merge, core release — without
deciding in advance whether it's a "full reload" or a "point update."
Docker Compose's own reconciliation decides per container/service.
The only remaining decision that isn't already handled by Compose is at the
whole-server level, not per-app: whether to re-fetch a new release bundle
at all (i.e. has flightdeck_app_ref / the config repo's version actually
changed since last run) — worth gating since it's comparatively expensive
I/O (download, extract, new timestamped release dir, symlink switch), unlike
the cheap/idempotent up.sh step.
Follow-ups considered, not required for this change
- A scheduled (cron) shared workflow reusing
deploy-shared.yml's
Tailscale/SSH plumbing to run up.sh periodically across all apps —
effectively replaces Watchtower without introducing a separate service,
container, or label, since pin depth/digest pinning alone already governs
predictability (an app pinned by @sha256:... digest never drifts; a
floating tag does, on whatever cadence the cron runs). - No separate mechanism needed for "my own app republished a new version" —
it's the same floating-tag-drift case, caught by the same cron. - No separate mechanism needed for a Dependabot/Renovate-style PR bumping a
pinned tag in the config repo — the env/image pin change flows through
generate-env.sh + up.sh like any other config change, and Compose's
config-hash comparison picks it up automatically.
Problem
deploy.shcurrently callsrestart.shfor every app inAPPS(exceptWatchtower-labeled ones):
restart.shisdown.sh+up.sh— an unconditional, forced stop+startregardless of whether anything about that app actually changed. So every
deploy (core release, config change, or in principle a scheduled check)
restarts every non-Watchtower app, whether or not it needed to. There's no
way today to do a predictable, targeted update of one service without
bypassing the pipeline and SSHing in by hand.
Discussion summary
Explored several designs for distinguishing "full stack redeploy" from
"point update of one service," roughly in this order:
need explicit control.
patch vs digest pin).
{core_version, config_version, digest},compared on every run to decide what to restart.
docker compose pull+docker compose up -dis already idempotent per service — it comparesthe freshly pulled image digest and the full resolved service config
(env vars, volumes, labels, etc.) against what the running container was
created from, and only recreates what actually changed. This also
naturally handles multi-container apps (compose files with 2-3 services)
at the right granularity, for free.
Proposed change
Swap
deploy.sh's per-app step fromrestart.sh(forceddown.sh+up.sh) to plainup.sh(docker compose pull+docker compose up -d,no forced
down.sh). This alone makes the per-app step safe to callunconditionally on any trigger — cron, PR merge, core release — without
deciding in advance whether it's a "full reload" or a "point update."
Docker Compose's own reconciliation decides per container/service.
The only remaining decision that isn't already handled by Compose is at the
whole-server level, not per-app: whether to re-fetch a new release bundle
at all (i.e. has
flightdeck_app_ref/ the config repo's version actuallychanged since last run) — worth gating since it's comparatively expensive
I/O (download, extract, new timestamped release dir, symlink switch), unlike
the cheap/idempotent
up.shstep.Follow-ups considered, not required for this change
deploy-shared.yml'sTailscale/SSH plumbing to run
up.shperiodically across all apps —effectively replaces Watchtower without introducing a separate service,
container, or label, since pin depth/digest pinning alone already governs
predictability (an app pinned by
@sha256:...digest never drifts; afloating tag does, on whatever cadence the cron runs).
it's the same floating-tag-drift case, caught by the same cron.
pinned tag in the config repo — the env/image pin change flows through
generate-env.sh+up.shlike any other config change, and Compose'sconfig-hash comparison picks it up automatically.