---
slug: 用 Caddy 做反向代理 + 用 Authentik 管理帳號（以 Gitea 為例）
title: 用 Caddy 做反向代理 + 用 Authentik 管理帳號（以 Gitea 為例）
description:
toc: true
authors:
    - awin
tags:
    - linux
    - selfhosted
categories:
    - System
series:
    - Linux
date: 2026-02-01T00:00:00
lastmod: 2026-02-01T00:00:00
featuredVideo:
featuredImage:
draft: false
enableComment: true
---

本篇會帶你用 **Caddy** 當反向代理（Reverse Proxy）與自動 TLS，並用 **Authentik** 當身分提供者（IdP / SSO）來管理帳號；最後以 **Gitea** 當範例，完成「反向代理 + 單一登入（OIDC）」整合。

這張可愛的圖是 Gemini 生成的。

<!--more-->

本文參考文件：

- Caddy 文件：[https://caddyserver.com/docs/](https://caddyserver.com/docs/)
- Authentik 文件：[https://docs.goauthentik.io/](https://docs.goauthentik.io/)
- Authentik 整合文件：[https://integrations.goauthentik.io/](https://integrations.goauthentik.io/)
- Authentik × Gitea（以官方整合為主）：[https://integrations.goauthentik.io/development/gitea/](https://integrations.goauthentik.io/development/gitea/)

範例網域：

- Authentik：`auth.www.myapp.example`
- Gitea：`git.www.myapp.example`

（以上都以 `www.myapp.example` 為「示意域名」。實作時請換成你自己的可解析網域，並確保 DNS A/AAAA 指向你的主機 IP。）

> 本文所有服務都以 Docker 方式部署，並以「分開三份 docker-compose」為主。為了降低複雜度，本文不使用共用 Docker network；Caddy 會透過 `host.docker.internal` 反向代理到宿主機上已 publish 的服務埠。

---

## 1) Caddy 的優點與功能

Caddy 是現代化的 Web Server / 反向代理，特別適合「自架服務入口」：

- **自動 HTTPS**：內建 ACME（例如 Let’s Encrypt），只要網域與 80/443 可用，Caddy 會自動申請與續期憑證。
- **設定檔簡潔**：Caddyfile 可讀性高，反向代理、gzip/zstd、header、redirect 等都很直覺。
- **反向代理體驗好**：`reverse_proxy` 直接把請求轉送到後端服務（HTTP/HTTPS/Unix socket 都可）。
- **可擴充**：模組化（插件）設計，常見需求如 forward auth、rate limit、WAF 等都有社群方案（是否採用依你需求而定）。
- **適合容器化**：用 Docker 跑 Caddy 非常普遍，資料（憑證、設定）用 volume 掛載即可。

在這篇架構裡，Caddy 的角色很單純：

- 對外只有一個入口（80/443）
- 負責 TLS、HTTP→HTTPS、與把流量分流到 `Authentik` 和 `Gitea`

---

## 2) Authentik 的優點與功能

Authentik 是一個自架的身分與存取管理（IAM）/ 單一登入（SSO）平台，你可以把它當成「你自己的 Google/Microsoft/Okta Login」。

常見價值：

- **統一帳號來源**：帳號、群組、權限集中管理，不用每個服務都自己養帳。
- **多種整合方式**：OIDC（OpenID Connect）、OAuth2、SAML、LDAP、Proxy/ForwardAuth 等。
- **安全能力**：MFA、條件式存取、登入流程（flow）可控。
- **自架友善**：Docker 部署成熟，社群整合文件多。

在這篇裡 Authentik 的角色是：

- 當 **OIDC Provider**（發行 token / 提供 userinfo）
- Gitea 透過 OIDC 把「登入」交給 Authentik

---

## 3) 以 Gitea 為例：Caddy、Authentik、Gitea 的角色與登入流程

### 三方關係（誰做什麼）

- **Caddy（反向代理 / 門口）**

  - 接收瀏覽器對 `auth.www.myapp.example` 與 `git.www.myapp.example` 的連線
  - 依網域把請求轉送到對應容器
- **Authentik（身分提供者 IdP / SSO）**

  - 管理使用者
  - 提供 OIDC discovery / authorization / token / userinfo
- **Gitea（應用服務 / Relying Party）**

  - 提供 Git service 與 Web UI
  - 使用者點「用 Authentik 登入」→ 把驗證交給 Authentik

### 登入流程（OIDC 概念流程）

以使用者從 `git.www.myapp.example` 進入為例：

1. 使用者進入 Gitea，選擇「用 Authentik 登入」（OIDC/OAuth2）。
2. Gitea 將瀏覽器導向 Authentik 的授權端點（authorize），並帶上 `client_id`、`redirect_uri`、`scope`、`state` 等參數。
3. 使用者在 Authentik 完成登入（可能含 MFA）。
4. Authentik 將瀏覽器導回 Gitea 的 `redirect_uri`（callback），並附上 code。
5. Gitea 以 server-to-server 方式向 Authentik 的 token endpoint 換取 token。
6. Gitea 用 token 取得使用者資訊（或驗證 ID Token），建立/綁定本地帳號並完成登入。

重點：

- **Caddy 不負責「帳號驗證」**，它只是把流量導到 Authentik/Gitea。
- **Authentik 负责身份**，Gitea 負責「使用 Authentik 身份」來登入。

---

## 4) 實作：用 Docker 部署三套服務並完成整合

### 4.1 先準備：DNS 與防火牆

- DNS：
  - `auth.www.myapp.example` → 你的主機 IP
  - `git.www.myapp.example` → 你的主機 IP
- 對外開放：
  - TCP 80、443（給 Caddy 申請/使用 TLS）

- 建議不要「對外網」放行（但會在主機上 publish 給 Caddy 用）：
	- Authentik 的 9000（本文用它讓 Caddy 反向代理）
	- Authentik 的 9443（通常不需要；除非你自己另外 publish 來除錯）
	- Gitea 的 8020（本文用它讓 Caddy 反向代理）
	- 2244/22（SSH）是否對外開放依你需求（可先不開）

> 如果你用「Caddy 反向代理」模式，後端服務的 port 原則上不應對外網可達，避免繞過入口直接連到後端。

> 本文為了不使用共用 network，後端服務會用 `ports:` publish 到主機；因此「不要對外放行」指的是：不要在路由器/NAT 或雲端安全群組把這些埠開給 Internet。

---

## 5) Caddy：docker-compose 與 Caddyfile

### 5.1 Caddy 的 docker-compose.yml（範例）

檔案位置示意：`docker/caddy/docker-compose.yml`

```yaml
services:
	caddy:
		image: caddy:2.10.2
		container_name: caddy
		restart: unless-stopped
		ports:
			- "80:80"
			- "443:443"
		extra_hosts:
			- "host.docker.internal:host-gateway"
		volumes:
			- ./config/Caddyfile:/etc/caddy/Caddyfile:ro
			- ./data/data:/data
			- ./data/config:/config
```

說明：

- `host.docker.internal` 在 Docker Desktop（Windows/macOS）通常可用。
- 在 Linux/Ubuntu 上常見需要加 `extra_hosts: host.docker.internal:host-gateway`，才能讓容器用 `host.docker.internal` 連到宿主機。

### 5.2 Caddyfile（範例）

檔案位置示意：`docker/caddy/config/Caddyfile`

```caddyfile
{
	# 可選：管理者信箱，用於 ACME
	email admin@www.myapp.example
}

# Authentik（對外入口）
auth.www.myapp.example {
	encode zstd gzip
	# 反向代理到宿主機上 publish 的 Authentik 9000（HTTP）
	reverse_proxy host.docker.internal:9000
}

# Gitea（對外入口）
git.www.myapp.example {
	encode zstd gzip
	# 反向代理到宿主機上 publish 的 Gitea 8020
	reverse_proxy host.docker.internal:8020
}
```

說明：

- 這份教學為了降低複雜度，讓 **TLS 只由 Caddy 對外負責**。
- 所以 Caddy 反向代理到 Authentik 的 `9000`（HTTP）即可，避免 `9443` 牽涉到後端 TLS 憑證驗證問題。

> 如果你堅持要反向代理到 Authentik 的 `9443`（HTTPS），才需要在 Caddy 端處理後端 TLS（例如自簽憑證可能需要 `tls_insecure_skip_verify`）。但這會讓設定更複雜，也比較不建議。

---

## 6) Authentik：docker-compose 與 .env

### 6.1 Authentik 的 .env（範例，請自行填值）

檔案位置示意：`docker/authentik/.env`

```dotenv
# Database
PG_PASS=請填入強密碼
PG_USER=authentik
PG_DB=authentik

# Authentik
AUTHENTIK_SECRET_KEY=請填入長度足夠的隨機字串

# 版本（可固定，避免 latest 帶來不可預期變更）
AUTHENTIK_TAG=2025.12.2
```

安全提醒：

- `.env` 不要上傳到公開 repo。
- `PG_PASS`、`AUTHENTIK_SECRET_KEY` 請使用密碼管理器產生。

### 6.2 Authentik 的 docker-compose.yml（範例）

檔案位置示意：`docker/authentik/docker-compose.yml`

```yaml
services:
	postgresql:
		image: postgres:16-alpine
		restart: unless-stopped
		healthcheck:
			test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
			start_period: 20s
			interval: 30s
			retries: 5
			timeout: 5s
		volumes:
			- ./data/postgresql_data:/var/lib/postgresql/data
		environment:
			POSTGRES_PASSWORD: ${PG_PASS:?database password required}
			POSTGRES_USER: ${PG_USER:-authentik}
			POSTGRES_DB: ${PG_DB:-authentik}
		env_file:
			- .env

	redis:
		image: redis:alpine
		command: --save 60 1 --loglevel warning
		restart: unless-stopped
		healthcheck:
			test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
			start_period: 20s
			interval: 30s
			retries: 5
			timeout: 3s
		volumes:
			- redis:/data

	server:
		image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2025.12.2}
		container_name: authentik-server
		restart: unless-stopped
		command: server
		environment:
			AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
			AUTHENTIK_REDIS__HOST: redis
			AUTHENTIK_POSTGRESQL__HOST: postgresql
			AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
			AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
			AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
		volumes:
			- ./data/authentik/media:/media
			- ./data/authentik/custom-templates:/templates
		env_file:
			- .env
		# 透過 ports publish 到宿主機，讓 Caddy 用 host.docker.internal 反向代理
		ports:
			- "9000:9000"
		depends_on:
			postgresql:
				condition: service_healthy
			redis:
				condition: service_healthy

	worker:
		image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2025.12.2}
		container_name: authentik-worker
		restart: unless-stopped
		command: worker
		environment:
			AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
			AUTHENTIK_REDIS__HOST: redis
			AUTHENTIK_POSTGRESQL__HOST: postgresql
			AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
			AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
			AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
		user: root
		volumes:
			- /var/run/docker.sock:/var/run/docker.sock
			- ./data/authentik/media:/media
			- ./data/authentik/certs:/certs
			- ./data/authentik/custom-templates:/templates
		env_file:
			- .env
		depends_on:
			postgresql:
				condition: service_healthy
			redis:
				condition: service_healthy

volumes:
	redis:
		driver: local
```

小提醒：
- 這個「不共用 network」的做法，後端服務需要透過 `ports:` publish 到宿主機，才能讓 Caddy 容器透過 `host.docker.internal` 連到它。
- 安全性上要靠「不要在路由器/NAT/雲端安全群組把 9000/8020 對外放行」，只放行 80/443 給 Caddy。

---

## 7) Gitea：docker-compose（範例）

檔案位置示意：`docker/gitea/docker-compose.yml`

```yaml
services:
	gitea:
		image: gitea/gitea:latest
		container_name: gitea
		restart: unless-stopped
		environment:
			- USER_UID=1000
			- USER_GID=1000
		volumes:
			- ./data:/data
			- /etc/timezone:/etc/timezone:ro
			- /etc/localtime:/etc/localtime:ro
		ports:
			- "8020:3000"
			- "2244:22"  # SSH 是否對外開放依你需求；如果你要用 git@... 的方式推送才需要
```

反向代理補充（很重要，避免 OIDC callback 出現網址不一致）：

- 請確認 Gitea 的「對外 Base URL」是 `https://git.www.myapp.example/`。
- 若 Base URL 設錯，常見症狀是：Gitea 顯示的 callback URL 會是 `http://...` 或 host/port 不對，導致 Authentik 報 `redirect_uri mismatch`。
- 設定位置依你的 Gitea 部署方式而不同（例如 `app.ini` 的 `ROOT_URL`，或在管理介面可調），原則是「以使用者實際從瀏覽器進入的網址」為準。

如果你要讓 `git.www.myapp.example` 對外提供 SSH（22）而不是用 2244，也可以改成 `"22:22"`，但請確認主機上沒有其他 SSH 服務衝突（例如 OpenSSH）。

---

## 8) 啟動順序與驗證

分開三份 compose 時，建議啟動順序：

1) Authentik（含 DB/Redis）
2) Gitea
3) Caddy（最後再把入口打開）

範例指令（在各自資料夾）：

```bash
docker compose up -d
```

驗證：

- 先確認 Authentik 後台能打開：`https://auth.www.myapp.example/`
- 再確認 Gitea 能打開：`https://git.www.myapp.example/`

---

## 9) Authentik × Gitea 整合（以官方文件流程為準）

本段建議你同時開著官方整合頁：[https://integrations.goauthentik.io/development/gitea/](https://integrations.goauthentik.io/development/gitea/)

整體思路：

1) 在 Authentik 建立一個「對 Gitea 用的 OIDC Provider」
2) 建立 Application，把 provider 掛上去
3) 把 Discovery URL / Client ID / Client Secret 填進 Gitea 的 Authentication Source

