A simplified Argo CD-style reconciliation loop for Docker Compose. If you deploy with docker compose instead of Kubernetes, you don't get automatic image updates out of the box. This fills that gap: a cron job polls GHCR for new semver tags, opens a PR when versions drift, and deploys on merge — giving you the same "desired state → actual state" loop that Argo CD provides, using only GitHub Actions and a .env file.
Image tags live in a .env file alongside docker-compose.yaml — Docker Compose loads it automatically. CI updates .env with simple source/sed (no yq needed).
Note: The
.envfile is committed to git and contains only image tags — never secrets. See Secrets for how to handle sensitive values.
templates/gitops-ci/ # Reusable templates with {{PLACEHOLDER}} syntax
demo/ # Filled-in example using ghcr.io/aquasecurity/trivy
.github/ # Live working workflows (manual dispatch)
test/smoke.sh # Local validation script
flowchart TD
subgraph trigger ["Trigger — ci-pr-trigger"]
A["Poll GHCR on cron"] --> B["Filter for latest semver tag"]
end
subgraph pr ["PR — ci-create-release-pr"]
C["source .env to read current tags"] --> D["sed to update changed tags"]
D --> E["Create PR + auto-merge"]
end
subgraph deploy ["Deploy — ci-deployment"]
F["PR merges → paths filter"] --> G["Copy files + docker compose up"]
G --> H["Slack notify with versions"]
end
trigger --> pr --> deploy
Three deployment strategies are included:
| Strategy | Workflow | Best for |
|---|---|---|
| EC2 / AWS | ci-deployment.yaml |
AWS infrastructure |
| SSH | ci-deployment-ssh.yaml |
Home servers, VPS |
| Self-Hosted Runner | ci-deployment-self-hosted.yaml |
On-prem, home servers |
Copy templates into your repo and replace {{PLACEHOLDER}} values:
cp -r templates/gitops-ci/.github /path/to/your/repo/
cp templates/gitops-ci/deployment.sh /path/to/your/repo/projects/<name>/
cp templates/gitops-ci/docker-compose.yaml /path/to/your/repo/projects/<name>/
cp templates/gitops-ci/.env /path/to/your/repo/projects/<name>/See the template README for full setup instructions (placeholders, secrets, customization).
The demo/ directory is a working example using ghcr.io/aquasecurity/trivy — a public image with many semver tags. Backend pinned to 0.24.0, frontend to 0.23.0 in .env so tag discovery always finds newer versions.
The .github/ directory runs the same demo workflows on this repo (cron disabled, deployment is echo-only). Trigger manually:
gh workflow run "CI: Dry-run validation"
gh workflow run "CI: Check for new releases"
gh workflow run "CI: Deploy (Demo)"./test/smoke.sh
# Requires: shellcheck (gracefully skips if missing)
# Optional: yq (used only for YAML lint validation)CI runs automatically on push to main and PRs with 5 parallel validation jobs.
The .env file is for image tags only and is committed to git. Keep secrets separate:
# docker-compose.yaml
services:
backend:
image: "ghcr.io/myorg/backend:${BACKEND_TAG}"
env_file:
- .env.secrets # API keys, DB passwords — gitignored, not managed by CI
environment:
- NODE_ENV=production# .gitignore
.env.secretsThis way CI manages .env (tags) and your secrets stay in .env.secrets (or are injected via the host environment, a secrets manager, etc.). The two files serve different purposes and have different lifecycles.
- Two-layer change detection — Semantic tag comparison + GitHub
pathsfilter - GitOps via PR — Audit trail and easy rollbacks
- GHCR tag discovery — Paginated API with cross-org token exchange and semver filtering
- Auto-merge with retry — Handles branch protection race conditions
- Scheduled run cleanup — Keeps the Actions tab clean