Author SHA1 Message Date
ahmza e133b01b00 Optimize webm videos and webp images
Re-encode all webm meme clips as VP9 at CRF 38 (audio stripped; they are
muted autoplay loops). Downscale oversized images: background.webp
3394->1920 wide q80, profile.webp 1920->512 q85 (animation preserved).

Results:
- webm: 3.2MB -> 1.37MB (-56%)
- webp: 1.2MB -> 0.70MB (-42%)
- Docker image: 23.1MB -> 18.7MB (~42% smaller than the original nginx image)

All videos verified valid/playable; profile.webp animation (5 frames)
kept intact.
2026-07-25 03:20:57 +01:00
ahmza b7145e1323 Remove temporary Netbird connectivity debug step
Build Hugo / build (push) Successful in 1m32s
Build Hugo / deploy (push) Successful in 39s
The deploy pipeline works end-to-end now (Gimli deploys successfully),
so the diagnostic step used to trace the WireGuard/TUN issue is no
longer needed. RPIC continue-on-error is kept until the cluster returns.
2026-07-25 03:07:00 +01:00
ahmza 87867b9f30 Point compose.yml at Gitea registry image with :latest tag
Build Hugo / build (push) Successful in 1m27s
Build Hugo / deploy (push) Successful in 40s
CI pushes to tea.ahmza.com/ahmza/blog-ahmza-com, not ghcr.io, and a
bare reference could resolve to the non-runnable :buildcache tag. Pin
the image to tea.ahmza.com/ahmza/blog-ahmza-com:latest so docker compose
pulls the actual runnable image.
2026-07-25 03:02:39 +01:00
ahmza 8a7833e8f5 Make RPIC deploy non-blocking while cluster is out of service
Build Hugo / build (push) Successful in 1m26s
Build Hugo / deploy (push) Failing after 38s
The Raspberry Pi cluster is temporarily out of service. Mark its deploy
step continue-on-error so its failure no longer skips the Gimli deploy
or fails the whole job. Revert once the cluster is back online.
2026-07-25 02:42:15 +01:00
ahmza 4426668518 Use registry build cache instead of gha cache
Build Hugo / build (push) Successful in 3m23s
Build Hugo / deploy (push) Failing after 39s
The GitHub-hosted 'gha' cache backend is unreachable from the
self-hosted Gitea runner (dial tcp 172.18.0.2:44181 i/o timeout), which
failed the build step even though the image pushed successfully. Switch
cache-from/cache-to to a registry-backed cache stored as a :buildcache
tag in the same Gitea registry, which works on self-hosted runners.
2026-07-25 02:21:38 +01:00
ahmza eacf71d676 Debug: check runner WireGuard capability instead of UDP ports
Build Hugo / build (push) Failing after 13m45s
Build Hugo / deploy (push) Skipped
Other Netbird peers connect fine, so the server and 51820/udp are OK --
the hang is specific to the Gitea runner. Replace the UDP port probes
with runner-side checks: /dev/net/tun presence, effective capabilities
(NET_ADMIN), WireGuard tooling/module, and a definitive 'ip tuntap add'
test. A runner container lacking TUN/NET_ADMIN cannot create the
WireGuard interface, which explains 'netbird up' timing out after the
control plane connects.
2026-07-25 01:53:09 +01:00
ahmza b05814980f Update Netbird debug to probe correct data-plane ports
Build Hugo / build (push) Successful in 1m47s
Build Hugo / deploy (push) Failing after 1m15s
The Netbird server multiplexes management + signal + dashboard behind
Traefik on :443 (no separate :10000 signal port). The tunnel data plane
is WireGuard on 51820/udp and STUN on 3478/udp. Update the debug step to
probe those UDP ports plus a general outbound-UDP check (public STUN) to
determine whether the runner network blocks UDP egress -- the likely
cause of 'netbird up' timing out after the control plane connects fine.
2026-07-25 01:46:28 +01:00
ahmza eb67d6361e Add Netbird connectivity debug step to deploy job
Build Hugo / build (push) Successful in 1m44s
Build Hugo / deploy (push) Failing after 1m8s
Temporary diagnostic step before 'Netbird Connect' that checks secret
presence (masked), DNS resolution, HTTPS reachability to the management
API, and the signal server TCP port from the runner. Helps distinguish
DNS/connectivity issues from auth/registration failures behind the
recurring 'netbird up' DeadlineExceeded error. Marked continue-on-error
so it never blocks the build.
2026-07-25 01:39:09 +01:00
ahmza 8062b9d208 Read Netbird management URL from secret
Build Hugo / build (push) Successful in 1m46s
Build Hugo / deploy (push) Failing after 58s
Switch the deploy job's Netbird Connect step to read management-url from
the NETBIRD_URL secret instead of a hardcoded value, so the URL can be
managed in the Gitea UI alongside the setup key.
2026-07-25 01:33:47 +01:00
ahmza aa01def998 Merge pull request 'Optimizations: switch to static-web-server + Caddy reverse proxy docs' (#1) from optimizations into main
Build Hugo / build (push) Successful in 1m51s
Build Hugo / deploy (push) Failing after 57s
Reviewed-on: #1
2026-07-25 00:19:53 +00:00
ahmza f23fe01f3c Fix GitLab/Gitea inconsistency in README
Build Hugo / deploy (push) Canceled after 0s
Build Hugo / build (push) Canceled after 48s
Update the README to reflect the actual infrastructure: CI/CD runs on
Gitea Actions (not GitLab), images live in the Gitea container registry,
and the clone URL points to tea.ahmza.com. Also clarify the Blowfish
theme is a Hugo module, so no submodules are needed.
2026-07-25 01:19:29 +01:00
ahmza 1ad79d40d8 Use caddy-cf image with Cloudflare DNS-01 challenge
Switch the Caddy doc to the custom caddy-cf image
(ghcr.io/ahmzaa/caddy-cf) which bundles the Cloudflare DNS module, so
TLS certs are issued via the ACME DNS-01 challenge instead of HTTP-01.
This removes the need to expose ports 80/443 to the public internet,
which suits the Netbird-only infrastructure.

- Add (cloudflare) snippet + import cloudflare to both Caddyfiles
- Read CF_API_TOKEN from env (.env for Docker, Secret for K8s)
- Document token scope (Zone:DNS:Edit) and Cloudflare-hosted DNS
  requirement
- Validated Caddyfile against the caddy-cf image (caddy adapt)
2026-07-25 01:17:35 +01:00
ahmza 7d4f1668a6 Add Caddy reverse proxy documentation
Document running Caddy in front of the SWS origin for both deployment
targets (Docker Compose on Gimli and the Kubernetes Pi cluster), with
Caddy handling automatic HTTPS, compression, and security headers.

Explains the layer separation: SWS serves static files + Cache-Control,
Caddy owns TLS/compression/security headers (matching sws.toml where
those are disabled). Caddyfile validated with 'caddy validate'.
2026-07-25 01:13:10 +01:00
ahmza 498231c160 Tune SWS config for running behind a reverse proxy
The site sits behind a reverse proxy that already handles compression
and security headers. Disable those in SWS to avoid redundant work and
duplicate/conflicting headers, letting each layer own one concern.

- compression / compression-static: off (proxy compresses)
- security-headers: off (proxy sets them)
- cache-control-headers, page-fallback, health: kept (origin concerns)
2026-07-25 01:09:42 +01:00
ahmza 898e3003a2 Switch static server from nginx to static-web-server
Replace nginx:alpine-slim with the scratch-based static-web-server
image, reducing the final image size ~32MB -> ~23MB (28% smaller).

- Dockerfile: serve from static-web-server:2, copy site to /public
- sws.toml: mirror previous nginx behavior (compression, cache-control,
  SPA index fallback) plus security headers and /health endpoint
- Remove now-unused nginx.conf
2026-07-25 01:04:17 +01:00
20 changed files with 403 additions and 85 deletions
+10 -3
View File
@@ -46,8 +46,11 @@ jobs:
tags: | tags: |
${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:latest ${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:latest
${{ env.REGISTRY }}/${{ gitea.repository_owner }}/blog-ahmza-com:${{ gitea.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
# 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: deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build needs: build
@@ -58,9 +61,13 @@ jobs:
with: with:
setup-key: ${{ secrets.NETBIRD_SETUP_KEY }} setup-key: ${{ secrets.NETBIRD_SETUP_KEY }}
hostname: 'gitea-actions-blog' hostname: 'gitea-actions-blog'
management-url: 'https://netbird.ahmza.com' management-url: ${{ secrets.NETBIRD_URL }}
- name: Deploy to RPIC-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.RPIC_HOST }} host: ${{ secrets.RPIC_HOST }}
+5 -4
View File
@@ -3,8 +3,9 @@ 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"]
+8 -7
View File
@@ -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
View File
@@ -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
+333
View File
@@ -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
View File
@@ -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.
+46
View File
@@ -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