### 9.1 在 Authentik 建立 Provider + Application

在 Authentik 管理介面：

1. 建立 Provider：

   - Providers → Create → **OAuth2/OpenID Provider**
   - 重要欄位（名稱以示意為主）：
     - Name：`gitea-oidc`
     - Client type：通常選 Confidential（Gitea 是 server-side）
     - Redirect URIs：填入 Gitea 的 callback URL（下一小節會說怎麼確定）
     - Scopes：至少包含 `openid`, `email`, `profile`
2. 建立 Application：

   - Applications → Create
   - Name：`Gitea`
   - Slug：`gitea`
   - Provider：選剛剛建立的 `gitea-oidc`

建立完成後，你會得到：

- Client ID
- Client Secret
  -（通常也會有）OpenID Configuration / Discovery URL

建議使用「Discovery URL」方式整合，因為端點與簽章金鑰都能自動跟上。

Discovery URL（常見樣式，請以你的 Authentik 介面顯示為準）：

- `https://auth.www.myapp.example/application/o/gitea/.well-known/openid-configuration`

### 9.2 在 Gitea 新增 Authentication Source（OIDC）

到 Gitea（需管理者權限）：

1. Site Administration → Authentication Sources → Add Authentication Source
2. Type 選 OAuth2（或 OpenID Connect，依 Gitea 版本 UI）
3. Provider 選 OpenID Connect
4. 填入：
   - Auto Discovery URL：`https://auth.www.myapp.example/application/o/gitea/.well-known/openid-configuration`
   - Client ID：從 Authentik 複製
   - Client Secret：從 Authentik 複製
   - Scopes：`openid email profile`

