Skip to Content
DeploymentCore Platform

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:

  1. metallb-system - Load balancer IP pool
  2. secrets-system - External Secrets + DerivedSecrets operator
  3. cert-manager - TLS certificate management
  4. external-dns - Automatic DNS synchronization
  5. ingress-nginx - HTTP(S) ingress controller
  6. external-tunnel - Cloudflare Tunnel + external ingress
  7. partial-ingress - Partial environment deployment operator
  8. victoria-metrics - Monitoring stack (includes Grafana Alert Operator)
  9. argocd - GitOps controller
  10. gitea - Git server + automation
  11. cnpg-system - PostgreSQL operator
  12. 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.yaml

integrations.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

  1. Log in to Cloudflare Dashboard 
  2. Navigate to My Profile > API Tokens
  3. Click Create Token
  4. Template: Edit zone DNS
  5. Zone Resources: Include > Specific zone > zengarden.space
  6. Click Continue to summary > Create Token
  7. 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>.json

1.4 Obtain GitHub Personal Access Token

  1. Navigate to GitHub Settings > Developer Settings > Personal Access Tokens 
  2. Click Generate new token (classic)
  3. Scopes:
    • repo (all)
    • read:org
    • write:packages
  4. Click Generate token
  5. 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 secrets

2.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.yaml

metallb-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.yaml

secrets-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.yaml

victoria-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 on

Deployment order (automatic via needs dependencies):

  1. metallb-system
  2. secrets-system (DerivedSecrets + External Secrets)
  3. cert-manager (TLS certificates)
  4. external-dns (DNS synchronization)
  5. ingress-nginx-internal
  6. external-tunnel (Cloudflare + ingress-nginx-external)
  7. partial-ingress (PartialIngress operator for CI/PR environments)
  8. victoria-metrics
  9. argocd
  10. gitea
  11. cnpg-system
  12. 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-namespaces

Check specific namespace:

kubectl -n argocd get pods -w kubectl -n gitea get pods -w kubectl -n victoria-metrics get pods -w

Check Helmfile releases:

helm list --all-namespaces

Step 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 assigned

3.2 Verify cert-manager

Check ClusterIssuers:

kubectl get clusterissuers # Expected: # NAME READY AGE # letsencrypt-prod True 10m # internal-ca True 10m

Check certificate issuance (after ingress created):

kubectl get certificates --all-namespaces # Certificates should be in Ready=True state

3.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/TCP

Test 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 echo

Access 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 list

3.5 Verify Gitea

Access Gitea UI:

URL: https://gitea.homelab.int.zengarden.space

Get 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 Completed

Check sync job:

kubectl -n gitea logs job/gitea-sync -f # Should show repository synchronization from GitHub

3.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 events

Verify 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.space

Initial 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:

  1. Navigate to Cloudflare Zero Trust Dashboard 
  2. Access > Tunnels > homelab
  3. Public Hostname tab
  4. Add hostnames:
    • *.homelab.zengarden.spacehttps://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-dns

Step 5: Post-Deployment Configuration

5.1 Configure Gitea Organization Secrets

Gitea Actions needs secrets for CI/CD pipelines:

  1. Navigate to Gitea UI
  2. Organization: zengarden-space
  3. Settings > Secrets > Actions
  4. Add secrets:
    • GITEA_TOKEN: Personal access token (for updating manifests)
    • DOCKER_USERNAME: Gitea username
    • DOCKER_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.git

If 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 10m

application 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=100

Ingress 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 exists

Check 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=50

Gitea 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 connectivity

Manually configure OAuth (if job fails):

  1. Navigate to Gitea UI
  2. Admin panel > Authentication Sources
  3. 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 disabled

Check restrictive proxy:

ssh [email protected] sudo systemctl status restrictive-proxy # Check logs sudo journalctl -u restrictive-proxy -f

DerivedSecrets 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=50

Verify master password:

kubectl -n derived-secret-operator get secret master-password # Ensure secrets-system/env.yaml masterPassword was set correctly

Next Steps

Core platform deployed! Continue to:

  1. Applications Deployment - Deploy applications via GitOps
  2. Operations - Learn about ongoing maintenance

Core platform deployment complete. Your Kubernetes cluster now has all platform services running!