Deploy Django with Argo CD (GitOps)
Deploy Django to Kubernetes with Argo CD: a config repository, an Application with automated sync, PreSync migration hooks, CI-driven image tag updates, and rollback by git revert.
Problem statement
When CI pipelines deploy to Kubernetes with kubectl apply or helm upgrade, the cluster's state is whatever the last successful pipeline left behind. Manual hotfixes drift from Git, credentials for the cluster live in CI, and answering "what is running in production and why?" means reading pipeline logs.
GitOps flips the direction: the desired state lives in a Git repository, and a controller in the cluster (Argo CD) pulls it and keeps the cluster in sync. For Django, that works well as long as three details are handled: how a new image tag reaches Git, how migrations run before new pods, and how rollback works. This guide covers all three.
- Keep Kubernetes manifests or a Helm chart in a config repository (or a
deploy/directory) separate from application code changes. - Install Argo CD and create an
Applicationpointing at that path, with automated sync, prune, and self-heal. - Annotate the migration Job with
argocd.argoproj.io/hook: PreSyncso it runs before the Deployment updates. - In CI, build and push the image tagged with the commit SHA, then commit the new tag to the config repository.
- Argo CD detects the commit, runs the PreSync migration, and rolls out the new pods.
- Roll back by reverting the tag commit; Argo CD syncs the previous version.
Step-by-step solution
1. Install Argo CD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
kubectl -n argocd rollout status deployment/argocd-server
argocd admin initial-password -n argocd
Log in with the CLI (argocd login <server>), change the admin password, and set up SSO for a team. Pin the install manifest to a specific version in production rather than stable, so upgrades are deliberate.
2. Structure the config repository
myapp-config/
chart/ # Helm chart for Django (Deployment, Service, Ingress, migrate Job)
envs/
staging/values.yaml
production/values.yaml
Each environment's values file holds the image tag:
# envs/production/values.yaml
image:
repository: ghcr.io/example/myapp
tag: 3f9c2a1
replicaCount: 3
A separate repository keeps CI's automated tag commits out of the application history and lets you restrict who can change production.
3. Create the Application
# argocd/myapp-production.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp-production
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/example/myapp-config.git
targetRevision: main
path: chart
helm:
valueFiles:
- ../envs/production/values.yaml
destination:
server: https://kubernetes.default.svc
namespace: myapp
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
kubectl apply -f argocd/myapp-production.yaml
argocd app get myapp-production
prunedeletes resources removed from Git.selfHealreverts manual changes made directly in the cluster.- Private repositories need credentials registered with
argocd repo add.
Argo CD renders Helm charts itself with helm template; it does not create Helm releases, so helm list does not show the app.
4. Run migrations as a PreSync hook
# chart/templates/migrate-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
generateName: myapp-migrate-
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
backoffLimit: 0
activeDeadlineSeconds: 600
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["python", "manage.py", "migrate", "--noinput"]
envFrom:
- secretRef: { name: myapp-secrets }
- configMapRef: { name: myapp-config }
A PreSync hook runs before Argo CD applies the other manifests, and the sync waits for it. If the Job fails, the sync fails and the running Deployment stays on the old version. Charts that already use Helm's pre-install/pre-upgrade hook annotations also work, because Argo CD maps them to PreSync.
The ConfigMap referenced by the Job must already exist; on the very first sync, create it in an earlier sync wave or reference only the Secret.
5. Update the tag from CI
After tests pass and the image is pushed, CI commits the new tag:
# .github/workflows/release.yml (excerpt)
- name: Bump production image tag
env:
GH_TOKEN: ${{ secrets.CONFIG_REPO_TOKEN }}
run: |
git clone https://x-access-token:${GH_TOKEN}@github.com/example/myapp-config.git
cd myapp-config
yq -i '.image.tag = "${{ github.sha }}"' envs/production/values.yaml
git config user.name "release-bot"
git config user.email "release-bot@example.com"
git commit -am "production: myapp ${{ github.sha }}"
git push
CI never needs cluster credentials; it only needs write access to the config repository. For a manual approval step, have CI open a pull request against the config repository instead of pushing to main.
6. Observe and roll back
argocd app get myapp-production
argocd app history myapp-production
kubectl -n myapp rollout status deployment/myapp
To roll back, revert the tag commit in the config repository and push. Argo CD syncs the previous tag. argocd app rollback exists, but it cannot be used while automated sync is enabled, and Git would no longer match the cluster, so reverting in Git is the cleaner path.
Verification check:
argocd app getshowsSyncedandHealthy.- The running pods use the expected image:
kubectl -n myapp get pods -o jsonpath='{..image}'. - The migrate Job for the release completed.
curl -sI https://example.com/healthz/returns200.
Explanation
Argo CD continuously compares the rendered manifests from Git with the live objects in the cluster. When they differ, it syncs: first PreSync hooks, then the resources in sync-wave order, then PostSync hooks. Because deploys are Git commits, the audit trail is the commit history, access control is repository permissions, and recovery is a revert. Django's ordering requirement (migrate before new code) maps directly onto the PreSync phase.
Edge cases / notes
Backward-compatible migrations
During a rolling update, old pods keep serving against the migrated schema. Rolling back by reverting the tag also runs old code against the new schema. Both are only safe with expand-and-contract migrations.
Secrets
Do not commit plain Secrets to the config repository. Use Sealed Secrets, the External Secrets Operator, or a secrets manager plugin so Git holds only encrypted or referenced values.
Image updater
Argo CD Image Updater can watch a registry and write new tags back to Git automatically. It saves CI steps but moves release decisions out of your pipeline; many teams prefer explicit commits from CI.
When to automate this
GitOps is itself the automation. What remains worth adding is a post-sync smoke test (a PostSync hook Job that calls the health endpoint and a critical page) and notifications on sync failures to the team channel.
Next steps
- Package the manifests with Deploy Django to Kubernetes with a Helm Chart.
- Review the base objects in Deploy Django on Kubernetes.
- Build and push images with Deploy Django with GitHub Actions CI/CD.
- Plan reversions with How to Roll Back a Django Release Safely.
FAQ
How do I run Django migrations with Argo CD?
Use a Kubernetes Job annotated with argocd.argoproj.io/hook: PreSync. It runs before the Deployment is updated, and a failure stops the sync.
Should CI deploy directly or commit to a config repository?
With GitOps, CI builds and pushes the image and commits the new tag. Argo CD performs the deployment, so CI does not need cluster credentials.
How do I roll back a Django release with Argo CD?
Revert the commit that changed the image tag. Argo CD syncs the previous version. Database migrations are not reversed.
Why does helm list not show my Argo CD application?
Argo CD renders Helm charts to plain manifests and applies them itself, so no Helm release exists in the cluster.