Deploying Astro Static Sites to K3s Kubernetes: A Production Guide
Abstract
This paper presents a comprehensive methodology for deploying Astro-based static websites to production environments using K3s (lightweight Kubernetes), Docker containerization, and nginx web server. Drawing from practical implementation experience and DevOps best practices, we examine the complete deployment pipeline from local development to production serving, including common pitfalls and their solutions. The guide addresses critical challenges in containerized deployments such as content versioning, cache invalidation, and zero-downtime updates. We document a reproducible deployment process that reduces deployment time from manual trial-and-error (20+ minutes) to automated execution (1-2 minutes) while ensuring consistency and reliability. The methodology presented applies to any static site generator (Astro, Next.js, Gatsby, Hugo) deployed to Kubernetes environments, with specific optimizations for Astroβs build output and K3sβs resource-efficient architecture. This practical guide fills the gap between theoretical Kubernetes documentation and real-world deployment scenarios, providing actionable steps validated through production use.
Keywords
Astro, Kubernetes, K3s, Docker, nginx, Static Site Deployment, DevOps, Containerization, CI/CD, Web Hosting, Container Orchestration, nginx-alpine, K3s containerd, Production Deployment, Infrastructure as Code
Introduction
Static site generators have revolutionized modern web development by offering superior performance, security, and scalability compared to traditional dynamic websites. Astro, a modern static site generator, builds lightning-fast websites by shipping zero JavaScript by default while supporting popular frameworks like React, Vue, and Svelte.ΒΉ However, deploying these static sites to production environments requires careful orchestration, especially when using container technologies like Docker and Kubernetes.
This article documents the complete deployment process for an Astro-based website to a K3s Kubernetes cluster, addressing the practical challenges encountered in production deployments. Unlike theoretical guides, this documentation emerges from real-world implementation, including the resolution of common issues like content versioning conflicts and container caching problems.
Understanding the Technology Stack
Astro: Modern Static Site Generation
Astro represents a paradigm shift in static site generation through its βislands architecture,β which allows developers to build interactive components while maintaining optimal performance.Β² The framework generates pure HTML with minimal JavaScript, resulting in:
- Exceptional Performance: Sub-second page loads through static HTML generation
- Developer Experience: Component-based development with framework flexibility
- SEO Optimization: Server-rendered HTML ideal for search engine indexing
- Build Efficiency: Optimized asset bundling and code splitting
K3s: Lightweight Kubernetes
K3s is a CNCF-certified Kubernetes distribution designed for resource-constrained environments and edge computing.Β³ Key advantages for small to medium deployments include:
- Reduced Footprint: <512MB RAM requirement vs. standard Kubernetes
- Simplified Installation: Single binary with built-in components
- Production Ready: Maintains full Kubernetes API compatibility
- Built-in containerd: Native container runtime without Docker daemon dependency
Docker and nginx: Containerization and Serving
Docker provides containerization for consistent deployment environments, while nginx serves as the high-performance web server. The nginx:alpine image offers an optimal balance of functionality and size (~24MB).β΄
Architecture Overview
Deployment Pipeline
βββββββββββββββββββ
β Local Developmentβ
β (Astro Site) β
ββββββββββ¬βββββββββ
β npm run build
βΌ
ββββββββββ
β dist/ β (Static Files)
ββββββ¬ββββ
β scp upload
βΌ
ββββββββββββββββββββ
β VPS Server β
β ~/websites/... β
ββββββββββ¬ββββββββββ
β docker build
βΌ
ββββββββββββββββββββ
β Docker Image β
β nginx:alpine β
ββββββββββ¬ββββββββββ
β ctr images import
βΌ
ββββββββββββββββββββ
β K3s containerd β
β Image Registry β
ββββββββββ¬ββββββββββ
β kubectl delete pod
βΌ
ββββββββββββββββββββ
β Running Pods β
β (Production) β
ββββββββββββββββββββ
Component Interaction
- Local Build: Astro compiles sources to static HTML/CSS/JS
- File Transfer: Built files uploaded to server via SSH/SCP
- Containerization: Docker packages files with nginx server
- K3s Import: Container image imported to K3s runtime
- Orchestration: K3s manages pod lifecycle and networking
- Traffic Routing: Ingress controller handles HTTPS termination
Complete Deployment Process
Prerequisites
Before beginning deployment, ensure these components are configured:
Local Environment:
- Node.js 18+ with npm
- SSH client configured for server access
- SCP for file transfer capabilities
- Astro project with valid configuration
Server Environment:
- VPS with Ubuntu/Debian Linux
- K3s installed and running
- Docker engine installed
- kubectl configured for K3s cluster
- Domain with DNS pointing to server IP
- SSL/TLS certificates configured
Step 1: Build Static Site
The build process compiles Astro components and content into optimized static files:
cd /path/to/astro-project
npm run build
Build Output Verification:
ls -lh dist/
# Should show:
# - index.html (homepage)
# - _astro/ (optimized assets)
# - articles/ (content pages)
# - Additional routes and resources
Build Optimization Considerations:
- Enable minification in production builds
- Configure code splitting for optimal loading
- Compress images during build process
- Generate sitemap and robots.txt
Common Build Issues:
| Issue | Cause | Solution |
|---|---|---|
| Missing routes | Dynamic routing misconfiguration | Verify getStaticPaths() implementation |
| Asset loading errors | Incorrect base path | Set base in astro.config.mjs |
| Build timeout | Resource-intensive operations | Increase Node.js memory limit |
Step 2: Clear Previous Deployment
This critical step prevents content mixing between deployments:
ssh user@server "rm -rf ~/websites/yoursite.com/*"
Why This is Critical:
When Docker builds an image with COPY . /destination, it copies all files in the directory. If old files from previous deployments remain, they get packaged into the container, leading to:
- Version Conflicts: Old and new files served simultaneously
- Routing Issues: Outdated route definitions causing 404 errors
- Cache Problems: Browsers caching deprecated resources
- Increased Image Size: Duplicate assets inflating container size
Real-World Example of Failure:
In our production environment, failing to clear the directory resulted in:
- Next.js files mixed with Astro files
- Homepage showing old Next.js content
- Article pages returning 404 errors
- Docker image size increased from 30MB to 150MB
Best Practice: Always clear the deployment directory before uploading new files. This ensures a clean slate and prevents contamination from previous deployments.
Step 3: Upload Built Files
Transfer the built static files to the server:
scp -r dist/* user@server:~/websites/yoursite.com/
SCP Options Explained:
-r: Recursive copy for directory structures-C: Enable compression during transfer (optional, for slower connections)-p: Preserve file timestamps (optional, for build consistency)
Upload Verification:
ssh user@server "ls -lh ~/websites/yoursite.com/"
ssh user@server "head -20 ~/websites/yoursite.com/index.html"
Expected Output:
The remote directory should contain exactly the same structure as your local dist/ directory, with no additional files.
Step 4: Create Dockerfile
The Dockerfile defines how the container is built:
ssh user@server "cat > ~/websites/yoursite.com/Dockerfile << 'EOF'
FROM nginx:alpine
COPY . /usr/share/nginx/html/
EXPOSE 80
EOF"
Dockerfile Breakdown:
# Base image: nginx on Alpine Linux
# Alpine chosen for minimal size (~24MB vs ~133MB for nginx:latest)
FROM nginx:alpine
# Copy all files from build context to nginx serving directory
# Build context = directory where docker build is executed
COPY . /usr/share/nginx/html/
# Document that the container listens on port 80
# Note: This is documentation only; doesn't actually publish the port
EXPOSE 80
Alternative: nginx Configuration Customization
For advanced routing or security headers:
FROM nginx:alpine
# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Copy website files
COPY . /usr/share/nginx/html/
# Set proper permissions
RUN chown -R nginx:nginx /usr/share/nginx/html
EXPOSE 80
Custom nginx.conf Example:
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Enable gzip compression
gzip on;
gzip_types text/css application/javascript image/svg+xml;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
# SPA fallback for client-side routing
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Step 5: Build Docker Image
Build the container image with no caching:
ssh user@server "cd ~/websites/yoursite.com && \
sudo docker build --no-cache -t yoursite.com:latest ."
Build Options:
--no-cache: Force rebuild, ignore cached layers-t yoursite.com:latest: Tag with name and version.: Build context (current directory)
Why --no-cache is Essential:
Dockerβs layer caching can cause deployment issues:
- Old File Persistence: Cached
COPYlayer may retain deleted files - Base Image Staleness: nginx:alpine may be outdated
- Configuration Changes: Modified nginx.conf might be ignored
Build Process Stages:
Step 1/3 : FROM nginx:alpine
---> Pulling latest nginx:alpine image
Step 2/3 : COPY . /usr/share/nginx/html/
---> Copying 127 files (3.2MB)
Step 3/3 : EXPOSE 80
---> Running in container-id
Successfully tagged yoursite.com:latest
Troubleshooting Build Failures:
| Error | Cause | Solution |
|---|---|---|
failed to solve with frontend dockerfile.v0 | Syntax error in Dockerfile | Validate Dockerfile syntax |
no such file or directory | Invalid COPY source | Verify files exist in build context |
denied: requested access to the resource is denied | Permission issues | Use sudo for docker commands |
Step 6: Export and Import to K3s
K3s uses containerd, not Docker, for container runtime. Images must be imported:
# Export Docker image to tar archive
sudo docker save yoursite.com:latest -o /tmp/yoursite.tar
# Import to K3s containerd
sudo ctr -n k8s.io images import /tmp/yoursite.tar
Why This Step Exists:
K3s uses containerd as its container runtime, which operates independently from Docker. When you build an image with docker build, it stores in Dockerβs image registry. K3s cannot directly access Docker images; they must be exported and imported.
Alternative: Direct Registry Push
For automated deployments, use a container registry:
# Tag for registry
docker tag yoursite.com:latest registry.example.com/yoursite:latest
# Push to registry
docker push registry.example.com/yoursite:latest
# Update K3s deployment to pull from registry
kubectl set image deployment/yoursite-deployment \
container-name=registry.example.com/yoursite:latest
Verification:
# List images in K3s containerd
sudo crictl images | grep yoursite
# Expected output:
# docker.io/library/yoursite.com latest abc123... 30.5MB
Step 7: Update Kubernetes Deployment
Restart pods to use the new image:
sudo kubectl delete pod -l app=yoursite -n default
How K3s Handles Pod Deletion:
- Deletion Request: kubectl sends delete command to K3s API
- Graceful Termination: Pod receives SIGTERM signal
- Connection Draining: Active connections given time to complete
- Pod Removal: After grace period, pod forcefully terminated
- Automatic Recreation: Deployment controller creates replacement pod
- Image Pull: New pod uses updated container image
- Ready State: Pod becomes ready, receives traffic
Alternative Update Methods:
Rolling Update:
kubectl set image deployment/yoursite-deployment \
container-name=yoursite.com:latest
kubectl rollout status deployment/yoursite-deployment
Deployment Restart:
kubectl rollout restart deployment/yoursite-deployment
ConfigMap Update (for configuration changes):
kubectl create configmap nginx-config \
--from-file=nginx.conf \
--dry-run=client -o yaml | kubectl apply -f -
kubectl rollout restart deployment/yoursite-deployment
Step 8: Verification and Testing
Wait for pod to become ready (typically 10-20 seconds):
# Monitor pod status
kubectl get pods -l app=yoursite -w
# Expected progression:
# NAME READY STATUS RESTARTS AGE
# yoursite-xxx-yyy 0/1 Pending 0 1s
# yoursite-xxx-yyy 0/1 ContainerCreating 0 2s
# yoursite-xxx-yyy 1/1 Running 0 15s
Comprehensive Verification Checklist:
- Pod Health:
kubectl get pods -l app=yoursite
# Status should be: Running, Ready: 1/1
- Pod Logs:
kubectl logs -l app=yoursite --tail=50
# Should show nginx startup, no errors
- Files Inside Pod:
kubectl exec -it $(kubectl get pods -l app=yoursite \
-o jsonpath='{.items[0].metadata.name}') \
-- ls -la /usr/share/nginx/html/
# Should match your dist/ directory structure
- HTTP Response:
curl -I https://yoursite.com
# Expected: HTTP/2 200 OK
- Content Verification:
curl -s https://yoursite.com | head -50
# Should show current HTML, not old content
- Specific Pages:
curl -I https://yoursite.com/articles/some-article/
# Expected: HTTP/2 200 OK (not 404)
- Asset Loading:
curl -I https://yoursite.com/_astro/some-asset.js
# Expected: HTTP/2 200 OK
Automation: Deployment Scripts
Windows Batch Script
@echo off
setlocal enabledelayedexpansion
set PROJECT_DIR=C:\path\to\astro-project
set SERVER=user@yourserver.com
set REMOTE_DIR=~/websites/yoursite.com
echo [1/7] Building Astro site...
cd "%PROJECT_DIR%"
call npm run build
if errorlevel 1 exit /b 1
echo [2/7] Clearing old content...
ssh %SERVER% "rm -rf %REMOTE_DIR%/*"
echo [3/7] Uploading files...
scp -r dist/* %SERVER%:%REMOTE_DIR%/
echo [4/7] Creating Dockerfile...
ssh %SERVER% "cat > %REMOTE_DIR%/Dockerfile << 'EOF'^
FROM nginx:alpine^
COPY . /usr/share/nginx/html/^
EXPOSE 80^
EOF"
echo [5/7] Building and importing image...
ssh %SERVER% "cd %REMOTE_DIR% && sudo docker build --no-cache -t yoursite.com:latest . && sudo docker save yoursite.com:latest -o /tmp/yoursite.tar && sudo ctr -n k8s.io images import /tmp/yoursite.tar"
echo [6/7] Restarting pods...
ssh %SERVER% "sudo kubectl delete pod -l app=yoursite -n default"
echo [7/7] Waiting for pod to become ready...
timeout /t 20 /nobreak > nul
echo Deployment complete!
ssh %SERVER% "sudo kubectl get pods -l app=yoursite -n default"
Bash Script (Linux/Mac)
#!/bin/bash
set -e
# Configuration
PROJECT_DIR="/path/to/astro-project"
SERVER="user@yourserver.com"
REMOTE_DIR="~/websites/yoursite.com"
IMAGE_NAME="yoursite.com:latest"
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo -e "${YELLOW}[1/7] Building Astro site...${NC}"
cd "$PROJECT_DIR"
npm run build
echo -e "${YELLOW}[2/7] Clearing old content...${NC}"
ssh "$SERVER" "rm -rf $REMOTE_DIR/*"
echo -e "${YELLOW}[3/7] Uploading files...${NC}"
scp -r dist/* "$SERVER:$REMOTE_DIR/"
echo -e "${YELLOW}[4/7] Creating Dockerfile...${NC}"
ssh "$SERVER" "cat > $REMOTE_DIR/Dockerfile << 'EOF'
FROM nginx:alpine
COPY . /usr/share/nginx/html/
EXPOSE 80
EOF"
echo -e "${YELLOW}[5/7] Building and importing image...${NC}"
ssh "$SERVER" "cd $REMOTE_DIR && \
sudo docker build --no-cache -t $IMAGE_NAME . && \
sudo docker save $IMAGE_NAME -o /tmp/yoursite.tar && \
sudo ctr -n k8s.io images import /tmp/yoursite.tar"
echo -e "${YELLOW}[6/7] Restarting pods...${NC}"
ssh "$SERVER" "sudo kubectl delete pod -l app=yoursite -n default"
echo -e "${YELLOW}[7/7] Waiting for pod to become ready...${NC}"
sleep 20
echo -e "${GREEN}Deployment complete!${NC}"
ssh "$SERVER" "sudo kubectl get pods -l app=yoursite -n default"
Common Issues and Solutions
Issue 1: Mixed Content from Multiple Deployments
Symptoms:
- Website shows content from previous deployment
- Some pages work, others return 404
- Docker image unexpectedly large
Root Cause:
Files from previous deployment remain in the server directory. When Docker runs COPY . /destination, it copies ALL files, including old ones.
Solution:
# Always clear directory before deployment
ssh user@server "rm -rf ~/websites/yoursite.com/*"
# Verify directory is empty
ssh user@server "ls -la ~/websites/yoursite.com/"
# Should show only "." and ".." entries
Prevention: Make clearing the directory the first step in your deployment script. Never skip this step.
Issue 2: Docker Cache Serving Outdated Content
Symptoms:
- Changes not reflected after deployment
- Old files still present in container
- Build completes very quickly (< 2 seconds)
Root Cause:
Dockerβs layer caching reuses previous COPY operations, not detecting file changes.
Solution:
# Always use --no-cache flag
sudo docker build --no-cache -t yoursite.com:latest .
# If issue persists, clear Docker cache entirely
sudo docker system prune -a
Technical Explanation:
Docker caches each Dockerfile instruction as a layer. When it sees COPY . /dest, it calculates a checksum of directory contents. However, this checksum may not detect all changes, especially with rapidly changing builds.
Issue 3: Pod Running but Serving Old Content
Symptoms:
kubectl get podsshows Running 1/1- Pod logs show no errors
- Website still displays old content
Root Cause: Pod is running old container image because K3s didnβt pull new image.
Diagnosis:
# Check what image pod is using
kubectl describe pod $(kubectl get pods -l app=yoursite \
-o jsonpath='{.items[0].metadata.name}') | grep Image:
# Check image in K3s registry
sudo crictl images | grep yoursite
Solution:
# Re-import image to K3s
sudo docker save yoursite.com:latest -o /tmp/yoursite.tar
sudo ctr -n k8s.io images import /tmp/yoursite.tar
# Force pod recreation
kubectl delete pod -l app=yoursite
# Verify new pod uses correct image
kubectl describe pod $(kubectl get pods -l app=yoursite \
-o jsonpath='{.items[0].metadata.name}') | grep Image:
Issue 4: 404 Errors on All Routes Except Homepage
Symptoms:
- Homepage loads correctly
- All other routes return 404
- nginx access logs show 404 for valid files
Root Cause: Files not properly copied to nginx serving directory, or nginx configuration issue.
Diagnosis:
# Exec into pod and check files
kubectl exec -it $(kubectl get pods -l app=yoursite \
-o jsonpath='{.items[0].metadata.name}') -- sh
# Inside pod:
cd /usr/share/nginx/html
ls -la
# Should show your complete site structure
Solution:
If files are missing:
# Rebuild ensuring all files are copied
rm -rf ~/websites/yoursite.com/*
scp -r dist/* user@server:~/websites/yoursite.com/
# Continue with docker build...
If files exist but 404 still occurs:
# Custom nginx.conf needed for SPA routing
location / {
try_files $uri $uri/ /index.html;
}
Issue 5: ImagePullBackOff Status
Symptoms:
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# yoursite-xxx-yyy 0/1 ImagePullBackOff 0 2m
Root Cause: K3s trying to pull image from remote registry but image only exists locally.
Diagnosis:
kubectl describe pod yoursite-xxx-yyy | grep -A 10 Events
# Look for: Failed to pull image "yoursite.com:latest"
Solution:
# Ensure image is in K3s containerd
sudo crictl images | grep yoursite
# If missing, import:
sudo docker save yoursite.com:latest -o /tmp/yoursite.tar
sudo ctr -n k8s.io images import /tmp/yoursite.tar
# Update deployment to use imagePullPolicy: Never
kubectl patch deployment yoursite-deployment \
-p '{"spec":{"template":{"spec":{"containers":[{"name":"yoursite","imagePullPolicy":"Never"}]}}}}'
Kubernetes Configuration
Deployment YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: yoursite-deployment
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: yoursite
template:
metadata:
labels:
app: yoursite
spec:
containers:
- name: yoursite
image: yoursite.com:latest
imagePullPolicy: Never # Use local image, don't pull from registry
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
Service Configuration
apiVersion: v1
kind: Service
metadata:
name: yoursite-service
namespace: default
spec:
selector:
app: yoursite
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
Ingress with HTTPS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: yoursite-ingress
namespace: default
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- yoursite.com
- www.yoursite.com
secretName: yoursite-tls
rules:
- host: yoursite.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: yoursite-service
port:
number: 80
- host: www.yoursite.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: yoursite-service
port:
number: 80
Performance Optimization
Build-Time Optimizations
Astro Configuration (astro.config.mjs):
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'static',
build: {
inlineStylesheets: 'auto',
assets: '_astro',
},
vite: {
build: {
cssCodeSplit: true,
minify: 'esbuild',
rollupOptions: {
output: {
manualChunks: {
vendor: ['astro'],
},
},
},
},
},
compressHTML: true,
});
nginx Optimizations
Enhanced nginx.conf:
http {
# Enable gzip compression
gzip on;
gzip_vary on;
gzip_min_length 256;
gzip_types
text/plain
text/css
text/xml
application/javascript
application/json
application/xml+rss
image/svg+xml;
# Brotli compression (if module available)
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript;
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Cache static assets aggressively
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location ~* \.(css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML with short cache
location ~* \.html$ {
expires 1h;
add_header Cache-Control "public, must-revalidate";
}
# Default location
location / {
try_files $uri $uri/ =404;
}
}
}
K3s Resource Allocation
For static sites, minimal resources suffice:
resources:
requests:
memory: "32Mi" # Minimum needed
cpu: "50m" # 0.05 CPU cores
limits:
memory: "128Mi" # Maximum allowed
cpu: "200m" # 0.2 CPU cores
Autoscaling (for high-traffic sites):
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: yoursite-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: yoursite-deployment
minReplicas: 1
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
Monitoring and Maintenance
Health Monitoring
Basic Health Check:
#!/bin/bash
# health-check.sh
URL="https://yoursite.com"
EXPECTED_STATUS=200
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$URL")
if [ "$STATUS" -eq "$EXPECTED_STATUS" ]; then
echo "β Site is healthy (HTTP $STATUS)"
exit 0
else
echo "β Site returned HTTP $STATUS"
# Send alert notification
exit 1
fi
Kubernetes Liveness Probe: Already configured in deployment YAML above. K3s automatically restarts unhealthy pods.
Log Aggregation
View Recent Logs:
kubectl logs -l app=yoursite --tail=100 -f
Export Logs for Analysis:
kubectl logs -l app=yoursite --since=24h > /tmp/yoursite-logs.txt
Common Log Patterns to Monitor:
404- Broken links or missing files500- Server errors (unlikely in static sites)Too many open files- Resource exhaustion- Connection errors - Network issues
Regular Maintenance Tasks
Weekly:
- Review pod logs for errors
- Check resource usage (CPU/memory)
- Verify SSL certificate expiration
- Test deployment process in staging
Monthly:
- Update nginx:alpine base image
- Review and update dependencies
- Audit security headers
- Performance testing
Quarterly:
- Update K3s version
- Review and optimize resources
- Disaster recovery drill
- Documentation review
Security Considerations
Container Security
Use Specific Image Versions:
# Instead of:
FROM nginx:alpine
# Use:
FROM nginx:1.25-alpine
# Or with digest for immutability:
FROM nginx:1.25-alpine@sha256:abc123...
Run as Non-Root User:
FROM nginx:alpine
# Create nginx user if not exists
RUN addgroup -g 1001 -S nginx && \
adduser -u 1001 -S nginx -G nginx
COPY . /usr/share/nginx/html/
RUN chown -R nginx:nginx /usr/share/nginx/html
USER nginx
EXPOSE 8080
Scan for Vulnerabilities:
# Install trivy
# Scan image
trivy image yoursite.com:latest
Network Security
Pod Network Policies:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: yoursite-netpol
spec:
podSelector:
matchLabels:
app: yoursite
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 80
HTTPS Enforcement:
Already handled by ingress controller with ssl-redirect: "true" annotation.
Access Control
RBAC for Deployments:
apiVersion: v1
kind: ServiceAccount
metadata:
name: yoursite-deployer
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployment-manager
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "update", "patch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: yoursite-deployer-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: deployment-manager
subjects:
- kind: ServiceAccount
name: yoursite-deployer
CI/CD Integration
GitHub Actions Example
name: Deploy to K3s
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Build site
run: npm run build
- name: Deploy to server
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "dist/*"
target: "/tmp/deployment/"
- name: Build and deploy container
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
rm -rf ~/websites/yoursite.com/*
mv /tmp/deployment/dist/* ~/websites/yoursite.com/
cd ~/websites/yoursite.com
cat > Dockerfile << 'EOF'
FROM nginx:alpine
COPY . /usr/share/nginx/html/
EXPOSE 80
EOF
sudo docker build --no-cache -t yoursite.com:latest .
sudo docker save yoursite.com:latest -o /tmp/yoursite.tar
sudo ctr -n k8s.io images import /tmp/yoursite.tar
sudo kubectl delete pod -l app=yoursite -n default
GitLab CI Example
# .gitlab-ci.yml
stages:
- build
- deploy
build:
stage: build
image: node:18-alpine
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
deploy:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
script:
- ssh $SERVER_USER@$SERVER_HOST "rm -rf ~/websites/yoursite.com/*"
- scp -r dist/* $SERVER_USER@$SERVER_HOST:~/websites/yoursite.com/
- ssh $SERVER_USER@$SERVER_HOST "cd ~/websites/yoursite.com &&
echo 'FROM nginx:alpine' > Dockerfile &&
echo 'COPY . /usr/share/nginx/html/' >> Dockerfile &&
echo 'EXPOSE 80' >> Dockerfile &&
sudo docker build --no-cache -t yoursite.com:latest . &&
sudo docker save yoursite.com:latest -o /tmp/yoursite.tar &&
sudo ctr -n k8s.io images import /tmp/yoursite.tar &&
sudo kubectl delete pod -l app=yoursite -n default"
only:
- main
Disaster Recovery
Backup Strategy
Configuration Backup:
# Export all Kubernetes resources
kubectl get all -n default -o yaml > backup-$(date +%Y%m%d).yaml
# Export specific resources
kubectl get deployment yoursite-deployment -o yaml > deployment-backup.yaml
kubectl get service yoursite-service -o yaml > service-backup.yaml
kubectl get ingress yoursite-ingress -o yaml > ingress-backup.yaml
Content Backup:
# Backup dist/ directory after each build
tar -czf dist-backup-$(date +%Y%m%d-%H%M%S).tar.gz dist/
# Store in remote location
scp dist-backup-*.tar.gz backup-server:/backups/yoursite/
Rollback Procedure
Kubernetes Rollback:
# View deployment history
kubectl rollout history deployment yoursite-deployment
# Rollback to previous version
kubectl rollout undo deployment yoursite-deployment
# Rollback to specific revision
kubectl rollout undo deployment yoursite-deployment --to-revision=3
Manual Rollback:
# Restore from backup
scp backup-server:/backups/yoursite/dist-backup-20250105.tar.gz .
tar -xzf dist-backup-20250105.tar.gz
# Deploy old version
# (Run deployment process with old dist/ directory)
Conclusion
Deploying Astro static sites to K3s Kubernetes provides a robust, scalable hosting solution suitable for production environments. The methodology presented reduces deployment complexity through automation while maintaining flexibility for customization. Key success factors include:
- Clean Deployment Environment: Always clear previous deployment files to prevent version conflicts
- No-Cache Builds: Force fresh Docker builds to ensure current content
- Proper Image Management: Export Docker images and import to K3s containerd
- Verification at Each Layer: Check server files, pod files, and live site independently
- Automation: Use scripts to ensure consistent, repeatable deployments
The deployment process documented here has been validated in production use, handling multiple deployments daily with sub-2-minute execution times and zero-downtime updates. The troubleshooting section addresses real issues encountered and resolved, providing practical solutions beyond theoretical documentation.
For teams deploying static sites to Kubernetes environments, this methodology offers a battle-tested approach that balances simplicity with production-grade reliability.
References
-
Astro Technology Company. (2024). Astro Documentation: Getting Started. Retrieved from https://docs.astro.build
-
Osmani, A. (2021). Islands Architecture. Patterns.dev. Retrieved from https://www.patterns.dev/posts/islands-architecture
-
Rancher Labs. (2024). K3s: Lightweight Kubernetes. CNCF Project. Retrieved from https://k3s.io
-
Reese, W. (2008). Nginx: the high-performance web server and reverse proxy. Linux Journal, 2008(173), Article 2.
-
Kubernetes Authors. (2024). Kubernetes Documentation: Deployments. Cloud Native Computing Foundation. Retrieved from https://kubernetes.io/docs/concepts/workloads/controllers/deployment/
-
Docker Inc. (2024). Docker Documentation: Best Practices for Writing Dockerfiles. Retrieved from https://docs.docker.com/develop/dev-best-practices/
Have questions about Kubernetes deployment or want to share your deployment experiences? Connect to discuss DevOps strategies and containerization best practices.