Skip to content

About

Tooling and manifests to build web apps from a GitHub repo without a Dockerfile (kpack/Buildpacks) and auto-deploy them into the k3s cluster via Flux image automation.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

4 Commits

Folders and files

Repository files navigation

πŸš€ homelab-gitops

Push to GitHub, get a website. No Dockerfile needed. Tooling and Flux manifests that build your Node / Python / PHP apps with Cloud Native Buildpacks and roll them out into a Kubernetes cluster automatically.

πŸ“¦ git push Β β†’Β  πŸ—οΈ kpack Β β†’Β  πŸ™ ghcr.io Β β†’Β  πŸ”„ Flux Β β†’Β  🌐 https://your.site

License Flux Kubernetes Buildpacks PRs welcome


🎯 What it is

A small toolkit on top of an existing Flux-managed cluster (referred to below as your kubernetes-config repo) that turns "a GitHub repo with a web app" into "a running, auto-updating website" with one command.

Piece Role
πŸ—οΈ kpack watches each app repo and builds an immutable, digest-pinned image with Paketo buildpacks on every commit, pushing it to ghcr.io
πŸ” Flux image automation scans ghcr for new builds and commits the newest tag back into the app's HelmRelease
🚒 bjw-s/app-template generic Helm chart that runs the app: Deployment, Service, Traefik Ingress
🧰 just new-website generates all manifests for a new site, optionally with a bundled MariaDB
git push (app repo)
   └─▢ kpack Image builds via buildpacks ──▢ ghcr.io/<owner>/<app>:b<N>.<timestamp>
          └─▢ Flux ImageRepository + ImagePolicy pick the highest build number
                 └─▢ Flux ImageUpdateAutomation commits the tag into release.yaml (git)
                        └─▢ Flux helm-controller rolls out the HelmRelease

Every deployed version is a git commit β€” one field owner, full audit trail, trivial rollback.


πŸ—‚οΈ Repository layout

This repo is a staging mirror: everything outside templates/, scripts/ and the justfile mirrors the path it belongs to inside kubernetes-config/, so you can copy it 1:1.

Here β†’ in kubernetes-config/ Action
πŸ“š sources/app-template-repo.yaml sources/ add to sources/kustomization.yaml
πŸ—οΈ infrastructure/kpack/ infrastructure/kpack/ copy, then vendor release.yaml (step 3)
πŸ” infrastructure/flux-image-automation/ infrastructure/flux-image-automation/ copy
πŸŽ›οΈ infrastructure/controllers/{kpack,flux-image-automation}.yaml infrastructure/controllers/ add to the controllers aggregate
🌐 apps/base/webhosting/websites/ apps/base/webhosting/websites/ reference websites from the webhosting aggregate
🧩 templates/website/ β€” manifest templates used by the onboarding script
🧰 scripts/new-website.sh, justfile β€” onboarding and helper commands

All websites share the namespace webhosting. The registry/git credentials and the kpack ServiceAccount live there exactly once. websites/demo/ is a sample site that shows the generated output β€” replace or delete it.

Note

The secret files in this repo contain placeholders only. In kubernetes-config they must hold real values and be SOPS-encrypted β€” Flux needs SOPS decryption configured for the Kustomization that applies apps/.


🧱 Prerequisites

  • A Kubernetes cluster managed by Flux v2 from a GitHub repo, with SOPS decryption set up
  • Traefik as ingress controller (see Adapt to your cluster)
  • A GitHub account for ghcr.io packages
  • Local tools: just, openssl, sops, flux, kubectl; for just validate additionally kustomize and kubeconform

πŸ› οΈ One-time cluster setup

1. Enable Flux image automation with a write-capable deploy key

The image controllers are not part of a standard bootstrap, and the automation has to push commits back to git:

flux bootstrap github \
  --owner=<you> --repository=kubernetes-config \
  --branch=main --path=clusters/production --personal \
  --read-write-key=true \
  --components-extra=image-reflector-controller,image-automation-controller

Re-running bootstrap is idempotent. ImageUpdateAutomation checks out and pushes to main and only touches files under apps/base/webhosting/websites/.

2. Create the credentials

Use three narrowly scoped tokens instead of one broad all-repo token:

Secret Token Used by
⬆️ ghcr-push classic PAT: read:packages + write:packages kpack pushes images and the builder
⬇️ ghcr-pull fine-grained PAT: Packages: read Flux registry scan + kubelet image pull
πŸ“₯ github-git fine-grained PAT: Contents: read on the app repos kpack clones private source repos

Important

ghcr-push must be a classic PAT. kpack pulls its lifecycle image from ghcr.io/buildpacks-community/kpack/lifecycle with these credentials, and a fine-grained PAT is rejected with DENIED for that foreign namespace.

Fill in and encrypt the files in apps/base/webhosting/websites/ β€” each one documents a kubectl create secret … --dry-run=client -o yaml | sops --encrypt one-liner. The website templates wire ghcr-pull in automatically (ImageRepository.secretRef and defaultPodOptions.imagePullSecrets).

3. Vendor the kpack core

kpack ships no Helm chart, so its release YAML is committed to git:

KPACK_VERSION=v0.17.1   # check the latest: github.com/buildpacks-community/kpack/releases
curl -sSL -o infrastructure/kpack/release.yaml \
  https://github.com/buildpacks-community/kpack/releases/download/$KPACK_VERSION/release-${KPACK_VERSION#v}.yaml

