This directory contains example Kubernetes/OpenShift manifests for deploying Simple Aircraft Manager in production.
The deployment consists of:
- Django Application - Gunicorn serving the Django app on port 8000
- nginx Sidecar - TLS termination, static/media file serving, security headers
- PostgreSQL Database - Crunchy Data PGO-managed PostgreSQL 16 cluster
- Persistent Storage - RWX volume for media uploads
-
Crunchy Postgres Operator (PGO) v5.x - For PostgreSQL database
# Install from OperatorHub in openshift-operators namespace -
External Secrets Operator (Optional) - If using Vault/external secret management
# Install from OperatorHub or use plain Kubernetes Secrets
- PostgreSQL: 2Gi RWO storage (persistent database)
- Backups: 2Gi RWO storage (PostgreSQL backups)
- Media Files: 50Gi RWX storage (user uploads - CephFS, NFS, etc.)
Replace your-app.apps.example.com with your actual route hostname in:
02-configmap.yaml- DJANGO_ALLOWED_HOSTS, DJANGO_CSRF_TRUSTED_ORIGINS03-nginx-config.yaml- server_name directive10-route.yaml- spec.host
The application requires these secrets (see 04-externalsecret.yaml or create plain Secret):
DJANGO_SECRET_KEY: "random-50-character-string"
DJANGO_SUPERUSER_USERNAME: "admin"
DJANGO_SUPERUSER_PASSWORD: "secure-password"
DJANGO_SUPERUSER_EMAIL: "admin@example.com"
# OIDC Authentication (optional)
OIDC_RP_CLIENT_ID: "your-client-id"
OIDC_RP_CLIENT_SECRET: "your-client-secret"
# AI Logbook Import (optional)
ANTHROPIC_API_KEY: "sk-ant-..."Generate a Django secret key:
python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'The pre-built image is published automatically to GHCR on every commit to main:
image: ghcr.io/marbindrakon/simple-aircraft-manager:latestThis is already set in 08-deployment.yaml. To use a custom build instead, update both image fields (init container and main container) to point to your registry.
If using OpenID Connect authentication:
- Set
OIDC_ENABLED: "true"in02-configmap.yaml - Update
OIDC_OP_DISCOVERY_ENDPOINTwith your provider's URL - Add
OIDC_RP_CLIENT_IDandOIDC_RP_CLIENT_SECRETto secrets - Update CSP
form-actionin03-nginx-config.yamlto include your IdP domain
If NOT using OIDC:
- Set
OIDC_ENABLED: "false"in ConfigMap - Remove OIDC variables from ConfigMap and Secret
The app supports AI-powered transcription of scanned maintenance logbook pages using Anthropic (Claude) and/or local Ollama models.
Using Anthropic models (cloud):
- Add
ANTHROPIC_API_KEYto your secrets - Built-in models (Sonnet, Haiku, Opus) are available by default
Using Ollama models (self-hosted):
- Set
OLLAMA_BASE_URLin02-configmap.yamlto your Ollama instance - Add models via
LOGBOOK_IMPORT_EXTRA_MODELSJSON array in ConfigMap - Optionally adjust
OLLAMA_TIMEOUT(default: 1200 seconds)
If NOT using logbook import:
- No configuration needed — the feature is optional and inactive without an API key
In 07-pvc.yaml, change storageClassName to match your cluster:
storageClassName: your-rwx-storage-class # Must support ReadWriteManyCommon options:
- OpenShift Data Foundation:
ocs-storagecluster-cephfs - NFS:
nfs-clientor similar - Other: Check
oc get storageclass
The sam container defaults to 512Mi request / 2Gi limit. The higher limit accommodates AI-powered logbook import which processes multiple images in memory. If not using this feature, you can lower the limit to 512Mi.
The manifests are numbered for deployment order:
# Deploy in sequence
oc apply -f 01-namespace.yaml
oc apply -f 02-configmap.yaml
oc apply -f 03-nginx-config.yaml
oc apply -f 04-externalsecret.yaml # Or your plain Secret
oc apply -f 05-pg-init-sql.yaml
oc apply -f 06-postgrescluster.yaml
oc apply -f 07-pvc.yaml
# Wait for PostgreSQL to be ready
oc wait --for=condition=Ready postgrescluster/sam-db -n sam --timeout=300s
# Deploy application
oc apply -f 08-deployment.yaml
oc apply -f 09-service.yaml
oc apply -f 10-route.yaml
# Check status
oc get pods -n sam
oc logs -f deployment/sam -n sam -c samIf using ArgoCD, the sync waves ensure proper ordering:
- Wave -1: Namespace
- Wave 0: ConfigMaps, PVC
- Wave 1: ExternalSecret, PostgresCluster
- Wave 3: Deployment, Service, Route
Add this annotation to enable:
metadata:
annotations:
argocd.argoproj.io/sync-wave: '0'The example uses OpenShift's service-serving certificate:
# In service.yaml
annotations:
service.beta.openshift.io/serving-cert-secret-name: sam-nginx-tlsThis auto-generates a cert in the sam-nginx-tls secret.
If using cert-manager, create a Certificate resource:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: sam-nginx-tls
namespace: sam
spec:
secretName: sam-nginx-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- your-app.apps.example.comCreate a TLS secret manually:
oc create secret tls sam-nginx-tls \
--cert=tls.crt \
--key=tls.key \
-n samThe PostgreSQL cluster auto-generates connection credentials:
# Secret created by PGO: sam-db-pguser-sam-db
host: sam-db-primary.sam.svc
port: 5432
dbname: sam-db
user: sam-db
password: <auto-generated>The deployment mounts these as environment variables (see 08-deployment.yaml).
- Endpoint:
/healthz/on port 8443 - Handled by: nginx (returns 200 without proxying to Django)
- Purpose: Avoids ALLOWED_HOSTS validation issues with kube-probe IPs
Strict CSP configured in nginx:
- Scripts:
'self',cdn.jsdelivr.net(Alpine.js) - Styles:
'self',unpkg.com(PatternFly) - Forms:
'self', OIDC provider (if enabled)
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY
- X-XSS-Protection: 1; mode=block
- Referrer-Policy: strict-origin-when-cross-origin
Consider adding NetworkPolicies to restrict:
- Ingress to port 8443 only
- Egress to PostgreSQL, OIDC provider, external APIs only
To scale horizontally:
-
Enable session affinity on the Service:
sessionAffinity: ClientIP
-
Use external session storage (Redis, Memcached):
- Configure Django session backend
- Update deployment with cache connection details
-
Scale replicas:
oc scale deployment/sam --replicas=3 -n sam
PGO handles automatic backups. To take a manual backup:
oc annotate postgrescluster sam-db \
postgres-operator.crunchydata.com/pgbackrest-backup="$(date +%Y%m%d-%H%M%S)" \
-n samBackup the media PVC:
# Using rsync or backup tool of choice
oc rsync sam-<pod-id>:/opt/app-root/src/mediafiles ./backup/Add Prometheus annotations for monitoring:
metadata:
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
prometheus.io/path: "/metrics"Install django-prometheus and configure in settings.
# Check logs
oc logs deployment/sam -n sam -c sam
oc logs deployment/sam -n sam -c nginx
# Check events
oc get events -n sam --sort-by='.lastTimestamp'# Check PGO cluster status
oc get postgrescluster -n sam
oc get pods -l postgres-operator.crunchydata.com/cluster=sam-db -n sam
# Test connection from pod
oc exec -it deployment/sam -n sam -c sam -- bash
psql -h sam-db-primary.sam.svc -U sam-db -d sam-db# Check nginx logs
oc logs deployment/sam -n sam -c nginx
# Verify static files were collected
oc exec deployment/sam -n sam -c nginx -- ls -la /opt/app-root/src/static/# Check OIDC configuration
oc exec deployment/sam -n sam -c sam -- env | grep OIDC
# Test discovery endpoint
oc exec deployment/sam -n sam -c sam -- \
curl -k https://your-idp.example.com/.well-known/openid-configuration- Secrets generated and stored securely
- Domain names updated in all manifests
- Container image built and pushed to registry
- Storage classes configured for your cluster
- PostgreSQL PGO operator installed
- TLS certificates configured
- OIDC provider configured (if using)
- Logbook import AI configured (if using)
- Resource limits tuned for workload
- Backups configured and tested
- Monitoring/alerting configured
- NetworkPolicies applied (optional)