建立後，Gitea 通常會顯示 callback URL 類似：

- `https://git.www.myapp.example/user/oauth2/<SOURCE_NAME>/callback`

把這個 callback URL 回填到 Authentik provider 的 Redirect URIs（兩邊要一致）。

### 9.3 測試登入

回到 Gitea 登入頁：

- 看到「用 Authentik 登入」的按鈕/選項
- 點下去會跳到 `auth.www.myapp.example` 完成登入
- 成功後回到 Gitea，首次登入可能會建立/連結本地使用者

---

## 10) 常見踩雷整理（避免再踩一次）

你提供的 Perplexity 連結在這個環境下會遇到 403（無法直接讀取頁面內容），所以我沒辦法逐字引用你當時的對話；但我可以把「你目前設定中已經出現/很常遇到」的坑整理成一章，讓你下次少繞路。

### 10.1 `host.docker.internal` 在 Linux/Ubuntu 不一定可用

- 在 Docker Desktop（Windows/macOS）通常可以用 `host.docker.internal`。
- 在 Linux 常見情況是 **預設沒有** 這個 DNS 名稱。

可行解法（本文採用）：在 Caddy container 加上：

- `extra_hosts: ["host.docker.internal:host-gateway"]`

### 10.2 反向代理到 Authentik 的 9443 可能遇到 TLS 驗證問題