Also set your owner in infrastructure/kpack/cluster-builder.yaml (tag: ghcr.io/<you>/kpack-builder). After Flux has applied it, wait for:

kubectl get clusterbuilder paketo-full   # READY=True

4. Pin the app-template version

Set ref.tag in sources/app-template-repo.yaml to the current app-template release.


🌐 Onboard a new website

just new-website <name> <repo-url> <host> <port> [owner] [db]

# example
just new-website myblog https://github.com/<you>/myblog myblog.example.com 3000 <you>
Argument Meaning
🏷️ name resource name and ghcr package name; only [a-z0-9-]
πŸ”— repo-url GitHub repo kpack builds from (branch main)
🌍 host public hostname for the Ingress
πŸ”Œ port port the app listens on; also exported as $PORT
πŸ‘€ owner ghcr owner; falls back to $GHCR_OWNER, lowercased automatically
πŸ—„οΈ db empty = no database, mariadb = bundle one (see Bundled database below)

This writes apps/base/webhosting/websites/<name>/ and registers the folder in the websites kustomization.yaml. By default the target is this repo; point it at your real config repo with KCONFIG_ROOT:

KCONFIG_ROOT=/path/to/kubernetes-config GHCR_OWNER=<you> \
  ./scripts/new-website.sh myblog https://github.com/<you>/myblog myblog.example.com 3000

Review the output (especially the port), commit, push β€” kpack builds, Flux deploys.

πŸ—„οΈ Bundled database (MariaDB)

Pass mariadb as the sixth argument (mysql and maria are accepted aliases):

just new-website myblog https://github.com/<you>/myblog myblog.example.com 3000 <you> mariadb

This adds a second controller db to the same HelmRelease β€” a MariaDB 11.4 StatefulSet with a 5 Gi PVC and a service <name>-db:3306 β€” through two extra files:

  • db-secret.yaml β€” database name, user and random passwords; the single source of truth for both MariaDB and the app
  • db-patch.yaml β€” a Kustomize strategic-merge patch, so release.yaml stays untouched

The app container receives DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME and DB_PASSWORD. Laravel uses exactly these names; for other frameworks rename the keys in db-patch.yaml. MariaDB speaks the MySQL wire protocol, so apps connect as to MySQL.

Warning

db-secret.yaml is generated with plaintext passwords. Encrypt it before committing: sops --encrypt --in-place apps/base/webhosting/websites/<name>/db-secret.yaml

The bundled DB has no HA and no automated backups. For production-grade databases install a dedicated operator (e.g. CloudNativePG for Postgres) under infrastructure/controllers/.


πŸ“¦ App requirements

The ClusterBuilder paketo-full detects the language in this order: Node.js β†’ Python β†’ PHP.

  • The app sits at the repo root β€” otherwise set spec.source.subPath in image.yaml.
  • Builds track the main branch β€” change spec.source.git.revision in image.yaml if needed.
  • It runs a long-lived web process on a port, ideally honoring $PORT.
  • Buildpacks can detect a start command; a Procfile (web: …) works for every stack.
Stack Minimum
🟩 Node.js package.json with a start script (or a Procfile)
🐍 Python usually a Procfile, e.g. web: gunicorn app:app -b 0.0.0.0:$PORT
🐘 PHP composer.json; set BP_PHP_WEB_DIR (e.g. public) under spec.build.env in image.yaml for a custom web root

πŸšͺ Escape hatch: apps that need a Dockerfile

Some apps don't fit "detect one language" β€” e.g. a Laravel app with a Vite/Vue frontend whose asset build calls php artisan and so needs PHP and Node at build time. Build those with a multi-stage Dockerfile in GitHub Actions and push to ghcr.io/<you>/<app>, then:

  1. delete image.yaml and remove it from the site's kustomization.yaml;
  2. in imagepolicy.yaml, replace filterTags with pattern: '^[0-9]+$' (drop extract) and tag the CI images with the numeric run_number;
  3. keep release.yaml unchanged.

βš™οΈ Adapt to your cluster

What Where
πŸ‘€ <you> / you β†’ your lowercase GitHub owner cluster-builder.yaml, websites/demo/, secret comments
πŸ“Œ Version pins: kpack v0.17.1, app-template 5.0.1, MariaDB 11.4, Paketo images (unpinned) β€” verify against current releases infrastructure/kpack/, sources/, templates/website/db-patch.yaml
🚦 Ingress: className: traefik, entrypoint websecure, middleware traefik-security-chain@kubernetescrd (must exist in your cluster) templates/website/release.yaml
πŸ”’ TLS is terminated by Traefik; uncomment the tls: block to use a per-site certificate secret instead templates/website/release.yaml

🩺 Day-2 commands

Command What it does
βœ… just validate renders every site with kustomize and checks it with kubeconform
πŸ“Š just status shows Flux kustomizations, image repositories and policies, and kpack images/builds

🀝 Contributing

Issues and pull requests are welcome. For larger changes, open an issue first. Run just validate before submitting changes to the manifests or templates.


πŸ“„ License

MIT β€” see LICENSE. Β© 2026 Jeffry WΓΌrmli.


Built for a homelab β€” kpack builds, Flux ships, git remembers.

About

Tooling and manifests to build web apps from a GitHub repo without a Dockerfile (kpack/Buildpacks) and auto-deploy them into the k3s cluster via Flux image automation.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages