Skip to content

Install on Kubernetes (Kustomize)

The deploy/kustomize/ tree in the repository ships base manifests plus three overlays (dev, staging, production). The base manifests are deliberately strict — runAsNonRoot, readOnlyRootFilesystem, allowPrivilegeEscalation: false, capabilities.drop: [ALL] — so the overlays only need to add what is environment-specific.

What you get out of the box

Manifest Purpose
namespace.yaml ampora namespace
serviceaccount.yaml Pod identity, no token automount
deployment.yaml 2 replicas by default, topology-spread, lifecycle drain
service.yaml ClusterIP for the HTTP + WebSocket port
ingress.yaml Nginx-compatible Ingress with WebSocket annotations
hpa.yaml HorizontalPodAutoscaler
pdb.yaml PodDisruptionBudget
configmap.yaml non-secret settings
secret.yaml placeholder; replace via your secret backend
networkpolicy.yaml egress to Postgres + DNS only by default
servicemonitor.yaml Prometheus Operator scrape config

Step 1 — pick or create an overlay

The production overlay is the right starting point for a real cluster. Copy it once and treat the copy as your environment's source of truth:

cp -r deploy/kustomize/overlays/production deploy/kustomize/overlays/acme-prod

Edit deploy/kustomize/overlays/acme-prod/kustomization.yaml:

namespace: ampora
nameSuffix: ""
images:
  - name: ampora/web
    newName: registry.acme.io/ampora/web
    newTag: 1.4.2          # pin a real version, not "latest"
configMapGenerator:
  - name: ampora-config
    behavior: merge
    literals:
      - Authentication__Oidc__Authority=https://login.acme.io/realms/ampora
      - OpenTelemetry__OtlpEndpoint=http://opentelemetry-collector.observability.svc.cluster.local:4317
secretGenerator:
  - name: ampora-secrets
    behavior: replace
    envs:
      - secrets.env       # never commit; produced by SOPS / sealed-secrets / ESO
patchesStrategicMerge:
  - deployment-patch.yaml
  - ingress-patch.yaml

Step 2 — provision the database

Ampora expects a reachable PostgreSQL with:

  • a dedicated database (ampora),
  • a dedicated role (ampora) with full DDL on that database,
  • TLS enabled — Ampora's connection string must be SSL Mode=Require in production.

A managed offering (RDS, Cloud SQL, Azure Database, Aiven, …) is the recommended path. If you prefer in-cluster, use a tested operator (CrunchyData PGO, Zalando, CNPG) — Ampora does not ship a Postgres operator of its own.

Apply migrations on first start either by:

  • letting the pod do it (AMPORA_AUTO_MIGRATE=1, the default in the base ConfigMap), or
  • running them out-of-band as a Job that exec's the same image with dotnet Ampora.Web.dll migrate. This is the safer choice for tightly controlled clusters.

Step 3 — configure secrets

The base Secret is a placeholder annotated ampora.io/placeholder=true so review tooling can refuse to ship it. Replace it via your real secret backend:

  • External Secrets Operator — easiest for managed-secrets users.
  • Sealed Secrets — git-friendly, no extra runtime dependency.
  • SOPS + Kustomize — works without a controller.
  • HashiCorp Vault Agent Injector — when you already run Vault.

The shape of the Secret expected by the deployment:

ConnectionStrings__Ampora: "Host=db.acme.io;Port=5432;Database=ampora;Username=ampora;Password=...;SSL Mode=Require;Trust Server Certificate=false"
Authentication__Oidc__Authority:    "https://login.acme.io/realms/ampora"
Authentication__Oidc__ClientId:     "ampora-web"
Authentication__Oidc__ClientSecret: "..."
KeyProtection__MasterKey:           "<base64 of a 32-byte CSPRNG key>"

KeyProtection__MasterKey wraps every encryption-at-rest field. Generate once with openssl rand 32 | base64 and rotate by setting a new key plus KeyProtection__PreviousMasterKey for one cycle, then drop the previous.

Step 4 — wire the Ingress

The default ingress.yaml is Nginx-Ingress-shaped:

metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/affinity-mode: "persistent"
    nginx.ingress.kubernetes.io/session-cookie-name: "ampora-affinity"

The session-cookie affinity is mandatory for HA: Blazor Server's SignalR circuit must stick to the same instance. See Operations → High availability for the equivalent Traefik / Application Gateway configurations.

Override host and tls via the overlay's ingress-patch.yaml. Use cert-manager or your own ACME / corporate-CA flow to provision the TLS certificate.

Step 5 — apply

kubectl apply -k deploy/kustomize/overlays/acme-prod
kubectl -n ampora rollout status deploy/ampora-web
kubectl -n ampora get pods,svc,ingress

The deployment is ready when:

  • both pods are Running and Ready,
  • /health/live and /health/ready return 200,
  • kubectl logs -l app.kubernetes.io/name=ampora shows Hosting environment: Production and Now listening on: ....

Step 6 — first login

Visit https://AMPORA_HOST in a browser. You should be bounced to your OIDC provider, log in, and land on the Dashboard.

If the first user has no role, Ampora bootstraps them as Admin. From there on, role assignment follows the OIDC group / claim mapping you configured.

Image pinning by digest in CI

Pinning by tag is fine for humans; CI should pin by digest:

cd deploy/kustomize/overlays/acme-prod
kustomize edit set image \
  registry.acme.io/ampora/web=registry.acme.io/ampora/web@sha256:<digest>

The release stage of .gitlab-ci.yml produces signed digests via cosign; your CD pipeline should use those digests directly.

NetworkPolicy

The base networkpolicy.yaml denies everything except:

  • DNS to kube-system,
  • TCP egress to the Postgres service,
  • TCP egress to the OTel collector,
  • TCP ingress on 8080 from the Ingress controller's namespace.

Add an extra policy for outbound to your Git host, OIDC provider, HSM/KMS endpoint, and federation peers as you turn those features on.

Where to next