By this point you likely have Terraform/OpenTofu files describing your VMs, Ansible playbooks configuring them, and Compose files (or Swarm stacks, or Kubernetes manifests) describing what runs on them - all, hopefully, in a Git repository. GitOps is the next logical step: instead of you running docker compose up -d or tofu apply by hand after editing a file, a push to the repo triggers the deploy automatically.

The core idea

A GitOps controller watches your repo. When it sees a change on the branch it tracks, it reconciles the running state to match - pulling new images, applying new configuration, restarting what changed. The repo becomes the single place you look to know what's actually running, because nothing is supposed to be running that isn't reflected there.

Two realistic approaches for a homelab

  • Portainer with GitOps stacks - if you're running plain Docker or Swarm, Portainer can point a stack at a Git repo and redeploy on a schedule or webhook when it changes. Low setup cost since you likely already have Portainer for container management, and it fits directly on top of the Compose-based setup from earlier tiers with no new orchestration layer.
  • ArgoCD - the standard GitOps tool in the Kubernetes world, continuously reconciling a cluster's actual state against manifests in a repo, with a web UI showing drift. Only relevant if you went the k3s/Kubernetes route in Container Orchestration - it's Kubernetes-native and not a fit for plain Compose or Swarm.

A minimal Portainer GitOps example

In Portainer: Stacks → Add stack → Repository, pointing at:

Repository URL: https://github.com/yourname/homelab-compose
Reference: refs/heads/main
Compose path: media/docker-compose.yml

Enable GitOps updates with either a polling interval or a webhook from your Git host, so a push to main redeploys media/ without you touching the server.

git add media/docker-compose.yml
git commit -m "Bump jellyfin to 10.9"
git push
  • A webhook is faster (near-instant redeploy) but needs your Git host able to reach Portainer's URL - fine for a self-hosted Gitea/Forgejo instance on your LAN, more involved if your repo lives on github.com and Portainer isn't reachable from the internet.
  • Polling is simpler to set up (no inbound webhook needed) at the cost of a delay equal to the polling interval before changes apply.

⚠️ Risk: an automatic-deploy pipeline will faithfully deploy a mistake just as fast as a good change. Test compose changes on a non-production stack or a scratch VM before pointing GitOps at anything you'd be annoyed to lose for an evening, and keep git revert as your fastest rollback path.

Why bother, running solo

The honest case here is smaller than "team collaboration" (the usual pitch for GitOps) - it's really about:

  • One less manual step to forget - no SSHing in to pull and redeploy after every config change.
  • A real audit trail - git log on the compose repo tells you exactly when and why something changed, which is useful when debugging "this broke sometime in the last two weeks."
  • Consistency with the IaC/config-management layers - if Terraform and Ansible are already Git-driven, having the deployed services follow the same pattern means one mental model across the whole stack instead of three different workflows.

The cost: another moving part (the GitOps controller itself) that can fail or drift, and a habit change - config changes only "count" once they're committed and pushed, which takes discipline if you're used to quick manual edits on the server.

Next: Advanced Networking.