{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreieykvujjt2y3figxigz6omxvhwmnh4rplwx4qqww44y45wcoyw5ea",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mokxb6r5sud2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreif6p3z2rd5cday3u3dlz2wqfhaxmgy3yy7gzvxwmonhoqyvp62fuy"
    },
    "mimeType": "image/webp",
    "size": 74766
  },
  "path": "/huangchengsir/you-probably-dont-need-argocd-good-enough-gitops-with-git-and-docker-compose-4jk3",
  "publishedAt": "2026-06-18T13:31:20.000Z",
  "site": "https://dev.to",
  "tags": [
    "devops",
    "go",
    "selfhosted",
    "docker",
    "https://github.com/huangchengsir/pipewright"
  ],
  "textContent": "Every time someone starts self-hosting - a homelab, a few internal services for a small team - they tend to fall down the same rabbit hole: should I run K8s? And if I run K8s, do I need ArgoCD or Flux for GitOps? Two weeks later they've read a pile of Helm charts and CRDs and still haven't deployed a single service.\n\nLet me say the quiet part out loud: **for a single host, or three-to-five machines, you do not need ArgoCD.** The part of GitOps you actually want - \"the git repo is the source of truth, changes deploy themselves, I can roll back, and there's a record\" - you can get 90% of it with git + docker compose and about twenty lines of script, with no control plane to babysit.\n\n##  What \"good-enough GitOps\" actually is\n\nGitOps boils down to two things:\n\n  1. **Declarative** : the desired state lives in git (for self-hosting, that's your `docker-compose.yml`).\n  2. **Pull-based** : the machine pulls changes and reconciles itself, instead of you SSHing in to type commands.\n\n\n\nOn one docker host, the minimal version looks like this: a webhook (or a 60-second `git fetch`) notices the tracked branch moved, and runs:\n\n\n\n    git pull --ff-only\n    docker compose pull\n    docker compose up -d --remove-orphans\n\n\n`--remove-orphans` matters: services you delete from the compose file actually get stopped, instead of lingering forever. That's it - you now have a working GitOps loop: edit compose -> push -> the machine reconciles itself.\n\n##  The two things that will actually bite you\n\nThe good-enough version runs, but there are two traps anyone who's done this knows:\n\n###  1. Zero-downtime\n\nPlain `docker compose up -d` **recreates** the container - old one stops, new one starts, with a few seconds of gap in between. Fine for background jobs, but for anything user-facing those few seconds are a 502.\n\n  * `compose up --wait` (v2.17+) at least **waits for the healthcheck** before calling the deploy a success, which catches the \"starts then crashes\" case;\n  * for true zero-downtime you end up running two service names (blue/green) behind a reverse proxy, flipping traffic once the new one is healthy, then stopping the old.\n\n\n\nDon't expect compose to do graceful rolling updates for you. It doesn't.\n\n###  2. Rollback\n\nThis is the one most people skip and the one that hurts most. If your image tag is `:latest`, then \"rollback\" means \"edit the file and pray\" - you have no idea which version last worked.\n\nThe fix: **pin image tags to the git commit SHA** (`myapp:9f8322f`, not `myapp:latest`), and **record which SHA is currently live**. Now rollback degrades to \"re-deploy the previous SHA\" - deterministic and repeatable. Without it, your GitOps quietly turns into \"git pull and hope\".\n\n##  When you should reach for a heavier tool\n\nThe boundary is clear:\n\n  * **One machine** : the twenty-line script is genuinely fine. Don't overthink it.\n  * **3+ machines, or you need an audit trail of who deployed what, when** : this is where hand-rolled scripts start accruing debt - you need concurrent multi-host deploys, unified rollback, a record of who triggered it. That's when a thicker tool starts paying for itself.\n\n\n\nBut note: that threshold is much later than most people assume. For the vast majority of self-hosting, you're still on the \"twenty-line script is fine\" side.\n\n##  I eventually packaged this up\n\nI understood all of the above, and still got tired of re-typing the webhook + pull + compose + SHA-pinning + rollback-record dance every time I set up a new machine. So I bundled it - along with the CI build step before it, agentless multi-host SSH deploys, and container management - into a single Go binary called **Pipewright** : scp one file to a server, run it, open the browser, and you get a visual pipeline + one-click deploy + a container panel, with no other runtime dependencies (the Vue frontend is compiled into the binary via `embed.FS`, SQLite by default).\n\nIt's basically the \"good-enough GitOps\" above, productized, with the zero-downtime and SHA-based rollback gaps filled in. MIT licensed, aimed at solo devs and small teams - not trying to replace GitLab for a 200-engineer org.\n\nRepo: https://github.com/huangchengsir/pipewright\n\nBut even if you never touch it, the takeaway stands: **for self-hosted GitOps, you probably don't need ArgoCD.** Start with git + compose, and only add weight when you actually hit the wall.\n\nIssues and pushback welcome.",
  "title": "You probably don't need ArgoCD - good-enough GitOps with git and docker compose"
}