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.

Django GitOps with Argo CDCI buildpush image by SHAConfig repobump image tagArgo CD syncPreSync migrateClusternew pods roll outGit is the source of truth: to roll back, revert the commit that changed the tag.
  1. Keep Kubernetes manifests or a Helm chart in a config repository (or a deploy/ directory) separate from application code changes.
  2. Install Argo CD and create an Application pointing at that path, with automated sync, prune, and self-heal.
  3. Annotate the migration Job with argocd.argoproj.io/hook: PreSync so it runs before the Deployment updates.
  4. In CI, build and push the image tagged with the commit SHA, then commit the new tag to the config repository.
  5. Argo CD detects the commit, runs the PreSync migration, and rolls out the new pods.
  6. 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
code in one repo, desired state in anotherApp repositoryDjango code and Dockerfile✓CI builds and pushes image:sha✓CI commits the new tag to configConfig repositorymanifests or Kustomize overlays✓Argo CD watches it✓git history is the deploy logNobody runs kubectl apply by hand; a commit is a deploydjango-deployment.com
Separate repositories keep CI commits from triggering an endless build loop.

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
  • prune deletes resources removed from Git.
  • selfHeal reverts 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 }
migrations as a PreSync hookkind: Jobmetadata:annotations:argocd.argoproj.io/hook:PreSyncargocd.argoproj.io/hook-delete-policy:BeforeHookCreationspec:backoffLimit:01231Runs before the Deployment changes; failure stops the sync.2The old Job is deleted so the next sync can create it again.3No automatic retries of a half-applied migration.The Job uses the same image tag as the Deployment it precedesdjango-deployment.com
Read the Job logs in the Argo CD UI when a sync stops at this step.

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
from merge to healthyMerge to mainapp repoImage pushed, tagcommittedconfig repoArgo CD sees OutOfSyncpoll or webhookPreSync migrate Jobmust succeedRollout Healthyor revert the commitRolling back is reverting the tag commit in the config repodjango-deployment.com
The Argo CD UI rollback is temporary: auto-sync reapplies whatever Git says.

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 get shows Synced and Healthy.
  • 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/ returns 200.

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

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.

Learning path · step 14 of 16

Django on Kubernetes

Keep going