| |

Docker 87 🐳 GitOps Workflows for Container Deployments using Watchtower and ArgoCD

⏱️ Estimated read: 15 min | |

GitOps is a deployment model in which the desired state of the infrastructure is stored in a Git repository, and a controller continuously reconciles the running state with the committed state. The model has two implementations that are often confused: pull-based reconciliation, where an in-cluster agent watches Git and applies changes, and image-update automation, where a sidecar watches a container registry and restarts containers when a new image tag appears. Argo CD is the first: a Kubernetes controller that compares the manifests in a Git repository against the live cluster and syncs the difference. Watchtower is the second: a Docker container that polls a registry for a new image digest and restarts the container with the new image. They solve different problems, and using one where the other is needed produces a deployment pipeline that either does not update or updates without a record.

The distinction matters because Git is the source of truth in a GitOps workflow, and Watchtower bypasses Git entirely. Watchtower pulls the new image and restarts the container, but it does not commit the new tag to the repository. The cluster now runs a version that the repository does not describe, and the next Argo CD sync would revert the change if Argo CD manages the same workload. Watchtower is useful for single-host Docker Compose deployments where there is no Kubernetes and no GitOps controller, and where the registry tag is the source of truth. Argo CD is the correct tool when Kubernetes is involved and the deployment must be auditable, reproducible, and reconciled from a Git commit .

This chapter covers the architecture of both tools, the GitOps reconciliation loop that Argo CD implements, the image-update model that Watchtower implements, the conflict that arises when both manage the same workload, and the patterns that combine them—image updater writing back to Git, Argo CD Image Updater as the Git-aware alternative to Watchtower, and the sealed-secrets and Kustomize patterns that make the Git repository the single source of truth. It assumes familiarity with Docker Compose and basic Kubernetes concepts.

Key point: Argo CD reconciles the cluster to Git; Watchtower updates containers to the latest registry image. The two are not interchangeable. A GitOps workflow requires the desired state to be committed to Git. Watchtower bypasses Git, so it is not a GitOps tool—it is an image-update tool for deployments where Git is not the source of truth. Argo CD Image Updater is the Git-aware alternative: it writes the new image tag back to Git, and Argo CD syncs it.


Why GitOps and image updates are separate concerns

GitOps is about the source of truth. The repository contains the desired state—Kubernetes manifests, Helm values, Kustomize overlays—and the controller’s job is to make the cluster match. A manual kubectl apply or a docker pull that is not recorded in Git creates drift. Argo CD detects drift and either reports it (manual sync) or corrects it (auto-sync). The audit trail is the Git history: every deployment is a commit, and every commit is a deployment .

Image updates are about the artifact. A container image is built and pushed to a registry with a new tag. The deployment needs to use the new tag. In a GitOps workflow, the tag change is a commit to the repository—the manifest’s image: field is updated, the commit is pushed, and Argo CD syncs. The commit is the deployment event, and the Git history shows who changed what and when. Argo CD Image Updater automates this commit: it watches the registry, detects a new tag that matches a configured policy, updates the manifest in Git, and lets Argo CD sync the change .

Watchtower does not commit. Watchtower polls the registry for the image that a container is running, compares the digest, and if it differs, stops the container and starts a new one with the new image. The container is updated, but the Compose file or the Kubernetes manifest still references the old tag. The running state and the declared state diverge. For a single-host Compose deployment where the Compose file is not under GitOps control, this is acceptable: the registry tag is the source of truth, and the Compose file is a starting point. For a GitOps-controlled cluster, Watchtower’s update is drift, and Argo CD will revert it on the next sync if the workload is managed .

The trade-off. Watchtower is simple: one container, one environment variable for the interval, and it works with any Docker host. It has no Git dependency, no Kubernetes requirement, and no controller to install. Argo CD is a platform: it requires a Kubernetes cluster, a Git repository with the manifests, and a sync policy. The platform provides auditability, drift detection, rollback, and multi-cluster management. The choice is between a tool that updates containers and a system that manages deployments.


a. Argo CD: the reconciliation loop and the Application resource

Argo CD runs as a set of pods in a Kubernetes cluster. The argocd-server provides the API and UI, the argocd-repo-server clones and renders the Git repository, and the argocd-application-controller compares the rendered manifests against the live cluster . The Application custom resource defines the source (repository, path, target revision) and the destination (cluster, namespace). The sync policy controls whether Argo CD applies changes automatically or waits for a manual sync .

# argocd/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/example/my-app.git
    targetRevision: HEAD
    path: k8s/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true      # Delete resources removed from Git.
      selfHeal: true   # Revert manual changes to the cluster.
    syncOptions:
      - CreateNamespace=true

