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_PASSWORDNamespace 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 --waitapiVersion: postgresql.cnpg.io/v1kind: Clustermetadata: name: indicat-dbspec: 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$kubectl -n get cluster indicat-db -o jsonpath='{.status.phase}'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"}]}'$kubectl -n get serviceaccount default -o jsonpath='{.imagePullSecrets[0].name}'regcred
Migrations
apiVersion: batch/v1kind: Jobmetadata: 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$kubectl -n wait --for=condition=complete job/indicat-migrate --timeout=10sjob.batch/indicat-migrate condition met
The application
apiVersion: apps/v1kind: Deploymentmetadata: name: indicatspec: 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$kubectl -n get pods -l app=indicat --field-selector=status.phase=Running --no-headers | wc -l$curl -s -o /dev/null -w '%{http_code}' https:///healthz200
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=50In 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_URLhas 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. OOMKilledin the pod status: raiselimits.memory.
Autoscaling
apiVersion: autoscaling/v2kind: HorizontalPodAutoscalermetadata: name: indicatspec: 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$kubectl -n top pods -l app=indicat --no-headers | wc -lDay 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 -- shMore 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 .