Applications Deployment
Deploy Applications via GitOps
This guide explains how to deploy applications using the GitOps workflow: GitHub → Gitea → Gitea Actions → ArgoCD → Kubernetes.
Overview
Application Deployment Flow:
1. Push code to GitHub (github.com/zengarden-space/<app>)
↓
2. Gitea sync (auto-syncs to gitea.homelab.int.zengarden.space/zengarden-space/<app>)
↓
3. Git push triggers Gitea Actions workflow
↓
4. Gitea Actions: build Docker image, push to Gitea registry
↓
5. Gitea Actions: render Helm chart, push manifest to manifests repo
↓
6. ArgoCD ApplicationSet auto-discovers new application
↓
7. ArgoCD syncs application to Kubernetes cluster
↓
8. Application running in Kubernetes (namespace: {component-type}-{env}-{projectname})Component Types & Organization
Applications are organized into three component types, each mapping to an ArgoCD AppProject:
| Component Type | AppProject | Purpose | Example Namespaces |
|---|---|---|---|
| apps | apps | Application workloads (default) | apps-dev-retroboard, apps-prod-api |
| system | system | System infrastructure services | system-dev-secret-editor, system-prod-monitoring |
| platform | platform | Core platform components | platform-prod-argocd-extensions |
Directory structure in manifests repository:
manifests/homelab/
├── apps/{env}/{projectname}/ # Application workloads
├── system/{env}/{projectname}/ # System services
└── platform/{env}/{projectname}/ # Platform componentsNamespace naming: {component-type}-{env}-{projectname}
- Examples:
apps-dev-myapp,system-prod-monitoring,apps-ci-abc123de-myapp
When to use each type:
- apps (default): Regular application workloads, APIs, frontends, backends
- system: Infrastructure tooling like secret editors, monitoring dashboards
- platform: Extensions to core platform (ArgoCD plugins, operators)
Prerequisites
- Core platform deployed (ArgoCD, Gitea running)
- Gitea-GitHub synchronization working
- ArgoCD application configured
- manifests repository exists in Gitea
Step 1: Verify Gitea-GitHub Sync
1.1 Check Sync Job
Verify sync job completed:
kubectl -n gitea get jobs
# Expected:
# NAME COMPLETIONS DURATION AGE
# gitea-sync 1/1 5m 1hCheck sync job logs:
kubectl -n gitea logs job/gitea-sync
# Should show:
# - Organization 'zengarden-space' created
# - Repositories synced from GitHub to Gitea
# - Push mirrors configured (Gitea → GitHub)
# - ArgoCD webhook created1.2 Verify Repositories in Gitea
Access Gitea UI:
URL: https://gitea.homelab.int.zengarden.space
Username: admin
Password: <from kubectl -n gitea get secret gitea-admin>Check organization:
- Navigate to zengarden-space organization
- Should see repositories synced from GitHub
- Example repos:
manifests,example-app, etc.
Verify push mirrors:
- Navigate to repository Settings > Repository > Mirrors
- Should see push mirror to GitHub (8h sync interval)
1.3 Verify ArgoCD Webhook
Check webhook in Gitea:
- Repository:
manifests - Settings > Webhooks
- Should see webhook to
https://argocd.homelab.int.zengarden.space/api/webhook - Events: Push, Pull Request, Create, Delete, Release
Test webhook (optional):
# Create test commit in manifests repo
# Webhook should trigger ArgoCD syncStep 2: Configure Gitea Actions Runner
2.1 Verify Gitea Runner Deployment
Check runner pods:
kubectl -n gitea get pods -l app=gitea-runner
# Expected:
# NAME READY STATUS RESTARTS AGE
# gitea-runner-xxxxx-yyyyy 1/1 Running 0 1hCheck runner registration:
- Gitea UI > Admin Panel > Actions > Runners
- Should see runner registered
2.2 Configure Organization Secrets
Secrets needed for CI/CD pipelines:
- GITEA_TOKEN: Personal access token for updating manifests
- DOCKER_USERNAME: Gitea username (usually
admin) - DOCKER_PASSWORD: Gitea password or token
Add secrets via Gitea UI:
- Organization:
zengarden-space - Settings > Secrets > Actions
- Add each secret
Or add via API:
# Get Gitea admin token
GITEA_TOKEN=$(kubectl -n gitea get secret gitea-admin -o jsonpath="{.data.apiToken}" | base64 -d)
# Add GITEA_TOKEN secret
curl -X PUT "https://gitea.homelab.int.zengarden.space/api/v1/orgs/zengarden-space/actions/secrets/GITEA_TOKEN" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"data\": \"$(echo -n $GITEA_TOKEN | base64)\"}"
# Add DOCKER secrets (similar)Step 3: Prepare Application Repository
3.1 Application Structure
Typical application repository structure:
example-app/
├── .gitea/
│ └── workflows/
│ └── build.yaml # Gitea Actions workflow
├── src/
│ └── ... # Application source code
├── Dockerfile # Container build instructions
├── kubernetes/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ └── kustomization.yaml
└── README.md3.2 Create Gitea Actions Workflow
.gitea/workflows/build.yaml:
name: Build and Deploy
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Gitea Container Registry
uses: docker/login-action@v2
with:
registry: gitea.homelab.int.zengarden.space
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
gitea.homelab.int.zengarden.space/zengarden-space/example-app:latest
gitea.homelab.int.zengarden.space/zengarden-space/example-app:${{ github.sha }}
- name: Checkout manifests repository
uses: actions/checkout@v3
with:
repository: zengarden-space/manifests
path: manifests
token: ${{ secrets.GITEA_TOKEN }}
- name: Update manifest with new image
run: |
cd manifests/example-app
kustomize edit set image example-app=gitea.homelab.int.zengarden.space/zengarden-space/example-app:${{ github.sha }}
git config user.name "Gitea Actions"
git config user.email "[email protected]"
git add .
git commit -m "Update example-app to ${{ github.sha }}"
git push3.3 Create Kubernetes Manifests
kubernetes/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: example-app
spec:
replicas: 2
selector:
matchLabels:
app: example-app
template:
metadata:
labels:
app: example-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: app
image: gitea.homelab.int.zengarden.space/zengarden-space/example-app:latest
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mikubernetes/service.yaml:
apiVersion: v1
kind: Service
metadata:
name: example-app
spec:
selector:
app: example-app
ports:
- port: 80
targetPort: 8080kubernetes/ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-app
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: internal
tls:
- hosts:
- example-app.homelab.int.zengarden.space
secretName: example-app-tls
rules:
- host: example-app.homelab.int.zengarden.space
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: example-app
port:
number: 80kubernetes/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: example-app
resources:
- deployment.yaml
- service.yaml
- ingress.yaml
images:
- name: example-app
newName: gitea.homelab.int.zengarden.space/zengarden-space/example-app
newTag: latest # Will be updated by Gitea ActionsStep 4: Create Manifests Repository Entry
4.1 Structure Manifests Repository
manifests repository structure:
manifests/homelab/
├── apps/ # Application workloads (AppProject: apps)
│ ├── dev/ # Development environment
│ │ ├── example-app/
│ │ │ └── manifest.yaml # Rendered Helm chart
│ │ └── another-app/
│ │ └── manifest.yaml
│ ├── prod/ # Production environment
│ │ └── example-app/
│ │ └── manifest.yaml
│ └── ci-<hash>/ # CI/PR environments (ephemeral)
│ └── example-app/
│ └── manifest.yaml
├── system/ # System infrastructure (AppProject: system)
│ ├── dev/
│ │ └── secret-editor/
│ │ └── manifest.yaml
│ └── prod/
│ └── monitoring-tools/
│ └── manifest.yaml
├── platform/ # Core platform (AppProject: platform)
│ └── prod/
│ └── argocd-extensions/
│ └── manifest.yaml
└── applications.yaml # Unified ApplicationSet (auto-discovers all apps)Key concepts:
- Component types:
apps,system,platform(maps to ArgoCD AppProjects) - Environments:
dev,prod,ci-<hash> - Namespace format:
{component-type}-{env}-{projectname}- Example:
apps-dev-example-app,system-prod-secret-editor
- Example:
- ApplicationSet: Single ApplicationSet auto-discovers all apps from
homelab/*/*/*pattern
4.2 Initialize manifests Repository
# Clone manifests repo (create if doesn't exist)
git clone https://gitea.homelab.int.zengarden.space/zengarden-space/manifests.git
cd manifests/homelab
# Create directory for application (apps component type, dev environment)
mkdir -p apps/dev/example-app
# Manifests will be auto-generated by Gitea Actions from Helm charts
# No manual manifest creation needed - CI/CD handles this
# Commit and push structure
git add .
git commit -m "Add example-app directory structure"
git pushNote: The Unified ApplicationSet (applications.yaml) automatically discovers and manages all applications based on the directory structure.
Step 5: Deploy Application via GitOps
5.1 Push Code to Trigger CI/CD
Push code to GitHub (or Gitea directly):
cd example-app
git add .
git commit -m "Initial application code"
git push origin main5.2 Monitor Gitea Actions Pipeline
Watch pipeline in Gitea UI:
- Repository:
zengarden-space/example-app - Actions tab
- Should see workflow running
Or monitor via CLI:
# Get recent workflow runs (via API)
curl -s "https://gitea.homelab.int.zengarden.space/api/v1/repos/zengarden-space/example-app/actions/workflows" \
-H "Authorization: token $GITEA_TOKEN" | jq
# Check logs (replace <run-id>)
curl -s "https://gitea.homelab.int.zengarden.space/api/v1/repos/zengarden-space/example-app/actions/runs/<run-id>/logs" \
-H "Authorization: token $GITEA_TOKEN"5.3 Verify Image Pushed to Registry
Check Gitea container registry:
- Gitea UI > Organization
zengarden-space> Packages - Should see
example-apppackage with tags
Or via CLI:
# List packages
curl -s "https://gitea.homelab.int.zengarden.space/api/v1/orgs/zengarden-space/packages" \
-H "Authorization: token $GITEA_TOKEN" | jq5.4 Verify manifests Repository Updated
Check manifests repo for commit:
cd manifests/homelab
git pull
# Check manifest file (apps component, dev environment)
cat apps/dev/example-app/manifest.yaml
# Should show:
# - Deployment with image: gitea.homelab.int.zengarden.space/zengarden-space/example-app:<git-sha>
# - Service pointing to example-app
# - Ingress with host: example-app.homelab.int.zengarden.space
# - All resources in namespace: apps-dev-example-app5.5 Verify ArgoCD Detected Application
Check ArgoCD application:
# List applications
argocd app list
# Should show:
# NAME CLUSTER NAMESPACE PROJECT STATUS HEALTH
# homelab.apps.dev.example-app https://kubernetes.default.svc apps-dev-example-app apps Synced HealthyApplication naming format: homelab.{component-type}.{env}.{projectname}
View application in ArgoCD UI:
- URL:
https://argocd.homelab.int.zengarden.space - Navigate to application
homelab.apps.dev.example-app - Should show all resources (Deployment, Service, Ingress)
- Namespace:
apps-dev-example-app
5.6 Verify Application Running in Kubernetes
Check namespace and pods:
# Namespace format: {component-type}-{env}-{projectname}
kubectl get ns apps-dev-example-app
kubectl -n apps-dev-example-app get pods
# Expected:
# NAME READY STATUS RESTARTS AGE
# example-app-xxxxx-yyyyy 1/1 Running 0 5m
# example-app-xxxxx-zzzzz 1/1 Running 0 5mCheck service and ingress:
kubectl -n apps-dev-example-app get svc,ingress
# Ingress should have address assignedTest application:
curl https://example-app.homelab.int.zengarden.space
# Should return application responseStep 6: Make Updates (GitOps Workflow)
6.1 Update Application Code
Make code changes:
cd example-app/src
# Edit application code
nano app.py
# Commit and push
git add .
git commit -m "Update application feature"
git push origin main6.2 Automatic Deployment
Gitea Actions automatically:
- Builds new Docker image with commit SHA tag
- Pushes to Gitea registry
- Updates manifests repository with new tag
- Commits and pushes to manifests repo
ArgoCD automatically:
- Detects change in manifests repository (via webhook)
- Syncs application to cluster
- Performs rolling update
Monitor deployment:
# Watch ArgoCD sync
argocd app get homelab.apps.dev.example-app --watch
# Watch pod rollout
kubectl -n apps-dev-example-app rollout status deployment/example-appTroubleshooting
Gitea Actions Workflow Fails
Check workflow logs:
- Gitea UI > Repository > Actions > Failed workflow
- Click on failed step to see logs
Common issues:
- Docker build failures: Check Dockerfile syntax
- Registry push failures: Verify DOCKER_USERNAME/PASSWORD secrets
- manifests update failures: Verify GITEA_TOKEN secret has write permissions
ArgoCD Not Detecting Application
Check ApplicationSet:
kubectl -n argocd get applicationset homelab-applications -o yaml
# Verify:
# - generators.git.directories.path: "homelab/*/*/*"
# - Applications are auto-generated with pattern: homelab.<component-type>.<env>.<projectname>Check ArgoCD logs:
kubectl -n argocd logs -l app.kubernetes.io/name=argocd-applicationset-controller --tail=50
kubectl -n argocd logs -l app.kubernetes.io/name=argocd-application-controller --tail=50List generated applications:
argocd app list | grep homelab
# Should show applications matching the directory structure in manifests/homelab/Note: With the unified ApplicationSet, applications are auto-discovered. Manual application creation is not needed.
ArgoCD Sync Fails
Check ArgoCD application status:
argocd app get homelab.apps.dev.example-app
# Look for sync errorsCommon issues:
- Invalid Kubernetes manifests: Validate YAML with
kubectl apply --dry-run - Namespace not created: ArgoCD should auto-create with
CreateNamespace=true - Image pull errors: Verify image tag exists in registry
- Wrong component-type: Verify AppProject (apps/system/platform) permissions match namespace pattern
Application Pods CrashLooping
Check pod logs:
kubectl -n apps-dev-example-app logs example-app-xxxxx-yyyyy
# Check events
kubectl -n apps-dev-example-app get events --sort-by='.lastTimestamp'Common issues:
- Image not found: Verify Gitea registry URL and credentials
- Pod Security Admission violations: Ensure pod security context is set
- Resource limits: Adjust CPU/memory requests/limits
- Namespace mismatch: Ensure manifest uses correct namespace format
{component-type}-{env}-{projectname}
Best Practices
Security
-
Always use image tags (not
latest)- Gitea Actions uses commit SHA:
:${{ github.sha }} - Ensures reproducible deployments
- Gitea Actions uses commit SHA:
-
Set pod security context
runAsNonRoot: truereadOnlyRootFilesystem: true- Drop ALL capabilities
-
Use resource limits
- Prevent resource exhaustion
- Helps Kubernetes scheduler
GitOps
-
Separate application code from manifests
- Application repo: Source code + Dockerfile
- Manifests repo: Kubernetes YAML
-
Use Kustomize for manifest management
- Easy image tag updates
- Overlay support for environments
-
Enable ArgoCD auto-sync
- Automatic deployment on manifest changes
- Self-healing if cluster state drifts
CI/CD
-
Use semantic versioning for releases
- Git tags trigger release builds
- Example:
v1.0.0,v1.1.0
-
Add tests to Gitea Actions
- Unit tests, integration tests
- Fail fast on test failures
-
Scan container images for vulnerabilities
- Add scanning step to workflow
- Example: Trivy, Grype
Next Steps
Applications deployed! Learn about:
- Operations - Ongoing maintenance and monitoring
- Monitoring - Set up dashboards and alerts
GitOps workflow complete. Applications deploy automatically from Git commits to running Kubernetes pods!