A complete, runnable learning project that demonstrates NGINX as an Ingress Controller and API gateway in a Kubernetes (K3s) environment, with Dapr sidecar for distributed application capabilities, TLS certificate automation via cert-manager and Let's Encrypt, and Redis as the backing store.
User Browser
|
v
DNS Resolution (*.localtest.me -> 127.0.0.1)
|
v
NGINX Ingress Controller (TLS termination)
|
v
Kubernetes Service (ClusterIP)
|
v
Load Balanced .NET API Pods (3 replicas, each with Dapr sidecar)
| |
v v
Dapr State Store (Redis) Dapr Pub/Sub (Redis Streams)
|
v
Zipkin (Distributed Tracing)
graph TB
B["Browser"] -->|"HTTPS"| NIC["NGINX Ingress Controller<br/>:30443"]
NIC -->|"TLS Termination"| SVC["Service: sample-api"]
SVC -->|"Round Robin"| P1["Pod 1<br/>.NET 8 + daprd"]
SVC -->|"Round Robin"| P2["Pod 2<br/>.NET 8 + daprd"]
SVC -->|"Round Robin"| P3["Pod 3<br/>.NET 8 + daprd"]
CM["cert-manager"] -->|"Provisions TLS"| NIC
LE["Let's Encrypt"] -->|"ACME"| CM
P1 -->|"state/pubsub"| REDIS["Redis<br/>(Bitnami)"]
P1 -->|"tracing"| ZIP["Zipkin"]
SENTRY["dapr-sentry"] -->|"mTLS certs"| P1
style NIC fill:#326CE5,color:#fff
style CM fill:#E6522C,color:#fff
style P1 fill:#512BD4,color:#fff
style P2 fill:#512BD4,color:#fff
style P3 fill:#512BD4,color:#fff
style REDIS fill:#D82C20,color:#fff
style ZIP fill:#F5A623,color:#000
style SENTRY fill:#E6522C,color:#fff
| Topic | Concepts |
|---|---|
| NGINX | Reverse proxy, load balancing, upstreams, path/host routing, rate limiting, header manipulation, TLS termination, connection reuse, buffering, timeouts |
| Kubernetes Networking | Services (ClusterIP), Ingress, NGINX Ingress Controller, traffic flow, CoreDNS |
| TLS & Certificates | TLS termination, Let's Encrypt ACME, HTTP-01 challenge, cert-manager, Issuer vs ClusterIssuer, automatic renewal |
| Dapr | Sidecar architecture, state management, pub/sub messaging, distributed tracing, mTLS, sentry, placement, operator, dashboard |
| Redis | Bitnami Helm chart, state backing store, pub/sub via Redis Streams, consumer groups |
| Container Runtime | containerd vs Docker, OCI images, nerdctl |
| K3s | K3s components, embedded containerd, replacing Traefik with NGINX |
| Deployment | Helmfile, multi-chart management, environment configuration |
.
├── src/SampleApi/ # .NET 8 Web API
│ ├── Controllers/
│ │ ├── HelloController.cs # Hello + load balancing demo
│ │ ├── PodInfoController.cs # Pod metadata + headers
│ │ ├── StateController.cs # Dapr state management (Redis)
│ │ ├── PubSubController.cs # Dapr pub/sub (Redis Streams)
│ │ └── DaprInfoController.cs # Dapr sidecar health/metadata
│ ├── Program.cs
│ └── SampleApi.csproj
│
├── docker/
│ └── Dockerfile # Multi-stage .NET build
│
├── k8s/
│ ├── base/
│ │ └── namespace.yaml
│ ├── api/
│ │ ├── deployment.yaml # 3 replicas + Dapr sidecar annotations
│ │ └── service.yaml # ClusterIP service
│ ├── ingress/
│ │ ├── ingress.yaml # Primary domain (TLS)
│ │ ├── ingress-second-domain.yaml
│ │ └── rate-limit-ingress.yaml
│ ├── cert-manager/
│ │ ├── cluster-issuer.yaml # Let's Encrypt + self-signed
│ │ └── certificate.yaml
│ ├── nginx/
│ │ └── nginx-config.yaml # NGINX global config
│ └── dapr/
│ ├── dapr-config.yaml # Tracing + mTLS configuration
│ ├── dapr-components.yaml # State store + pub/sub (Redis)
│ ├── dapr-subscription.yaml # Declarative pub/sub subscription
│ └── zipkin.yaml # Zipkin deployment + service
│
├── helmfile/
│ └── helmfile.yaml # NGINX + cert-manager + Dapr + Redis
│
├── scripts/
│ ├── mac/
│ │ ├── setup.sh # Full macOS setup
│ │ └── teardown.sh
│ └── wsl/
│ ├── setup.sh # Full WSL setup
│ └── teardown.sh
│
└── docs/
├── architecture.md
├── nginx-ingress-deep-dive.md
├── cert-manager-explained.md
├── tls-handshake-flow.md
├── dns-resolution-flow.md
├── helmfile-deployment.md
├── k3s-setup.md
├── nerdctl-containerd.md
├── dapr-deep-dive.md # Dapr architecture + building blocks
└── redis-bitnami.md # Redis as Dapr backing store
- macOS: Homebrew, 4GB RAM, 20GB disk
- Windows: WSL2 with Ubuntu 22.04+, systemd enabled, 4GB RAM, 20GB disk
git clone <repo-url> && cd DotnetNginx
chmod +x scripts/mac/setup.sh
./scripts/mac/setup.sh
export KUBECONFIG=~/.kube/config-k3s-demogit clone <repo-url> && cd DotnetNginx
# Ensure systemd is enabled in /etc/wsl.conf:
# [boot]
# systemd=true
chmod +x scripts/wsl/setup.sh
./scripts/wsl/setup.sh# WSL / Linux
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable=traefik --write-kubeconfig-mode=644" sh -
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/configsudo nerdctl --address /run/k3s/containerd/containerd.sock --namespace k8s.io \
build -t demo-api:local -f docker/Dockerfile .
sudo nerdctl --address /run/k3s/containerd/containerd.sock --namespace k8s.io \
tag demo-api:local localhost:5000/demo-api:latest
sudo nerdctl --address /run/k3s/containerd/containerd.sock --namespace k8s.io \
push localhost:5000/demo-api:latestcd helmfile/
helmfile syncThis installs:
- NGINX Ingress Controller (NodePort on 30080/30443)
- cert-manager (with CRDs)
- Redis (Bitnami, standalone mode)
- Dapr runtime (sentry, placement, operator, dashboard, sidecar-injector)
# Zipkin for distributed tracing
kubectl apply -f k8s/dapr/zipkin.yaml
# Namespace (if not already created)
kubectl apply -f k8s/base/namespace.yaml
# Dapr configuration and components
kubectl apply -f k8s/dapr/dapr-config.yaml
kubectl apply -f k8s/dapr/dapr-components.yaml
kubectl apply -f k8s/dapr/dapr-subscription.yamlkubectl apply -f k8s/base/namespace.yaml
kubectl apply -f k8s/api/deployment.yaml
kubectl apply -f k8s/api/service.yaml
kubectl apply -f k8s/cert-manager/cluster-issuer.yaml
kubectl apply -f k8s/cert-manager/certificate.yaml
kubectl apply -f k8s/ingress/ingress.yaml
kubectl apply -f k8s/ingress/ingress-second-domain.yaml*.localtest.me automatically resolves to 127.0.0.1. No configuration needed.
For WSL, map to the node IP:
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
echo "${NODE_IP} api.demo.localtest.me api-v2.demo.localtest.me" | sudo tee -a /etc/hosts# Health check
curl http://api.demo.localtest.me:30080/health -H "Host: api.demo.localtest.me"
# Hello endpoint
curl http://api.demo.localtest.me:30080/api/hello -H "Host: api.demo.localtest.me"
# Pod info (shows which pod handled the request)
curl http://api.demo.localtest.me:30080/api/podinfo -H "Host: api.demo.localtest.me"
# Dapr sidecar status
curl http://api.demo.localtest.me:30080/api/daprinfo -H "Host: api.demo.localtest.me"# Save state
curl -X POST http://api.demo.localtest.me:30080/api/state/mykey \
-H "Host: api.demo.localtest.me" \
-H "Content-Type: application/json" \
-d '{"value":"hello from Dapr!"}'
# Read state (may return from a different pod — state is shared via Redis)
curl http://api.demo.localtest.me:30080/api/state/mykey \
-H "Host: api.demo.localtest.me"
# Delete state
curl -X DELETE http://api.demo.localtest.me:30080/api/state/mykey \
-H "Host: api.demo.localtest.me"# Publish a message to the "orders" topic
curl -X POST http://api.demo.localtest.me:30080/api/pubsub/publish/orders \
-H "Host: api.demo.localtest.me" \
-H "Content-Type: application/json" \
-d '{"orderId":"123","item":"Widget","quantity":5}'
# Check logs to see which pod received the message
kubectl logs -n demo-api -l app=sample-api -c sample-api --tail=5for i in {1..6}; do
echo "--- Request $i ---"
curl -s http://api.demo.localtest.me:30080/api/hello \
-H "Host: api.demo.localtest.me" | jq .pod
donekubectl describe certificate api-demo-tls -n demo-api
kubectl get secret api-demo-tls -n demo-api
curl -k https://api.demo.localtest.me:30443/api/hello \
-H "Host: api.demo.localtest.me"# Dapr Dashboard — inspect apps, components, configurations
kubectl port-forward svc/dapr-dashboard -n dapr-system 8080:8080
# Open http://localhost:8080
# Zipkin — visualize distributed traces
kubectl port-forward svc/zipkin -n dapr-monitoring 9411:9411
# Open http://localhost:9411| Endpoint | Method | Description |
|---|---|---|
/health |
GET | Kubernetes health probe |
/api/hello |
GET | Returns greeting + pod hostname |
/api/hello/{name} |
GET | Personalized greeting |
/api/podinfo |
GET | Detailed pod metadata + request headers |
/api/daprinfo |
GET | Dapr sidecar health + metadata |
/api/state/{key} |
GET | Read from Dapr state store (Redis) |
/api/state/{key} |
POST | Save to Dapr state store (Redis) |
/api/state/{key} |
DELETE | Delete from Dapr state store |
/api/pubsub/publish/{topic} |
POST | Publish message via Dapr pub/sub |
/api/events/orders |
POST | Receive messages from "orders" topic (Dapr subscription) |
| Release | Chart | Namespace | Purpose |
|---|---|---|---|
| ingress-nginx | ingress-nginx/ingress-nginx |
ingress-nginx | NGINX Ingress Controller |
| cert-manager | jetstack/cert-manager |
cert-manager | TLS certificate automation |
| redis | bitnami/redis |
redis | State store + pub/sub backing |
| dapr | dapr/dapr |
dapr-system | Dapr runtime (sentry, placement, operator, dashboard, injector) |
sequenceDiagram
participant U as User
participant CM as cert-manager
participant LE as Let's Encrypt
participant NIC as NGINX Ingress
U->>U: 1. Apply Ingress with TLS + cert-manager annotation
CM->>CM: 2. Detect Ingress, create Certificate resource
CM->>LE: 3. Request certificate for api.demo.localtest.me
LE->>CM: 4. Challenge: serve token at /.well-known/acme-challenge/
CM->>CM: 5. Create temp Ingress + Pod to serve token
LE->>NIC: 6. GET /.well-known/acme-challenge/{token}
NIC->>LE: 7. Token response
LE->>CM: 8. Verified! Here's your certificate
CM->>CM: 9. Store cert in Secret "api-demo-tls"
NIC->>NIC: 10. Load TLS Secret, serve HTTPS
| Document | Topics Covered |
|---|---|
| architecture.md | System architecture, component details, resource relationships |
| nginx-ingress-deep-dive.md | Reverse proxy, load balancing, upstreams, routing, rate limiting, headers, TLS, health checks, buffering, timeouts |
| cert-manager-explained.md | cert-manager architecture, HTTP-01 challenges, certificate renewal, staging vs production |
| tls-handshake-flow.md | TLS 1.3 handshake, SNI, certificate chains, TLS termination |
| dns-resolution-flow.md | External DNS, CoreDNS internals, service discovery, DNS search domains |
| helmfile-deployment.md | Helmfile concepts, release ordering, environments, commands |
| k3s-setup.md | K3s vs full K8s, architecture, embedded containerd, setup |
| nerdctl-containerd.md | containerd vs Docker, OCI images, nerdctl commands, multi-stage builds |
| dapr-deep-dive.md | Dapr control plane (sentry, placement, operator, injector, dashboard), building blocks (state, pub/sub, invocation), tracing, mTLS |
| redis-bitnami.md | Bitnami Redis chart, state store internals, pub/sub via Redis Streams, consumer groups, monitoring |
kubectl describe pod -n demo-api -l app=sample-api
kubectl logs -n demo-api -l app=sample-api -c sample-api
kubectl logs -n demo-api -l app=sample-api -c daprd# Verify annotations
kubectl get deployment sample-api -n demo-api -o jsonpath='{.spec.template.metadata.annotations}'
# Check sidecar injector
kubectl logs -n dapr-system -l app=dapr-sidecar-injector
# Verify 2 containers (app + daprd)
kubectl get pods -n demo-api -o jsonpath='{.items[0].spec.containers[*].name}'kubectl get components -n demo-api
kubectl describe component statestore -n demo-api
kubectl logs -n dapr-system -l app=dapr-operator
# Test Redis
kubectl exec -it -n redis redis-master-0 -- redis-cli -a demo-redis-password PINGkubectl logs -n ingress-nginx -l app.kubernetes.io/component=controllerkubectl describe certificate -n demo-api
kubectl get challenges --all-namespaces
kubectl logs -n cert-manager deployment/cert-managernslookup api.demo.localtest.me
kubectl run dns-test --image=busybox:1.36 --rm -it --restart=Never -- \
nslookup sample-api.demo-api.svc.cluster.local# macOS
./scripts/mac/teardown.sh
# WSL
./scripts/wsl/teardown.sh