Commit, Push, Reconcile

August 20, 2026

Flux was installed and watching the repository. I wanted to test it with an application, so I chose Linkding, a self-hosted bookmark manager, and gave it its own namespace.

The rule for this exercise was simple: define the desired state, push it to Git and let Flux perform the deployment. No kubectl apply.

Following the source

Flux's bootstrap process had already committed gotk-sync.yaml under clusters/staging/flux-system. Among other resources, that file defines a Flux GitRepository pointing at my zbook-cluster repository.

GitRepository is not a built-in Kubernetes resource. Flux installs the Custom Resource Definition and runs a controller that knows how to reconcile it. I could inspect the resulting object with:

kubectl -n flux-system get gitrepositories.source.toolkit.fluxcd.io
NAME          URL                                        READY   STATUS
flux-system   ssh://git@github.com/joshzee/zbook-cluster True    stored artifact for revision 'main@sha1:44b008d2...'

That status meant the Source Controller had fetched the repository and stored an artifact for the displayed commit. I could now add the application manifests under the path Flux was watching.

Giving the repository a shape

I followed Flux's monorepo structure guidance, separating reusable application definitions from environment-specific configuration:

.
├── apps
│   ├── base
│   │   └── linkding
│   │       ├── deployment.yaml
│   │       ├── kustomization.yaml
│   │       └── namespace.yaml
│   └── staging
│       └── linkding
│           └── kustomization.yaml
└── clusters
    └── staging
        ├── apps.yaml
        └── flux-system
            ├── gotk-components.yaml
            ├── gotk-sync.yaml
            └── kustomization.yaml

The separation gives each layer a clear responsibility:

  • clusters/staging is the entry point already watched by the bootstrap configuration.
  • clusters/staging/apps.yaml defines a Flux Kustomization that points at apps/staging.
  • apps/staging/linkding selects the reusable Linkding base and applies it to the staging environment.
  • apps/base/linkding contains the ordinary Kubernetes resources shared by any future environment.

The local kubeconfig used by my workstation does not belong in this structure. It remains ignored by Git because it contains cluster-administrator credentials.

Two kinds named Kustomization

This setup exposed an initially confusing naming collision.

The resource in apps.yaml is a Flux Kustomization, from the kustomize.toolkit.fluxcd.io API group. It tells a Flux controller which repository path to build and how often to reconcile it. In my staging configuration, the interval is one minute:

spec:
  interval: 1m0s

The files named kustomization.yaml inside the application directories are Kustomize configurations, using kustomize.config.k8s.io/v1beta1. They compose and customise Kubernetes YAML.

The base configuration maps the two resources needed for this first deployment:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - namespace.yaml

namespace.yaml creates the dedicated linkding namespace. deployment.yaml requests one replica of the Linkding container and exposes its application port inside the pod:

spec:
  replicas: 1
  template:
    spec:
      containers:
        - name: linkding
          image: sissbruecker/linkding:1.31.0
          ports:
            - containerPort: 9090

This is intentionally a minimal first deployment. It does not yet include persistent storage, hardened security configuration or public ingress.

The deployment command was a Git push

With the desired state written, I committed it using a Conventional Commits-style message and pushed it to main:

git add .
git commit -m "feat: deploy linkding"
git push origin main

Using git add . assumes sensitive local files are already ignored. Before publishing a GitOps repository, git ls-files kubeconfig should return no output.

Immediately after the push, Flux was still reporting the previous revision:

NAME          REVISION            READY   MESSAGE
flux-system   main@sha1:44b008d2  True    Applied revision: main@sha1:44b008d2

That was expected. Reconciliation is asynchronous: the Source Controller first needs to observe and publish the new Git artifact, then the Kustomizations react to that revision. With one-minute intervals configured in this lab, the old revision briefly remained visible. Shortly afterwards, both Kustomizations reported the new commit:

NAME          REVISION            READY   MESSAGE
apps          main@sha1:a9e88d65  True    Applied revision: main@sha1:a9e88d65
flux-system   main@sha1:a9e88d65  True    Applied revision: main@sha1:a9e88d65

The chain had completed: the bootstrap Kustomization reconciled clusters/staging, apps.yaml created the application-level reconciliation, and the apps Kustomization built and applied the staging manifests.

Verifying what Git created

Git was the deployment interface, but kubectl remained useful for observation. First, I checked the namespaces:

kubectl get namespaces
NAME          STATUS
default       Active
flux-system   Active
kube-system   Active
linkding      Active

Then I checked the workload:

kubectl get pods -n linkding
NAME                        READY   STATUS    RESTARTS
linkding-678cf4b484-kmtxf   1/1     Running   0

I still used kubectl throughout this process to inspect namespaces and pods. Flux handled their creation from the manifests I'd committed.

To reach the application locally, I forwarded port 8080 on my MacBook to port 9090 in the deployment:

kubectl -n linkding port-forward deployment/linkding 8080:9090

Opening http://localhost:8080 presented the Linkding login page. The first application was alive, and its arrival in the cluster could be traced to a specific Git commit.

At this checkpoint:

  • Flux is reconciling both the cluster entry point and the staging applications path.
  • Linkding has its own namespace and a running pod.
  • The deployment is declared in Git and reproducible from the repository.
  • kubectl apply played no part in the deployment.
  • Linkding is still temporary, locally accessible and not yet hardened.

Before I start keeping bookmarks in it, Linkding needs persistent storage and tighter permissions. Access beyond the local port-forward will come after that.