Skip to content

Replace deploy.sh's forced restart with idempotent up.sh (pull+up), drop Watchtower-style label thinking #100

Description

@ineedjet

Problem

deploy.sh currently calls restart.sh for every app in APPS (except
Watchtower-labeled ones):

for app in "${apps[@]}"; do
  ...
  "$(dirname "$0")/restart.sh" "$app"
done

restart.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:

  1. A dedicated label (Watchtower-style) marking which apps auto-update vs
    need explicit control.
  2. No label — infer predictability from pin depth (floating tag vs exact
    patch vs digest pin).
  3. A per-app state file recording {core_version, config_version, digest},
    compared on every run to decide what to restart.
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    🚩Temporary fast-track flag

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions