Top Tags

Flux CD + Next.js — Step-by-Step Integration Guide

step-by-step checklist for adding Flux CD GitOps to any Next.js app, including Dockerization, Kubernetes manifests, CI pipeline, and Flux setup.

Checklist for adding Flux CD GitOps to any Next.js app. Replace APP_NAME with your app name (e.g. chat2md, my-dashboard). Replace GITHUB_USER with your GitHub username.

Phase 1: Prerequisites

  • Kubernetes cluster (k3s or similar) with kubectl access
  • Flux CLI installed: brew install fluxcd/tap/flux
  • GitHub account with a repo for the app
  • GitHub PAT or SSH key for Flux to access the repo
  • Docker installed for local testing

Phase 2: Dockerize the App

Create Dockerfile in the repo root:

dockerfile
1FROM node:22-alpine AS deps
2WORKDIR /app
3COPY package.json package-lock.json* pnpm-lock.yaml* ./
4RUN corepack enable
5RUN if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; \
6 elif [ -f package-lock.json ]; then npm ci; \
7 else npm install; fi
8
9FROM node:22-alpine AS builder
10WORKDIR /app
11COPY --from=deps /app/node_modules ./node_modules
12COPY . .
13RUN npm run build
14
15FROM node:22-alpine AS runner
16WORKDIR /app
17ENV NODE_ENV=production
18COPY --from=builder /app/public ./public
19COPY --from=builder /app/.next ./.next
20COPY --from=builder /app/package.json ./package.json
21COPY --from=builder /app/next.config.ts ./next.config.ts
22COPY --from=builder /app/node_modules ./node_modules
23EXPOSE 3000
24CMD ["npm", "start"]

Test locally:

bash
1docker build -t APP_NAME .
2docker run -p 3000:3000 APP_NAME

Phase 3: Kubernetes Manifests

Create k8s/ directory with 4 files:

k8s/deployment.yaml

yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: APP_NAME
5spec:
6 replicas: 1
7 selector:
8 matchLabels:
9 app: APP_NAME
10 template:
11 metadata:
12 labels:
13 app: APP_NAME
14 spec:
15 containers:
16 - name: APP_NAME
17 image: ghcr.io/GITHUB_USER/APP_NAME:latest
18 ports:
19 - containerPort: 3000

k8s/service.yaml

yaml
1apiVersion: v1
2kind: Service
3metadata:
4 name: APP_NAME
5spec:
6 selector:
7 app: APP_NAME
8 ports:
9 - port: 80
10 targetPort: 3000
11 type: ClusterIP

k8s/ingress.yaml

yaml
1apiVersion: networking.k8s.io/v1
2kind: Ingress
3metadata:
4 name: APP_NAME
5 annotations:
6 kubernetes.io/ingress.class: "nginx"
7spec:
8 rules:
9 - host: APP_NAME.tk.com
10 http:
11 paths:
12 - path: /
13 pathType: Prefix
14 backend:
15 service:
16 name: APP_NAME
17 port:
18 number: 80

k8s/kustomization.yaml

CRITICAL: Flux does nothing without this file!

yaml
1apiVersion: kustomize.config.k8s.io/v1beta1
2kind: Kustomization
3resources:
4 - deployment.yaml
5 - service.yaml
6 - ingress.yaml

Phase 4: CI Pipeline (GitHub Actions)

Note: Replace GITHUB_USER/APP_NAME in the sed command with actual values (lowercase).

yaml
1name: Build and Push Docker Image
2
3on:
4 push:
5 branches: [main]
6
7env:
8 REGISTRY: ghcr.io
9 IMAGE_NAME: ${{ github.repository }}
10
11jobs:
12 build-and-push:
13 runs-on: ubuntu-latest
14 permissions:
15 contents: write
16 packages: write
17
18 steps:
19 - name: Checkout
20 uses: actions/checkout@v4
21
22 - name: Log in to GitHub Container Registry
23 uses: docker/login-action@v3
24 with:
25 registry: ghcr.io
26 username: ${{ github.actor }}
27 password: ${{ secrets.GITHUB_TOKEN }}
28
29 - name: Extract metadata
30 id: meta
31 uses: docker/metadata-action@v5
32 with:
33 images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
34 tags: |
35 type=sha,prefix=
36 type=raw,value=latest
37
38 - name: Set up QEMU
39 uses: docker/setup-qemu-action@v3
40
41 - name: Set up Docker Buildx
42 uses: docker/setup-buildx-action@v3
43
44 - name: Build and push
45 uses: docker/build-push-action@v5
46 with:
47 context: .
48 push: true
49 platforms: linux/amd64,linux/arm64
50 tags: ${{ steps.meta.outputs.tags }}
51
52 - name: Update k8s deployment image tag
53 run: |
54 SHORT_SHA=$(echo ${{ github.sha }} | cut -c1-7)
55 sed -i "s|image: ghcr.io/GITHUB_USER/APP_NAME:.*|image: ghcr.io/GITHUB_USER/APP_NAME:${SHORT_SHA}|" k8s/deployment.yaml
56 git config user.name "github-actions"
57 git config user.email "[email protected]"
58 git add k8s/deployment.yaml
59 git commit -m "Update image tag to ${SHORT_SHA}" || echo "No changes"
60 git push

Phase 5: Flux Setup

5A: First-Time Bootstrap (once per cluster)

Run this from your machine with kubectl access:

bash
1flux bootstrap github \
2 --owner=GITHUB_USER \
3 --repository=APP_NAME \
4 --path=clusters/my-cluster \
5 --branch=main \
6 --personal

This creates:

  • clusters/my-cluster/flux-system/gotk-components.yaml
  • clusters/my-cluster/flux-system/gotk-sync.yaml

Then create the cluster-level kustomization:

yaml
1apiVersion: kustomize.config.k8s.io/v1beta1
2kind: Kustomization
3resources:
4 - flux-system/gotk-components.yaml
5 - flux-system/gotk-sync.yaml

5B: Add App to Flux (repeat for each new app)

Create the Flux Kustomization resource for the app:

yaml
1apiVersion: kustomize.toolkit.fluxcd.io/v1
2kind: Kustomization
3metadata:
4 name: APP_NAME
5 namespace: flux-system
6spec:
7 interval: 5m
8 targetNamespace: default
9 path: "./k8s"
10 prune: true
11 sourceRef:
12 kind: GitRepository
13 name: flux-system

Then register it in the cluster kustomization:

yaml
1apiVersion: kustomize.config.k8s.io/v1beta1
2kind: Kustomization
3resources:
4 - flux-system/gotk-components.yaml
5 - flux-system/gotk-sync.yaml
6 - flux-system/kustomization-APP_NAME.yaml

Commit and push to main. Flux picks it up automatically.

Phase 6: Verification

bash
1flux get sources git -A
bash
1flux get kustomizations -A
bash
1kubectl get deployment,service,ingress
bash
1flux reconcile kustomization APP_NAME -n flux-system

Common Gotchas

#IssueDetails
1Missing kustomization.yamlFlux silently does nothing without it in each target directory
2API version mismatchUse kustomize.toolkit.fluxcd.io/v1 (not v1beta1)
3Missing targetNamespaceFlux can't apply resources without knowing the namespace
4Branch mismatchCI and Flux must watch the same branch (main)
5SSH key not configuredflux bootstrap creates a deploy key; make sure it has repo access

Flow Summary

Push code to main → GitHub Actions builds Docker image → pushes to GHCR → CI updates k8s/deployment.yaml with new SHA tag → commits to main → Flux detects new commit → applies k8s manifests to cluster → App is live