Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94c561581a | ||
|
|
ce7a72728d | ||
|
|
c24ed01e0a | ||
|
|
7e1728a037 | ||
|
|
30ef38b143 | ||
|
|
b7145e1323 | ||
|
|
87867b9f30 | ||
|
|
8a7833e8f5 | ||
|
|
4426668518 | ||
|
|
eacf71d676 | ||
|
|
b05814980f | ||
|
|
eb67d6361e | ||
|
|
8062b9d208 | ||
|
|
aa01def998 | ||
|
|
f23fe01f3c |
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@
|
||||
This repository contains the source code for my personal blog hosted at [blog.ahmza.com](https://blog.ahmza.com).
|
||||
|
||||
## Features
|
||||
Using GitLab provides several advantages:
|
||||
- **Highly Customizable CI/CD Pipelines**: Automated processes for building and deploying the blog.
|
||||
- **GitLab Container Image Repository**: Efficient management and storage of container images.
|
||||
- **Custom GitLab Runners**: Flexibility in managing the CI/CD environment.
|
||||
Using Gitea provides several advantages:
|
||||
- **Highly Customizable CI/CD Pipelines**: Automated processes for building and deploying the blog via Gitea Actions.
|
||||
- **Gitea Container Image Repository**: Efficient management and storage of container images.
|
||||
- **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
|
||||
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
|
||||
- **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.
|
||||
|
||||
## To-Do List
|
||||
@@ -47,7 +47,8 @@ sacrificing performance.
|
||||
## Notes
|
||||
|
||||
### 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
|
||||
git clone git@gitlab.com:ahmza/blog.git
|
||||
git clone ssh://gitea@tea.ahmza.com:2222/ahmza/blog-ahmza-com.git
|
||||
```
|
||||
|
||||
+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
|
||||
|
||||
@@ -1,15 +1,44 @@
|
||||
# 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).
|
||||
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. 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.
|
||||
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?
|
||||
|
||||
@@ -21,7 +50,7 @@ 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 |
|
||||
| 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` |
|
||||
@@ -50,11 +79,72 @@ Whatever the deployment, the SWS origin exposes:
|
||||
- **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 +
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user