Skip to content

Drop Watchtower entirely - idempotent deploy already makes it redundant #118

Description

@ineedjet

Context

Split out from #100. #100's core proposal (swap forced restart for
idempotent docker compose pull && up -d, since Compose's own
image-digest + resolved-config comparison already decides per-service what
actually needs recreating) landed via #115deploy/deploy.py now runs
exactly that per app, on every deploy. #100's own "Follow-ups considered"
section already floated the next logical step but didn't commit to it:

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.

This issue is that step, now that the idempotent half is real.

Current state

Three apps carry com.centurylinklabs.watchtower.enable=true: traefik,
semaphore, and watchtower itself. deploy/deploy.py's
is_watchtower_managed check means the automated deploy path never runs
docker compose pull/up for any of them — their entire update path
depends on an actual watchtower container running on that target and
polling on its own schedule.

This has a real, already-flagged consequence (#106): hawkeye's target
doesn't include a watchtower app in its apps mapping at all, so as
implemented, traefik would never actually get started by the automated
path on a fresh hawkeye deploy — nothing else ever runs docker compose up
for it.

Proposal

Drop Watchtower as a concept entirely, now that it's redundant with what
deploy/deploy.py already does on every deploy:

  • Remove the com.centurylinklabs.watchtower.enable=true label from
    apps/traefik/docker-compose.yml and apps/semaphore/docker-compose.yml.
  • Remove (or stop recommending) apps/watchtower/ from the catalog.
  • Remove is_watchtower_managed/WATCHTOWER_LABEL and the run_apps vs
    all_apps split from deploy/deploy.py — every app in a target's apps
    mapping just gets docker compose pull && docker compose up -d --remove-orphans uniformly, no label-based special-casing anywhere.
    This also directly fixes Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's traefik gap, since there'd be nothing
    left to skip it.

What this loses, and the replacement

Watchtower's one piece of unique value: catching a spontaneous upstream
image update (a floating-tag app getting a new image pushed somewhere
else) without needing a flightdeck-side trigger at all. Drop Watchtower
without anything else, and floating-tag drift only gets picked up on the
next actual deploy (release or manual workflow_dispatch).

If that gap matters, the replacement is the scheduled-cron idea #100
already floated: a periodic workflow_dispatch-equivalent (real cron
trigger) re-running deploy-shared.yml/deploy/deploy.py for a target,
which achieves the same outcome — fresh floating-tag images pulled on some
cadence — without a Watchtower container needing Docker socket access on
every target host (a real attack-surface reduction, not just a
simplification). Not scoping that into this issue; can be its own
follow-up if wanted once this lands.

Related

#100 (idempotent reconciliation this depends on, closed), #106 (the
traefik/Watchtower gap this directly resolves).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
       blocks
      (function() {
      function addCopyButtons() {
      document.querySelectorAll('pre code').forEach(function(codeBlock) {
      if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
      codeBlock.parentElement.setAttribute('data-copy-added', 'true');
      var btn = document.createElement('button');
      btn.textContent = 'Copy';
      btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
      btn.onmouseover = function() { this.style.opacity = '1'; };
      btn.onmouseout = function() { this.style.opacity = '0.7'; };
      btn.onclick = function() {
      navigator.clipboard.writeText(codeBlock.textContent).then(function() {
      btn.textContent = 'Copied!';
      setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
      });
      };
      codeBlock.parentElement.style.position = 'relative';
      codeBlock.parentElement.appendChild(btn);
      });
      }
      addCopyButtons();
      // Re-run on dynamic content
      var observer = new MutationObserver(addCopyButtons);
      observer.observe(document.body, { childList: true, subtree: true });
      })();
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      Drop Watchtower entirely - idempotent deploy already makes it redundant · Issue #118 · rubykatzen/flightdeck · GitHub
      Skip to content

      Drop Watchtower entirely - idempotent deploy already makes it redundant #118

      Description

      @ineedjet

      Context

      Split out from #100. #100's core proposal (swap forced restart for
      idempotent docker compose pull && up -d, since Compose's own
      image-digest + resolved-config comparison already decides per-service what
      actually needs recreating) landed via #115deploy/deploy.py now runs
      exactly that per app, on every deploy. #100's own "Follow-ups considered"
      section already floated the next logical step but didn't commit to it:

      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.

      This issue is that step, now that the idempotent half is real.

      Current state

      Three apps carry com.centurylinklabs.watchtower.enable=true: traefik,
      semaphore, and watchtower itself. deploy/deploy.py's
      is_watchtower_managed check means the automated deploy path never runs
      docker compose pull/up for any of them — their entire update path
      depends on an actual watchtower container running on that target and
      polling on its own schedule.

      This has a real, already-flagged consequence (#106): hawkeye's target
      doesn't include a watchtower app in its apps mapping at all, so as
      implemented, traefik would never actually get started by the automated
      path on a fresh hawkeye deploy — nothing else ever runs docker compose up
      for it.

      Proposal

      Drop Watchtower as a concept entirely, now that it's redundant with what
      deploy/deploy.py already does on every deploy:

      • Remove the com.centurylinklabs.watchtower.enable=true label from
        apps/traefik/docker-compose.yml and apps/semaphore/docker-compose.yml.
      • Remove (or stop recommending) apps/watchtower/ from the catalog.
      • Remove is_watchtower_managed/WATCHTOWER_LABEL and the run_apps vs
        all_apps split from deploy/deploy.py — every app in a target's apps
        mapping just gets docker compose pull && docker compose up -d --remove-orphans uniformly, no label-based special-casing anywhere.
        This also directly fixes Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's traefik gap, since there'd be nothing
        left to skip it.

      What this loses, and the replacement

      Watchtower's one piece of unique value: catching a spontaneous upstream
      image update (a floating-tag app getting a new image pushed somewhere
      else) without needing a flightdeck-side trigger at all. Drop Watchtower
      without anything else, and floating-tag drift only gets picked up on the
      next actual deploy (release or manual workflow_dispatch).

      If that gap matters, the replacement is the scheduled-cron idea #100
      already floated: a periodic workflow_dispatch-equivalent (real cron
      trigger) re-running deploy-shared.yml/deploy/deploy.py for a target,
      which achieves the same outcome — fresh floating-tag images pulled on some
      cadence — without a Watchtower container needing Docker socket access on
      every target host (a real attack-surface reduction, not just a
      simplification). Not scoping that into this issue; can be its own
      follow-up if wanted once this lands.

      Related

      #100 (idempotent reconciliation this depends on, closed), #106 (the
      traefik/Watchtower gap this directly resolves).

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Drop Watchtower entirely - idempotent deploy already makes it redundant · Issue #118 · rubykatzen/flightdeck · GitHub
          Skip to content

          Drop Watchtower entirely - idempotent deploy already makes it redundant #118

          Description

          @ineedjet

          Context

          Split out from #100. #100's core proposal (swap forced restart for
          idempotent docker compose pull && up -d, since Compose's own
          image-digest + resolved-config comparison already decides per-service what
          actually needs recreating) landed via #115deploy/deploy.py now runs
          exactly that per app, on every deploy. #100's own "Follow-ups considered"
          section already floated the next logical step but didn't commit to it:

          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.

          This issue is that step, now that the idempotent half is real.

          Current state

          Three apps carry com.centurylinklabs.watchtower.enable=true: traefik,
          semaphore, and watchtower itself. deploy/deploy.py's
          is_watchtower_managed check means the automated deploy path never runs
          docker compose pull/up for any of them — their entire update path
          depends on an actual watchtower container running on that target and
          polling on its own schedule.

          This has a real, already-flagged consequence (#106): hawkeye's target
          doesn't include a watchtower app in its apps mapping at all, so as
          implemented, traefik would never actually get started by the automated
          path on a fresh hawkeye deploy — nothing else ever runs docker compose up
          for it.

          Proposal

          Drop Watchtower as a concept entirely, now that it's redundant with what
          deploy/deploy.py already does on every deploy:

          • Remove the com.centurylinklabs.watchtower.enable=true label from
            apps/traefik/docker-compose.yml and apps/semaphore/docker-compose.yml.
          • Remove (or stop recommending) apps/watchtower/ from the catalog.
          • Remove is_watchtower_managed/WATCHTOWER_LABEL and the run_apps vs
            all_apps split from deploy/deploy.py — every app in a target's apps
            mapping just gets docker compose pull && docker compose up -d --remove-orphans uniformly, no label-based special-casing anywhere.
            This also directly fixes Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's traefik gap, since there'd be nothing
            left to skip it.

          What this loses, and the replacement

          Watchtower's one piece of unique value: catching a spontaneous upstream
          image update (a floating-tag app getting a new image pushed somewhere
          else) without needing a flightdeck-side trigger at all. Drop Watchtower
          without anything else, and floating-tag drift only gets picked up on the
          next actual deploy (release or manual workflow_dispatch).

          If that gap matters, the replacement is the scheduled-cron idea #100
          already floated: a periodic workflow_dispatch-equivalent (real cron
          trigger) re-running deploy-shared.yml/deploy/deploy.py for a target,
          which achieves the same outcome — fresh floating-tag images pulled on some
          cadence — without a Watchtower container needing Docker socket access on
          every target host (a real attack-surface reduction, not just a
          simplification). Not scoping that into this issue; can be its own
          follow-up if wanted once this lands.

          Related

          #100 (idempotent reconciliation this depends on, closed), #106 (the
          traefik/Watchtower gap this directly resolves).

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Drop Watchtower entirely - idempotent deploy already makes it redundant · Issue #118 · rubykatzen/flightdeck · GitHub
              Skip to content

              Drop Watchtower entirely - idempotent deploy already makes it redundant #118

              Description

              @ineedjet

              Context

              Split out from #100. #100's core proposal (swap forced restart for
              idempotent docker compose pull && up -d, since Compose's own
              image-digest + resolved-config comparison already decides per-service what
              actually needs recreating) landed via #115deploy/deploy.py now runs
              exactly that per app, on every deploy. #100's own "Follow-ups considered"
              section already floated the next logical step but didn't commit to it:

              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.

              This issue is that step, now that the idempotent half is real.

              Current state

              Three apps carry com.centurylinklabs.watchtower.enable=true: traefik,
              semaphore, and watchtower itself. deploy/deploy.py's
              is_watchtower_managed check means the automated deploy path never runs
              docker compose pull/up for any of them — their entire update path
              depends on an actual watchtower container running on that target and
              polling on its own schedule.

              This has a real, already-flagged consequence (#106): hawkeye's target
              doesn't include a watchtower app in its apps mapping at all, so as
              implemented, traefik would never actually get started by the automated
              path on a fresh hawkeye deploy — nothing else ever runs docker compose up
              for it.

              Proposal

              Drop Watchtower as a concept entirely, now that it's redundant with what
              deploy/deploy.py already does on every deploy:

              • Remove the com.centurylinklabs.watchtower.enable=true label from
                apps/traefik/docker-compose.yml and apps/semaphore/docker-compose.yml.
              • Remove (or stop recommending) apps/watchtower/ from the catalog.
              • Remove is_watchtower_managed/WATCHTOWER_LABEL and the run_apps vs
                all_apps split from deploy/deploy.py — every app in a target's apps
                mapping just gets docker compose pull && docker compose up -d --remove-orphans uniformly, no label-based special-casing anywhere.
                This also directly fixes Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's traefik gap, since there'd be nothing
                left to skip it.

              What this loses, and the replacement

              Watchtower's one piece of unique value: catching a spontaneous upstream
              image update (a floating-tag app getting a new image pushed somewhere
              else) without needing a flightdeck-side trigger at all. Drop Watchtower
              without anything else, and floating-tag drift only gets picked up on the
              next actual deploy (release or manual workflow_dispatch).

              If that gap matters, the replacement is the scheduled-cron idea #100
              already floated: a periodic workflow_dispatch-equivalent (real cron
              trigger) re-running deploy-shared.yml/deploy/deploy.py for a target,
              which achieves the same outcome — fresh floating-tag images pulled on some
              cadence — without a Watchtower container needing Docker socket access on
              every target host (a real attack-surface reduction, not just a
              simplification). Not scoping that into this issue; can be its own
              follow-up if wanted once this lands.

              Related

              #100 (idempotent reconciliation this depends on, closed), #106 (the
              traefik/Watchtower gap this directly resolves).

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Drop Watchtower entirely - idempotent deploy already makes it redundant · Issue #118 · rubykatzen/flightdeck · GitHub
                  Skip to content

                  Drop Watchtower entirely - idempotent deploy already makes it redundant #118

                  Description

                  @ineedjet

                  Context

                  Split out from #100. #100's core proposal (swap forced restart for
                  idempotent docker compose pull && up -d, since Compose's own
                  image-digest + resolved-config comparison already decides per-service what
                  actually needs recreating) landed via #115deploy/deploy.py now runs
                  exactly that per app, on every deploy. #100's own "Follow-ups considered"
                  section already floated the next logical step but didn't commit to it:

                  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.

                  This issue is that step, now that the idempotent half is real.

                  Current state

                  Three apps carry com.centurylinklabs.watchtower.enable=true: traefik,
                  semaphore, and watchtower itself. deploy/deploy.py's
                  is_watchtower_managed check means the automated deploy path never runs
                  docker compose pull/up for any of them — their entire update path
                  depends on an actual watchtower container running on that target and
                  polling on its own schedule.

                  This has a real, already-flagged consequence (#106): hawkeye's target
                  doesn't include a watchtower app in its apps mapping at all, so as
                  implemented, traefik would never actually get started by the automated
                  path on a fresh hawkeye deploy — nothing else ever runs docker compose up
                  for it.

                  Proposal

                  Drop Watchtower as a concept entirely, now that it's redundant with what
                  deploy/deploy.py already does on every deploy:

                  • Remove the com.centurylinklabs.watchtower.enable=true label from
                    apps/traefik/docker-compose.yml and apps/semaphore/docker-compose.yml.
                  • Remove (or stop recommending) apps/watchtower/ from the catalog.
                  • Remove is_watchtower_managed/WATCHTOWER_LABEL and the run_apps vs
                    all_apps split from deploy/deploy.py — every app in a target's apps
                    mapping just gets docker compose pull && docker compose up -d --remove-orphans uniformly, no label-based special-casing anywhere.
                    This also directly fixes Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's traefik gap, since there'd be nothing
                    left to skip it.

                  What this loses, and the replacement

                  Watchtower's one piece of unique value: catching a spontaneous upstream
                  image update (a floating-tag app getting a new image pushed somewhere
                  else) without needing a flightdeck-side trigger at all. Drop Watchtower
                  without anything else, and floating-tag drift only gets picked up on the
                  next actual deploy (release or manual workflow_dispatch).

                  If that gap matters, the replacement is the scheduled-cron idea #100
                  already floated: a periodic workflow_dispatch-equivalent (real cron
                  trigger) re-running deploy-shared.yml/deploy/deploy.py for a target,
                  which achieves the same outcome — fresh floating-tag images pulled on some
                  cadence — without a Watchtower container needing Docker socket access on
                  every target host (a real attack-surface reduction, not just a
                  simplification). Not scoping that into this issue; can be its own
                  follow-up if wanted once this lands.

                  Related

                  #100 (idempotent reconciliation this depends on, closed), #106 (the
                  traefik/Watchtower gap this directly resolves).

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Drop Watchtower entirely - idempotent deploy already makes it redundant · Issue #118 · rubykatzen/flightdeck · GitHub
                      Skip to content

                      Drop Watchtower entirely - idempotent deploy already makes it redundant #118

                      Description

                      @ineedjet

                      Context

                      Split out from #100. #100's core proposal (swap forced restart for
                      idempotent docker compose pull && up -d, since Compose's own
                      image-digest + resolved-config comparison already decides per-service what
                      actually needs recreating) landed via #115deploy/deploy.py now runs
                      exactly that per app, on every deploy. #100's own "Follow-ups considered"
                      section already floated the next logical step but didn't commit to it:

                      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.

                      This issue is that step, now that the idempotent half is real.

                      Current state

                      Three apps carry com.centurylinklabs.watchtower.enable=true: traefik,
                      semaphore, and watchtower itself. deploy/deploy.py's
                      is_watchtower_managed check means the automated deploy path never runs
                      docker compose pull/up for any of them — their entire update path
                      depends on an actual watchtower container running on that target and
                      polling on its own schedule.

                      This has a real, already-flagged consequence (#106): hawkeye's target
                      doesn't include a watchtower app in its apps mapping at all, so as
                      implemented, traefik would never actually get started by the automated
                      path on a fresh hawkeye deploy — nothing else ever runs docker compose up
                      for it.

                      Proposal

                      Drop Watchtower as a concept entirely, now that it's redundant with what
                      deploy/deploy.py already does on every deploy:

                      • Remove the com.centurylinklabs.watchtower.enable=true label from
                        apps/traefik/docker-compose.yml and apps/semaphore/docker-compose.yml.
                      • Remove (or stop recommending) apps/watchtower/ from the catalog.
                      • Remove is_watchtower_managed/WATCHTOWER_LABEL and the run_apps vs
                        all_apps split from deploy/deploy.py — every app in a target's apps
                        mapping just gets docker compose pull && docker compose up -d --remove-orphans uniformly, no label-based special-casing anywhere.
                        This also directly fixes Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's traefik gap, since there'd be nothing
                        left to skip it.

                      What this loses, and the replacement

                      Watchtower's one piece of unique value: catching a spontaneous upstream
                      image update (a floating-tag app getting a new image pushed somewhere
                      else) without needing a flightdeck-side trigger at all. Drop Watchtower
                      without anything else, and floating-tag drift only gets picked up on the
                      next actual deploy (release or manual workflow_dispatch).

                      If that gap matters, the replacement is the scheduled-cron idea #100
                      already floated: a periodic workflow_dispatch-equivalent (real cron
                      trigger) re-running deploy-shared.yml/deploy/deploy.py for a target,
                      which achieves the same outcome — fresh floating-tag images pulled on some
                      cadence — without a Watchtower container needing Docker socket access on
                      every target host (a real attack-surface reduction, not just a
                      simplification). Not scoping that into this issue; can be its own
                      follow-up if wanted once this lands.

                      Related

                      #100 (idempotent reconciliation this depends on, closed), #106 (the
                      traefik/Watchtower gap this directly resolves).

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Drop Watchtower entirely - idempotent deploy already makes it redundant · Issue #118 · rubykatzen/flightdeck · GitHub
                          Skip to content

                          Drop Watchtower entirely - idempotent deploy already makes it redundant #118

                          Description

                          @ineedjet

                          Context

                          Split out from #100. #100's core proposal (swap forced restart for
                          idempotent docker compose pull && up -d, since Compose's own
                          image-digest + resolved-config comparison already decides per-service what
                          actually needs recreating) landed via #115deploy/deploy.py now runs
                          exactly that per app, on every deploy. #100's own "Follow-ups considered"
                          section already floated the next logical step but didn't commit to it:

                          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.

                          This issue is that step, now that the idempotent half is real.

                          Current state

                          Three apps carry com.centurylinklabs.watchtower.enable=true: traefik,
                          semaphore, and watchtower itself. deploy/deploy.py's
                          is_watchtower_managed check means the automated deploy path never runs
                          docker compose pull/up for any of them — their entire update path
                          depends on an actual watchtower container running on that target and
                          polling on its own schedule.

                          This has a real, already-flagged consequence (#106): hawkeye's target
                          doesn't include a watchtower app in its apps mapping at all, so as
                          implemented, traefik would never actually get started by the automated
                          path on a fresh hawkeye deploy — nothing else ever runs docker compose up
                          for it.

                          Proposal

                          Drop Watchtower as a concept entirely, now that it's redundant with what
                          deploy/deploy.py already does on every deploy:

                          • Remove the com.centurylinklabs.watchtower.enable=true label from
                            apps/traefik/docker-compose.yml and apps/semaphore/docker-compose.yml.
                          • Remove (or stop recommending) apps/watchtower/ from the catalog.
                          • Remove is_watchtower_managed/WATCHTOWER_LABEL and the run_apps vs
                            all_apps split from deploy/deploy.py — every app in a target's apps
                            mapping just gets docker compose pull && docker compose up -d --remove-orphans uniformly, no label-based special-casing anywhere.
                            This also directly fixes Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's traefik gap, since there'd be nothing
                            left to skip it.

                          What this loses, and the replacement

                          Watchtower's one piece of unique value: catching a spontaneous upstream
                          image update (a floating-tag app getting a new image pushed somewhere
                          else) without needing a flightdeck-side trigger at all. Drop Watchtower
                          without anything else, and floating-tag drift only gets picked up on the
                          next actual deploy (release or manual workflow_dispatch).

                          If that gap matters, the replacement is the scheduled-cron idea #100
                          already floated: a periodic workflow_dispatch-equivalent (real cron
                          trigger) re-running deploy-shared.yml/deploy/deploy.py for a target,
                          which achieves the same outcome — fresh floating-tag images pulled on some
                          cadence — without a Watchtower container needing Docker socket access on
                          every target host (a real attack-surface reduction, not just a
                          simplification). Not scoping that into this issue; can be its own
                          follow-up if wanted once this lands.

                          Related

                          #100 (idempotent reconciliation this depends on, closed), #106 (the
                          traefik/Watchtower gap this directly resolves).

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Drop Watchtower entirely - idempotent deploy already makes it redundant · Issue #118 · rubykatzen/flightdeck · GitHub
                              Skip to content

                              Drop Watchtower entirely - idempotent deploy already makes it redundant #118

                              Description

                              @ineedjet

                              Context

                              Split out from #100. #100's core proposal (swap forced restart for
                              idempotent docker compose pull && up -d, since Compose's own
                              image-digest + resolved-config comparison already decides per-service what
                              actually needs recreating) landed via #115deploy/deploy.py now runs
                              exactly that per app, on every deploy. #100's own "Follow-ups considered"
                              section already floated the next logical step but didn't commit to it:

                              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.

                              This issue is that step, now that the idempotent half is real.

                              Current state

                              Three apps carry com.centurylinklabs.watchtower.enable=true: traefik,
                              semaphore, and watchtower itself. deploy/deploy.py's
                              is_watchtower_managed check means the automated deploy path never runs
                              docker compose pull/up for any of them — their entire update path
                              depends on an actual watchtower container running on that target and
                              polling on its own schedule.

                              This has a real, already-flagged consequence (#106): hawkeye's target
                              doesn't include a watchtower app in its apps mapping at all, so as
                              implemented, traefik would never actually get started by the automated
                              path on a fresh hawkeye deploy — nothing else ever runs docker compose up
                              for it.

                              Proposal

                              Drop Watchtower as a concept entirely, now that it's redundant with what
                              deploy/deploy.py already does on every deploy:

                              • Remove the com.centurylinklabs.watchtower.enable=true label from
                                apps/traefik/docker-compose.yml and apps/semaphore/docker-compose.yml.
                              • Remove (or stop recommending) apps/watchtower/ from the catalog.
                              • Remove is_watchtower_managed/WATCHTOWER_LABEL and the run_apps vs
                                all_apps split from deploy/deploy.py — every app in a target's apps
                                mapping just gets docker compose pull && docker compose up -d --remove-orphans uniformly, no label-based special-casing anywhere.
                                This also directly fixes Normalize the hawkeye/rybbit deploy: configure prerequisites and get the first real deployment running #106's traefik gap, since there'd be nothing
                                left to skip it.

                              What this loses, and the replacement

                              Watchtower's one piece of unique value: catching a spontaneous upstream
                              image update (a floating-tag app getting a new image pushed somewhere
                              else) without needing a flightdeck-side trigger at all. Drop Watchtower
                              without anything else, and floating-tag drift only gets picked up on the
                              next actual deploy (release or manual workflow_dispatch).

                              If that gap matters, the replacement is the scheduled-cron idea #100
                              already floated: a periodic workflow_dispatch-equivalent (real cron
                              trigger) re-running deploy-shared.yml/deploy/deploy.py for a target,
                              which achieves the same outcome — fresh floating-tag images pulled on some
                              cadence — without a Watchtower container needing Docker socket access on
                              every target host (a real attack-surface reduction, not just a
                              simplification). Not scoping that into this issue; can be its own
                              follow-up if wanted once this lands.

                              Related

                              #100 (idempotent reconciliation this depends on, closed), #106 (the
                              traefik/Watchtower gap this directly resolves).

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions