Full-stack web application with a Domain-Driven Design backend and a React/TypeScript frontend, fully containerised with Docker. Kubernetes manifests and a production Helm chart are included for cluster deployment.
| Layer | Technology |
|---|---|
| Backend API | Python 3.12, Django 5, Django REST Framework |
| Frontend | React 18, TypeScript, Vite 5 |
| Database | PostgreSQL 16 |
| Cache | Redis 7 |
| Web server (prod) | nginx 1.25 |
| Containerisation | Docker, Docker Compose |
| Orchestration | Kubernetes + Helm 3 |
The backend follows Hexagonal Architecture / Domain-Driven Design with four strict layers. The dependency arrow points inward only — the domain never imports from any outer layer.
interfaces → application → domain
↑
infrastructure
| Layer | Location | Responsibility |
|---|---|---|
| Domain | backend/domain/ |
Business rules, entities, value objects, abstract ports. Zero framework dependencies. |
| Application | backend/application/ |
Use cases (execute() pattern). Orchestrates domain objects. No Django. |
| Infrastructure | backend/infrastructure/ |
Concrete adapters: PostgreSQL, Redis. Django ORM models live here only. |
| Interfaces | backend/interfaces/ |
DRF views, serializers, URL routing. HTTP boundary. |
.
├── backend/
│ ├── domain/ # Business rules — pure Python
│ │ ├── shared/ # Base Entity, ValueObject, exceptions
│ │ └── health/ # Health bounded context
│ ├── application/ # Use cases and DTOs
│ ├── infrastructure/ # Django/Redis adapters and ORM models
│ │ └── persistence/ # All Django models live here
│ ├── interfaces/api/ # DRF views, serializers, urls
│ ├── config/ # Django settings (split by environment)
│ │ └── settings/ # dev.py / staging.py / prod.py
│ └── scripts/ # Dev tooling (layer import validator)
├── frontend/
│ └── src/
├── k8s/
│ ├── base/ # Kustomize base: deployments, services, RBAC, NetworkPolicies, PDBs
│ ├── cert-manager/ # Let's Encrypt ClusterIssuers
│ ├── helm/django-react/ # Production Helm chart
│ └── overlays/ # Per-environment Kustomize overrides (local/qa/staging/production)
├── nginx/ # nginx config for prod/staging
├── docker-compose.yml # Dev (hot reload)
├── docker-compose.staging.yml
└── docker-compose.prod.yml
- Docker Desktop (includes Compose v2) — or Docker Engine + the
docker composeplugin - Ports 5173 and 8000 free on your machine
Verify before starting:
docker --version # Docker version 24+ recommended
docker compose version # v2.x required (not legacy docker-compose)
lsof -i :5173 -i :8000 # should return nothinggit clone <repo-url>
cd django-react.env.dev is pre-configured and works out of the box — no changes needed for local development:
DATABASE_URL=postgres://postgres:postgres@db:5432/myapp_dev
REDIS_URL=redis://redis:6379/0
CORS_ALLOWED_ORIGINS=http://localhost:5173
Never edit .env.staging or .env.prod locally. Those are injected by CI at deploy time.
docker compose up --buildThis builds four containers and starts them in order:
- db (PostgreSQL) — health-checked before anything else starts
- redis — health-checked before the backend starts
- backend — waits for DB, runs
migrate --noinput, then starts the Django dev server - frontend — starts the Vite dev server with HMR
First run takes ~2 minutes (pulling base images, installing deps). Subsequent starts are under 10 seconds.
# In a second terminal — check all four containers are Up (not Restarting)
docker compose ps
# Quick health check
curl http://localhost:8000/api/health/Expected response:
{
"status": "ok",
"services": [
{ "name": "db", "status": "ok" },
{ "name": "cache", "status": "ok" }
]
}| Service | URL |
|---|---|
| Frontend (Vite HMR) | http://localhost:5173 |
| Backend health | http://localhost:8000/api/health/ |
| Backend ping | http://localhost:8000/api/ping/ |
# Stop containers (preserves DB data)
docker compose down
# Stop and wipe the database volume (full reset)
docker compose down -v
# Rebuild a single service after a Dockerfile change
docker compose up --build backend# Django management commands run inside the container
docker compose exec backend python manage.py makemigrations
docker compose exec backend python manage.py migrate
docker compose exec backend python manage.py createsuperuser
docker compose exec backend python manage.py shell
# Run tests
docker compose exec backend pytest
# Validate DDD layer imports (catches architectural violations before commit)
docker compose exec backend python scripts/check_layer_imports.py --allThe layer import validator also runs automatically after every file edit via the Claude Code PostToolUse hook in .claude/settings.json.
The Vite dev server with hot module replacement is already running inside the frontend container. Most of the time you just edit files and the browser updates instantly.
If you need to run npm commands directly (outside Docker):
cd frontend
npm install
npm run dev # starts on :5173
npm run build # production build → dist/
npm run lint# All services
docker compose logs -f
# Single service
docker compose logs -f backend
docker compose logs -f frontend# After editing backend/requirements.txt
docker compose up --build backend
# After editing frontend/package.json
docker compose up --build frontendPort already in use
# Find what's using port 8000
lsof -i :8000
# Kill it or change the port in docker-compose.ymlBackend stuck on "Waiting for database..."
The backend loops until PostgreSQL passes its health check. If it spins for more than 30 seconds:
docker compose logs db # look for startup errors
docker compose restart db # restart Postgres and let backend retryMigrations failed on startup
docker compose logs backend # read the traceback
docker compose exec backend python manage.py migrate # re-run manuallyFrontend shows blank page or Vite proxy errors
docker compose logs frontend # check for build errors
# Vite proxies /api/* to localhost:8000 — confirm the backend is up first
curl http://localhost:8000/api/ping/"relation does not exist" errors
The database volume exists but migrations haven't run. Run them manually:
docker compose exec backend python manage.py migrateFull reset (when all else fails)
docker compose down -v --remove-orphans
docker compose up --build- Domain —
backend/domain/<context>/entities.py,services.py,exceptions.py - Application —
backend/application/<context>/dtos.py,queries.py,commands.py - Infrastructure —
backend/infrastructure/<context>/adapters.py+ models ininfrastructure/persistence/ - Interface —
backend/interfaces/api/<context>/views.py,serializers.py,urls.py - Wire the new URL file into
backend/config/urls.py - Run
makemigrationsif you added models
Each layer has its own CLAUDE.md with precise import rules. The hook enforces these automatically on every edit.
All env files share the same keys — only values differ per environment.
| Variable | Description | Dev default |
|---|---|---|
DJANGO_SETTINGS_MODULE |
Settings file to load | config.settings.dev |
DJANGO_SECRET_KEY |
Django signing key — never commit real values | dev-secret-key-not-for-production |
DATABASE_URL |
PostgreSQL connection string | postgres://postgres:postgres@db:5432/myapp_dev |
REDIS_URL |
Redis connection string | redis://redis:6379/0 |
CORS_ALLOWED_ORIGINS |
Allowed frontend origins | http://localhost:5173 |
VITE_API_URL |
Backend origin for the Vite proxy | http://localhost:8000 |
VITE_ENV |
Frontend environment label | development |
| Method | Path | Description |
|---|---|---|
GET |
/api/health/ |
Checks DB and Redis connectivity |
GET |
/api/ping/ |
Always returns { "message": "pong" } |
The backend/scripts/check_layer_imports.py validator runs after every file edit (Claude Code hook) and on CI:
| Layer | May import from |
|---|---|
domain/ |
stdlib only |
application/ |
domain/, stdlib |
infrastructure/ |
domain/, Django, third-party libs |
interfaces/ |
application/, infrastructure/, domain/, DRF |
Cross-context interface imports are blocked — interfaces/api/orders/ cannot import from interfaces/api/users/. Data flows through the application layer.
# Run manually at any time
python3 backend/scripts/check_layer_imports.py --allTesting the full Kubernetes stack on your laptop before pushing to AKS. Two cluster options — pick one.
Docker Desktop ships a single-node Kubernetes cluster. You already have it if you're using Docker Desktop.
Enable it: Docker Desktop → Settings → Kubernetes → Enable Kubernetes → Apply & Restart. Wait for the green "Kubernetes running" indicator (~60 seconds).
# Verify it's up
kubectl cluster-info
kubectl get nodes # should show one node, STATUS Readykind runs Kubernetes inside Docker containers. It's closer to a real cluster and supports NetworkPolicy enforcement with Cilium.
brew install kind # or: go install sigs.k8s.io/kind@latest
# Standard cluster (no NetworkPolicy enforcement)
kind create cluster --name local
# With Cilium CNI (enforces NetworkPolicies — matches production behaviour)
kind create cluster --name local --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
disableDefaultCNI: true
podSubnet: "10.244.0.0/16"
nodes:
- role: control-plane
- role: worker
EOF
kubectl apply -f https://raw.githubusercontent.com/cilium/cilium/v1.14/install/kubernetes/quick-install.yamlSwitch between clusters at any time: kubectl config use-context kind-local or kubectl config use-context docker-desktop
The local cluster shares your Docker daemon (Docker Desktop) or needs images loaded explicitly (kind).
# Build both images
docker build --target production \
-f backend/Dockerfile \
-t backend:local .
docker build --target production \
-f frontend/Dockerfile \
--build-arg VITE_API_URL=http://localhost \
--build-arg VITE_ENV=development \
-t frontend:local .Docker Desktop only — images built above are automatically available. Skip to step 2.
kind only — load images into the cluster after building:
kind load docker-image backend:local frontend:local --name localVerify they're available inside the cluster:
docker exec -it local-worker crictl images | grep -E "backend|frontend"One-time setup. Skippable if you plan to use kubectl port-forward instead.
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.service.type=NodePort # NodePort works on kind; LoadBalancer works on Docker Desktop
# Wait for it to be ready
kubectl rollout status deployment/ingress-nginx-controller -n ingress-nginxhelm upgrade --install django-react ./k8s/helm/django-react \
-f k8s/helm/django-react/values-local.yaml \
--set backend.image.tag=local \
--set frontend.image.tag=local \
--set secrets.djangoSecretKey=local-dev-secret-key \
--set "secrets.databaseUrl=postgres://postgres:postgres@db:5432/myapp_local" \
--set secrets.redisUrl=redis://redis:6379/0 \
--set secrets.postgresUser=postgres \
--set secrets.postgresPassword=postgres \
--set secrets.postgresDb=myapp_local \
-n local --create-namespaceWatch everything come up:
kubectl get pods -n local -w
# Wait until all pods are Running (db takes ~20s, backend waits for migrations)The pre-upgrade migration Job runs automatically before any app pods start. Check it:
kubectl get jobs -n local
kubectl logs job/db-migrate-django-react -n localkubectl apply -k k8s/overlays/local
# Check status
kubectl get all -n localWith nginx ingress controller installed:
# Docker Desktop — ingress controller binds to localhost:80
curl http://localhost/api/health/
open http://localhost
# kind — get the NodePort
kubectl get svc -n ingress-nginx ingress-nginx-controller
# Look for 80:<NodePort>/TCP — access via http://localhost:<NodePort>Without ingress — port-forward directly to services:
kubectl port-forward svc/backend 8000:8000 -n local &
kubectl port-forward svc/frontend 8080:80 -n local &
curl http://localhost:8000/api/health/
open http://localhost:8080helm test django-react -n local --logsThis runs a pod inside the cluster that curls /api/health/, /api/ping/, and /health against the ClusterIP services directly.
# Watch all pods
kubectl get pods -n local -w
# Tail backend logs
kubectl logs -f deployment/backend -n local
# Re-run migrations manually
kubectl exec deployment/backend -n local -- python manage.py migrate
# Open a Django shell
kubectl exec -it deployment/backend -n local -- python manage.py shell
# Check HPA (won't scale — metrics-server not installed — but shows config)
kubectl get hpa -n local
# Check PDB status (ALLOWED DISRUPTIONS column)
kubectl get pdb -n local
# Verify NetworkPolicies were applied (enforcement depends on CNI)
kubectl get networkpolicies -n local# Helm
helm uninstall django-react -n local
kubectl delete namespace local
# Kustomize
kubectl delete -k k8s/overlays/local
# Stop the cluster entirely
kind delete cluster --name local # kind
# Docker Desktop: Settings → Kubernetes → Reset Kubernetes Cluster| Local | Production (AKS) | |
|---|---|---|
| Images | Built locally, imagePullPolicy: Never |
Pulled from ACR, imagePullPolicy: Always |
| StorageClass | hostpath (Docker Desktop) / standard (kind) |
managed-premium (Azure Premium SSD) |
| TLS | Disabled | cert-manager + Let's Encrypt |
| NetworkPolicies | Not enforced (Docker Desktop) | Enforced by Calico/Cilium |
| Replicas | 1 each | 2+ with HPA |
| Resource limits | Minimal | Sized for real traffic |
| HPA | Disabled (no metrics-server) | Active, scales on CPU/memory |
# Staging
docker compose -f docker-compose.staging.yml up --build -d
# Production
docker compose -f docker-compose.prod.yml up --build -dnginx serves the built React assets and proxies /api/* to gunicorn.
Install cluster-level infrastructure once per cluster:
# nginx ingress controller
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx --create-namespace
# cert-manager (TLS via Let's Encrypt)
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace \
--version v1.14.0 --set installCRDs=true
# Let's Encrypt ClusterIssuers (staging + production)
kubectl apply -f k8s/cert-manager/clusterissuers.yamlDeploy the application with Helm:
helm upgrade --install django-react ./k8s/helm/django-react \
-f k8s/helm/django-react/values-production.yaml \
--set global.image.registry=myacr.azurecr.io \
--set backend.image.tag=$(git rev-parse --short HEAD) \
--set frontend.image.tag=$(git rev-parse --short HEAD) \
--set secrets.djangoSecretKey=$DJANGO_SECRET_KEY \
--set secrets.databaseUrl=$DATABASE_URL \
--set secrets.redisUrl=$REDIS_URL \
--set secrets.postgresUser=$POSTGRES_USER \
--set secrets.postgresPassword=$POSTGRES_PASSWORD \
--set secrets.postgresDb=$POSTGRES_DB \
-n production --create-namespace \
--atomic --timeout 5m
# Validate the deployment
helm test django-react -n production --logsThe Helm chart runs python manage.py migrate as a pre-upgrade Job before any new pods come up. If migrations fail, the deploy aborts and the previous release stays running.
Or use Kustomize directly:
kubectl apply -k k8s/overlays/productionSee k8s/README.md for the full Kubernetes architecture — RBAC, NetworkPolicies, PodDisruptionBudgets, StorageClasses, and cert-manager are all wired in.