CI/CD for Custom Apps
Custom applications built and hosted in this home lab follow a GitOps-native CI/CD pattern: GitHub Actions builds and publishes the container image, then updates a tag file in the k8s-apps repository, and ArgoCD picks up the change and deploys it automatically.
How It Works
1. The App Is Defined in k8s-apps
Every application running in the cluster has a Helm chart under apps/<category>/<name>/ in the k8s-apps repository. Apps that use a custom container image keep the image reference in a separate file called values-image.yaml:
# apps/services/docs/values-image.yaml
image:
repository: ghcr.io/sbeckstrand/docs
pullPolicy: IfNotPresent
tag: "11cff50"
Splitting the image tag into its own file keeps it isolated from the rest of the chart configuration, so automated commits only ever touch one file.
2. ArgoCD Tracks the App with Auto-Sync
The app is registered in ArgoCD's root values file with autoSync: true and the values-image.yaml file listed as an additional Helm values source:
# argocd/values.yaml
services:
- name: docs
namespace: docs
path: apps/services/docs
autoSync: true
valueFiles:
- values-image.yaml
With autoSync enabled, ArgoCD polls the repository and applies any changes within a minute or two of a new commit landing on main.
3. GitHub Actions Builds, Publishes, and Updates the Tag
On every push to main, a workflow in the application's own repository:
- Builds the Docker image
- Pushes it to the GitHub Container Registry (
ghcr.io) tagged with the short commit SHA - Checks out
k8s-appsusing a Personal Access Token - Updates
values-image.yamlwith the new tag - Commits and pushes the change back to
k8s-apps
ArgoCD detects the new commit in k8s-apps and rolls out the updated image automatically.
push to main
│
▼
GitHub Actions
├── build & push image → ghcr.io/sbeckstrand/<app>:<sha>
└── update k8s-apps/apps/services/<app>/values-image.yaml
│
▼
ArgoCD detects change → deploys new image
Implementing This for a New App
Step 1: Create the Helm Chart in k8s-apps
Add a chart under apps/services/<app-name>/. At minimum you need a Chart.yaml, a values.yaml, and a values-image.yaml:
# values-image.yaml
image:
repository: ghcr.io/sbeckstrand/<app-name>
pullPolicy: IfNotPresent
tag: "latest"
The tag: "latest" is just a placeholder — the workflow will overwrite it on the first run.
Step 2: Register the App in ArgoCD
Add an entry to argocd/values.yaml under the appropriate category:
services:
- name: <app-name>
namespace: <app-name>
path: apps/services/<app-name>
autoSync: true
valueFiles:
- values-image.yaml
Commit and push — ArgoCD's root app will pick up the new entry and create the Application on the next sync.
Step 3: Add the GitHub Actions Workflow
In the application's own repository, create .github/workflows/docker-build.yaml:
name: Build and Push Docker Image
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Set short SHA
run: echo "SHORT_SHA=$(echo $GITHUB_SHA | cut -c1-7)" >> $GITHUB_ENV
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository_owner }}/<app-name>:${{ env.SHORT_SHA }}
update-image-tag:
runs-on: ubuntu-latest
needs: build-and-push
steps:
- name: Set short SHA
run: echo "SHORT_SHA=$(echo $GITHUB_SHA | cut -c1-7)" >> $GITHUB_ENV
- name: Checkout k8s-apps Repository
uses: actions/checkout@v4
with:
repository: sbeckstrand/k8s-apps
path: k8s-apps
ref: main
token: ${{ secrets.K8S_APPS_PAT }}
- name: Update Image Tag
run: |
sed -i "s/tag: \".*\"/tag: \"${{ env.SHORT_SHA }}\"/" \
k8s-apps/apps/services/<app-name>/values-image.yaml
- name: Commit and Push Changes
run: |
cd k8s-apps
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
git add apps/services/<app-name>/values-image.yaml
git commit -m "Update <app-name> image tag to ${{ env.SHORT_SHA }}"
git push
Replace <app-name> with the actual application name.
Step 4: Configure the GitHub Actions Secret
The workflow needs a Personal Access Token (PAT) with write access to k8s-apps to commit the tag update.
- Retrieve the
K8S_APPS_PATtoken from 1Password - In the application's GitHub repository, go to Settings → Secrets and variables → Actions
- Create a new repository secret named
K8S_APPS_PATand paste the token value
The GITHUB_TOKEN secret (used to push to the container registry) is provided automatically by GitHub Actions and requires no setup.
!!! note
If the PAT has expired, generate a new one with Contents: Read and Write access scoped to the sbeckstrand/k8s-apps repository, store the new value in 1Password, and update the K8S_APPS_PAT secret in every repo that uses it.