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:
| Resource | Location | Purpose |
|---|---|---|
hello-world-platform namespace | Cluster | Holds deploy keys, AGE keys, Flux config |
hello-world-platform GitHub repo | GitHub (private) | Your app's Kubernetes manifests |
| SSH deploy key | Platform namespace | Flux authenticates to clone your repo |
| AGE encryption key | Platform namespace | SOPS decryption for secrets in your repo |
| Flux GitRepository | Platform namespace | Watches your repo for changes |
| Flux Kustomization | Platform namespace | Applies 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: applabrats.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:
- Select your app in the sidebar context selector
- Navigate to Secrets > Create Secret
- Enter a name, select target namespace(s), and add key-value pairs
- 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
| Step | What You Do | Time |
|---|---|---|
| 1 | Create AppPlatformRegistration (12 lines of YAML) | 30s |
| 2 | Create app namespace via dashboard | 10s |
| 3 | Push Kubernetes manifests to auto-created repo | 60s |
| Total | App deployed with GitOps, TLS, RBAC | ~2 min |