The selfHeal: true setting is what makes the reconciliation continuous. If a user runs kubectl edit and changes a deployment’s image, Argo CD detects the drift on the next reconciliation and reverts it to the Git state. The prune: true setting deletes resources that are no longer in Git. Together, they enforce the Git repository as the source of truth .

The reconciliation interval is configurable and defaults to three minutes. A webhook from the Git provider can trigger an immediate sync when a commit is pushed, reducing the latency from minutes to seconds. The Argo CD documentation describes the webhook configuration for GitHub, GitLab, and Bitbucket .


b. Watchtower: the image-polling model

Watchtower runs as a container on the Docker host. It mounts the Docker socket, polls the registry for the images of the running containers, and when it detects a new digest for the same tag, it stops the container and starts a new one with the new image. The interval is configured with --interval (default 24 hours), and the scope can be limited to specific containers with labels .

# docker-compose.yaml
services:
  app:
    image: ghcr.io/example/my-app:latest
    labels:
      - "com.centurylinklabs.watchtower.enable=true"
    ports:
      - "8080:8080"

  watchtower:
    image: containrrr/watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --interval 300 --label-enable
    restart: unless-stopped

The --label-enable flag restricts Watchtower to containers with the com.centurylinklabs.watchtower.enable=true label, which prevents it from restarting the database or the reverse proxy. The --cleanup flag removes the old image after the update, which prevents the disk from filling with unused layers . The --monitor-only flag reports the available updates without applying them, which is useful for a staging environment that should not auto-update.

Watchtower’s update is a container restart, not a rolling update. The old container is stopped and the new one is started, which means a brief period of downtime unless the service has a load balancer with multiple replicas. For a single-host Compose deployment with a maintenance window, this is acceptable. For a production service with an uptime requirement, the restart is a gap in availability that Argo CD’s Kubernetes rolling updates avoid .


c. The conflict and the Git-aware alternative

When Argo CD and Watchtower manage the same workload, the result is a loop. Watchtower updates the container to a new image, Argo CD detects that the running image does not match the Git manifest, and reverts it. Watchtower sees the revert as an image change and updates again. The loop is bounded by the reconciliation interval and the Watchtower interval, but it produces a workload that oscillates between two images and a log full of sync events.

Argo CD Image Updater is the Git-aware alternative. It runs as an additional controller, watches the registry for new tags that match a configured policy, and writes the new tag back to the Git repository. Argo CD then syncs the commit. The update is recorded in Git, the history shows the change, and there is no conflict because the image updater and Argo CD agree on the source of truth .

# Application with image updater annotations.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  annotations:
    argocd-image-updater.argoproj.io/image-list: app=ghcr.io/example/my-app
    argocd-image-updater.argoproj.io/app.update-strategy: semver
    argocd-image-updater.argoproj.io/app.allow-tags: regexp:^v[0-9]+\.[0-9]+\.[0-9]+$
    argocd-image-updater.argoproj.io/write-back-method: git
spec:
  # ... source, destination, syncPolicy

The update-strategy: semver selects the highest semantic version that matches the tag pattern. The write-back-method: git commits the updated manifest to the repository. The git write-back requires a credential secret with push access to the repository. The alternative is argocd, which updates the Application resource instead of the Git repository—useful when the manifests are generated and the repository is read-only.

The comparison between the two is not close for a GitOps workflow. Watchtower is a Docker tool for Docker hosts. Argo CD Image Updater is a Kubernetes tool for Kubernetes clusters that are managed by Argo CD. The correct pairing is Argo CD + Argo CD Image Updater, not Argo CD + Watchtower. Watchtower is the right tool when there is no Kubernetes and no GitOps requirement.


Complete Example Session

# ============================================
# PART 1: ARGO CD APPLICATION
# ============================================
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/example/my-app.git
    targetRevision: HEAD
    path: k8s/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

# ============================================
# PART 2: IMAGE UPDATER ANNOTATIONS
# ============================================
# metadata:
#   annotations:
#     argocd-image-updater.argoproj.io/image-list: app=ghcr.io/example/my-app
#     argocd-image-updater.argoproj.io/app.update-strategy: semver
#     argocd-image-updater.argoproj.io/write-back-method: git

# ============================================
# PART 3: KUSTOMIZE IMAGE OVERLAY
# ============================================
# k8s/overlays/production/kustomization.yaml
# resources:
#   - ../../base
# images:
#   - name: ghcr.io/example/my-app
#     newTag: v1.4.2

# ============================================
# PART 4: WATCHTOWER FOR COMPOSE HOSTS
# ============================================
# services:
#   watchtower:
#     image: containrrr/watchtower
#     volumes:
#       - /var/run/docker.sock:/var/run/docker.sock
#     command: --interval 300 --label-enable --cleanup
#     restart: unless-stopped

