In an earlier post I described my homelab: a single EC2 instance running a small k3s cluster, with zero open ports to the public internet while still allowing secure remote access and selective app exposure. Here’s a quick recap:
The overall design worked well, but a few pain points motivated a rethink:
Those limitations pushed me to find a more robust, maintainable approach.
My plan was to adopt GitOps with FluxCD to manage the cluster. Flux is lightweight compared to ArgoCD and a natural fit for managing Kubernetes resources and Helm charts from Git. With values.yaml checked into Git, you get a clean, reusable setup and straightforward configuration for each app.
The main challenges I faced were:
I keep secrets in Terraform Cloud for convenience and to access auto-generated outputs (for example, DNS records). I looked for a clean way to get those values into Git-managed values.yaml files and to deliver sensitive values into Kubernetes without exposing them in Git. The solution I built has two pieces:
1) A small Terraform provider( ip812/gitsync) that syncs value files in a Git repo — it updates values.yaml with non-sensitive outputs from Terraform so FluxCD can pick them up and reconcile the cluster.
resource "gitsync_values_yaml" "go-template" {
branch = "main"
path = "values/${local.go_template_app_name}.yaml"
content = <<EOT
isInit: false
name: "${local.go_template_app_name}"
image: "ghcr.io/iypetrov/go-template:1.15.0"
hostname: "${cloudflare_dns_record.go_template_dns_record.name}"
replicas: 1
minMemory: "64Mi"
maxMemory: "128Mi"
minCPU: "50m"
maxCPU: "100m"
healthCheckEndpoint: "/healthz"
env:
- name: APP_ENV
value: "${local.env}"
- name: APP_DOMAIN
value: "${cloudflare_dns_record.go_template_dns_record.name}"
- name: APP_PORT
value: "8080"
- name: DB_NAME
value: "${local.go_template_db_name}"
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: "${local.go_template_app_name}-creds"
key: PG_USERNAME
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: "${local.go_template_app_name}-creds"
key: PG_PASSWORD
- name: DB_ENDPOINT
value: "${local.go_template_db_name}-pg-rw.${local.go_template_app_name}.svc.cluster.local"
- name: DB_SSL_MODE
value: disable
database:
postgres:
name: "${local.go_template_db_name}"
host: "${local.go_template_db_name}-pg-rw.${local.go_template_app_name}.svc.cluster.local"
image: "ghcr.io/cloudnative-pg/postgresql:16.1"
username: "${var.pg_username}"
storageSize: "1Gi"
retentionPolicy: "7d"
backupsBucket: "${aws_s3_bucket.pg_backups.bucket}"
backupSchedule: "0 0 0 * * *"
EOT
}
The important part is that sensitive values are not stored in the committed values.yaml — instead, they are referenced from Kubernetes secrets by name.
2) For secret management I chose Doppler. HashiCorp Vault Dedicated is far too expensive for a hobby project(at the time of writing ~457$ per month), while Doppler offers a generous free tier plus good Terraform and Kubernetes integrations. Creating a secret in Doppler looks like this:
resource "doppler_secret" "pg_password" {
project = "prod"
config = "prd"
name = "PG_PASSWORD"
value = var.pg_password
}
From Doppler I generate Kubernetes secrets (the Doppler Kubernetes operator supports processors to transform key names if necessary). Example:
---
apiVersion: secrets.doppler.com/v1alpha1
kind: DopplerSecret
metadata:
name: ghcr-auth-go-template
namespace: doppler-operator-system
spec:
tokenSecret:
name: doppler-token-secret
project: prod
config: prd
managedSecret:
name: ghcr-auth
namespace: go-template
type: kubernetes.io/dockerconfigjson
processors:
GHCR_DOCKERCONFIGJSON:
type: plain
asName: .dockerconfigjson
With this setup the infra repository (Terraform) owns the source of truth for infrastructure and secrets. When Terraform produces values that should land in the cluster, the gitsync provider writes non-sensitive values into the values.yaml files in Git (Flux picks them up). Sensitive values live in Doppler and are projected into Kubernetes secrets via the Doppler operator. FluxCD reconciles the cluster from the Git repo, and Kubernetes pulls secrets from Doppler — no secrets committed to Git and no awkward Terraform workspace choreography.
I consider this my final GitOps-driven iteration for the homelab: a blend of Terraform for infra, FluxCD for cluster reconciliation, a small gitsync bridge for non-sensitive outputs, and Doppler for secrets. The result is simpler, more reliable, and easier to manage — and the approach scales beyond homelabs to production environments.
If you have questions or suggestions, jump into the comments or find me on social media. The infra repo is available here and the apps repo is here.