πŸ“šBook Signing at KubeCon EU 2026Meet us at Booking.com HQ (Mon 18:30-21:00) & vCluster booth #521 (Tue 24 Mar, 12:30-1:30pm) β€” free book giveaway!RSVP Booking.com Event
Deployments advanced ⏱ 60 minutes K8s 1.31+

Full GitOps Pipeline k3s to Production

End-to-end GitOps pipeline: git push triggers Gitea Actions build, pushes to quay.io, Octopus Deploy creates release with ephemeral preview.

By Luca Berton β€’ β€’ πŸ“– 5 min read

πŸ’‘ Quick Answer: The complete pipeline: git push β†’ Gitea Actions builds + pushes to quay.io β†’ Octopus Deploy creates release β†’ deploys to ephemeral preview β†’ manual review β†’ promotes to production β†’ ArgoCD syncs desired state to cluster. Full audit trail, zero manual deployments.

The Problem

You need a production-grade deployment pipeline that:

  • Builds and tests on every push (CI)
  • Creates versioned releases with deployment history (CD)
  • Provides preview environments for review before production
  • Maintains GitOps desired-state with ArgoCD
  • Has approval gates and rollback capability

The Solution

Combine four tools, each doing what it does best:

  1. Gitea Actions β€” CI (build, test, push image)
  2. quay.io β€” Image registry (vulnerability scanning, robot accounts)
  3. Octopus Deploy β€” Release orchestration (previews, approvals, promotion)
  4. ArgoCD β€” GitOps state reconciliation (desired state β†’ actual state)

Architecture

graph TD
    A[Developer] -->|git push| B[Gitea]
    B -->|webhook| C[Gitea Actions]
    C -->|build + test| D[Container Image]
    D -->|push| E[quay.io]
    C -->|notify| F[Octopus Deploy]
    F -->|create release| G[Release v1.2.3]
    G -->|deploy| H[Preview Namespace]
    H -->|HTTPRoute| I[preview-abc.example.com]
    I -->|review| J[Reviewer]
    J -->|approve| K[Octopus Promotion]
    K -->|update| L[Git Repo - manifests]
    L -->|detect change| M[ArgoCD]
    M -->|sync| N[Production Namespace]
    
    style A fill:#e1f5fe
    style N fill:#c8e6c9
    style H fill:#fff3e0

Step 1: Install ArgoCD

kubectl create namespace argocd

helm repo add argo https://argoproj.github.io/argo-helm
helm repo update

helm install argocd argo/argo-cd \
  --namespace argocd \
  --set server.extraArgs="{--insecure}" \
  --set configs.params."server\.insecure"=true \
  --set controller.resources.requests.memory=256Mi \
  --set server.resources.requests.memory=128Mi

Step 2: ArgoCD Application Definition

# argocd-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp-production
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://git.example.com/org/myapp-manifests.git
    targetRevision: main
    path: overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
      - PrunePropagationPolicy=foreground
    retry:
      limit: 3
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

Step 3: Kustomize Overlay Structure

myapp-manifests/
β”œβ”€β”€ base/
β”‚   β”œβ”€β”€ kustomization.yaml
β”‚   β”œβ”€β”€ deployment.yaml
β”‚   β”œβ”€β”€ service.yaml
β”‚   └── httproute.yaml
β”œβ”€β”€ overlays/
β”‚   β”œβ”€β”€ preview/
β”‚   β”‚   β”œβ”€β”€ kustomization.yaml
β”‚   β”‚   └── patch-resources.yaml
β”‚   └── production/
β”‚       β”œβ”€β”€ kustomization.yaml
β”‚       └── patch-replicas.yaml
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
  - httproute.yaml
images:
  - name: myapp
    newName: quay.io/myorg/myapp
    newTag: latest  # Updated by CI/Octopus

Step 4: Octopus Deploy Promotion Script

#!/bin/bash
# promote-to-production.sh
# Called by Octopus Deploy after approval

set -euo pipefail

RELEASE_VERSION="${1}"
MANIFESTS_REPO="https://git.example.com/org/myapp-manifests.git"
BRANCH="main"

# Clone manifests repo
git clone "${MANIFESTS_REPO}" /tmp/manifests
cd /tmp/manifests

# Update image tag in production overlay
cd overlays/production
kustomize edit set image "myapp=quay.io/myorg/myapp:${RELEASE_VERSION}"

# Commit and push
git add .
git commit -m "chore: promote ${RELEASE_VERSION} to production