# ============================================
# PART 5: LABEL-SCOPED WATCHTOWER
# ============================================
# services:
#   app:
#     image: ghcr.io/example/my-app:latest
#     labels:
#       - "com.centurylinklabs.watchtower.enable=true"

# ============================================
# PART 6: THE CONFLICT
# ============================================
# Watchtower updates container to new image.
# Argo CD detects drift, reverts to Git manifest.
# Watchtower sees old image, updates again.
# Loop continues every interval.
# Fix: use Argo CD Image Updater, not Watchtower.

# ============================================
# PART 7: ARGO CD WEBHOOK
# ============================================
# Configure GitHub webhook to argocd-server:
# https://argocd.example.com/api/webhook
# Reduces sync latency from minutes to seconds.

# ============================================
# PART 8: ROLLBACK
# ============================================
# Argo CD: revert the Git commit, sync.
# Watchtower: pin the old image tag, restart.
# Git history is the audit trail.

# ============================================
# PART 9: SECRETS
# ============================================
# Sealed Secrets: encrypt with kubeseal, commit.
# External Secrets: reference a vault, commit the reference.
# Never commit plaintext secrets.

# ============================================
# PART 10: THE COMPLETE FLOW
# ============================================
# GitOps (Kubernetes):
#   CI builds image → Argo CD Image Updater
#   commits new tag → Argo CD syncs.
#
# Non-GitOps (Docker host):
#   CI builds image → Watchtower pulls and restarts.

Quick Reference

Argo CD vs Watchtower

AspectArgo CDWatchtower
EnvironmentKubernetesDocker host
Source of truthGit repositoryRegistry tag
Update mechanismSync manifestsRestart container
Audit trailGit historyContainer logs
Drift detectionYesNo
RollbackGit revertPin old tag
Git requiredYesNo

Argo CD Sync Policy

SettingEffect
automated.pruneDelete resources removed from Git
automated.selfHealRevert manual cluster changes
syncOptions.CreateNamespaceCreate target namespace
syncOptions.ApplyOutOfSyncOnlyApply only changed resources

Watchtower Flags

FlagPurpose
--interval <seconds>Poll interval (default 86400)
--label-enableOnly containers with enable label
--cleanupRemove old images
--monitor-onlyReport, do not update
--rolling-restartRestart containers together

Image Update Strategies (Argo CD Image Updater)

StrategyBehavior
semverHighest semver matching constraint
latestHighest version by timestamp
digestTrack mutable tags by digest
nameSort by tag name alphabetically

Best Practices

✅ Do This:

# Use Argo CD for Kubernetes deployments.                    // ✅
spec.syncPolicy.automated.selfHeal: true

# Use Argo CD Image Updater, not Watchtower, for GitOps.     // ✅
argocd-image-updater.argoproj.io/write-back-method: git

# Scope Watchtower with labels on Docker hosts.              // ✅
command: --label-enable --cleanup

# Configure Git webhook for fast sync.                       // ✅
# Reduces latency from minutes to seconds.

❌ Don’t Do This:

# Run Watchtower alongside Argo CD on the same workload.     // ❌
# Produces sync loop; image oscillates.

# Use Watchtower for Kubernetes workloads.                   // ❌
# It restarts containers; does not update manifests.

# Use Argo CD without Git as source of truth.                // ❌
# Manual kubectl changes are reverted by selfHeal.

Common Pitfalls

PitfallWhy It HappensFix
Sync loopWatchtower + Argo CD on same workloadUse Image Updater
Watchtower restarts databaseNo label scoping--label-enable
Downtime on Watchtower updateContainer restart, not rollingUse Kubernetes rolling update
Argo CD reverts manual changeselfHeal: trueCommit the change to Git
Image Updater cannot pushMissing Git credential secretConfigure write-back secret

Real-World Examples

1. Argo CD Application

apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
  source: { repoURL: ..., path: ... }
  syncPolicy: { automated: { selfHeal: true, prune: true } }

2. Image Updater Annotations

argocd-image-updater.argoproj.io/image-list: app=ghcr.io/example/my-app
argocd-image-updater.argoproj.io/app.update-strategy: semver

3. Watchtower Compose

watchtower:
  image: containrrr/watchtower
  volumes: ["/var/run/docker.sock:/var/run/docker.sock"]

4. Label Scope

labels: ["com.centurylinklabs.watchtower.enable=true"]

5. Kustomize Image Override

images: [{ name: ghcr.io/example/my-app, newTag: v1.4.2 }]

6. Git Webhook

# POST https://argocd.example.com/api/webhook

7. Rollback via Git

git revert HEAD && git push

8. Rollback via Tag

image: ghcr.io/example/my-app:v1.4.1

9. Monitor Only

command: --monitor-only

10. Prune

syncPolicy: { automated: { prune: true } }

Visual

Argo CD Reconciliation

