Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94c561581a | ||
|
|
ce7a72728d | ||
|
|
c24ed01e0a | ||
|
|
7e1728a037 | ||
|
|
30ef38b143 | ||
|
|
b7145e1323 | ||
|
|
87867b9f30 | ||
|
|
8a7833e8f5 | ||
|
|
4426668518 | ||
|
|
eacf71d676 | ||
|
|
b05814980f | ||
|
|
eb67d6361e | ||
|
|
8062b9d208 | ||
|
|
aa01def998 | ||
|
|
1ad79d40d8 | ||
|
|
7d4f1668a6 | ||
|
|
498231c160 | ||
|
|
898e3003a2 |
@@ -46,8 +46,11 @@ jobs:
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:latest
|
||||
${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:${{ gitea.event.head_commit.id }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
# Registry-backed build cache. The GitHub-hosted 'gha' cache is not
|
||||
# reachable from self-hosted Gitea runners, so store the cache as a
|
||||
# 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
|
||||
needs: build
|
||||
@@ -58,9 +61,13 @@ jobs:
|
||||
with:
|
||||
setup-key: ${{ secrets.NETBIRD_SETUP_KEY }}
|
||||
hostname: 'gitea-actions-blog'
|
||||
management-url: 'https://netbird.ahmza.com'
|
||||
management-url: ${{ secrets.NETBIRD_URL }}
|
||||
|
||||
- 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
|
||||
with:
|
||||
host: ${{ secrets.RPIC_HOST }}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Caddyfile for the blog behind Cloudflare.
|
||||
#
|
||||
# Topology: Cloudflare (terminates TLS) -> Caddy (HTTP :80) -> SWS (blog:80)
|
||||
#
|
||||
# Cloudflare handles the public HTTPS cert, so Caddy serves plain HTTP on :80
|
||||
# and does NOT need the Cloudflare DNS-01 / automatic-HTTPS setup. Set the
|
||||
# Cloudflare SSL/TLS mode to "Full" so CF->origin is encrypted at the network
|
||||
# layer (e.g. over your tunnel/Netbird), even though this listener is HTTP.
|
||||
#
|
||||
# Copy this file to /etc/caddy/Caddyfile on the caddy_cf host and reload:
|
||||
# docker exec caddy_cf caddy reload --config /etc/caddy/Caddyfile
|
||||
# or restart the container.
|
||||
{
|
||||
# Trust only Cloudflare's edge IPs so real visitor IPs (Cf-Connecting-Ip /
|
||||
# X-Forwarded-For) are honoured only when the request actually comes from
|
||||
# Cloudflare. This prevents header spoofing from any other source.
|
||||
#
|
||||
# The `cloudflare` IP source (WeidiDeng/caddy-cloudflare-ip, baked into the
|
||||
# caddy-cf image) auto-fetches the ranges from cloudflare.com/ips-v4 and
|
||||
# /ips-v6 and refreshes them periodically, so there's no static list to
|
||||
# keep in sync.
|
||||
servers {
|
||||
trusted_proxies cloudflare {
|
||||
interval 12h
|
||||
timeout 15s
|
||||
}
|
||||
# Read the real visitor IP from these headers (only honoured for
|
||||
# requests from trusted_proxies above). Cf-Connecting-Ip is
|
||||
# Cloudflare's single authoritative end-user IP; X-Forwarded-For is
|
||||
# the fallback if a request ever arrives without the CF header.
|
||||
client_ip_headers Cf-Connecting-Ip X-Forwarded-For
|
||||
}
|
||||
}
|
||||
|
||||
# Bind to the hostname on the HTTP port only. Because the address has an
|
||||
# explicit ":80" scheme/port, Caddy will NOT try to provision a TLS cert.
|
||||
http://blog.ahmza.com {
|
||||
# THE responder: proxy every request to the blog (SWS) container.
|
||||
# This is what was missing before (requests fell through to a NOP -> blank page).
|
||||
reverse_proxy blog:80
|
||||
|
||||
# Compression happens here (SWS has it disabled on purpose).
|
||||
encode zstd gzip
|
||||
|
||||
# Security headers (single source of truth; SWS has security-headers off).
|
||||
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"
|
||||
# Drop Caddy's default Server header.
|
||||
-Server
|
||||
}
|
||||
|
||||
log {
|
||||
output stdout
|
||||
format console
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -3,8 +3,9 @@ WORKDIR /tmp/hugo
|
||||
COPY . .
|
||||
RUN hugo --gc --minify
|
||||
|
||||
FROM nginx:alpine-slim
|
||||
WORKDIR /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --from=build /tmp/hugo/public .
|
||||
FROM ghcr.io/static-web-server/static-web-server:2
|
||||
COPY sws.toml /etc/sws.toml
|
||||
COPY --from=build /tmp/hugo/public /public
|
||||
ENV SERVER_CONFIG_FILE=/etc/sws.toml
|
||||
EXPOSE 80/tcp
|
||||
CMD ["--config-file", "/etc/sws.toml"]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
blog:
|
||||
image: ghcr.io/ahmzaa/blog-ahmza-com
|
||||
image: tea.ahmza.com/ahmza/blog-ahmza-com:latest
|
||||
ports:
|
||||
- 80:80
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
# 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 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.
|
||||
|
||||
## Deployment topologies
|
||||
|
||||
There are two supported ways to run Caddy in front of the blog. **Pick one.**
|
||||
|
||||
### 1. Cloudflare in front (current production) — HTTP only
|
||||
|
||||
```
|
||||
Cloudflare (terminates TLS) -> Caddy (HTTP :80) -> SWS (blog:80)
|
||||
```
|
||||
|
||||
Cloudflare proxies the domain and provides the public HTTPS certificate, so
|
||||
Caddy only needs to serve plain **HTTP on :80** and reverse-proxy to the blog.
|
||||
Caddy does **not** provision a TLS cert in this mode. This is the live setup and
|
||||
its config is the committed [`Caddyfile`](../Caddyfile) at the repo root —
|
||||
see [Cloudflare-fronted setup](#cloudflare-fronted-setup-http-only) below.
|
||||
|
||||
Set the Cloudflare **SSL/TLS mode to "Full"** so the Cloudflare→origin hop is
|
||||
still encrypted at the network layer (e.g. over Netbird), even though Caddy's
|
||||
listener is HTTP.
|
||||
|
||||
### 2. Caddy terminates TLS via Cloudflare DNS-01
|
||||
|
||||
```
|
||||
Client -> Caddy (HTTPS :443, ACME DNS-01) -> SWS (blog:80)
|
||||
```
|
||||
|
||||
Caddy obtains/renews the certificate itself using the ACME **DNS-01** challenge
|
||||
through the Cloudflare API — no inbound `80`/`443` from the public internet is
|
||||
required, which suits a Netbird-only setup with no Cloudflare proxy in front.
|
||||
The stock Caddy image cannot do this; the `caddy-cf` build bundles the
|
||||
Cloudflare DNS provider plugin. See the Docker Compose / Kubernetes examples
|
||||
later in this document.
|
||||
|
||||
## 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 | Cloudflare *or* Caddy | Cloudflare in topology 1; Caddy (DNS-01) in topology 2 |
|
||||
| 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 handle TLS (or let Cloudflare do it), add compression +
|
||||
security headers, and forward requests to that origin.
|
||||
|
||||
---
|
||||
|
||||
## Cloudflare-fronted setup (HTTP only)
|
||||
|
||||
**This is the current production topology.** The canonical config is the
|
||||
committed [`Caddyfile`](../Caddyfile) at the repo root; copy it to the
|
||||
`caddy_cf` host and reload. Highlights:
|
||||
|
||||
- **`http://blog.ahmza.com`** — the explicit `http://` scheme binds Caddy to
|
||||
`:80` only and stops it from trying to provision a TLS certificate (Cloudflare
|
||||
already serves HTTPS to visitors).
|
||||
- **`reverse_proxy blog:80`** — proxies to the blog (SWS) container by its
|
||||
compose service name. Caddy and the blog container must share a Docker
|
||||
network. Omitting this responder makes Caddy return an empty `NOP` response —
|
||||
i.e. a **blank page**.
|
||||
- **`encode zstd gzip`** — compression happens here (SWS has it disabled).
|
||||
- **`header { ... }`** — security headers (SWS has them disabled).
|
||||
- **`trusted_proxies cloudflare { ... }`** + **`client_ip_headers
|
||||
Cf-Connecting-Ip X-Forwarded-For`** — so the real visitor IP is used for
|
||||
logging/matchers, but only when the request genuinely comes from Cloudflare
|
||||
(prevents header spoofing from any other source). The `cloudflare` IP source
|
||||
([WeidiDeng/caddy-cloudflare-ip](https://github.com/WeidiDeng/caddy-cloudflare-ip),
|
||||
baked into the `caddy-cf` image) auto-fetches the ranges from
|
||||
<https://www.cloudflare.com/ips-v4/> and <https://www.cloudflare.com/ips-v6/>
|
||||
and refreshes them on the configured `interval`, so there is no static list to
|
||||
maintain:
|
||||
|
||||
```caddy
|
||||
trusted_proxies cloudflare {
|
||||
interval 12h
|
||||
timeout 15s
|
||||
}
|
||||
```
|
||||
|
||||
`client_ip_headers` lists which header holds the real visitor IP:
|
||||
`Cf-Connecting-Ip` is Cloudflare's single authoritative end-user IP, with
|
||||
`X-Forwarded-For` as the fallback. It is only trusted for requests coming from
|
||||
the `trusted_proxies` above.
|
||||
|
||||
> If you ever run without the `caddy-cloudflare-ip` plugin, you can instead
|
||||
> pin the static ranges with `trusted_proxies static <ip/cidr ...>` copied
|
||||
> from the Cloudflare IP pages, but then you must keep them in sync manually.
|
||||
|
||||
Deploy and reload:
|
||||
|
||||
```bash
|
||||
# Put the Caddyfile in place on the caddy_cf host (e.g. into the ./conf mount),
|
||||
# then reload without downtime:
|
||||
docker exec caddy_cf caddy reload --config /etc/caddy/Caddyfile
|
||||
|
||||
# Verify from the host (expect HTTP 200, Content-Encoding: gzip, X-Frame-Options):
|
||||
curl -sI -H "Accept-Encoding: gzip" http://localhost/ | grep -iE 'HTTP|content-encoding|x-frame'
|
||||
```
|
||||
|
||||
> **Cloudflare SSL/TLS mode:** set it to **Full** so the Cloudflare→origin hop
|
||||
> is encrypted at the network layer (e.g. over Netbird), even though Caddy's
|
||||
> listener is plain HTTP.
|
||||
|
||||
---
|
||||
|
||||
The remaining sections describe **topology 2** (Caddy terminates TLS itself via
|
||||
the Cloudflare DNS-01 challenge) for setups without a Cloudflare proxy in front.
|
||||
|
||||
## 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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