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:
1FROM node:22-alpine AS deps2WORKDIR /app3COPY package.json package-lock.json* pnpm-lock.yaml* ./4RUN corepack enable5RUN if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; \6 elif [ -f package-lock.json ]; then npm ci; \7 else npm install; fi8
9FROM node:22-alpine AS builder10WORKDIR /app11COPY --from=deps /app/node_modules ./node_modules12COPY . .13RUN npm run build14
15FROM node:22-alpine AS runner16WORKDIR /app17ENV NODE_ENV=production18COPY --from=builder /app/public ./public19COPY --from=builder /app/.next ./.next20COPY --from=builder /app/package.json ./package.json21COPY --from=builder /app/next.config.ts ./next.config.ts22COPY --from=builder /app/node_modules ./node_modules23EXPOSE 300024CMD ["npm", "start"]Test locally:
1docker build -t APP_NAME .2docker run -p 3000:3000 APP_NAMEPhase 3: Kubernetes Manifests
Create k8s/ directory with 4 files:
k8s/deployment.yaml
1apiVersion: apps/v12kind: Deployment3metadata:4 name: APP_NAME5spec:6 replicas: 17 selector:8 matchLabels:9 app: APP_NAME10 template:11 metadata:12 labels:13 app: APP_NAME14 spec:15 containers:16 - name: APP_NAME17 image: ghcr.io/GITHUB_USER/APP_NAME:latest18 ports:19 - containerPort: 3000k8s/service.yaml
1apiVersion: v12kind: Service3metadata:4 name: APP_NAME5spec:6 selector:7 app: APP_NAME8 ports:9 - port: 8010 targetPort: 300011 type: ClusterIPk8s/ingress.yaml
1apiVersion: networking.k8s.io/v12kind: Ingress3metadata:4 name: APP_NAME5 annotations:6 kubernetes.io/ingress.class: "nginx"7spec:8 rules:9 - host: APP_NAME.tk.com10 http:11 paths:12 - path: /13 pathType: Prefix14 backend:15 service:16 name: APP_NAME17 port:18 number: 80k8s/kustomization.yaml
CRITICAL: Flux does nothing without this file!
1apiVersion: kustomize.config.k8s.io/v1beta12kind: Kustomization3resources:4 - deployment.yaml5 - service.yaml6 - ingress.yamlPhase 4: CI Pipeline (GitHub Actions)
Note: Replace
GITHUB_USER/APP_NAMEin the sed command with actual values (lowercase).
1name: Build and Push Docker Image2
3on:4 push:5 branches: [main]6
7env:8 REGISTRY: ghcr.io9 IMAGE_NAME: ${{ github.repository }}10
11jobs:12 build-and-push:13 runs-on: ubuntu-latest14 permissions:15 contents: write16 packages: write17
18 steps:19 - name: Checkout20 uses: actions/checkout@v421
22 - name: Log in to GitHub Container Registry23 uses: docker/login-action@v324 with:25 registry: ghcr.io26 username: ${{ github.actor }}27 password: ${{ secrets.GITHUB_TOKEN }}28
29 - name: Extract metadata30 id: meta31 uses: docker/metadata-action@v532 with:33 images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}34 tags: |35 type=sha,prefix=36 type=raw,value=latest37
38 - name: Set up QEMU39 uses: docker/setup-qemu-action@v340
41 - name: Set up Docker Buildx42 uses: docker/setup-buildx-action@v343
44 - name: Build and push45 uses: docker/build-push-action@v546 with:47 context: .48 push: true49 platforms: linux/amd64,linux/arm6450 tags: ${{ steps.meta.outputs.tags }}51
52 - name: Update k8s deployment image tag53 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.yaml56 git config user.name "github-actions"57 git config user.email "[email protected]"58 git add k8s/deployment.yaml59 git commit -m "Update image tag to ${SHORT_SHA}" || echo "No changes"60 git pushPhase 5: Flux Setup
5A: First-Time Bootstrap (once per cluster)
Run this from your machine with kubectl access:
1flux bootstrap github \2 --owner=GITHUB_USER \3 --repository=APP_NAME \4 --path=clusters/my-cluster \5 --branch=main \6 --personalThis creates:
clusters/my-cluster/flux-system/gotk-components.yamlclusters/my-cluster/flux-system/gotk-sync.yaml
Then create the cluster-level kustomization:
1apiVersion: kustomize.config.k8s.io/v1beta12kind: Kustomization3resources:4 - flux-system/gotk-components.yaml5 - flux-system/gotk-sync.yaml5B: Add App to Flux (repeat for each new app)
Create the Flux Kustomization resource for the app:
1apiVersion: kustomize.toolkit.fluxcd.io/v12kind: Kustomization3metadata:4 name: APP_NAME5 namespace: flux-system6spec:7 interval: 5m8 targetNamespace: default9 path: "./k8s"10 prune: true11 sourceRef:12 kind: GitRepository13 name: flux-systemThen register it in the cluster kustomization:
1apiVersion: kustomize.config.k8s.io/v1beta12kind: Kustomization3resources:4 - flux-system/gotk-components.yaml5 - flux-system/gotk-sync.yaml6 - flux-system/kustomization-APP_NAME.yamlCommit and push to main. Flux picks it up automatically.
Phase 6: Verification
1flux get sources git -A1flux get kustomizations -A1kubectl get deployment,service,ingress1flux reconcile kustomization APP_NAME -n flux-systemCommon Gotchas
| # | Issue | Details |
|---|---|---|
| 1 | Missing kustomization.yaml | Flux silently does nothing without it in each target directory |
| 2 | API version mismatch | Use kustomize.toolkit.fluxcd.io/v1 (not v1beta1) |
| 3 | Missing targetNamespace | Flux can't apply resources without knowing the namespace |
| 4 | Branch mismatch | CI and Flux must watch the same branch (main) |
| 5 | SSH key not configured | flux 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