Flux CD Flux CD

Structure a Repository with Kustomize Overlays per Environment

Organize one repository for dev and production, and avoid the ConfigMap rollout trap.

15 min Intermediate

Real applications run in several environments that differ in small ways: replica counts, hostnames, configuration values. Flux applies plain YAML or Kustomize overlays, and detects which one a folder uses. This guide shows how to structure a repository for multiple environments with Kustomize, how to tell Flux about each one, and covers a pitfall that catches people with configuration changes. It builds on the webapp example from earlier guides.

Choose a layout: folders, not branches

You can model environments as long-lived branches (dev, staging, main), but that makes promotion a merge exercise and lets environments drift apart. A single branch with one folder per environment is easier to reason about: every environment is visible side by side, and a change to one is a normal diff.

Flux’s documentation describes three common repository strategies:

  • Monorepo. All clusters, apps, and infrastructure in one repository. Easiest to start with and the one used here.
  • Repository per environment. For example, one for staging and one for production, with separate access rules.
  • Repository per team or per application. Strong isolation, with a platform repository that registers each team’s repository.

A typical monorepo looks like this:

fleet-infra/
├── clusters/
│   ├── dev/                 # Flux Kustomizations for the dev cluster
│   └── production/          # Flux Kustomizations for the production cluster
├── infrastructure/          # ingress controller, cert-manager, monitoring
└── apps/
    ├── base/webapp/         # shared manifests
    ├── dev/                 # overlay for dev
    └── production/          # overlay for production

Build a Kustomize base and overlays

apps/base/webapp/kustomization.yaml lists the shared resources:

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

In apps/base/webapp/deployment.yaml, have the container read its settings from a ConfigMap that each overlay will generate (leave the namespace field out of the base, since overlays set it):

          envFrom:
            - configMapRef:
                name: webapp-config

apps/dev/kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: webapp-dev
resources:
  - namespace.yaml
  - ../base/webapp
replicas:
  - name: webapp
    count: 2
configMapGenerator:
  - name: webapp-config
    literals:
      - HEADER=Hello from dev

with a namespace.yaml that creates webapp-dev. apps/production/kustomization.yaml looks the same, with namespace: webapp-prod, count: 4, its own namespace manifest, and its own HEADER value. You can check what an overlay renders before committing:

kustomize build apps/dev
# or, without installing kustomize separately:
kubectl kustomize apps/dev

Point a Flux Kustomization at each overlay

Flux detects the kustomization.yaml in the path and builds it for you. In each cluster’s folder, add one Flux Kustomization per environment. clusters/dev/apps.yaml:

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: apps
  namespace: flux-system
spec:
  interval: 10m
  sourceRef:
    kind: GitRepository
    name: flux-system
  path: ./apps/dev
  prune: true
  wait: true
  timeout: 3m

For the production cluster, clusters/production/apps.yaml uses path: ./apps/production. Because each cluster was bootstrapped with its own --path, each cluster only sees its own folder.

Make production deliberate

Unlike tools with a manual sync button, Flux applies automatically, so you gate production through Git. One strong pattern is to let dev follow a branch and let production follow released tags. In the production cluster’s source:

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: fleet-releases
  namespace: flux-system
spec:
  interval: 5m
  url: https://github.com/your-org/fleet-infra
  ref:
    semver: ">=1.0.0"

Production then only changes when you tag a release. Pull request approval on the main branch is the other common gate.

Handle configuration changes on purpose

Change the HEADER value in one environment and see what happens. Kustomize appends a hash of the content to generated ConfigMap names. A new value produces a new ConfigMap (webapp-config-<hash>) and updates the Deployment’s reference, which triggers a Pod rollout. The old ConfigMap is now orphaned, and with prune: true Flux deletes it for you. That behavior is exactly what you want: config changes roll the Pods.

Plain, hand-written ConfigMaps are different. If you edit a ConfigMap that a Deployment reads through envFrom, the Deployment spec doesn’t change, so running Pods keep the old values. Use configMapGenerator for anything that should restart workloads when it changes. Helm charts have an equivalent technique, covered in the Helm guide.

Substitute per-cluster values

Sometimes a value differs per cluster but you don’t want a whole overlay for it, for example a domain name. Flux Kustomizations can substitute variables after the build:

spec:
  postBuild:
    substitute:
      cluster_domain: dev.example.com
    substituteFrom:
      - kind: ConfigMap
        name: cluster-vars

Any ${cluster_domain} in your manifests is replaced at apply time. Use it sparingly, since overlays are easier to review.

Promote changes through environments

With this layout, promotion is a sequence of ordinary commits. Change the dev overlay first (for example, an image bump with kustomize edit set image nginx=nginx:1.26 run in apps/dev), let Flux apply it, and verify. Then make the same change in apps/production, through a pull request that requires approval. A CI pipeline can automate the dev commit, and a reviewer gates the production one. Flux’s image automation can also do the dev step for you, which the image automation guide covers.

Popular Guides