如果你用：

```caddyfile
reverse_proxy https://...:9443
```

而 Authentik 端使用自簽憑證或內部憑證，Caddy 會因為無法驗證而拒絕。

解法：

- 最簡單：反向代理到 `host.docker.internal:9000`（HTTP），把 TLS 全交給 Caddy 對外處理。
- 若必須反向代理到 `:9443`：才處理後端 TLS（可能需要 `tls_insecure_skip_verify`），但要理解它是「跳過驗證」。

### 10.3 後端服務不要再對外暴露 port（避免繞過 Caddy）

在本文「不共用 network」的簡化架構裡，Authentik/Gitea 必須 publish port 到宿主機，Caddy 才連得到。

建議的安全作法是：

- **路由器/NAT**：只轉發 80/443 到這台機器
- **雲端安全群組（Security Group）**：只開 80/443 inbound
- 讓 9000/8020 只留在「主機本地用途」（給 Caddy 反向代理），不要對外網暴露

### 10.4 OIDC 回呼網址（Redirect URI）最容易填錯

症狀通常是：

- 登入後跳轉回來直接錯誤（redirect_uri mismatch / invalid redirect）

原則：

- 以 **Gitea 顯示的 callback URL** 為準
- 100% 原樣貼到 Authentik Provider 的 Redirect URIs
- domain、path、https 都要一致

