Deploy and Manage Applications
This guide explains how I deploy and manage applications on my Kubernetes cluster using GitOps with ArgoCD.
Quick Start
Applications live in /k8s/applications/
organized by function:
ai/
- AI tools like OpenWebUI, KaraKeepautomation/
- Home automation (Frigate, MQTT)media/
- Media servers and tools (Jellyfin, *arr stack)network/
- Network apps (AdGuard Home, Omada)tools/
- Utility apps (IT-Tools, Whoami, Unrar)web/
- Web applications (BabyBuddy, Pedrobot)external/
- Services outside Kubernetes but referenced internally
How Application Deployment Works
I use ArgoCD ApplicationSet to automatically deploy apps from Git. Here's the process:
- Add your app files to a category folder (e.g.,
/k8s/applications/media/myapp/
) - For most applications, ensure they are discoverable by the main ApplicationSet in
/k8s/applications/application-set.yaml
(which scans subdirectories). - For applications in the
external/
category (like HAOS, Proxmox, TrueNAS), these are managed by a separate ApplicationSet located at/k8s/applications/external/application-set.yaml
. This ApplicationSet specifically scans paths likek8s/apps/external/*
. - ArgoCD detects the change and deploys automatically.
Key Files
kustomization.yaml
- Groups apps in each categoryproject.yaml
- Sets ArgoCD permissions and controlsapplication-set.yaml
- Main deployment configuration (additional ApplicationSets may exist for specific categories likeexternal
)
Application Structure
Each app folder should contain:
myapp/
├── kustomization.yaml # App configuration
├── namespace.yaml # Kubernetes namespace
├── deployment.yaml # Container settings
├── service.yaml # Network exposure
├── pvc.yaml # Storage for Deployments (optional)
├── statefulset.yaml # Stateful workloads with volumeClaimTemplates
├── http-route.yaml # External access
└── values.yaml # Helm values (only if the app uses a Helm chart)
Example: KaraKeep Configuration
Here's how KaraKeep (/k8s/applications/ai/karakeep/
) is structured:
-
Basic Setup
- Uses namespace:
karakeep
- Configures non-sensitive settings via ConfigMap
- Manages versions through Kustomize
- Uses namespace:
-
Security
- Runs as non-root
- Drops unnecessary privileges
- Uses default security profiles
- Helm charts explicitly set
runAsUser
,runAsGroup
, andfsGroup
to1000
withseccompProfile: RuntimeDefault
andallowPrivilegeEscalation: false
. - Default UID and GID for Meilisearch are set to
1000
. AdjustrunAsUser
andrunAsGroup
inmeilisearch-deployment.yaml
if those IDs conflict with your environment.
-
Storage
- Uses Longhorn for app data via PersistentVolumeClaims (e.g.,
data-pvc
,meilisearch-pvc
), which use the default StorageClass (Longhorn). - While a shared NFS media store exists for other media applications, KaraKeep primarily uses its dedicated PVCs for its operational data.
- Uses Longhorn for app data via PersistentVolumeClaims (e.g.,
-
Network Access
- The primary web interface is exposed via a
LoadBalancer
service (e.g.,karakeep-web-svc
with IP10.25.150.230
). Internal components like Meilisearch or Chrome might useClusterIP
services. - External access through Gateway API
- Custom IPs via Cilium
- The primary web interface is exposed via a
-
Secrets
- Managed by ExternalSecrets
- Stored in Bitwarden
- Automatically synced to Kubernetes
Shared Resources
Media Storage
I use NFS for shared media files:
- Location:
172.20.20.103:/mnt/wd1/media_share
- Access: ReadWriteMany
- Retention: Persistent
- Used by: All media apps
Best Practices
- Use Kustomize for configuration
- Store secrets in Bitwarden
- Set resource limits
- Configure security contexts and keep the root filesystem read-only when possible. s6-overlay containers like Frigate must run as root (
runAsUser: 0
), mount/run
as an emptyDir so writes stay ephemeral, and require theCHOWN
,FOWNER
,SETGID
, andSETUID
capabilities so startup scripts can change permissions. If the container needs to write to/etc
during startup, disablereadOnlyRootFilesystem
for that pod - Use automated sync with ArgoCD
- Use the
Recreate
strategy for any Deployment that mounts a PVC. - Be aware that
Recreate
causes downtime during updates, so plan a short maintenance window. - Pin container images to explicit versions and set
imagePullPolicy: IfNotPresent
.
OpenWebUI Notes
OpenWebUI provides a chat interface backed by local AI models. The deployment integrates with Authentik using OIDC and merges accounts by email so users can sign in with any provider. The OLLAMA_BASE_URL
variable is intentionally omitted because the Ollama stack is not managed in this repository.
Chrome and Ollama define both liveness and readiness probes so Kubernetes can restart them if they crash and only route traffic when each pod is ready. Mosquitto and Unrar use similar probes. Pedro Bot relies on PersistentVolumeClaims for logs and data, and Jellyfin and Unrar can run on any available node.
Karakeep Notes
Karakeep authenticates through Authentik using OIDC. The client ID and secret live in Bitwarden and sync to a Kubernetes secret via ExternalSecrets. Password logins are disabled so users sign in only with Authentik.
The container keeps its root filesystem read-only. Temporary paths like /run
and /tmp
come from emptyDir
volumes so s6-overlay can write runtime files.
BabyBuddy Notes
BabyBuddy runs on port 3000
and is deployed purely with Kustomize manifests. The deployment does not include a values.yaml
file, avoiding confusion. Update your service and readiness probes to point to this port if you override the default configuration.
Home Assistant Notes
Home Assistant runs as a StatefulSet. Configuration, media, and data paths each
use their own persistent volume created through volumeClaimTemplates
. It
connects to the shared mosquitto
service in the mqtt
namespace—configure the
integration to use mosquitto.mqtt.svc.cluster.local
and your Bitwarden
credentials. A ConfigMap provides configuration.yaml
so the cluster gateway can
proxy requests using the X-Forwarded-For
header. The main container starts as
root
so its init system can run, but Kubernetes sets the volume group to 1000
so Home Assistant can drop privileges. The BlueZ sidecar now drops all
capabilities and runs unprivileged, reducing risk.
Zigbee2MQTT Notes
Zigbee2MQTT manages the Zigbee adapter without privileged mode. The
zigbee2mqtt
namespace is labeled pod-security.kubernetes.io/enforce=baseline
so
it adheres to the cluster's standard policies.
Need help? Check the application examples in /k8s/applications/
for reference implementations.