┌──────────────────────────────────────────────┐
│  GIT REPOSITORY                              │
│  manifests: image: app:v1.4.2                │
│       │                                      │
│       │ clone + render                       │
│       ▼                                      │
│  ARGO CD CONTROLLER                          │
│       │                                      │
│       │ compare                              │
│       ▼                                      │
│  LIVE CLUSTER                                │
│  deployment: image: app:v1.4.1               │
│       │                                      │
│       │ drift detected                       │
│       ▼                                      │
│  APPLY: image: app:v1.4.2                    │
│                                              │
│  selfHeal: true reverts manual changes.      │
│  Audit trail: Git history.                   │
│                                              │
└──────────────────────────────────────────────┘

Watchtower Update

┌──────────────────────────────────────────────┐
│  DOCKER HOST                                 │
│                                              │
│  Watchtower container                        │
│    polls registry every N seconds            │
│       │                                      │
│       ▼                                      │
│  New digest for app:latest?                  │
│       │                                      │
│       ├── No → wait                          │
│       │                                      │
│       ▼                                      │
│  Stop container, start new image             │
│       │                                      │
│       ▼                                      │
│  Compose file still says :latest             │
│  Running state != declared state             │
│  No Git record. No audit trail.              │
│                                              │
└──────────────────────────────────────────────┘

The Conflict

┌──────────────────────────────────────────────┐
│  SYNC LOOP                                   │
│                                              │
│  T=0: Watchtower updates to v1.4.2           │
│       │                                      │
│  T=1: Argo CD sync detects drift             │
│       reverts to Git v1.4.1                  │
│       │                                      │
│  T=2: Watchtower sees v1.4.1, updates again  │
│       │                                      │
│  T=3: Argo CD reverts again                  │
│       │                                      │
│  Cycle continues. Workload oscillates.       │
│                                              │
│  Fix: Argo CD Image Updater commits to Git.  │
│  One source of truth. No conflict.           │
│                                              │
└──────────────────────────────────────────────┘

Correct Pairing

┌──────────────────────────────────────────────┐
│  GITOPS (Kubernetes)                         │
│                                              │
│  CI → image push → Image Updater             │
│       commits new tag to Git                 │
│       → Argo CD syncs                        │
│                                              │
│  NON-GITOPS (Docker host)                    │
│                                              │
│  CI → image push → Watchtower pulls          │
│       restarts container                     │
│                                              │
│  Do not mix. Each has its environment.       │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
GitOps controllerArgo CD
Image updater (K8s)Argo CD Image Updater
Image updater (Docker host)Watchtower
Argo CD source of truthGit repository
Watchtower source of truthRegistry tag
Argo CD updateSync manifests
Watchtower updateRestart container
ConflictWatchtower + Argo CD = sync loop
Audit trailGit history (Argo CD)
RollbackGit revert (Argo CD), pin tag (Watchtower)

Key takeaways:

  • GitOps requires Git as the source of truth. Argo CD compares the manifests in Git against the live cluster and syncs the difference. selfHeal: true reverts manual changes, and prune: true deletes resources removed from Git. The audit trail is the Git history .
  • Watchtower updates containers, not Git. It polls the registry, detects a new image digest, and restarts the container. The Compose file or manifest still references the old tag. This is acceptable on a single Docker host where the registry tag is the source of truth, but it is drift in a GitOps workflow .
  • Watchtower and Argo CD conflict. Watchtower updates the container, Argo CD reverts to the Git manifest, and Watchtower updates again. The workload oscillates between two images. The fix is Argo CD Image Updater, which commits the new tag to Git and lets Argo CD sync it .
  • Argo CD Image Updater is the Git-aware alternative. It watches the registry, selects a tag by policy (semver, latest, digest), writes the tag back to Git, and records the update in the commit history. The write-back requires a credential secret with push access .
  • The choice follows the environment. Kubernetes with GitOps: Argo CD + Image Updater. Single Docker host with Compose: Watchtower with label scoping and cleanup. Mixing the two produces the sync loop, not a hybrid workflow.
  • Scope Watchtower with labels and clean up old images. --label-enable prevents the database from being restarted, and --cleanup prevents the disk from filling. These are the two flags that make Watchtower safe on a host with multiple containers .

Remember: Argo CD and Watchtower are both continuous deployment tools, but they reconcile different things. Argo CD reconciles the cluster to Git; Watchtower reconciles a container to the registry. In a GitOps workflow, Git is the source of truth, and any tool that bypasses Git creates drift. Watchtower is the right tool when there is no GitOps controller and no Kubernetes, and the registry tag is the deployment artifact. Argo CD Image Updater is the right tool when the cluster is managed by Argo CD and the deployment must be auditable. The two do not combine; they conflict. Choose the environment first, then the tool that fits it.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!