Deploy indicat

A real web application on the cluster: a namespace, its secrets, PostgreSQL (operator or StatefulSet), the Deployment with probes and limits, migrations, a Service, an Ingress with TLS at your domain, and an autoscaler.

intermediate~50 min hands-on
#kubernetes#k3s#postgresql#cloudnativepg#ingress#hpa

Not validated end to end yet — be the first.Report a problem

Draft — not yet run end to end. This page was written but its author has not yet run it on a real machine. Commands may be wrong: read before you run, and tell us what breaks.

The gistindicat on the cluster
Cluster (kube-system and operators)
Your namespace
HTTPSIngressDATABASE_URLpull
Usershttps://
Registry
TraefikIngress · TLS
indicat pods · :
PostgreSQLindicat-db-rw:5432

Traefik terminates TLS for ${APP_DOMAIN} and routes to the indicat Service, which spreads requests over the pods. The pods read their configuration from a Secret and write to PostgreSQL through the database Service. The image comes from the registry at each pod start.

indicat is a containerised web application: one image, , that listens on , needs a DATABASE_URL and a SECRET_KEY in its environment, and answers /healthz when it is fine. This page gives it a namespace, its secrets, a PostgreSQL, replicas with probes and limits, migrations that run before each rollout, an Ingress with TLS at , and an autoscaler.

Before you start

$openssl rand -hex 32 # APP_SECRET
$openssl rand -hex 16 # DB_PASSWORD

Namespace and secrets

$kubectl create namespace --dry-run=client -o yaml | kubectl apply -f -
$kubectl -n create secret generic indicat-db-app --type=kubernetes.io/basic-auth \
$ --from-literal=username=indicat --from-literal=password="" \
$ --dry-run=client -o yaml | kubectl apply -f -
$kubectl -n create secret generic indicat-secrets \
$ --from-literal=DATABASE_URL="postgresql://indicat:@indicat-db-rw:5432/indicat" \
$ --from-literal=SECRET_KEY="" \
$ --dry-run=client -o yaml | kubectl apply -f -

PostgreSQL

$helm repo add cnpg https://cloudnative-pg.github.io/charts --force-update
$helm upgrade --install cnpg cnpg/cloudnative-pg --namespace cnpg-system --create-namespace --wait
deploy/postgres.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: indicat-db
spec:
instances: 1
imageName: ghcr.io/cloudnative-pg/postgresql:16
storage:
size: 10Gi
storageClass: local-path
bootstrap:
initdb:
database: indicat
owner: indicat
secret:
name: indicat-db-app
$kubectl -n apply -f deploy/postgres.yaml
$kubectl -n get cluster indicat-db -w
Check
$kubectl -n  get cluster indicat-db -o jsonpath='{.status.phase}'
Expected output
Cluster in healthy state

Pull secret

$kubectl -n create secret docker-registry regcred \
$ --docker-server=$(echo | cut -d/ -f1) \
$ --docker-username= --docker-password="" \
$ --dry-run=client -o yaml | kubectl apply -f -
$kubectl -n patch serviceaccount default -p '{"imagePullSecrets":[{"name":"regcred"}]}'
Check
$kubectl -n  get serviceaccount default -o jsonpath='{.imagePullSecrets[0].name}'
Expected output
regcred

Migrations

deploy/migrate.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: indicat-migrate
labels: { app: indicat-migrate }
spec:
backoffLimit: 2
ttlSecondsAfterFinished: 600
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image:
command: ["npm", "run", "migrate"]
envFrom:
- secretRef: { name: indicat-secrets }
$kubectl -n delete job indicat-migrate --ignore-not-found
$kubectl -n apply -f deploy/migrate.yaml
$kubectl -n wait --for=condition=complete job/indicat-migrate --timeout=300s
Check
$kubectl -n  wait --for=condition=complete job/indicat-migrate --timeout=10s
Expected output
job.batch/indicat-migrate condition met

The application

deploy/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: indicat
spec:
replicas:
selector:
matchLabels: { app: indicat }
strategy:
type: RollingUpdate
rollingUpdate: { maxUnavailable: 0, maxSurge: 1 }
template:
metadata:
labels: { app: indicat }
spec:
containers:
- name: indicat
image:
ports:
- containerPort:
envFrom:
- secretRef: { name: indicat-secrets }
readinessProbe:
httpGet: { path: /healthz, port: }
periodSeconds: 5
livenessProbe:
httpGet: { path: /healthz, port: }
initialDelaySeconds: 15
periodSeconds: 10
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 256Mi }
$kubectl -n apply -f deploy/deployment.yaml -f deploy/service.yaml -f deploy/ingress.yaml
$kubectl -n rollout status deploy/indicat
Check
$kubectl -n  get pods -l app=indicat --field-selector=status.phase=Running --no-headers | wc -l
Expected output
Check
$curl -s -o /dev/null -w '%{http_code}' https:///healthz
Expected output
200
If the pods never become Ready

Start with the events and the logs:

$kubectl -n describe pod -l app=indicat | tail -20
$kubectl -n logs -l app=indicat --tail=50

In order of likelihood:

  • ImagePullBackOff: wrong image name or tag, or the pull secret is missing or expired.
  • Readiness probe failed: connection refused: the process does not listen on , or not on all interfaces (0.0.0.0).
  • The logs show a database error: DATABASE_URL has the wrong host or password. Decode the Secret to see what the pod sees: kubectl get secret indicat-secrets -o jsonpath='{.data.DATABASE_URL}' | base64 -d.
  • OOMKilled in the pod status: raise limits.memory.

Autoscaling

deploy/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: indicat
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: indicat
minReplicas:
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
$kubectl -n apply -f deploy/hpa.yaml
Check
$kubectl -n  top pods -l app=indicat --no-headers | wc -l
Expected output

Day to day: rollout, rollback, logs, exec

$kubectl -n rollout status deploy/indicat
$kubectl -n rollout history deploy/indicat
$kubectl -n rollout undo deploy/indicat
$kubectl -n logs -l app=indicat --tail=100 -f
$kubectl -n exec -it deploy/indicat -- sh

More than one replica

With pods, three things that were free on one server need a decision: sessions, migrations, and what happens during node maintenance.

Done

indicat answers at from pods, with its database, its secrets, migrations that run before each rollout, and an autoscaler. Every object is a file in deploy/, which is exactly what the next page needs: a pipeline that builds the image on each push, tags it with the commit, and applies these files with a ServiceAccount that can only touch .

Did everything work?

If you followed this page to the end on a real machine, say so. Your validation is dated and records your stack, so the next reader on the same path knows it still works.

This copy is read-only. To report that it works, or that it does not, open an issue

Only your stack choices are recorded, never your values. The pseudonym stays on this browser.