Deploy a Django SaaS
Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.
Hello-world deployments hide everything that makes production interesting. This guide deploys UpCheck — a real, open-source Django SaaS (an uptime monitor with signups, background checks, and public status pages) — end to end: database, background workers, scheduled tasks, migrations, HTTPS. It’s the same setup that runs the live instance at upcheck.paasbox.com, and the shape generalizes to almost any Django (or Rails, or Laravel) application.
The shape of a real app
Section titled “The shape of a real app”One container image (ghcr.io/mitja/upcheck) runs four roles via command overrides:
| Role | What it runs | Kubernetes shape |
|---|---|---|
| web | gunicorn + WhiteNoise | Deployment + Service |
| worker | Celery worker (the actual uptime checks) | Deployment |
| beat | Celery beat (schedules the checks) | Deployment, single replica |
| migrate | manage.py migrate + task bootstrap |
one-off Job, per release |
Plus PostgreSQL via the CloudNativePG operator
and a single-instance Redis as the Celery broker. The manifests live in the repo under
deploy/ as a kustomize base with overlays (kind for local, paasbox for production).
What you’ll need
Section titled “What you’ll need”- A cluster (quickstart — one
cpx32worker is plenty for UpCheck),kubectl,helm, and a domain you control. - The repo:
git clone https://github.com/mitja/upcheck && cd upcheck
1. Install the cluster add-ons (once)
Section titled “1. Install the cluster add-ons (once)”Three add-ons every production cluster wants anyway — the Ingress + TLS guide explains each in depth:
# CloudNativePG operatorkubectl apply --server-side -f \ https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.30/releases/cnpg-1.30.0.yaml
# ingress-nginx — provisions a Hetzner load balancer (on your Hetzner bill)helm upgrade --install ingress-nginx ingress-nginx \ --repo https://kubernetes.github.io/ingress-nginx \ --namespace ingress-nginx --create-namespace \ --set controller.service.annotations."load-balancer\.hetzner\.cloud/location"=nbg1
# cert-manager + a Let's Encrypt issuer (edit the email in the file first)helm upgrade --install cert-manager cert-manager \ --repo https://charts.jetstack.io \ --namespace cert-manager --create-namespace \ --set crds.enabled=truekubectl apply -f deploy/cluster-issuer.yaml2. Point DNS at the load balancer
Section titled “2. Point DNS at the load balancer”kubectl -n ingress-nginx get svc ingress-nginx-controller \ -o jsonpath='{.status.loadBalancer.ingress[0].ip}'Create an A record for your host pointing at that IP, and change the hostname in
deploy/overlays/paasbox/ from upcheck.paasbox.com to your own domain.
3. Create the secrets (out of band, never committed)
Section titled “3. Create the secrets (out of band, never committed)”kubectl create namespace upcheckkubectl -n upcheck create secret generic upcheck-secrets \ --from-literal=SECRET_KEY="$(python3 -c 'import secrets; print(secrets.token_urlsafe(50))')"That’s the minimum — UpCheck runs its billing through Polar, and without the optional
POLAR_* keys it simply runs with all accounts on the free plan, which is exactly what you want
for a walkthrough. Note what’s not here: no database password. CNPG generates the pg-app
Secret itself, and the app consumes its uri key as DATABASE_URL.
4. Deploy — and run migrations the honest way
Section titled “4. Deploy — and run migrations the honest way”kubectl apply -k deploy/overlays/paasbox
# wait for Postgres, then migratekubectl -n upcheck wait --for=condition=Ready cluster/pg --timeout=420skubectl -n upcheck delete job upcheck-migrate --ignore-not-foundkubectl apply -f deploy/overlays/paasbox/migrate-job.yamlkubectl -n upcheck wait --for=condition=complete job/upcheck-migrate --timeout=300sThe migrate Job is deliberately not part of the kustomization: Kubernetes Jobs are immutable,
so “apply everything” can’t rerun them. Delete-then-apply per release is the boring, reliable
pattern — it runs manage.py migrate and registers the beat schedule, and the web/worker/beat
Deployments start serving once the schema exists.
5. Verify
Section titled “5. Verify”kubectl -n upcheck get podscurl -I https://upcheck.example.com # your domain: valid Let's Encrypt cert + 200Open the site, sign up, create a monitor against example.com, and watch the worker check it on
schedule — a complete SaaS loop: web tier, async workers, scheduler, database, TLS.
6. Releasing updates
Section titled “6. Releasing updates”CI publishes ghcr.io/mitja/upcheck:latest and :sha-<commit> on every commit. For reproducible
deploys, pin newTag in overlays/paasbox/kustomization.yaml (and the image in its
migrate-job.yaml) to a sha- tag, then:
kubectl apply -k deploy/overlays/paasboxkubectl -n upcheck delete job upcheck-migrate --ignore-not-foundkubectl apply -f deploy/overlays/paasbox/migrate-job.yamlkubectl -n upcheck rollout restart deploy/web deploy/worker deploy/beatTo run that from CI instead of a laptop, the CI/CD with GitHub Actions guide shows the same commands driven by a short-lived kubeconfig.
Adapting this to your app
Section titled “Adapting this to your app”The UpCheck manifests are a template worth stealing: one image with command overrides per role, a
CNPG cluster with the generated pg-app secret, secrets created out of band, an immutable-Job
migration step, and an Ingress with the cert-manager.io/cluster-issuer annotation. Swap the
image, the hostname, and the env vars, and the same deploy/ tree runs most web applications.
Two honest footnotes: set resource requests on every role (the repo’s manifests do — it’s what lets the scheduler pack a small cluster efficiently), and when your walkthrough cluster is idle, hibernate it rather than leaving it to bill.
Clean up
Section titled “Clean up”kubectl delete namespace upcheckhelm -n cert-manager uninstall cert-managerhelm -n ingress-nginx uninstall ingress-nginx # deletes the Hetzner load balancer