Approved-by: Octopus Deploy
Release: ${RELEASE_VERSION}
Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)"

git push origin ${BRANCH}

# ArgoCD auto-sync will pick up the change
echo "βœ… Production promotion complete. ArgoCD will sync within 3 minutes."

Step 5: Preview Environment Lifecycle

# preview-deploy.sh (Octopus step)
#!/bin/bash
PREVIEW_ID=$(echo "${RELEASE_VERSION}" | tr '.' '-')
NAMESPACE="preview-${PREVIEW_ID}"
HOSTNAME="preview-${PREVIEW_ID}.example.com"

# Create preview namespace
kubectl create namespace ${NAMESPACE} --dry-run=client -o yaml | kubectl apply -f -

# Deploy with kustomize
cd overlays/preview
kustomize edit set image "myapp=quay.io/myorg/myapp:${RELEASE_VERSION}"
kustomize edit set namespace ${NAMESPACE}
kustomize build | kubectl apply -f -

# Create HTTPRoute for preview
cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: preview-route
  namespace: ${NAMESPACE}
spec:
  parentRefs:
    - name: main-gateway
      namespace: gateway-system
      sectionName: https
  hostnames:
    - "${HOSTNAME}"
  rules:
    - backendRefs:
        - name: myapp
          port: 80
EOF

# Set TTL β€” auto-cleanup after 24h
kubectl annotate namespace ${NAMESPACE} \
  "janitor/ttl=24h" \
  --overwrite

echo "πŸ”— Preview: https://${HOSTNAME}"

Complete Pipeline Flow

# 1. Developer pushes code
git push origin feature/new-thing

# 2. Gitea Actions triggers
#    β†’ runs tests
#    β†’ builds image: quay.io/myorg/myapp:abc1234
#    β†’ pushes to quay.io
#    β†’ notifies Octopus Deploy

# 3. Octopus Deploy
#    β†’ creates Release v1.2.3
#    β†’ deploys to preview-1-2-3 namespace
#    β†’ posts preview URL to PR/chat
#    β†’ waits for manual approval

# 4. Reviewer
#    β†’ visits preview-1-2-3.example.com
#    β†’ tests functionality
#    β†’ approves in Octopus UI

# 5. Octopus promotion
#    β†’ updates manifests repo (image tag)
#    β†’ commits to main branch

# 6. ArgoCD
#    β†’ detects drift (new commit in manifests repo)
#    β†’ syncs production namespace
#    β†’ reports healthy status

# Total time: ~3 minutes (build) + review time + ~30 seconds (promotion + sync)

Common Issues

IssueCauseFix
ArgoCD sync loopResource modified by external controllerAdd ignoreDifferences for managed fields
Preview DNS not resolvingWildcard DNS not configuredAdd *.example.com A record to Hetzner IP
Octopus can’t push to GitAuth token expiredUse deploy key with push access
ArgoCD prune deletes preview resourcesSame namespaceUse separate namespace per environment
Rollback failsNo previous image in registryKeep at least 10 image tags in quay.io

Best Practices

  1. Separate app code from manifests β€” two repos: myapp (source) and myapp-manifests (k8s YAML)
  2. ArgoCD auto-sync for production only β€” previews are imperative (kubectl apply)
  3. Preview TTL with kube-janitor β€” auto-delete stale previews after 24h
  4. Image immutability β€” never reuse tags, always push unique SHA or semver
  5. Octopus for orchestration, ArgoCD for reconciliation β€” don’t make ArgoCD handle approvals

Key Takeaways

  • Four tools, each with a clear responsibility: build β†’ store β†’ orchestrate β†’ reconcile
  • Preview environments enable pre-production review without shared staging conflicts
  • GitOps means production state is always a git commit β€” full audit trail
  • ArgoCD self-heal ensures no drift between desired and actual state
  • Total pipeline time is ~3 minutes + human review time β€” zero manual deployment steps
#gitops #argocd #octopus-deploy #gitea #ci-cd #pipeline #k3s
Luca Berton
Written by Luca Berton

Principal Solutions Architect specializing in Kubernetes, AI/GPU infrastructure, and cloud-native platforms. Author of Kubernetes Recipes and creator of CopyPasteLearn courses.

Kubernetes Recipes book cover

Want More Kubernetes Recipes?

This recipe is from Kubernetes Recipes, our 750-page practical guide with hundreds of production-ready patterns.

Luca Berton Ansible Pilot Ansible by Example Open Empower K8s Recipes Terraform Pilot CopyPasteLearn ProteinLens