Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e133b01b00 | ||
|
|
b7145e1323 | ||
|
|
87867b9f30 | ||
|
|
8a7833e8f5 | ||
|
|
4426668518 | ||
|
|
eacf71d676 | ||
|
|
b05814980f | ||
|
|
eb67d6361e | ||
|
|
8062b9d208 | ||
|
|
aa01def998 | ||
|
|
f23fe01f3c | ||
|
|
1ad79d40d8 | ||
|
|
7d4f1668a6 | ||
|
|
498231c160 | ||
|
|
898e3003a2 | ||
|
|
0d425c0ed1 | ||
|
|
cd6f85f903 | ||
|
|
0bef6a1907 | ||
|
|
83ca9b61b8 | ||
|
|
e9bcafb64b | ||
|
|
2480790384 | ||
|
|
22d3759756 | ||
|
|
9a6555f1c3 | ||
|
|
d71dc7a86b |
@@ -5,8 +5,8 @@ on:
|
|||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: tea.ahmza.com
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ gitea.repository }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -22,12 +22,12 @@ jobs:
|
|||||||
- name: Checkout Repo
|
- name: Checkout Repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to Gitea Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ gitea.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITEATOKEN }}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
@@ -44,11 +44,14 @@ jobs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
BUILDKIT_CONTEXT_KEEP_GIT_DIR=true
|
BUILDKIT_CONTEXT_KEEP_GIT_DIR=true
|
||||||
tags: |
|
tags: |
|
||||||
ghcr.io/${{ github.repository_owner }}/blog-ahmza-com:latest
|
${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:latest
|
||||||
ghcr.io/${{ github.repository_owner }}/blog-ahmza-com:${{ github.event.head_commit.id }}
|
${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:${{ gitea.event.head_commit.id }}
|
||||||
cache-from: type=gha
|
# Registry-backed build cache. The GitHub-hosted 'gha' cache is not
|
||||||
cache-to: type=gha,mode=max
|
# reachable from self-hosted Gitea runners, so store the cache as a
|
||||||
deploy-to-rpic:
|
# dedicated tag in the same registry instead.
|
||||||
|
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:buildcache
|
||||||
|
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:buildcache,mode=max
|
||||||
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: build
|
||||||
|
|
||||||
@@ -57,35 +60,27 @@ jobs:
|
|||||||
uses: Alemiz112/netbird-connect@v1
|
uses: Alemiz112/netbird-connect@v1
|
||||||
with:
|
with:
|
||||||
setup-key: ${{ secrets.NETBIRD_SETUP_KEY }}
|
setup-key: ${{ secrets.NETBIRD_SETUP_KEY }}
|
||||||
hostname: 'blog-gh-actions'
|
hostname: 'gitea-actions-blog'
|
||||||
management-url: ${{ secrets.NETBIRD_URL }}
|
management-url: ${{ secrets.NETBIRD_URL }}
|
||||||
|
|
||||||
- name: Deploy to Cluster
|
- name: Deploy to RPIC-Cluster
|
||||||
|
# RPIC cluster is temporarily out of service. Don't let its failure
|
||||||
|
# block the Gimli deploy or fail the whole job. Remove this line once
|
||||||
|
# the cluster is back.
|
||||||
|
continue-on-error: true
|
||||||
uses: appleboy/ssh-action@v1
|
uses: appleboy/ssh-action@v1
|
||||||
with:
|
with:
|
||||||
host: ${{ secrets.SSH_HOST }}
|
host: ${{ secrets.RPIC_HOST }}
|
||||||
username: ${{ secrets.SSH_USER }}
|
username: ${{ secrets.SSH_USER }}
|
||||||
key: ${{ secrets.SSH_KEY }}
|
key: ${{ secrets.RPIC_KEY }}
|
||||||
port: ${{ secrets.SSH_PORT }}
|
port: ${{ secrets.SSH_PORT }}
|
||||||
script: 'kubectl rollout restart deployment blog -n blog-ahmza-com'
|
script: 'kubectl rollout restart deployment blog -n blog-ahmza-com'
|
||||||
|
|
||||||
deploy-to-gimli-blog:
|
- name: Deploy to Gimli
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Netbird Connect
|
|
||||||
uses: Alemiz112/netbird-connect@v1
|
|
||||||
with:
|
|
||||||
setup-key: ${{ secrets.NETBIRD_SETUP_KEY }}
|
|
||||||
hostname: 'blog-gh-actions'
|
|
||||||
management-url: ${{ secrets.NETBIRD_URL }}
|
|
||||||
|
|
||||||
- name: Deploy to gimli-blog
|
|
||||||
uses: appleboy/ssh-action@v1
|
uses: appleboy/ssh-action@v1
|
||||||
with:
|
with:
|
||||||
host: ${{ secrets.GIMLI_BLOG_HOST }}
|
host: ${{ secrets.GIMLI_HOST }}
|
||||||
username: ${{ secrets.GIMLI_BLOG_USER }}
|
username: ${{ secrets.SSH_USER }}
|
||||||
key: ${{ secrets.GIMLI_BLOG_KEY }}
|
key: ${{ secrets.GIMLI_KEY }}
|
||||||
port: ${{ secrets.GIMLI_BLOG_PORT }}
|
port: ${{ secrets.SSH_PORT }}
|
||||||
script: 'docker compose down && docker compose pull && docker compose up -d'
|
script: 'docker compose down && docker compose pull && docker compose up -d'
|
||||||
|
|||||||
+6
-5
@@ -1,10 +1,11 @@
|
|||||||
FROM ghcr.io/hugomods/hugo:0.128.2 AS build
|
FROM ghcr.io/hugomods/hugo:0.157.0 AS build
|
||||||
WORKDIR /tmp/hugo
|
WORKDIR /tmp/hugo
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN hugo --gc --minify
|
RUN hugo --gc --minify
|
||||||
|
|
||||||
FROM nginx:alpine-slim
|
FROM ghcr.io/static-web-server/static-web-server:2
|
||||||
WORKDIR /usr/share/nginx/html
|
COPY sws.toml /etc/sws.toml
|
||||||
COPY nginx.conf /etc/nginx/nginx.conf
|
COPY --from=build /tmp/hugo/public /public
|
||||||
COPY --from=build /tmp/hugo/public .
|
ENV SERVER_CONFIG_FILE=/etc/sws.toml
|
||||||
EXPOSE 80/tcp
|
EXPOSE 80/tcp
|
||||||
|
CMD ["--config-file", "/etc/sws.toml"]
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
This repository contains the source code for my personal blog hosted at [blog.ahmza.com](https://blog.ahmza.com).
|
This repository contains the source code for my personal blog hosted at [blog.ahmza.com](https://blog.ahmza.com).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
Using GitLab provides several advantages:
|
Using Gitea provides several advantages:
|
||||||
- **Highly Customizable CI/CD Pipelines**: Automated processes for building and deploying the blog.
|
- **Highly Customizable CI/CD Pipelines**: Automated processes for building and deploying the blog via Gitea Actions.
|
||||||
- **GitLab Container Image Repository**: Efficient management and storage of container images.
|
- **Gitea Container Image Repository**: Efficient management and storage of container images.
|
||||||
- **Custom GitLab Runners**: Flexibility in managing the CI/CD environment.
|
- **Custom Gitea Runners**: Flexibility in managing the CI/CD environment.
|
||||||
|
|
||||||
The CI/CD pipeline is configured to build the blog for both amd64 and arm64
|
The CI/CD pipeline is configured to build the blog for both amd64 and arm64
|
||||||
architectures, allowing me to host the site on my [Pi cluster](https://blog.ahmza.com/posts/pi-docker-swarm/)
|
architectures, allowing me to host the site on my [Pi cluster](https://blog.ahmza.com/posts/pi-docker-swarm/)
|
||||||
@@ -33,7 +33,7 @@ sacrificing performance.
|
|||||||
|
|
||||||
## Implementation
|
## Implementation
|
||||||
- **Hugo**: Converts Markdown files into static HTML/CSS/JS files.
|
- **Hugo**: Converts Markdown files into static HTML/CSS/JS files.
|
||||||
- **GitLab CI/CD**: Automates the build process and creates Docker images.
|
- **Gitea Actions**: Automates the build process and creates Docker images.
|
||||||
- **Kubernetes Cluster**: Serves the static files to users.
|
- **Kubernetes Cluster**: Serves the static files to users.
|
||||||
|
|
||||||
## To-Do List
|
## To-Do List
|
||||||
@@ -47,7 +47,8 @@ sacrificing performance.
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
### Cloning the Repository
|
### Cloning the Repository
|
||||||
To clone this repository along with its submodules, use the following command:
|
The Blowfish theme is managed as a Hugo module, so no submodules are required.
|
||||||
|
Clone the repository with:
|
||||||
```bash
|
```bash
|
||||||
git clone git@gitlab.com:ahmza/blog.git
|
git clone ssh://gitea@tea.ahmza.com:2222/ahmza/blog-ahmza-com.git
|
||||||
```
|
```
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 782 KiB After Width: | Height: | Size: 441 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 73 KiB |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
blog:
|
blog:
|
||||||
image: ghcr.io/ahmzaa/blog-ahmza-com
|
image: tea.ahmza.com/ahmza/blog-ahmza-com:latest
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -0,0 +1,333 @@
|
|||||||
|
# Caddy as a Reverse Proxy
|
||||||
|
|
||||||
|
This document describes how to run [Caddy](https://caddyserver.com/) as a
|
||||||
|
reverse proxy in front of the blog. It covers both deployment targets used by
|
||||||
|
this project: the Kubernetes (Raspberry Pi) cluster and the Docker Compose host
|
||||||
|
(Gimli).
|
||||||
|
|
||||||
|
It uses the custom [`caddy-cf`](https://github.com/ahmzaa/caddy-cf) image
|
||||||
|
(`ghcr.io/ahmzaa/caddy-cf:latest`) — a Caddy build with the **Cloudflare DNS
|
||||||
|
module** baked in. The stock Caddy image cannot solve the ACME **DNS-01**
|
||||||
|
challenge for Cloudflare; this custom build can, which is what lets us obtain
|
||||||
|
certificates without exposing ports `80`/`443` to the public internet.
|
||||||
|
|
||||||
|
## Why a reverse proxy?
|
||||||
|
|
||||||
|
The blog is served by [Static Web Server (SWS)](https://static-web-server.net/),
|
||||||
|
a tiny Rust binary that ships static files over plain HTTP on port `80`. SWS is
|
||||||
|
intentionally kept lean — it does **not** terminate TLS, and its compression and
|
||||||
|
security headers are disabled (see `sws.toml`). Those concerns are delegated to
|
||||||
|
the reverse proxy so that each layer owns exactly one job:
|
||||||
|
|
||||||
|
| Concern | Owned by | Notes |
|
||||||
|
| -------------------- | -------- | ------------------------------------------------- |
|
||||||
|
| TLS / HTTPS certs | Caddy | Automatic certs via Cloudflare **DNS-01** challenge |
|
||||||
|
| Compression (gzip/zstd) | Caddy | `encode` directive; SWS compression is off |
|
||||||
|
| Security headers | Caddy | Single source of truth, avoids duplicate headers |
|
||||||
|
| Serving static files | SWS | Reads from `/public`, sets `Cache-Control`/`ETag` |
|
||||||
|
| Cache-Control / ETag | SWS | Declared at the origin, passed through by Caddy |
|
||||||
|
|
||||||
|
### Why the Cloudflare DNS-01 challenge?
|
||||||
|
|
||||||
|
Caddy does **automatic HTTPS** out of the box, but the default HTTP-01 /
|
||||||
|
TLS-ALPN-01 challenges require ports `80`/`443` to be publicly reachable so
|
||||||
|
Let's Encrypt can verify domain ownership. This project's infrastructure is
|
||||||
|
reached over [Netbird](https://netbird.io/) (a private overlay network), so
|
||||||
|
those inbound challenges are not an option.
|
||||||
|
|
||||||
|
The **DNS-01** challenge instead proves domain ownership by creating a TXT
|
||||||
|
record via the Cloudflare API — no inbound connectivity required. The `caddy-cf`
|
||||||
|
image bundles the Cloudflare DNS provider plugin so Caddy can do this. All it
|
||||||
|
needs is a Cloudflare API token with **Zone:DNS:Edit** permission on the
|
||||||
|
`ahmza.com` zone.
|
||||||
|
|
||||||
|
## Origin contract
|
||||||
|
|
||||||
|
Whatever the deployment, the SWS origin exposes:
|
||||||
|
|
||||||
|
- **Protocol:** plain HTTP
|
||||||
|
- **Port:** `80`
|
||||||
|
- **Health endpoint:** `GET /health` → `200 OK`
|
||||||
|
- **Cache headers:** already set by SWS (Caddy should pass them through, not override)
|
||||||
|
|
||||||
|
Caddy only needs to terminate TLS (via Cloudflare DNS-01), add compression +
|
||||||
|
security headers, and forward requests to that origin.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker Compose (Gimli)
|
||||||
|
|
||||||
|
Run Caddy and the blog container on the same Docker network. Caddy reaches the
|
||||||
|
blog by its service name (`blog`) over the internal network, so the blog's port
|
||||||
|
does **not** need to be published to the host.
|
||||||
|
|
||||||
|
The Cloudflare API token is passed to Caddy as the `CF_API_TOKEN` environment
|
||||||
|
variable and referenced from the Caddyfile — keep it out of the committed
|
||||||
|
config. Store it in a `.env` file (git-ignored) next to `compose.yml`:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
# .env
|
||||||
|
CF_API_TOKEN=your-cloudflare-api-token-with-zone-dns-edit
|
||||||
|
```
|
||||||
|
|
||||||
|
### `compose.yml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
blog:
|
||||||
|
image: ghcr.io/ahmzaa/blog-ahmza-com
|
||||||
|
restart: unless-stopped
|
||||||
|
# No host port mapping needed — only Caddy talks to it, over the
|
||||||
|
# internal Docker network. Remove the previous "80:80" publish.
|
||||||
|
expose:
|
||||||
|
- "80"
|
||||||
|
|
||||||
|
caddy:
|
||||||
|
image: ghcr.io/ahmzaa/caddy-cf:latest # Caddy + Cloudflare DNS module
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: caddy-cf
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
- "443:443/udp" # HTTP/3 (QUIC)
|
||||||
|
environment:
|
||||||
|
# Consumed by the {env.CF_API_TOKEN} placeholder in the Caddyfile.
|
||||||
|
- CF_API_TOKEN=${CF_API_TOKEN}
|
||||||
|
volumes:
|
||||||
|
- ./conf:/etc/caddy # contains the Caddyfile
|
||||||
|
- ./data:/data # persists certificates — DO NOT lose this
|
||||||
|
- ./config:/config
|
||||||
|
```
|
||||||
|
|
||||||
|
Place the Caddyfile at `./conf/Caddyfile`.
|
||||||
|
|
||||||
|
### `./conf/Caddyfile`
|
||||||
|
|
||||||
|
```caddy
|
||||||
|
# Reusable Cloudflare DNS-01 TLS snippet. The token is read from the
|
||||||
|
# CF_API_TOKEN environment variable so it never lives in the config file.
|
||||||
|
(cloudflare) {
|
||||||
|
tls {
|
||||||
|
dns cloudflare {env.CF_API_TOKEN}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blog.ahmza.com {
|
||||||
|
# Obtain/renew the cert via the Cloudflare DNS-01 challenge.
|
||||||
|
import cloudflare
|
||||||
|
|
||||||
|
# Reverse proxy to the blog container by its compose service name.
|
||||||
|
reverse_proxy blog:80 {
|
||||||
|
# Use the SWS health endpoint for active upstream checks.
|
||||||
|
health_uri /health
|
||||||
|
health_interval 10s
|
||||||
|
health_timeout 2s
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compression is done here, not in SWS.
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
# Security headers live here so there is a single source of truth.
|
||||||
|
header {
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
# Remove Caddy's default Server header
|
||||||
|
-Server
|
||||||
|
}
|
||||||
|
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Certificate persistence:** the `./data` volume stores the issued
|
||||||
|
> certificates and ACME account key. Keep it — deleting it forces Caddy to
|
||||||
|
> re-request certificates and can hit Let's Encrypt rate limits.
|
||||||
|
|
||||||
|
Bring it up with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Kubernetes (Raspberry Pi cluster)
|
||||||
|
|
||||||
|
The blog runs as a `Deployment` behind a `Service`. Caddy runs as its own
|
||||||
|
`Deployment` and proxies to that Service via in-cluster DNS
|
||||||
|
(`blog.blog-ahmza-com.svc.cluster.local`, or just `blog` from within the same
|
||||||
|
namespace).
|
||||||
|
|
||||||
|
### Cloudflare API token Secret
|
||||||
|
|
||||||
|
Store the Cloudflare API token in a `Secret` and inject it into the Caddy
|
||||||
|
container as the `CF_API_TOKEN` environment variable (never bake it into the
|
||||||
|
ConfigMap):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl create secret generic caddy-cloudflare \
|
||||||
|
--namespace blog-ahmza-com \
|
||||||
|
--from-literal=CF_API_TOKEN='your-cloudflare-api-token-with-zone-dns-edit'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caddyfile via ConfigMap
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: caddy-config
|
||||||
|
namespace: blog-ahmza-com
|
||||||
|
data:
|
||||||
|
Caddyfile: |
|
||||||
|
(cloudflare) {
|
||||||
|
tls {
|
||||||
|
dns cloudflare {env.CF_API_TOKEN}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blog.ahmza.com {
|
||||||
|
import cloudflare
|
||||||
|
|
||||||
|
reverse_proxy blog:80 {
|
||||||
|
health_uri /health
|
||||||
|
health_interval 10s
|
||||||
|
health_timeout 2s
|
||||||
|
}
|
||||||
|
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
header {
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "SAMEORIGIN"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
-Server
|
||||||
|
}
|
||||||
|
|
||||||
|
log {
|
||||||
|
output stdout
|
||||||
|
format console
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caddy Deployment + Service
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: caddy
|
||||||
|
namespace: blog-ahmza-com
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: caddy
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: caddy
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: caddy
|
||||||
|
image: ghcr.io/ahmzaa/caddy-cf:latest # Caddy + Cloudflare DNS module
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
- containerPort: 443
|
||||||
|
- containerPort: 443
|
||||||
|
protocol: UDP
|
||||||
|
env:
|
||||||
|
- name: CF_API_TOKEN
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: caddy-cloudflare
|
||||||
|
key: CF_API_TOKEN
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /etc/caddy
|
||||||
|
- name: data
|
||||||
|
mountPath: /data # certificate storage — must persist
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: caddy-config
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: caddy-data
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: caddy-data
|
||||||
|
namespace: blog-ahmza-com
|
||||||
|
spec:
|
||||||
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 256Mi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: caddy
|
||||||
|
namespace: blog-ahmza-com
|
||||||
|
spec:
|
||||||
|
type: LoadBalancer # or NodePort, depending on the cluster
|
||||||
|
selector:
|
||||||
|
app: caddy
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
|
- name: https
|
||||||
|
port: 443
|
||||||
|
targetPort: 443
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Blog Service:** the existing `blog` Service should expose port `80` and
|
||||||
|
> target the SWS container port `80`. Caddy reaches it as `blog:80` from inside
|
||||||
|
> the same namespace.
|
||||||
|
|
||||||
|
> **Certificate persistence:** the `caddy-data` PVC is the K8s equivalent of the
|
||||||
|
> `./data` Docker volume. Without a persistent volume, a pod restart wipes
|
||||||
|
> the certificates and re-triggers ACME issuance.
|
||||||
|
|
||||||
|
> **DNS-01 and Netbird:** because certificates are issued via the Cloudflare
|
||||||
|
> DNS-01 challenge, the cluster does **not** need inbound `80`/`443` from the
|
||||||
|
> public internet for ACME. The `LoadBalancer`/`NodePort` above only needs to be
|
||||||
|
> reachable by your clients (e.g. over Netbird or your LAN).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes and gotchas
|
||||||
|
|
||||||
|
- **DNS-01 needs no inbound reachability.** Because the `caddy-cf` image solves
|
||||||
|
the ACME challenge via the Cloudflare API (DNS-01), Caddy does **not** need
|
||||||
|
ports `80`/`443` exposed to the public internet to issue certificates. This is
|
||||||
|
what makes it work behind Netbird. It does need outbound HTTPS to the Let's
|
||||||
|
Encrypt and Cloudflare APIs.
|
||||||
|
- **Cloudflare API token scope.** The token needs **Zone → DNS → Edit** on the
|
||||||
|
`ahmza.com` zone. Keep it in a `.env` file (Docker) or a `Secret` (K8s), never
|
||||||
|
in the committed Caddyfile — the config references it as `{env.CF_API_TOKEN}`.
|
||||||
|
- **DNS must be managed by Cloudflare.** The `ahmza.com` zone has to be hosted on
|
||||||
|
Cloudflare for the DNS-01 challenge to create the required `_acme-challenge`
|
||||||
|
TXT record.
|
||||||
|
- **Do not double-compress.** SWS compression is off by design; Caddy's `encode`
|
||||||
|
directive is the only place compression happens. Enabling both wastes CPU.
|
||||||
|
- **Do not duplicate headers.** Security headers are set in the `Caddyfile`. SWS
|
||||||
|
has `security-headers = false` so there is no conflict.
|
||||||
|
- **Cache-Control comes from the origin.** SWS already sets long-lived caching on
|
||||||
|
static assets and short/no-cache on HTML. Caddy passes these through; avoid
|
||||||
|
adding conflicting `Cache-Control` header directives in the `Caddyfile`.
|
||||||
|
- **Health checks.** Both examples use SWS's `/health` endpoint for active
|
||||||
|
upstream health checking, and it also works as a K8s liveness/readiness probe
|
||||||
|
on the blog pod itself.
|
||||||
|
- **HTTP/3.** The Docker example publishes `443/udp` to enable QUIC. Remove it if
|
||||||
|
your network path does not support UDP.
|
||||||
-70
@@ -1,70 +0,0 @@
|
|||||||
# auto detects a good number of processes to run
|
|
||||||
worker_processes auto;
|
|
||||||
|
|
||||||
#Provides the configuration file context in which the directives that affect connection processing are specified.
|
|
||||||
events {
|
|
||||||
# Sets the maximum number of simultaneous connections that can be opened by a worker process.
|
|
||||||
worker_connections 1024;
|
|
||||||
# Tells the worker to accept multiple connections at a time
|
|
||||||
multi_accept on;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
http {
|
|
||||||
# what times to include
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
# what is the default one
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
# Sets the path, format, and configuration for a buffered log write
|
|
||||||
log_format compression '$remote_addr - $remote_user [$time_local] '
|
|
||||||
'"$request" $status $upstream_addr '
|
|
||||||
'"$http_referer" "$http_user_agent"';
|
|
||||||
|
|
||||||
gzip on;
|
|
||||||
gzip_disable "MSIE [1-6]\.(?!.*SV1)";
|
|
||||||
gzip_vary on;
|
|
||||||
gzip_proxied any;
|
|
||||||
gzip_comp_level 9;
|
|
||||||
gzip_buffers 10 8k;
|
|
||||||
gzip_http_version 1.1;
|
|
||||||
gzip_types text/plain text/css text/xml text/javascript image/webp image/svg+xml image/x-icon application/javascript application/x-javascript application/json application/xml application/xml+rss;
|
|
||||||
|
|
||||||
server {
|
|
||||||
# listen on port 80
|
|
||||||
listen 80;
|
|
||||||
# save logs here
|
|
||||||
access_log /var/log/nginx/access.log compression;
|
|
||||||
|
|
||||||
# where's the root dir
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
# what file to serve as index
|
|
||||||
index index.html index.htm;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
# First attempt to serve request as file, then
|
|
||||||
# as directory, then fall back to redirecting to index.html
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Media: images, icons, video, audio, HTC
|
|
||||||
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|webp)$ {
|
|
||||||
expires 1y;
|
|
||||||
access_log off;
|
|
||||||
add_header Cache-Control "public";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Javascript and CSS files
|
|
||||||
location ~* \.(?:css|js)$ {
|
|
||||||
try_files $uri =404;
|
|
||||||
expires 1y;
|
|
||||||
access_log off;
|
|
||||||
add_header Cache-Control "public";
|
|
||||||
}
|
|
||||||
|
|
||||||
# Any route containing a file extension (e.g. /devicesfile.js)
|
|
||||||
location ~ ^.+\..+$ {
|
|
||||||
try_files $uri =404;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,46 @@
|
|||||||
|
[general]
|
||||||
|
|
||||||
|
#### Server
|
||||||
|
|
||||||
|
# Listen on all interfaces, port 80 (matches previous nginx setup)
|
||||||
|
host = "0.0.0.0"
|
||||||
|
port = 80
|
||||||
|
|
||||||
|
# Root directory of the static site (Hugo `public/` output)
|
||||||
|
root = "/public"
|
||||||
|
|
||||||
|
# Number of worker threads (0 = auto-detect based on CPU cores)
|
||||||
|
threads-multiplier = 1
|
||||||
|
|
||||||
|
# Log level
|
||||||
|
log-level = "info"
|
||||||
|
|
||||||
|
#### Performance / caching
|
||||||
|
|
||||||
|
# Compression is handled by the upstream reverse proxy, so it is disabled
|
||||||
|
# here to avoid doing the work twice (wasted CPU per request). Re-enable if
|
||||||
|
# SWS is ever exposed directly without a compressing proxy in front.
|
||||||
|
compression = false
|
||||||
|
compression-static = false
|
||||||
|
|
||||||
|
# Emit Cache-Control + ETag headers with sensible per-content-type defaults.
|
||||||
|
# The origin is the correct place to declare cacheability; the proxy/CDN
|
||||||
|
# passes these through. SWS applies long-lived caching to static assets
|
||||||
|
# (images, css, js, fonts) and short/no-cache to HTML.
|
||||||
|
cache-control-headers = true
|
||||||
|
|
||||||
|
#### Routing
|
||||||
|
|
||||||
|
# SPA-style fallback: serve index.html when a path is not found on disk,
|
||||||
|
# mirroring the previous nginx `try_files $uri $uri/ /index.html`.
|
||||||
|
page-fallback = "/public/index.html"
|
||||||
|
|
||||||
|
#### Security
|
||||||
|
|
||||||
|
# Security headers are set by the upstream reverse proxy, so they are
|
||||||
|
# disabled here to avoid duplicate/conflicting headers. Let one layer own
|
||||||
|
# the security posture. Re-enable if SWS is exposed without such a proxy.
|
||||||
|
security-headers = false
|
||||||
|
|
||||||
|
# Health endpoint at /health for container/K8s liveness probes
|
||||||
|
health = true
|
||||||
Reference in New Issue
Block a user