A hands-on guide to achieving zero-downtime deployments using K3s and ArgoCD, covering GitOps workflows, rolling update strategies, health checks, and complete YAML manifests.
Introduction
Zero-downtime deployments are no longer a luxury reserved for large engineering organizations. With K3s (a lightweight Kubernetes distribution) and ArgoCD (a GitOps continuous delivery tool), even small teams can ship code multiple times a day without dropping a single request. This guide walks through the full setup from a bare server to a working GitOps pipeline.
Prerequisites
- A Linux server (Ubuntu 22.04 or 24.04) with at least 2 CPU cores and 4 GB RAM
- A Git repository for Kubernetes manifests
- A container registry (Docker Hub, GitHub Container Registry, or a private registry)
kubectlinstalled on the local machine
1. Install K3s
K3s installs in under 60 seconds:
curl -sfL https://get.k3s.io | sh -s - \
--disable traefik \
--write-kubeconfig-mode 644
# Verify the cluster is running
sudo kubectl get nodes
We disable Traefik because we will use our own ingress controller (or Cloudflare Tunnel) later.
Copy the kubeconfig for remote access:
cat /etc/rancher/k3s/k3s.yaml
Replace 127.0.0.1 with the server's public IP and save it as ~/.kube/config on the local machine.
2. Install ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Wait for all pods to be ready
kubectl -n argocd rollout status deployment argocd-server
# Get the initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
Expose the ArgoCD UI (for initial setup only; in production, use an ingress or port-forward):
kubectl -n argocd port-forward svc/argocd-server 8080:443
3. Structure the GitOps Repository
We use a simple directory layout:
k8s-manifests/
base/
deployment.yaml
service.yaml
hpa.yaml
overlays/
production/
kustomization.yaml
patch-replicas.yaml
base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app: myapp
spec:
replicas: 3
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
terminationGracePeriodSeconds: 30
containers:
- name: myapp
image: ghcr.io/myorg/myapp:latest
ports:
- containerPort: 3000
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
readinessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 15
periodSeconds: 20
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]
Key settings for zero downtime:
maxUnavailable: 0ensures no pod is terminated before a new one is ready.readinessProbetells Kubernetes when the new pod is ready to receive traffic.preStoplifecycle hook adds a 5-second delay before SIGTERM, giving the ingress controller time to remove the pod from the load balancer.
base/service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: 3000
type: ClusterIP
base/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myapp
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
4. Create the ArgoCD Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main
path: overlays/production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Apply it:
kubectl apply -f argocd-app.yaml
Now whenever we push a change to the main branch of k8s-manifests, ArgoCD detects the drift and syncs the cluster to match the desired state.
5. The Deployment Workflow
- Build a new container image in CI and push it with a unique tag (e.g., the Git SHA).
- Update the image tag in
deployment.yamland push to the manifest repo. - ArgoCD detects the change and begins a rolling update.
- Kubernetes creates new pods with the updated image.
- Readiness probes confirm the new pods are healthy.
- Old pods are gracefully drained and terminated.
- Zero requests are dropped throughout the process.
6. Testing Zero Downtime
We can verify zero downtime with a simple load test during a deployment:
# In one terminal, run a continuous request loop
while true; do
curl -s -o /dev/null -w "%{http_code}\n" http://myapp.example.com/healthz
sleep 0.1
done
# In another terminal, trigger a deployment
kubectl set image deployment/myapp myapp=ghcr.io/myorg/myapp:v2.0.0
Every response should return 200. If we see 502 or 503 errors, we need to check the readiness probe configuration and the preStop hook.
K3s gives us a production-grade Kubernetes cluster in under a minute. ArgoCD gives us a GitOps workflow that keeps the cluster in sync with our repository. Together, they provide a deployment pipeline where shipping new code is as simple as merging a pull request, and users never notice a thing.
Frequently asked questions
How do I install K3s without Traefik?
Run the K3s install script with --disable traefik and --write-kubeconfig-mode 644, then confirm the node with sudo kubectl get nodes. Disabling Traefik leaves room for a different ingress controller or Cloudflare Tunnel. For remote kubectl access, copy /etc/rancher/k3s/k3s.yaml, replace 127.0.0.1 with the server's public IP and save the file as ~/.kube/config on the local machine.
What server specs do I need for K3s and ArgoCD?
A Linux server with Ubuntu 22.04 or 24.04, at least 2 CPU cores and 4 GB of RAM is the baseline. The setup also needs a Git repository for the Kubernetes manifests, a container registry such as Docker Hub, GitHub Container Registry or a private one, and kubectl on the local machine. K3s itself installs in under 60 seconds.
Which rolling update settings give zero-downtime deployments in Kubernetes?
Use the RollingUpdate strategy with maxSurge set to 1 and maxUnavailable set to 0, so no pod is terminated before its replacement is ready. A readinessProbe, such as an HTTP check on /healthz every 10 seconds, tells Kubernetes when a new pod can take traffic. A preStop hook running sleep 5 adds a pause before SIGTERM so the ingress controller can remove the old pod from the load balancer first.
How do I install ArgoCD on a K3s cluster?
Create the argocd namespace and apply the upstream stable install.yaml manifest with kubectl apply -n argocd. Wait on kubectl -n argocd rollout status deployment argocd-server, then decode the initial admin password from the argocd-initial-admin-secret secret with base64 -d. For initial setup only, reach the UI with kubectl -n argocd port-forward svc/argocd-server 8080:443, and use an ingress or a port-forward in production.
How does a GitOps deployment with ArgoCD work?
CI builds a container image and pushes it with a unique tag such as the Git SHA, and the new tag is committed to deployment.yaml in the manifest repository. An ArgoCD Application that tracks the main branch and the overlays/production path detects the drift and syncs the cluster, which starts a rolling update. With automated sync and both prune and selfHeal enabled, shipping a release comes down to merging a pull request.
How do I autoscale an app on K3s?
Add a HorizontalPodAutoscaler from the autoscaling/v2 API that targets the Deployment and scales on CPU. An example configuration keeps between 3 and 10 replicas and adds pods when average CPU utilization goes above 70 percent. The matching Deployment requests 100m CPU and 128Mi of memory per pod, with limits of 500m and 512Mi.
How can I test that a Kubernetes rollout drops no requests?
A simple curl loop is enough. In one terminal, curl the app's /healthz endpoint every 0.1 seconds and print only the HTTP status code, then trigger a deployment from a second terminal with kubectl set image deployment/myapp myapp=ghcr.io/myorg/myapp:v2.0.0. Every response during the rollout should be 200.
What causes 502 or 503 errors during a Kubernetes rolling update?
Errors like 502 or 503 during a rollout point to the readiness probe configuration or the preStop hook. The readiness probe decides when a new pod gets traffic, and the preStop delay gives the ingress controller time to drop a terminating pod before SIGTERM arrives. If either one is missing or misconfigured, requests can reach a pod that is not ready yet or is already shutting down.
Read how we handle it in Cloud & Server Management.
Related Articles
Zero-Downtime Kubernetes Deployments: Complete Guide
Master rolling updates, blue-green deployments, and canary releases in Kubernetes to achieve true zero-downtime production deployments.
Next.jsHow We Run Next.js at Scale on K3s with Zero Downtime
A production-grade guide to running Next.js on K3s with zero downtime - container registry, CI/CD pipelines, rolling updates, Cloudflare CDN and Tunnel, Prometheus monitoring, and automated cache purging.
Server & DevOpsKubernetes HPA Deep Dive: Scaling Under Load
Configure Kubernetes Horizontal Pod Autoscaler with custom metrics, scaling policies, and behavior tuning for predictable production auto-scaling.