指令
📌 下方 TLS 範例使用目前工作目錄的 ./certs/;請放入自己的憑證、配對金鑰或用戶端 CA 檔案。驗證也會讀取這些檔案。
每個條目都以固定的表頭開始:語法、沒寫這個指令時的預設值,以及它可以出現的位置。接著說明指令做什麼、拒絕什麼,以及與 Caddy 的不同之處。
📌 本頁描述的是 v0.2.2。與 0.1.x 系列及 0.2.0 候選版不同的行為,會以 0.2.0 變更 標示;升級 把這些變更集中在同一處。
📖 本頁只涵蓋語言的一部分。沒有列在這裡的指令仍會由 pingclair validate 檢查,而伺服器沒有實作的指令會被指名拒絕,而不是接受後忽略。
目前有六個 Caddy 標準指令會被指名拒絕,其中 map 是實際設定最常缺的一個:它把值算一次然後重複使用,而這裡沒有機械式的改寫方式。同樣被拒絕的還有 tracing(OpenTelemetry span)、push(HTTP/2 伺服器推送)、invoke(呼叫具名路由),以及兩個存取日誌指令 log_append(在紀錄中加一個欄位)與 log_name(逐請求選擇日誌記錄器)。每個拒絕都會指名該指令,因此搬遷時遇到的是清楚的句子,而不是靜默失效。
basic_auth
Section titled “basic_auth”Syntax: basic_auth [<matcher>] [bcrypt|argon2id [<realm>]] { <username> <hashed_password> ... }Default: no authenticationContext: site block, handle, route在請求繼續往下之前,要求 HTTP Basic 認證。區塊中的每一行是一個帳號:使用者名稱與密碼雜湊,絕不是密碼本身。雜湊由 pingclair hash-password 產生(命令列)。
指令那一行上的演算法,就是區塊中每個雜湊要比對的演算法,預設為 bcrypt。其他演算法名稱都會被拒絕,沒有區塊的 basic_auth 也一樣。雜湊不是所宣告演算法的有效雜湊時(包括明文密碼),該行會在載入時被拒絕。
帶有匹配器的 basic_auth 會保護所有回應它所匹配請求的路由,包括指令順序排在它前面的 respond 或 reverse_proxy。redir 仍會在它之前回應,與 Caddy 相同。
http://:8080 { basic_auth /admin/* { alice $2b$04$aKz8E/FgvYZuyOZpoHXKJuenUlormXHm8m7WJff0S8hMu7ehuMY7i } respond "ok"}Syntax: bind <host>Default: every interface ([::]), or the global default_bindContext: site block把網站的每個監聽器限制在一個主機位址上。這個主機會取代網站所監聽的每個位址的主機部分,TCP、TLS 與 HTTP/3 皆然,pingclair adapt 會顯示結果位址。IPv6 主機可以加或不加方括號,在監聽器中一律加上方括號:bind ::1 會監聽 [::1]:443。
HTTPS 網站的自動 HTTP 重新導向監聽器也會監聽 bind 主機,因此從其他介面無法到達。
當每個網站綁定不同介面時,兩個網站區塊可以共用同一個連接埠。綁定的介面是網站身分的一部分,因此 http://:8080 { bind 127.0.0.1 } 與 http://:8080 { bind [::1] } 是兩個網站,各自只能從自己的介面到達。
拒絕條件:
- 超過一個位址會被拒絕:
`bind 127.0.0.1 ::1` names 2 addresses, and this build binds one。請只寫一個位址、用[::]代表所有介面,或每個介面寫一個網站。 - 受
bind限制的網站,若與監聽所有介面的網站共用連接埠,會被拒絕:一個連接埠就是一個 socket,受限的網站會變得可以從bind排除的介面到達。請讓該連接埠上的每個網站綁定相同位址,或把其中一個移到別的連接埠。
與 Caddy 的差異:Caddy 會綁定列出的每個位址。Pingclair 讓每個監聽器只在一個介面上,並拒絕第二個位址,而不是忽略它。
0.2.0 變更:bind 也適用於有明確位址或連接埠的網站,例如 http://example.test:8080。先前它只適用於沒有自身位址的網站,這類網站會監聽所有介面。
http://example.test:8080 { bind 127.0.0.1 respond "loopback only"}Syntax: reverse_proxy <upstream> { cache { ttl <duration> max_size <bytes> } }Default: disabled; max_size 134217728 when enabledContext: reverse_proxy block此 Pingclair 選項啟用 H1/H2 代理回應快取。ttl 必填,提供後備有效期限;max_size 是所有快取路由共用的正整數位元組容量。容量不一致、零或未知選項會被拒絕。重載調整儲存容量,縮小時立即逐出項目,移除快取時清空。
新鮮度包含上游 Age、依 Date 推算的年齡及回應延遲;Expires 相對於 Date 計算。所有 Vary 行與指定的請求欄位皆參與變體;無效 Vary 或 Vary: * 不會儲存。請求的 no-cache 與 no-store 在所有欄位行依指令名稱辨識。SSE 與 flush_interval -1 不納入快取。
http://:8080 { reverse_proxy 127.0.0.1:3000 { cache { ttl 30s max_size 134217728 } }}沒有可用的來源有效期限時,ttl 僅供 200 使用;未指定期限的 404 與 410 最長快取十秒或較短的 ttl,未指定期限的伺服器錯誤不儲存。206、428、429、431 與 511 永不儲存。快取保留來源位元組,輸出時依各用戶端壓縮,且命中與未命中都套用 header_down。HTTP/3 不使用此回應快取。
encode
Section titled “encode”Syntax: encode [*] [<format> ...] encode [*] { gzip [<level>] zstd minimum_length <bytes> match { status <codes...> header <field> [<value>] } } encode offDefault: no compressionContext: site block壓縮回應。格式依偏好順序列出:用戶端以相同權重接受多種格式時,列在最前面的勝出。支援的格式是 zstd 與 gzip,單獨寫 encode 代表 gzip。encode off 會關閉該網站的壓縮。
gzip <level>設定 gzip 等級,1 到 9,預設為 5。zstd使用等級 3。minimum_length是會被壓縮的最小本文;預設為 512 位元組。match只壓縮符合這些狀態碼或標頭的回應。明確寫出的match區塊會取代預設的內容類型清單。
壓縮對回應的影響:
- 有
encode的網站,其回應都帶有Vary: Accept-Encoding,未壓縮送出的也一樣。壓縮會把Accept-Encoding加進既有的Vary欄位,而不是取代它。 - 帶有強
ETag的代理回應被重新編碼時,會改成弱ETag,因為編碼後的位元組與來源不同。 - 請求或回應上的
Cache-Control: no-transform會停用該回應的壓縮。 Accept-Encoding: *不會選出任何編碼;除非用戶端寫出gzip或zstd,否則收到的是未壓縮的本文。- 大於 8 MiB 的靜態檔案會以未壓縮方式送出。要以壓縮形式提供大檔案,請使用預先壓縮檔(
file_server { precompressed })。
靜態與代理編碼使用相同的預設內容類型清單,identity 也參與品質協商。部分或沒有本文的代理回應不壓縮;轉換後移除過期 digest 欄位與 H3 完整性 trailers。壓縮失敗會中止回應,不會接上明文。
拒絕條件:
encode br會在載入時被拒絕。代理未實作串流式 Brotli 編碼器,也不會自動改用 gzip:`encode br`: Brotli is not implemented for proxied responses; use `encode zstd gzip`。- 未知的格式,或區塊內未知的設定,會被拒絕。
- 帶有區塊的
encode off會被拒絕。 - 路徑匹配器或具名匹配器會被拒絕,因為壓縮是以網站為單位設定,而不是以路由為單位。匹配所有請求的
*匹配器則可以接受。
0.2.0 變更:網站只在 encode 要求的地方壓縮,與 Caddy 相同。依賴先前 gzip 預設值的網站必須加上 encode gzip 或 encode zstd gzip。區塊中的設定現在會生效;先前的版本會把每個區塊都編譯成單純的 gzip。
example.com { encode { zstd gzip 6 minimum_length 1024 } file_server}file_server
Section titled “file_server”Syntax: file_server [<matcher>] [<root>] [browse] file_server [<matcher>] [<root>] { root <path> index <filenames...> browse { file_limit <n> } compress [off|false] precompressed [br|zstd|gzip ...] hide <paths...> status <code> pass_thru disable_canonical_uris etag_file_extensions <extensions...> }Default: disabledContext: site block, handle, route從磁碟提供檔案。它會判斷 MIME 類型、回應位元組範圍請求與條件式請求,並送出 ETag 與 Last-Modified。檔案來自 root 設定的網站根目錄,或只給這個指令用的根目錄。
ETag 採用 Caddy 的寫法:"<以 36 進位表示的修改時間>-<以 36 進位表示的大小>",取自檔案的奈秒修改時間與大小,因此在兩台伺服器之間搬移網站時,用戶端與 CDN 已保存的驗證器仍然有效。每種表示各自帶有標籤——gzip 預先壓縮檔以 -sidecar-gzip 結尾、即時壓縮的內容以 -gzip-<level> 結尾——因為它們的位元組不同。若檔案旁有 .etag 側檔(見檔案伺服器選項中的 etag_file_extensions),則以該值為驗證器。
index指定目錄要嘗試的檔案;預設為index.html。索引必須是相對檔名。browse為沒有索引檔的目錄產生列表。file_limit限制顯示的項目數;預設上限是 10,000。compress off讓這個檔案伺服器在原本會壓縮的網站上免於壓縮。precompressed在用戶端接受該編碼時,提供像app.js.gz這樣的預先壓縮檔。沒有參數時,順序是br zstd gzip。只有寫了這個選項才會提供預先壓縮檔。用戶端接受該編碼時,範圍請求會從預先壓縮檔的位元組提供,Content-Range與ETag描述的是壓縮後的表示。hide讓指定的路徑不被提供,也不出現在列表中。不含/的模式會隱藏任何同名的路徑元件(.git會隱藏/a/.git/b);含/的模式是根目錄下的路徑。重複的行會累加。status讓每個檔案都以這個狀態碼回應,適合維護頁面。pass_thru把不存在的檔案交給下一個處理器,而不是回應404。disable_canonical_uris停用為目錄補上結尾斜線的重新導向。
條件式請求依照 RFC 9110:相符的 If-None-Match 或仍然有效的 If-Modified-Since 得到 304,不成立的 If-Match 或 If-Unmodified-Since 得到 412,而 If-Range 已不相符的 Range 會以 200 得到整個檔案。GET 與 HEAD 以外的方法會得到附帶 Allow: GET, HEAD 的 405。不論網站是否壓縮,靜態回應都帶有 Vary: Accept-Encoding。
拒絕條件:fs 會被拒絕,因為只支援本機檔案系統。超出 100–599 的 status、未知的子指令、絕對路徑或含 .. 的索引、列表模板、reveal_symlinks、sort,以及 [ 集合沒有關閉的 hide 模式,都會被拒絕。
與 Caddy 的差異:位置參數 <root> 是 Pingclair 自己加的。在 Caddy 中,file_server 後面單獨的路徑是路徑匹配器。如果設定也必須能在 Caddy 中載入,請優先使用 root。
0.2.0 變更:ETag 改由奈秒精度的修改時間組成,並依內容編碼而不同,所以升級後每個靜態 ETag 會改變一次,快取會對每個檔案重新驗證一次。
localhost:8080 { file_server ./public}預先壓縮 sidecar 使用自身的大小與修改時間產生 ETag,優先於即時壓縮快取;gzip 驗證值包含等級。範圍回應使用 identity 編碼並以有界區塊串流傳送。靜態本文快取共用位元組容量與 16,384 項上限,包含空檔案。標準重新導向清理路徑、跳脫反斜線並保留查詢字串,避免變成其他主機的參照。設定的 ETag 標頭就是重新驗證時比對的驗證器。
forward_auth
Section titled “forward_auth”Syntax: forward_auth <upstream> { uri <path> copy_headers <fields...> transport http { tls tls_server_name <name> tls_trusted_ca_certs <files...> tls_client_auth <cert> <key> tls_insecure_skip_verify } }Default: no authentication subrequestContext: site block, handle, route先以不帶本文的 GET 向驗證服務發出子請求,並附上原始方法與 URI。2xx 會複製指定的身分標頭再繼續,其他回應串流傳給用戶端。複製前先移除目的標頭,即使目的名稱被改名也一樣。transport http 只接受列出的 TLS 選項;未知選項、缺少配對金鑰,或同時設定自訂 CA 與略過驗證會被拒絕。只有明確需要時才停用憑證驗證。
http://:8080 { forward_auth https://auth.example.com { uri /check copy_headers Remote-User transport http { tls_server_name auth.example.com } } reverse_proxy 127.0.0.1:3000}handle
Section titled “handle”Syntax: handle [<matcher>] { <directives...> }Default: noneContext: site block, handle, route, handle_errors把多個指令組成一條路由。同層的 handle 區塊彼此互斥:只有第一個匹配的區塊會執行,即使該區塊沒有寫出回應。有路徑的區塊會依路由的方式排序(見哪條路由回應),沒有匹配器的 handle 則是後備。同一個區塊內的每個指令都會依指令順序執行。
匹配器記號只能是 *、以 / 開頭的路徑,或具名匹配器(@name)。其他任何記號都會被拒絕:expected at most one matcher (`*`, a path starting with `/`, or `@name`) before the block, got `*.php` 。
0.2.0 變更:先前無法辨識的記號會被丟棄,所以 handle *.php { … } 會回應網站上的每個請求。請寫成 @php path *.php 與 handle @php { … }。
example.com { @php path *.php handle @php { respond "PHP" 200 } handle { respond "Not PHP" 200 }}handle_errors
Section titled “handle_errors”Syntax: handle_errors [<status|Nxx> ...] { <directives...> }Default: the built-in error text, or the site's error_pageContext: site block當請求以錯誤狀態結束時執行一條路由。參數是三位數的確切狀態碼或 Nxx 範圍,彼此以「或」結合;沒有參數時,區塊會接下所有錯誤。區塊內的 {err.status_code} 與其他 {err.*} 預留位置描述這個錯誤。
錯誤在兩種情況下會進入這個區塊:處理器引發錯誤時(error,或檔案不存在的 file_server),以及伺服器自己產生錯誤時:無法連上上游的 reverse_proxy(502、503、504)、超過 request_body 上限的本文(413),以及停止傳送的本文(408)。在路由之前就被拒絕的請求——目前是超過上限的標頭區塊——不會進入這個區塊:它會以內建拒絕回應,並指出過大的欄位名稱;error_page <status> … 仍可提供該回應主體。
- 區塊內的
root設定錯誤路由自己的文件根目錄,可以寫在區塊的任何位置。以匹配器限定的root @name …會被拒絕。 - 區塊內的
file_server依錯誤路由的設定提供檔案。頁面會以錯誤的狀態碼送出,失敗請求的Range與驗證欄位會被忽略,所以錯誤絕不會變成206或304。 - 在錯誤路由內引發的錯誤會直接回應,不會再次執行錯誤路由。
- 區塊內的
reverse_proxy會在載入時被拒絕。這裡的上游交換是在處理鏈之外的生命週期步驟,該處理器會編譯成功卻什麼都不回應;請在網站路由中代理,並在這裡用respond或file_server呈現它的錯誤。區塊內的其他內容都是一般路由主體:指令依 Caddy 的順序執行、@name匹配器可用、rewrite使用區塊自己的已編譯樣式。
0.2.0 變更:閘道錯誤與本文大小錯誤會進入 handle_errors,錯誤路由中的 file_server 會提供它的頁面,而不是錯誤文字。因此,接下所有錯誤的 handle_errors { … } 也會回應 502、504 與 413;若只想處理原本設想的錯誤,請為它加上狀態碼。它對閘道錯誤的回應不帶 Proxy-Status 欄位,而內建的閘道錯誤會帶。
example.com { reverse_proxy 127.0.0.1:3000 handle_errors 502 503 504 { root * /srv/errors rewrite * /{err.status_code}.html file_server }}handle_path
Section titled “handle_path”Syntax: handle_path <path-matcher> { <directives...> }Default: noneContext: site block, handle, route作用與 handle 相同,另外會在區塊內的指令執行前移除匹配到的路徑前綴:handle_path /api/* 會把 /api/users 以 /users 轉送。前綴比對與選中該區塊的路由一樣忽略 ASCII 字母大小寫,所以 handle_path /API/* 也會從 /api/users 移除 /api。匹配器記號的規則與 handle 相同。
example.com { handle_path /api/* { reverse_proxy 127.0.0.1:3000 }}header
Section titled “header”Syntax: header [<matcher>] <field> [<value> [<replacement>]] header [<matcher>] { <field> <value> # set +<field> <value> # append -<field> # remove ?<field> <value> # set only if absent <field> <search> <replacement> # regular-expression replace match { status <codes...> header <field> [<value>] } defer }Default: noneContext: site block, handle, route修改回應標頭。直接寫欄位名稱會設定該標頭,加上 + 前綴會附加一個值,加上 - 前綴會移除該欄位。? 前綴只在回應還沒有該欄位時才設定值。有三個參數時,第二個是正規表示式,第三個會取代它匹配到的內容。
標頭永遠套用在完成的回應上,所以 defer 與 > 前綴會被接受,但不會改變任何事。header 區塊內的 match 區塊,會讓整個區塊依完成回應的狀態碼(404、2xx)或標頭決定是否套用。
拒絕條件:
- 同時有參數與區塊的指令會被拒絕。
- 沒有值的
header X-Name會被拒絕。Caddy 會設定一個空值,但空的回應標頭幾乎總是打錯的移除操作。 - 含有 CR、LF 或 NUL 的欄位值,或不是有效記號的欄位名稱,會在載入時被拒絕(RFC 9110 §5.5)。
handle_response { header { … } }內的match區塊會被拒絕。
⚠️ 沒有 set 關鍵字。區塊中的 set X-Name value 這一行,會被解讀成對一個名為 set 的標頭進行正規表示式取代。
與 Caddy 的差異:單寫欄位名稱會取代回應已有的值,而 Caddy 的 header X-Name value 在不加 defer 時會在其旁新增一行欄位。nginx 也是如此——add_header 只把欄位加進回應,不會動到上游的——且同樣沒有通用的取代指令:在那裡取代上游欄位要寫 proxy_hide_header 加 add_header。因此上游以 X-Name: from-upstream 回應時,兩者都會保留該行,這裡則不會。要同時保留兩行請寫 +X-Name;只在回應沒有該欄位時設定,請寫 ?X-Name。
Strict-Transport-Security 只會在加密的回應上送出,並依 RFC 6797 的要求從每個明文回應中移除。開啟 HSTS 的方法是寫 header Strict-Transport-Security "max-age=…"。
example.com { header { X-Frame-Options "DENY" X-Content-Type-Options "nosniff" Strict-Transport-Security "max-age=31536000; includeSubDomains" -X-Powered-By }}limits
Section titled “limits”Syntax: limits { header_timeout <duration> body_timeout <duration> idle_timeout <duration> request_timeout <duration> max_headers <count> max_header_bytes <bytes> max_connections <count> upload_bytes_per_sec <bytes> download_bytes_per_sec <bytes> long_connections { idle_timeout <duration|off> request_timeout <duration|off> } }Default: header_timeout 60s; a body may pause 60s between readsContext: site block設定網站連線與請求的資源上限。limits 是 Pingclair 自己的指令,Caddy 沒有對應的指令。
header_timeout限制整個請求標頭的時間:從連線被接受(或前一個 keepalive 請求結束)起,到標頭最後一個位元組為止。預設為 60 秒。因此,閒置的 HTTP/1 keepalive 連線在 60 秒內沒有請求就會被關閉,而標頭在這段時間後仍不完整的 HTTP/3 請求串流會以H3_REQUEST_INCOMPLETE重設。body_timeout是請求本文兩次讀取之間允許的最長停頓。沒有設定時,停頓上限是 60 秒。停止傳送的用戶端會得到408;經由reverse_proxy的 HTTP/2 上傳則改為重設串流。WebSocket 與立即刷新的路由只採用明確設定的值。long_connections為 WebSocket 與串流路由覆寫idle_timeout與request_timeout;off會移除期限。
0.2.0 變更:兩個 60 秒的預設值都是新的。刻意長時間不傳資料的用戶端,例如 gRPC 用戶端串流,需要明確設定 body_timeout,或在其路由上設定 flush_interval -1。
example.com { limits { header_timeout 30s body_timeout 2m } reverse_proxy 127.0.0.1:3000}listen
Section titled “listen”Syntax: listen [http://|https://]<address> [proxy_protocol]Default: the listeners named by the site addressContext: site block為網站加上一個監聽器。listen 是 Pingclair 自己的指令,讀法與 nginx 的同名指令相同:listen 127.0.0.1:8080 與 listen [::1]:8080 綁定該位址;listen :8080、listen 8080 與 listen *:8080 綁定所有介面;沒有連接埠的位址使用 HTTP 連接埠,加上 https:// 時則使用 HTTPS 連接埠。proxy_protocol 要求該監聽器收到 PROXY protocol 標頭。listen 指定了位址的網站不會繼承 default_bind。
拒絕條件:主機名稱(listen 只綁定、從不解析名稱)、沒有方括號的 IPv6 位址、不是 0 到 65535 的連接埠、未知的旗標,以及與網站 bind 不一致的位址。
0.2.0 變更:listen 會保留它寫出的位址。先前的版本只保留連接埠,所以 listen 127.0.0.1:8080 會監聽所有介面。要繼續監聽所有介面,請寫 listen :<port>。
http://:8080 { listen 127.0.0.1:9090 respond "two listeners"}Syntax: log [<name>] [{ <options> }]Default: no access logContext: site block; global options寫入存取日誌。網站層級的幾種寫法意義各不相同:
log為網站開啟預設的存取日誌,輸出到標準輸出。log { … }設定網站的存取日誌。log <name> { … }為網站加上一個具名的日誌記錄器,有自己的輸出。log <name>把網站的紀錄送到在全域選項中以log <name> { … }宣告的同名通道。
在全域選項區塊中,沒有名稱的 log { … } 改為設定伺服器自己的程序日誌:output file <path>、output stdout、output stderr、format json|text 與 level。檔案輸出端不存在時會以 0600 權限建立。RUST_LOG 仍然優先於設定的 level,啟動橫幅則留在標準輸出。
JSON 記錄的形狀是本伺服器自己的,不是 Caddy 的外層信封,因此針對 Caddy 部署寫的管線會讀到不同的鍵。兩者的對照如下:
| Caddy | 這裡 | 說明 |
|---|---|---|
ts |
ts |
值與形狀相同:請求開始時的 Unix 秒數,含小數 |
.request.uri |
.path |
同一個值,提升到最上層 |
.request.method |
.method |
提升到最上層 |
.request.host |
.host |
提升到最上層 |
.request.proto |
.protocol |
改名 |
.request.client_ip |
.client_ip |
提升到最上層;由 trusted-proxy 政策決定 |
.request.headers |
.request_headers |
提升到最上層;欄位名轉為小寫 |
.resp_headers |
.response_headers |
改名 |
.size |
.bytes |
同一個量:回應主體位元組數 |
.duration |
.duration_ms |
這裡是毫秒,單位寫在名稱裡,且低於 1 毫秒的請求保留小數 |
.status |
.status |
兩邊唯一原本就同名的鍵 |
| — | .ttfb_ms |
首字節時間(毫秒);Caddy 沒有對應欄位 |
.bytes_read |
— | 不記錄請求主體位元組數 |
.level、.logger、.msg |
— | Caddy 的日誌信封;這裡記錄本身就是整個物件 |
存取日誌是監聽器的屬性,與 Caddy 相同:任一網站的 log 會為該監聽器服務的每個請求開啟紀錄,而所有網站都沒寫 log 的監聽器一行都不寫。Host 未匹配任何網站、或在路由前就被拒絕的請求,會寫進程序日誌,也就是預設存取記錄器的位置。
區塊選項包括 output(stdout、stderr 或 file <path>)、format(json 或 console)、level、hostnames 選擇器、include 與 exclude 過濾器、sampling,以及檔案輪替(roll_size、roll_keep、roll_keep_for、mode、dir_mode 與其他 roll_* 選項)。每筆 JSON 存取紀錄都帶有 ts 欄位:請求開始時距 Unix 紀元的秒數。
拒絕條件:全域通道不能使用 hostnames,因為它不屬於任何網站。宣告兩次的通道會被拒絕。
紀錄在寫入前會先批次累積。跟不上的輸出端會丟棄紀錄,並在 pingclair_access_log_dropped_total 中計數。
example.com { log { output file /var/log/pingclair/access.log }}metrics
Section titled “metrics”Syntax: metrics [<matcher>] [{ disable_openmetrics }]Default: no metrics routeContext: site block, handle, route從網站路由提供 Prometheus 抓取端點,讓抓取程式不必存取 Admin API 就能讀取數據。這條路由和它所在的網站一樣開放;在公開網站上,請在它前面加上匹配器或 basic_auth。
只有在收集開啟時,這條路由才會提供數據,而收集由全域 metrics 選項控制(見全域選項)。收集關閉時,它會以空的本文回應 200。
輸出格式是 Prometheus 文字(text/plain; version=0.0.4; charset=utf-8),且不隨用戶端的 Accept 標頭改變:這個建置不協商 OpenMetrics,因此 disable_openmetrics 會被接受,並描述已經生效的行為,而不是關掉某個功能。要求 OpenMetrics 的抓取程式會收到 Prometheus 文字,這類抓取程式都能讀取。
{ metrics}
http://:9180 { bind 127.0.0.1 metrics /metrics}php_fastcgi
Section titled “php_fastcgi”Syntax: php_fastcgi [<matcher>] <upstream...> { root <path> split <suffix...> index <filename|off> try_files <candidates...> env <name> <value> resolve_root_symlink dial_timeout <duration> read_timeout <duration> write_timeout <duration> capture_stderr }Default: disabled; split .php; index index.phpContext: site block, handle, route將檔案匹配與重寫展開為 FastCGI 代理,上游可為 PHP-FPM。也接受已支援的反向代理選項。請求本文緩衝政策會到達 FastCGI 傳輸層;腳本路徑保留非 UTF-8 檔名字元的原始位元組,多行 Cookie 會合併。HEAD 不傳本文,下載速率限制會生效;參數超過 FastCGI record 容量回應 431,截斷或格式錯誤的本文會中止回應。
沒有宣告本文長度的請求——chunked 上傳,或沒有本文的 POST——會在這裡讀取並量測長度,上限為路由的 request_buffers;路由未設定時,則以本伺服器的緩衝上限為準。超過上限的無長度本文會得到 413。
http://:8080 { root * /srv/php php_fastcgi 127.0.0.1:9000 { read_timeout 30s write_timeout 30s } file_server}request_body
Section titled “request_body”Syntax: request_body [<matcher>] { max_size <size> read_timeout <duration> write_timeout <duration> set <body> }Default: no size limitContext: site block, handle, route限制或取代請求本文。
max_size以413拒絕更大的本文,不論長度是事先宣告的,還是串流中途超過上限。大小依 SI/IEC 區分:10MB是 10,000,000 位元組,10MiB是 10,485,760 位元組。read_timeout與write_timeout限制這條路由讀取本文與寫出回應的時間。停滯的上傳會得到408。set以展開預留位置後的文字取代本文。用戶端自己傳來的位元組會在抵達時丟棄,而不是暫存起來。
網站層級、沒有匹配器的 request_body 適用於網站中的每個請求,包括在 handle 區塊內回應的請求。handle 內的 request_body 會為該路由覆寫它。
0.2.0 變更:除非有設定,否則請求本文沒有大小上限。先前的版本預設會拒絕超過 1 MiB 的代理本文。
example.com { request_body { max_size 10MB } reverse_proxy 127.0.0.1:3000}reverse_proxy
Section titled “reverse_proxy”Syntax: reverse_proxy [<matcher>] <upstream> [<upstream> ...] reverse_proxy [<matcher>] [<upstream> ...] { ... }Default: noneContext: site block, handle, route把請求轉送到一個或多個上游。lb_policy 決定請求如何分配給它們;預設為 random,也就是 Caddy 的預設值。lb_policy first 會把每個請求都送到第一個可用的上游。
以主機名稱指定的上游,會依全域 dns_refresh 設定的間隔重新解析,所以換了位址重新啟動的後端不需要重新載入就能跟上。解析失敗時,先前的位址會繼續留在輪替中。
header_up 與 header_down 分別在前往上游的請求、以及回程的回應上編輯標頭。兩者與 header 指令一樣有四種寫法,意義相同:X-Name value 設定、+X-Name value 附加、-X-Name 移除、?X-Name value 僅在欄位不存在時設定,>X-Name find replacement 以正規表示式改寫既有值。值可以是模板,因此 header_up X-Real-IP {client_ip} 會轉送經 trusted-proxy 政策解析後的位址。
上游自己的 Server 欄位行會原樣到達用戶端:Caddy 在處理鏈之前先設定自己的 Server,接著代理把上游標頭複製上去而覆蓋它,本伺服器遵循同一規則,只對自己產生的回應加上 Server: Pingclair。Via 是附加而非取代,並在回應原本經過的鏈之後標示這個中間節點(1.1 Pingclair)。回應也會帶著伺服器為每個請求產生的 X-Request-Id。
主動健康檢查會在請求之外探測每個上游。失敗的上游會在使用者請求到達之前離開輪替,並在達到設定的成功探測次數後重新加入。backup 上游只有在所有主要上游都無法使用時才會被使用。權重為 0 的上游會被排空:它不會收到任何請求。
逾時設定寫在 transport http 區塊中:connect_timeout(Caddy 的 dial_timeout)、first_byte_timeout(Caddy 的 response_header_timeout)、read_timeout 與 write_timeout。lb_try_duration 限制請求抵達後多久之內還能開始新的嘗試;它不會切斷已在進行中的回應。
上游的 103 Early Hints 會在最終回應之前送達 HTTP/1.1 用戶端;HTTP/2 路徑會由代理程式庫(pingora-core 0.9.0)丟棄過渡回應,HTTP/3 路徑則依設計略過。
Pingclair 自己產生的 502 或 504 會帶有 Proxy-Status: pingclair; error=…,因此可以與後端送出的回應區分。一旦上游可能已經看到請求,自動重試只會重複冪等的方法。
拒絕條件:
- 未知的選項會以完整名稱被拒絕,例如
Unknown directive 'reverse_proxy: dial_timeout'。 - 大於 100 的權重會被拒絕,所有主要上游權重都是 0 的上游池也會被拒絕。
- 在這個建置中沒有對應實作的
transport http選項(read_buffer、write_buffer、max_conns_per_host、keepalive_interval,以及其他 Caddy 從 Go HTTP 用戶端沿用的選項)會被指名拒絕。
0.2.0 變更:
- 預設的
lb_policy是random;要保留先前的輪流分配,請寫lb_policy round_robin。 lb_try_duration不再切斷慢速回應或長時間的事件串流。請改用first_byte_timeout或read_timeout限制慢速後端。- 在
trusted_proxies後方,{remote_host}是連線的對端,{client_ip}才是用戶端。header_up X-Real-IP {client_ip}會轉送用戶端位址。
:80 :8080 { reverse_proxy { lb_policy least_conn to 10.0.0.1:8080 { weight 3 } to 10.0.0.2:8080 to 10.0.0.3:8080 { backup } health_check { path /health interval 5s timeout 2s status 200 204 consecutive_failure 3 consecutive_success 2 } }}反向代理指南會逐一說明每個選項。
request_buffers <size|unlimited> 與 response_buffers <size|unlimited> 在轉送前先緩衝,達上限後串流傳送其餘資料;unlimited 在此仍有 8 MiB 的記憶體上限。使用 SI/IEC 單位,1MB 與 1MiB 不同。FastCGI 會套用請求緩衝政策;未宣告 Content-Length 的 FastCGI 請求(分塊上傳,或 HTTP/2、HTTP/3 的內容)必須先讀取才能量出長度,因此對這種請求而言上限是硬限制:超過上限會回應 413,不會以 PHP-FPM 讀成空內容的形式轉送。flush_interval -1 立即排出回應,不納入回應快取。
Syntax: root [<matcher>] <path>Default: noneContext: site block, handle_errors設定網站根目錄:file_server、try_files 與其他處理檔案的指令解析路徑時所依據的目錄。file_server 可以有自己的根目錄,但在這裡設定,可以讓所有指令都指向同一個位置。在 handle_errors 內,root 設定錯誤路由自己的根目錄。
example.com { root * /srv/public file_server}拒絕條件:一般 handle 或 route 內的 root 不受支援;帶路徑或具名匹配器的 root 也會被拒絕。網站或錯誤路由請使用 root * <path>。
Syntax: route [<matcher>] { <directives...> }Default: noneContext: site block, handle, route依撰寫順序執行區塊內的指令,而不是依指令順序。當較窄的指令必須在指令順序排在前面的較寬指令之前執行時,就使用它。匹配器記號的規則與 handle 相同。
example.com { route { file_server /assets/* respond "fallback" 200 }}Syntax: tls internal tls <cert_file> <key_file> tls <email> tls { <options> }Default: automatic HTTPS for public namesContext: site block控制網站憑證的來源。沒有 tls 這一行時,公開名稱會自動從 Let’s Encrypt 取得憑證。
| 形式 | 行為 |
|---|---|
tls internal |
由持久的本機憑證授權單位簽發:一個根憑證,以及簽發 90 天葉憑證的中繼憑證。用戶端必須信任它的根憑證;pingclair trust 會安裝它。 |
tls <cert> <key>,或區塊中的 cert 與 key |
使用在別處簽發的憑證與金鑰檔。validate 會讀取兩個檔案,並拒絕不相符的一對。 |
tls <email> |
設定 ACME 帳號的電子郵件,並保留自動簽發。 |
tls { auto } |
透過 ACME 取得公開憑證並續期,這也是公開名稱的預設行為。 |
區塊也接受 acme_email(或 email)、http3、default_sni、client_auth、renewal_window_ratio,以及 DNS-01 選項(dns、resolvers、dns_ttl、propagation_delay、propagation_timeout、dns_challenge_override_domain)。
網站的每個位址都有憑證:tls internal 為每個名稱簽發一張葉憑證,tls <cert> <key> 這一對則為網站的每個名稱回應。*.example.com 網站會申請一張萬用字元憑證,它恰好涵蓋一層標籤。
http3 off 讓這個網站退出 HTTP/3:它的 QUIC 交握會被拒絕,它的回應也不會在 Alt-Svc 中宣告 HTTP/3,而同一連接埠上的其他網站保持不變。它不會建立或移除 QUIC 監聽器;由全域 servers { protocols … } 清單決定(TLS:可以調整什麼)。
client_auth 和憑證一樣,依用戶端送出的名稱選擇最具體的網站:沒有 client_auth 的確切網站不會要求用戶端憑證,即使同一連接埠上的萬用字元網站會要求。用途擴充欄位排除用戶端驗證的用戶端憑證會被拒絕。
client_auth 也接受 verifier leaf file <paths...>、verifier leaf folder <directory> 或含葉憑證載入器的區塊,在鏈驗證後固定允許的葉憑證。資料夾遞迴掃描 .pem,重載時重新掃描;其他 verifier 模組被拒絕。
拒絕條件:
- 裸的
tls——沒有參數也沒有區塊——會被拒絕,Caddy 也拒絕它:它什麼都沒有指名,而帶主機名稱的站台位址本來就會自動取得 HTTPS。請寫tls internal、tls <cert> <key>、tls <email>或tls { … }區塊。 dns只接受cloudflare。其他供應商都會被拒絕:DNS provider `route53` is not implemented; this build ships `cloudflare` only。protocols、ciphers、curves、alpn、on_demand、key_type、issuer,以及上面沒有列出的其他 Caddy 選項,都會被指名拒絕。tls internal不能與auto、ACME 電子郵件或憑證檔一起使用。- 沒有名稱的網站或
_網站上的憑證檔會被拒絕。
0.2.0 變更:內部授權單位的檔案移到 <store>/pki/authorities/local/,與 Caddy 的配置相同。舊的 <store>/internal/ 目錄不會被搬移:伺服器會建立新的授權單位,用戶端必須重新信任它的根憑證。
example.com { tls { cert ./certs/example.com.pem key ./certs/example.com.key } reverse_proxy localhost:3000}try_files
Section titled “try_files”Syntax: try_files <candidates...> { policy first_exist|first_exist_fallback|smallest_size|largest_size|most_recently_modified }Default: no rewrite; policy first_existContext: site block, handle, route選出檔案候選並重寫請求。每個位置參數都是候選,包括第一個路徑;此指令不接受匹配器 token。候選依設定的根目錄解析,支援佔位符與 glob,保留非 UTF-8 檔名的原始位元組。=404 是錯誤後備。未知政策與不安全路徑會被拒絕。
http://:8080 { root * /srv/site try_files {path} /index.html file_server}Syntax: uri [<matcher>] strip_prefix <prefix> uri [<matcher>] strip_suffix <suffix> uri [<matcher>] path_regexp <pattern> <replacement>Default: unchanged URIContext: site block, handle, route修改請求路徑。移除前綴與後綴忽略 ASCII 大小寫,與路徑路由及 handle_path 相同。運算前先展開運算元佔位符;正規表示式取代以 $1 引用擷取群組。${1} 會被視為佔位符 {1},無法保留擷取值。未知操作、錯誤參數數量、區塊與無效正規表示式會被拒絕。
兩個 Caddy 運算會被指名拒絕,而不是勉強近似:replace 取代路徑中的子字串,而這裡的 rewrite 取代整條路徑;query 編輯查詢字串,這裡目前沒有任何東西會改寫它。訊息會指名運算與原因,因此拒絕不會被讀成打錯字。
http://:8080 { uri path_regexp ^/old/(.*)$ /new/$1 reverse_proxy 127.0.0.1:3000}Global options
Section titled “Global options”全域選項寫在檔案最上方的未命名區塊中。Caddy 巢狀放在 servers { … } 下的選項,在那裡也可以使用。
| 選項 | 語法 | 說明 |
|---|---|---|
acme_dns |
acme_dns cloudflare <token> |
全域 Cloudflare DNS-01 憑證;其他 provider 被拒絕。 |
client_ip_headers |
client_ip_headers <field> ... |
依序查詢的用戶端身分來源,也可放在 servers;只有受信任對端可提供。 |
default_sni |
default_sni <name> |
沒有 SNI 時選用的憑證名稱,包含 HTTP/3;明確但未知的名稱仍被拒絕。 |
ocsp_stapling |
ocsp_stapling off |
本建置不附 OCSP 回應,因此接受 off;裸指令與 on 被拒絕。 |
renewal_window_ratio |
renewal_window_ratio <ratio> |
憑證生命週期中的續期比例,可由網站的 tls 覆寫。 |
admin |
admin [<address> [<token>]] [{ origins …; enforce_origin }] | off |
啟用 Admin API;預設位址為 127.0.0.1:2019。有權杖時,請求必須送出 Authorization: Bearer <token>;沒有權杖時,只接受迴路用戶端。沒有這個選項就沒有 Admin API(Admin API)。 |
auto_https |
auto_https on | off | disable_redirects | ignore_loaded_certs |
控制自動 HTTPS 與 80 連接埠的重新導向。disable_redirects 不會綁定自動的 HTTP 連接埠。disable_certs 會被指名拒絕。 |
default_bind |
default_bind <host> |
為沒有寫 bind、且 listen 沒有指定位址的每個網站提供 bind 主機。只能寫一個位址。 |
dns_refresh |
dns_refresh <duration> | off |
重新解析主機名稱上游的間隔。預設為 30s。off 會保留啟動時解析到的位址。單獨的數字會被拒絕。 |
email |
email <address> |
ACME 帳號的電子郵件。 |
grace_period |
grace_period <duration> |
平順停止時讓進行中的請求完成的時間。預設為 30s。最後一個請求結束後,程序就會立即結束。 |
http_port、https_port |
http_port <port> |
只寫配置方式的位址(http://example.com、https://example.com)與自動 HTTPS 所使用的連接埠。預設為 80 與 443。 |
log |
log [<name>] { … } |
未命名的區塊設定程序日誌;具名區塊宣告一個存取日誌通道(log)。 |
metrics |
metrics [{ per_host; observe_catchall_hosts }] |
開啟指標收集。沒有它就不收集任何指標,抓取端點會回應空的內容。per_host 為設定中提供服務的主機加上 host 標籤。 |
order |
order <directive> first|last|before <d>|after <d> |
調整某個指令在指令順序中的位置。 |
servers |
servers [<address>] { … } |
監聽器選項:protocols、trusted_proxies static …、client_ip_headers、expected_underscore_headers、listener_wrappers { proxy_protocol } 與 metrics。指定位址的區塊只套用到那一個監聽器,且只能設定這些選項。 |
storage |
storage file_system <path> |
TLS 儲存區的目錄。優先於 PINGCLAIR_TLS_STORE。其他儲存模組會被拒絕。 |
trusted_proxies |
trusted_proxies <cidr> ... |
可以在轉送標頭中陳述用戶端位址的對端。在 servers { … } 內請使用 Caddy 的寫法 trusted_proxies static <cidr | private_ranges> ...。每個範圍只能寫一行。 |
在 servers 內,client_ip_headers <field> ... 依序列出可以指出用戶端的標頭。沒有它時,用戶端取自 X-Forwarded-For 與 Forwarded,兩者都沒送時才取 X-Real-IP。
有幾個 Caddy 的全域選項會被指名拒絕,而不是接受後忽略:acme_ca 與 acme_ca_root(自訂 ACME 目錄與簽署其回應的 CA)、on_demand_tls(由用戶端握手驅動的簽發)、filesystem(具名檔案系統;本建置只註冊本機那一種),以及 preferred_chains(偏好的簽發者鏈——這裡的 ACME 用戶端採用憑證頒發機構先回傳的那一條)。每個拒絕都會指名該選項;preferred_chains 是唯一在 validate 而非 adapt 階段被拒絕的,因為文件本身可以轉換,只有佈建步驟無法照辦。servers { timeouts { … } } 同樣被拒絕:這裡表達同一組限制的寫法是網站層級的 limits 區塊。
0.2.0 變更:
- 只有
client_ip_headers列出CF-Connecting-IP時,它才會指出用戶端。 - 只有設定
metrics才會收集指標。 servers內的trusted_proxies必須寫出static模組名稱,同一範圍內的第二行trusted_proxies會被拒絕。- 有多個位址的
bind與default_bind會被拒絕。
在 servers 內,expected_underscore_headers <name> ... 允許名稱含底線的請求欄位,結尾的 * 代表前綴比對。沒有這個選項時,任何含底線的欄位都不會到達處理常式,這也是 Caddy 的預設行為。指定位址的 servers <address> { … } 區塊會以該監聽器的清單取代未指定位址的清單。
{ email admin@example.com admin 127.0.0.1:2019 dns_refresh 30s servers { trusted_proxies static 173.245.48.0/20 client_ip_headers CF-Connecting-IP }}📚 上述 0.2.0 變動的依據與完整升級清單請見CHANGELOG。
⚠️ 變更全域 metrics 後需要重啟服務。透過 /load 套用此變更會收到 409 restart_required。詳見:runtime_listeners.rs。