### 10.5 反向代理後網址不對（http/host/port 亂跳）

如果你發現登入流程中 URL 變成 `http://`、或 host/port 被改掉，通常是「應用程式不知道自己在反向代理後面」造成的。

- Gitea：先回頭檢查 Base URL/`ROOT_URL` 是否為 `https://git.www.myapp.example/`。
- Authentik：確保你是透過 `https://auth.www.myapp.example/` 存取管理介面，並檢查是否有「反向代理/受信任代理（trusted proxy）」或「外部 URL（external host/url）」相關設定需要調整。

> 這一段在不同 Authentik 版本/部署方式名稱會不一樣，遇到時建議直接依官方文件用關鍵字搜尋：`reverse proxy`、`trusted proxy`、`X-Forwarded-Proto`。

---

## 11) 小結

Authentik 解決了自己架站一個站就要建一次帳號的問題。

1.  **Caddy**：負責自動化 TLS 與反向代理，解決了惱人的憑證問題，並統一了對外入口（80/443）。
2.  **Authentik**：作為核心的身分驗證中心，未來若要新增其他服務（如 Portainer、Grafana、Nextcloud 等），只要它們支援 OIDC/OAuth2 或 ForwardAuth，都能輕鬆接入，實現真正的單一登入（SSO）。
3.  **Gitea**：成功展示了從「各服務獨立帳號」轉向「集中式帳號管理」的整合流程。

雖然初期在理解 OIDC 流程與反向代理設定上需要花點時間，但建立起這套基礎設施後，未來的維護與擴充將會變得非常輕鬆且安全。希望這篇文章能幫助你順利搭建出自己的 Authentik 單一登入環境！

希望這篇文章有幫到你，謝謝你的閱讀。
