aRustyDev / aRustyDev/mdbook-htmx
docs(examples): Kubernetes Basic Deployment
- Dominant language
- Rust
- Stars
- 0
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
# Kubernetes Basic Deployment
Deploy mdbook-htmx on Kubernetes with a simple static file server.
## Overview
This example uses:
- **NGINX** or **Caddy**: Static file serving
- **ConfigMap**: Configuration
- **PersistentVolume**: Documentation storage (optional)
- **Ingress**: External access
## Architecture
```
┌─────────────┐
│ Ingress │
│ (NGINX) │
└──────┬──────┘
│
┌──────▼──────┐
│ Service │
│ (ClusterIP)│
└──────┬──────┘
│
┌────────────┼────────────┐
│ │ │
┌─────▼─────┐┌─────▼─────┐┌─────▼─────┐
│ Pod #1 ││ Pod #2 ││ Pod #3 │
│ (nginx) ││ (nginx) ││ (nginx) │
└───────────┘└───────────┘└───────────┘
```
## Kubernetes Manifests
### namespace.yaml
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: docs
labels:
app.kubernetes.io/name: mdbook-htmx
app.kubernetes.io/component: documentation
```
### configmap.yaml
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-config
namespace: docs
data:
nginx.conf: |
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript
application/xml+rss application/atom+xml image/svg+xml;
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.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
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Don't cache HTML
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
# Health check endpoint
location /healthz {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
}
}
```
### deployment.yaml
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: docs
namespace: docs
labels:
app.kubernetes.io/name: mdbook-htmx
app.kubernetes.io/component: server
spec:
replicas: 3
selector:
matchLabels:
app: docs
template:
metadata:
labels:
app: docs
app.kubernetes.io/name: mdbook-htmx
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
name: http
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
readOnly: true
- name: docs-content
mountPath: /usr/share/nginx/html
readOnly: true
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 5
periodSeconds: 5
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 101 # nginx user
capabilities:
drop:
- ALL
volumes:
- name: nginx-config
configMap:
name: nginx-config
- name: docs-content
configMap:
name: docs-content
securityContext:
fsGroup: 101
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- docs
topologyKey: kubernetes.io/hostname
```
### service.yaml
```yaml
apiVersion: v1
kind: Service
metadata:
name: docs
namespace: docs
labels:
app.kubernetes.io/name: mdbook-htmx
spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
app: docs
```
### ingress.yaml
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: docs
namespace: docs
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "10m"
spec:
tls:
- hosts:
- docs.example.com
secretName: docs-tls
rules:
- host: docs.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: docs
port:
number: 80
```
## CI/CD Integration
### Build and Push Docs
```yaml
# .github/workflows/deploy-k8s.yml
name: Deploy to Kubernetes
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build documentation
run: |
cargo install mdbook mdbook-htmx
mdbook build
- name: Create docs ConfigMap
run: |
kubectl create configmap docs-content \
--from-file=book/ \
--dry-run=client -o yaml > k8s/docs-content.yaml
- name: Deploy to Kubernetes
uses: azure/k8s-deploy@v4
with:
manifests: |
k8s/namespace.yaml
k8s/configmap.yaml
k8s/docs-content.yaml
k8s/deployment.yaml
k8s/service.yaml
k8s/ingress.yaml
namespace: docs
```
### Using Container Image Instead
For larger documentation sets, bake docs into a container:
```dockerfile
# Dockerfile
FROM nginx:alpine
# Copy nginx config
COPY nginx.conf /etc/nginx/nginx.conf
# Copy documentation
COPY book/ /usr/share/nginx/html/
# Create non-root user
RUN chown -R nginx:nginx /usr/share/nginx/html && \
chmod -R 755 /usr/share/nginx/html
USER nginx
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost/healthz || exit 1
```
Updated deployment:
```yaml
spec:
containers:
- name: docs
image: ghcr.io/org/docs:latest
# Remove docs-content volume mount
```
## Scaling
### Horizontal Pod Autoscaler
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: docs
namespace: docs
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: docs
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
```
### Pod Disruption Budget
```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: docs
namespace: docs
spec:
minAvailable: 1
selector:
matchLabels:
app: docs
```
## Monitoring
### ServiceMonitor (Prometheus)
```yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: docs
namespace: docs
spec:
selector:
matchLabels:
app.kubernetes.io/name: mdbook-htmx
endpoints:
- port: http
path: /metrics
interval: 30s
```
### Grafana Dashboard
Create a dashboard showing:
- Request rate
- Error rate
- Response time percentiles
- Pod health
## Resource Estimation
| Component | CPU | Memory | Replicas | Total |
|-----------|-----|--------|----------|-------|
| nginx | 50m-200m | 64-128Mi | 3 | 150-600m CPU, 192-384Mi |
For 10K daily users: 3 replicas is typically sufficient.
## Next Steps
- Add [Meilisearch for search](./k8s-meilisearch.md)
- Configure [Ingress authentication](./k8s-ingress-auth.md)
- Deploy with [Helm chart](./helm-chart.md)
Contributor guide
Assessment
This issue has not been assessed yet.