Core Platform Deployment
Deploy Platform Services with Helmfile
This guide walks through deploying core Kubernetes platform services using Helmfile.
Prerequisites
- K3s cluster running (from Infrastructure Deployment)
- kubectl configured with cluster access
- helm and helmfile installed on management workstation
- Cloudflare account with API token
- GitHub personal access token
- Domain name registered
Overview
The core platform includes:
- metallb-system - Load balancer IP pool
- secrets-system - External Secrets + DerivedSecrets operator
- cert-manager - TLS certificate management
- external-dns - Automatic DNS synchronization
- ingress-nginx - HTTP(S) ingress controller
- external-tunnel - Cloudflare Tunnel + external ingress
- partial-ingress - Partial environment deployment operator
- victoria-metrics - Monitoring stack (includes Grafana Alert Operator)
- argocd - GitOps controller
- gitea - Git server + automation
- cnpg-system - PostgreSQL operator
- metabase - Analytics platform
Step 1: Configure External Integrations
1.1 Create integrations.yaml
Navigate to helmfile directory:
cd helmfile
cp ../integrations.yaml.template integrations.yaml
nano integrations.yamlintegrations.yaml structure:
# Cloudflare Credentials
cert-manager:
cloudflareEmail: "[email protected]"
cloudflareApiToken: "<cloudflare-api-token>"
external-tunnel:
accountId: "<cloudflare-account-id>"
tunnelId: "<cloudflare-tunnel-id>"
tunnelSecret: "<cloudflare-tunnel-secret>"
# GitHub Credentials
gitea:
githubToken: "<github-personal-access-token>"
googleClientId: "<google-client-id>.apps.googleusercontent.com"
googleClientSecret: "<google-client-secret>"
# ArgoCD Credentials
argocd:
googleClientId: "<google-client-id>.apps.googleusercontent.com"
googleClientSecret: "<google-client-secret>"
adminEmail: "[email protected]"1.2 Obtain Cloudflare API Token
- Log in to Cloudflare Dashboard
- Navigate to My Profile > API Tokens
- Click Create Token
- Template: Edit zone DNS
- Zone Resources: Include > Specific zone > zengarden.space
- Click Continue to summary > Create Token
- Copy token to
integrations.yaml
1.3 Create Cloudflare Tunnel
# Install cloudflared CLI
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64.deb
sudo dpkg -i cloudflared-linux-arm64.deb
# Authenticate with Cloudflare
cloudflared tunnel login
# Create tunnel
cloudflared tunnel create homelab
# Note the Tunnel ID from output
# Copy credentials.json content to integrations.yaml (tunnelSecret)
cat ~/.cloudflared/<tunnel-id>.json1.4 Obtain GitHub Personal Access Token
- Navigate to GitHub Settings > Developer Settings > Personal Access Tokens
- Click Generate new token (classic)
- Scopes:
repo(all)read:orgwrite:packages
- Click Generate token
- Copy token to
integrations.yaml
Step 2: Deploy Core Services
2.1 Create Kubernetes Secret for Integrations
Apply integrations.yaml as Kubernetes secret:
# Create integrations namespace
kubectl create namespace integrations
# Create secret from integrations.yaml
kubectl -n integrations create secret generic integrations \
--from-file=integrations.yaml
# Verify secret
kubectl -n integrations get secrets2.2 Configure Component Environment Files
Each component has an env.yaml.template - copy and customize:
# MetalLB (IP pool configuration)
cd metallb-system
cp env.yaml.template env.yaml
nano env.yamlmetallb-system/env.yaml:
ipRanges:
- 192.168.77.200-192.168.77.254# Secrets System (master password for DerivedSecrets)
cd ../secrets-system
cp env.yaml.template env.yaml
nano env.yamlsecrets-system/env.yaml:
masterPassword: "<generate-strong-password>"
# Use: openssl rand -base64 32# Victoria Metrics (Gotify notification settings)
cd ../victoria-metrics
cp env.yaml.template env.yaml
nano env.yamlvictoria-metrics/env.yaml:
domain: zengarden.space
notifications:
adminPassword: "<generate-password>"
apiToken: "<will-be-generated-after-deployment>"Repeat for all components with .env.yaml.template files.
2.3 Deploy Helmfile
Deploy all components in order:
cd .. # Back to helmfile/ directory
# Dry run (see what will be deployed)
helmfile diff
# Deploy all releases
helmfile apply
# Or deploy specific component
helmfile -f metallb-system/helmfile.yaml.gotmpl apply
helmfile -f secrets-system/helmfile.yaml.gotmpl apply
# ... and so onDeployment order (automatic via needs dependencies):
- metallb-system
- secrets-system (DerivedSecrets + External Secrets)
- cert-manager (TLS certificates)
- external-dns (DNS synchronization)
- ingress-nginx-internal
- external-tunnel (Cloudflare + ingress-nginx-external)
- partial-ingress (PartialIngress operator for CI/PR environments)
- victoria-metrics
- argocd
- gitea
- cnpg-system
- metabase
Duration: ~30-60 minutes (depends on image pulls and readiness checks)
2.4 Monitor Deployment Progress
Watch all pods:
watch kubectl get pods --all-namespacesCheck specific namespace:
kubectl -n argocd get pods -w
kubectl -n gitea get pods -w
kubectl -n victoria-metrics get pods -wCheck Helmfile releases:
helm list --all-namespacesStep 3: Verify Core Platform
3.1 Verify MetalLB
Check IP pool:
kubectl -n metallb-system get ipaddresspools
# Expected output:
# NAME AUTO ASSIGN AVOID BUGGY IPS ADDRESSES
# default true false ["192.168.77.200-192.168.77.254"]Check LoadBalancer IPs assigned:
kubectl get svc --all-namespaces -o wide | grep LoadBalancer
# Expected: ingress-nginx services have EXTERNAL-IP assigned3.2 Verify cert-manager
Check ClusterIssuers:
kubectl get clusterissuers
# Expected:
# NAME READY AGE
# letsencrypt-prod True 10m
# internal-ca True 10mCheck certificate issuance (after ingress created):
kubectl get certificates --all-namespaces
# Certificates should be in Ready=True state3.3 Verify Ingress
Check internal ingress:
kubectl -n ingress-nginx get svc ingress-nginx-internal-controller
# Expected:
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
# ingress-nginx-internal-controller LoadBalancer 10.43.xxx.xxx 192.168.77.200 80:xxx/TCP,443:xxx/TCPTest ingress (should return 404, but shows it’s working):
curl -k https://192.168.77.200
# Expected: 404 page not found (nginx default)3.4 Verify ArgoCD
Get initial admin password:
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
echoAccess ArgoCD UI:
URL: https://argocd.homelab.int.zengarden.space
Username: admin
Password: <from above>Login via CLI:
argocd login argocd.homelab.int.zengarden.space --username admin
# List applications (should be empty initially)
argocd app list3.5 Verify Gitea
Access Gitea UI:
URL: https://gitea.homelab.int.zengarden.spaceGet admin credentials:
# Admin password is derived from DerivedSecret
kubectl -n gitea get secret gitea-admin -o jsonpath="{.data.password}" | base64 -d
echo
# Username: admin
# Password: <from above>Check OAuth setup job:
kubectl -n gitea get jobs
# gitea-oauth-setup should be CompletedCheck sync job:
kubectl -n gitea logs job/gitea-sync -f
# Should show repository synchronization from GitHub3.6 Verify External DNS
Check external-dns logs:
kubectl -n external-dns logs -l app.kubernetes.io/name=external-dns --tail=50
# Should show DNS record creation eventsVerify MikroTik DNS records:
# SSH to MikroTik
ssh [email protected]
# Check DNS static entries
/ip dns static print
# Should see entries for:
# - gitea.homelab.int.zengarden.space
# - argocd.homelab.int.zengarden.space
# - grafana.homelab.int.zengarden.space
# - etc.3.7 Verify Victoria Metrics
Access Grafana:
URL: https://grafana.homelab.int.zengarden.space
Username: admin
Password: <from victoria-metrics/env.yaml or generated>Check data sources:
- Navigate to Configuration > Data Sources
- Should see VictoriaMetrics configured
- Should see Loki configured (preconfigured datasource)
Check dashboards:
- Navigate to Dashboards
- Import Kubernetes dashboards (if not auto-imported)
Check logs (Loki):
- Navigate to Explore
- Select Loki data source
- Try query:
{namespace="victoria-metrics"} - Should see logs from Promtail
3.8 Verify Metabase
Access Metabase:
URL: https://metabase.homelab.int.zengarden.spaceInitial setup:
- Create admin account
- Skip adding database (CNPG operator will do this automatically)
Check CNPG operator logs:
kubectl -n metabase logs -l app=metabase-cnpg-operator --tail=50
# Should show database auto-discovery events (after CNPG databases are created)Step 4: Configure DNS
4.1 Internal DNS (homelab.int.zengarden.space)
Already configured automatically by external-dns!
Verify from home network:
nslookup gitea.homelab.int.zengarden.space
# Should resolve to MetalLB IP (192.168.77.200-254 range)4.2 External DNS (homelab.zengarden.space via Cloudflare Tunnel)
Configure Cloudflare Tunnel DNS:
- Navigate to Cloudflare Zero Trust Dashboard
- Access > Tunnels > homelab
- Public Hostname tab
- Add hostnames:
*.homelab.zengarden.space→https://ingress-nginx-external-controller.external-tunnel.svc:443- (Specific services as needed)
Or use Cloudflare DNS API:
# Add CNAME for Tunnel
# This is done via Cloudflare Tunnel configuration, not external-dnsStep 5: Post-Deployment Configuration
5.1 Configure Gitea Organization Secrets
Gitea Actions needs secrets for CI/CD pipelines:
- Navigate to Gitea UI
- Organization: zengarden-space
- Settings > Secrets > Actions
- Add secrets:
GITEA_TOKEN: Personal access token (for updating manifests)DOCKER_USERNAME: Gitea usernameDOCKER_PASSWORD: Gitea password or token
5.2 Configure ArgoCD Repository
Add manifests repository to ArgoCD:
# ArgoCD automation chart should auto-configure this, but verify:
argocd repo list
# Should show:
# https://gitea.homelab.int.zengarden.space/zengarden-space/manifests.gitIf not configured, add manually:
argocd repo add https://gitea.homelab.int.zengarden.space/zengarden-space/manifests.git \
--username admin \
--password <gitea-admin-password>5.3 Verify ArgoCD application
Check application:
kubectl -n argocd get applications
# Expected:
# NAME AGE
# applications 10mapplication should auto-discover apps from manifests/ repo directories
Troubleshooting
Helm Release Fails
Check Helm release status:
helm list --all-namespaces --failed
# Get release details
helm status <release-name> -n <namespace>
# Check logs
kubectl -n <namespace> logs -l app=<app-name> --tail=100Ingress Not Accessible
Check ingress resource:
kubectl -n <namespace> get ingress
kubectl -n <namespace> describe ingress <ingress-name>
# Verify:
# - Hosts configured correctly
# - TLS certificate issued (cert-manager)
# - Backend service existsCheck certificate issuance:
kubectl -n <namespace> get certificate
kubectl -n <namespace> describe certificate <cert-name>
# If not ready, check cert-manager logs
kubectl -n cert-manager logs -l app=cert-manager --tail=50Gitea OAuth Setup Fails
Check OAuth setup job:
kubectl -n gitea logs job/gitea-oauth-setup --tail=100
# Common issues:
# - Gitea not ready yet (job retries automatically)
# - Wrong Google OAuth credentials
# - Network connectivityManually configure OAuth (if job fails):
- Navigate to Gitea UI
- Admin panel > Authentication Sources
- Add OAuth2 source:
- Provider: Google
- Client ID/Secret: From integrations.yaml
- Redirect URI:
https://gitea.homelab.int.zengarden.space/user/oauth2/google/callback
External DNS Not Creating Records
Check external-dns logs:
kubectl -n external-dns logs -l app.kubernetes.io/name=external-dns --tail=100
# Common issues:
# - Restrictive proxy not running
# - Wrong MikroTik credentials
# - MikroTik REST API disabledCheck restrictive proxy:
ssh [email protected]
sudo systemctl status restrictive-proxy
# Check logs
sudo journalctl -u restrictive-proxy -fDerivedSecrets Not Created
Check DerivedSecret CRD:
kubectl get derivedsecrets --all-namespaces
# Check operator logs
kubectl -n derived-secret-operator logs -l app=derived-secret-operator --tail=50Verify master password:
kubectl -n derived-secret-operator get secret master-password
# Ensure secrets-system/env.yaml masterPassword was set correctlyNext Steps
Core platform deployed! Continue to:
- Applications Deployment - Deploy applications via GitOps
- Operations - Learn about ongoing maintenance
Core platform deployment complete. Your Kubernetes cluster now has all platform services running!