ci: build, push and deploy to personal cluster on push to master

- Gitea Actions workflow: build, push sha-tagged image, apply kustomize, wait for rollout with auto-rollback
- Deploy to personal-site namespace, served at jesseinit.dev with TLS
- Run container as non-root and bind to 0.0.0.0

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jesse Egbosionu
2026-10-24 21:19:22 +02:00
co-authored by Claude Opus 5.5
parent 3bb0a5e4a9
commit 0927b9d665
8 changed files with 228 additions and 242 deletions
+31 -214
View File
@@ -1,225 +1,42 @@
# Kubernetes Deployment Guide
# Kubernetes deployment
This directory contains all the necessary Kubernetes manifests to deploy the jesseinit portfolio application.
The site runs on the personal k3s cluster in the `personal-site` namespace and is served at
https://jesseinit.dev (`www.` redirects to the apex). TLS comes from cert-manager (`letsencrypt-prod`).
## Prerequisites
## How deploys work
- Kubernetes cluster (local or cloud)
- kubectl CLI installed and configured
- Docker for building the image
- (Optional) kustomize for managing configurations
`.github/workflows/deploy.yml` (run by Gitea Actions) on every push to `master`:
## Files Overview
1. **Build site**: `npm ci && npm run build`, and renders the kustomization to catch manifest errors.
2. **Build and push image**: `tea.jesseinit.dev/jesseinit/personal-site:sha-<commit>` and `:latest`.
3. **Deploy**: pins `newTag` in `kustomization.yaml` to `sha-<commit>`, runs `kubectl apply -k k8s/`,
waits for the rollout, and rolls back automatically if it fails.
- `deployment.yaml` - Kubernetes Deployment with 2 replicas, health checks, and resource limits
- `service.yaml` - ClusterIP Service exposing the application on port 80
- `ingress.yaml` - Ingress resource for external access (requires ingress controller)
- `configmap.yaml` - ConfigMap for environment variables
- `kustomization.yaml` - Kustomize configuration for easy deployment management
Pull requests run step 1 only. Old images are cleaned up by the nightly `gitea-package-prune` cronjob.
## Quick Start
### Required secrets (repo or `production` environment in Gitea)
### 1. Build the Docker Image
| Secret | Value |
| ---------------- | ----------------------------------------------------- |
| `REGISTRY_TOKEN` | Gitea token with `write:package` for `jesseinit` |
| `KUBE_CONFIG` | base64-encoded kubeconfig for the personal cluster |
## Files
- `namespace.yaml`: `personal-site` namespace
- `deployment.yaml`: 2 replicas, zero-downtime rolling updates, non-root, health checks
- `service.yaml`: ClusterIP on port 80 → container port 3000
- `ingress.yaml`: nginx ingress + TLS for `jesseinit.dev` / `www.jesseinit.dev`
- `configmap.yaml`: optional environment variables
- `kustomization.yaml`: ties it together and sets the image
## Manual operations
```bash
# Build the image
docker build -t jesseinit-portfolio:latest .
kubectl --context personal-cluster -n personal-site get pods,ingress,certificate
kubectl --context personal-cluster -n personal-site logs -l app=jesseinit-portfolio -f
kubectl --context personal-cluster -n personal-site rollout undo deployment/jesseinit-portfolio
# If using a registry, tag and push
docker tag jesseinit-portfolio:latest <your-registry>/jesseinit-portfolio:latest
docker push <your-registry>/jesseinit-portfolio:latest
# Deploy by hand (uses :latest)
kubectl --context personal-cluster apply -k k8s/
```
### 2. Update Image Reference (if using a registry)
Edit `k8s/kustomization.yaml` and update the image reference:
```yaml
images:
- name: jesseinit-portfolio
newName: <your-registry>/jesseinit-portfolio
newTag: latest
```
### 3. Deploy to Kubernetes
#### Option A: Using kubectl
```bash
# Apply all manifests
kubectl apply -f k8s/
# Verify deployment
kubectl get pods -l app=jesseinit-portfolio
kubectl get svc jesseinit-portfolio
kubectl get ingress jesseinit-portfolio
```
#### Option B: Using Kustomize
```bash
# Deploy using kustomize
kubectl apply -k k8s/
# Or build and preview first
kubectl kustomize k8s/ | kubectl apply -f -
```
### 4. Configure Ingress (Optional)
Edit `k8s/ingress.yaml` to match your setup:
1. Update the `host` field with your domain
2. Set the correct `ingressClassName` (nginx, traefik, etc.)
3. Uncomment TLS section if using HTTPS
4. Add appropriate annotations for your ingress controller
Example for NGINX with cert-manager:
```yaml
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
```
### 5. Add Environment Variables (Optional)
Edit `k8s/configmap.yaml` to add any required environment variables:
```yaml
data:
API_URL: "https://api.example.com"
NEXT_PUBLIC_SITE_URL: "https://jesseinit.com"
```
## Local Development with Minikube
```bash
# Start minikube
minikube start
# Build image in minikube's Docker daemon
eval $(minikube docker-env)
docker build -t jesseinit-portfolio:latest .
# Deploy
kubectl apply -k k8s/
# Access the application
minikube service jesseinit-portfolio
```
## Monitoring
### Check Pod Status
```bash
kubectl get pods -l app=jesseinit-portfolio
kubectl logs -l app=jesseinit-portfolio -f
kubectl describe pod <pod-name>
```
### Check Service
```bash
kubectl get svc jesseinit-portfolio
kubectl describe svc jesseinit-portfolio
```
### Port Forward for Local Testing
```bash
kubectl port-forward svc/jesseinit-portfolio 8080:80
# Access at http://localhost:8080
```
## Scaling
```bash
# Scale to 3 replicas
kubectl scale deployment jesseinit-portfolio --replicas=3
# Or edit deployment.yaml and reapply
```
## Updating the Application
```bash
# Build new image with new tag
docker build -t jesseinit-portfolio:v2 .
docker push <your-registry>/jesseinit-portfolio:v2
# Update the image
kubectl set image deployment/jesseinit-portfolio portfolio=<your-registry>/jesseinit-portfolio:v2
# Or update kustomization.yaml and reapply
kubectl apply -k k8s/
```
## Troubleshooting
### Pods not starting
```bash
kubectl describe pod <pod-name>
kubectl logs <pod-name>
```
### Service not accessible
```bash
kubectl get endpoints jesseinit-portfolio
kubectl describe svc jesseinit-portfolio
```
### Image pull errors
If using a private registry, create an image pull secret:
```bash
kubectl create secret docker-registry regcred \
--docker-server=<your-registry> \
--docker-username=<username> \
--docker-password=<password> \
--docker-email=<email>
```
Then add to `deployment.yaml`:
```yaml
spec:
imagePullSecrets:
- name: regcred
```
## Resource Management
The deployment includes default resource requests and limits:
- Requests: 128Mi memory, 100m CPU
- Limits: 512Mi memory, 500m CPU
Adjust these in `deployment.yaml` based on your application's needs.
## Cleanup
```bash
# Delete all resources
kubectl delete -k k8s/
# Or delete individually
kubectl delete -f k8s/
```
## Production Considerations
1. **Use a specific image tag** instead of `latest` for better version control
2. **Set up HorizontalPodAutoscaler** for automatic scaling
3. **Configure persistent storage** if needed (for logs, cache, etc.)
4. **Add network policies** for security
5. **Set up monitoring** with Prometheus/Grafana
6. **Configure TLS/SSL** certificates for HTTPS
7. **Use secrets** for sensitive data instead of ConfigMaps
8. **Implement resource quotas** and limit ranges
9. **Set up backup and disaster recovery** procedures
10. **Use namespace isolation** for multi-environment deployments
+14
View File
@@ -6,6 +6,12 @@ metadata:
app: jesseinit-portfolio
spec:
replicas: 2
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: jesseinit-portfolio
@@ -24,12 +30,20 @@ spec:
env:
- name: NODE_ENV
value: "production"
- name: HOSTNAME
value: "0.0.0.0"
- name: PORT
value: "3000"
- name: NEXT_TELEMETRY_DISABLED
value: "1"
envFrom:
- configMapRef:
name: jesseinit-portfolio-config
optional: true
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
resources:
requests:
memory: "128Mi"
+20 -19
View File
@@ -5,24 +5,25 @@ metadata:
labels:
app: jesseinit-portfolio
annotations:
# Uncomment and configure based on your ingress controller
# nginx.ingress.kubernetes.io/ssl-redirect: "true"
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# Redirect www.jesseinit.dev -> jesseinit.dev
nginx.ingress.kubernetes.io/from-to-www-redirect: "true"
spec:
ingressClassName: nginx # Change to your ingress class (nginx, traefik, etc.)
ingressClassName: nginx
tls:
- hosts:
- jesseinit.dev
- www.jesseinit.dev
secretName: personal-site-tls
rules:
- host: jesseinit.com # Replace with your domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: jesseinit-portfolio
port:
number: 80
# Uncomment for TLS/HTTPS support
# tls:
# - hosts:
# - jesseinit.com
# secretName: jesseinit-portfolio-tls
- host: jesseinit.dev
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: jesseinit-portfolio
port:
number: 80
+8 -4
View File
@@ -1,18 +1,22 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: default
namespace: personal-site
resources:
- namespace.yaml
- configmap.yaml
- deployment.yaml
- service.yaml
- ingress.yaml
commonLabels:
app: jesseinit-portfolio
labels:
- pairs:
app.kubernetes.io/name: personal-site
app.kubernetes.io/managed-by: kustomize
# CI rewrites newTag to sha-<commit> before applying.
images:
- name: jesseinit-portfolio
newName: jesseinit-portfolio
newName: tea.jesseinit.dev/jesseinit/personal-site
newTag: latest
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: personal-site