Deploy an App with the App Platform: Hello World Walkthrough

This guide shows how to deploy a simple nginx container using the App Platform. The entire process takes about 2 minutes.

What You Get

When you register an app with the App Platform, the operator automatically:

  • Creates a private GitHub repo for your app's Kubernetes manifests
  • Creates a platform namespace with deploy keys and encryption keys
  • Sets up Flux GitOps sync so changes to your repo auto-deploy
  • Configures RBAC for team access

OIDC clients are registered separately via labeled Secrets — see Enabling SSO below.

Step 1: Register Your App (1 file, 12 lines)

Create an AppPlatformRegistration CR in the Flux repo:

# appplatformregistrations/helloworld.yaml
apiVersion: labrats.work/v1alpha1
kind: AppPlatformRegistration
metadata:
  name: hello-world
spec:
  displayName: "Hello World"
  hostname: hello-world.hcl.labrats.work
  access:
    - group: helloworld-ops
      role: admin

Add it to the kustomization:

# appplatformregistrations/kustomization.yaml
resources:
  - helloworld.yaml

Add a Flux Kustomization entry in clusters/main/apps.yaml:

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: apps-hello-world-platform
  namespace: flux-system
spec:
  dependsOn:
    - name: apps-app-platform
  interval: 10m
  sourceRef:
    kind: GitRepository
    name: flux-system
  path: ./apps/hello-world-platform
  prune: true
  decryption:
    provider: sops
    secretRef:
      name: sops-age

Commit and push. Within seconds, the operator reconciles and creates:

ResourceLocationPurpose
hello-world-platform namespaceClusterHolds deploy keys, AGE keys, Flux config
hello-world-platform GitHub repoGitHub (private)Your app's Kubernetes manifests
SSH deploy keyPlatform namespaceFlux authenticates to clone your repo
AGE encryption keyPlatform namespaceSOPS decryption for secrets in your repo
Flux GitRepositoryPlatform namespaceWatches your repo for changes
Flux KustomizationPlatform namespaceApplies manifests from your repo

Verify:

$ kubectl get appplatformregistrations hello-world
NAME          DISPLAYNAME   READY   AGE
hello-world   Hello World   True    30s

$ kubectl get ns hello-world-platform
NAME                   STATUS   AGE
hello-world-platform   Active   30s

Step 2: Create an App Namespace

Use the dashboard API to create a namespace for your workloads:

# Via the App Platform dashboard UI, or:
curl -X POST http://app-platform-api.app-platform.svc.cluster.local/api/namespaces \
  -H "Content-Type: application/json" \
  -H "Cookie: auth_token=<your-jwt>" \
  -d '{"name": "hello-world"}'

This creates the hello-world namespace with labels:

  • labrats.work/namespace-role: app
  • labrats.work/app: hello-world

Step 3: Add Your Kubernetes Manifests (Just Push to Git)

Clone the auto-created platform repo and add your manifests:

gh repo clone labrats-work/hello-world-platform
cd hello-world-platform

The repo already has a README.md and .sops.yaml (for encrypting secrets).

Create your deployment:

# base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-world
  namespace: hello-world
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hello-world
  template:
    metadata:
      labels:
        app: hello-world
    spec:
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 10m
              memory: 16Mi
            limits:
              cpu: 100m
              memory: 32Mi
# base/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: hello-world
  namespace: hello-world
spec:
  selector:
    app: hello-world
  ports:
    - port: 80
      targetPort: 80
# base/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: hello-world
  namespace: hello-world
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  rules:
    - host: hello-world.hcl.labrats.work
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: hello-world
                port:
                  number: 80
  tls:
    - hosts:
        - hello-world.hcl.labrats.work
      secretName: hello-world-tls
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
  - ingress.yaml
# kustomization.yaml (root)
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - base

Commit and push:

git add -A && git commit -m "feat: deploy hello-world nginx" && git push

Step 4: Done

Within 60 seconds, Flux syncs your repo and deploys the app:

$ kubectl -n hello-world get pods
NAME                           READY   STATUS    RESTARTS   AGE
hello-world-6bcdc49c5d-qc6wt   1/1     Running   0          21s

$ kubectl -n hello-world get ingress
NAME          HOSTS                          PORTS     AGE
hello-world   hello-world.hcl.labrats.work   80, 443   21s

Your app is live at https://hello-world.hcl.labrats.work.

What Happened Behind the Scenes

1. You created an AppPlatformRegistration CR
   --- Operator reconciled in ~10 seconds

2. Operator automatically created:
   - hello-world-platform namespace (with labels)
   - GitHub repo: labrats-work/hello-world-platform (private)
   - SSH deploy key (Ed25519, registered in GitHub)
   - AGE encryption keypair (for SOPS secrets)
   - Flux GitRepository CR (watches your repo via SSH)
   - Flux Kustomization CR (applies manifests from your repo)

3. You pushed Kubernetes manifests to the platform repo
   --- Flux detected the change and applied them

4. Result: nginx running with TLS ingress

Updating Your App

Just push to the platform repo. Flux watches for changes and applies them automatically:

# Change replica count, update image, add a ConfigMap, etc.
vim base/deployment.yaml
git add -A && git commit -m "scale to 3 replicas" && git push
# Flux applies the change within 60 seconds

Adding Secrets

Use the Secrets page in the dashboard:

  1. Select your app in the sidebar context selector
  2. Navigate to Secrets > Create Secret
  3. Enter a name, select target namespace(s), and add key-value pairs
  4. Click Create — the operator encrypts and commits to Git automatically
Dashboard → K8s Secret (intent) → Operator (encrypt + commit) → Flux (decrypt + apply)

Secrets are write-only — the dashboard shows key names but never values.

Enabling SSO (Optional) {#enabling-sso}

OIDC clients are not declared on the APR. Register an Authelia OIDC client by creating a labeled Secret in the <app>-platform namespace:

apiVersion: v1
kind: Secret
metadata:
  name: hello-world-oidc
  namespace: hello-world-platform
  labels:
    labrats.work/oidc-client: hello-world      # client_id in Authelia
    labrats.work/app: hello-world
type: Opaque
stringData:
  hostname: hello-world.hcl.labrats.work
  callbackPath: /api/auth/oidc/callback        # optional
  # clientSecret is operator-generated

The OidcClientReconciler watches these Secrets, generates the clientSecret, registers with Authelia, and reports status via the labrats.work/oidc-status annotation. Multiple OIDC clients per app are supported — one Secret per client/hostname.

Summary

StepWhat You DoTime
1Create AppPlatformRegistration (12 lines of YAML)30s
2Create app namespace via dashboard10s
3Push Kubernetes manifests to auto-created repo60s
TotalApp deployed with GitOps, TLS, RBAC~2 min