← 返回上一頁
Docker 開源軟體

nginx-proxy 實戰:Docker 容器自動化反向代理,告別手寫 Nginx conf

最後更新:2026-04-30
本頁目錄

每次在自家機器上多開一個 Docker 服務,就要 ssh 進去 vim /etc/nginx/conf.d/xxx.conf,加 upstream、加 servernginx -tsystemctl reload nginx。這套流程跑久了,腦袋會自動把它編譯成肌肉記憶——但只要中間哪個變數打錯,整台 reverse proxy 就會 502,然後在 log 裡找半天問題出在哪個 server block。

容器這層本來就主打「即開即關」,IP 會變、port 會變、實例會橫向擴展,再用人工去同步 nginx conf 完全是反模式。docker socket 上的事件流原本就帶有所有需要的資訊:哪個容器起來了、暴露了哪個 port、屬於哪個網路、帶了哪些環境變數。把這些事件當成輸入訊號自動算出一份 nginx conf,這就是 nginx-proxy 在做的事。

這篇整理一下它的運作機制、實際 docker-compose 用法、HTTPS 自動化(搭配 acme-companion)、以及跟 Traefik 的取捨。Repo 累積到 25k 顆星,原始作者 jwilder 後來把專案移交給社群維護,現在是個更新節奏穩定的純社群專案,連帶把姐妹專案 letsencrypt-nginx-proxy-companion 改名叫 acme-companion,背後的 ACME client 從原生 lego 換成更輕量的 acme.sh。

一、痛點:傳統 Nginx 反向代理的維運成本

把容器藏在 Nginx 後面這個架構大家都會寫,但真正煩的是「容器拓撲一直在變」這件事:

  • 新增一個服務 → 寫 server { ... proxy_pass http://127.0.0.1:3000; }
  • 容器重啟 → 如果是 bridge 網路,IP 重新分配,proxy_pass 失效
  • 橫向加一個實例 → 改 upstream 加新節點
  • 換 domain → 找出對應的 server 區塊修改
  • 上 HTTPS → certbot 申請、放憑證路徑、改 ssl_certificate、設 cron 自動續期

每一步單獨看都不難,全部加起來就變成「每次部署都得 30 分鐘起跳」。我自己過去在內部測試環境上踩過幾次坑:因為手動 reload nginx 漏掉某條設定,新版服務的流量轉錯地方,要從 access log 比對 5 分鐘前的舊 vhost 才發現問題。事後檢討起來,根本原因不是 nginx 本身難用,而是「容器拓撲是動態的、conf 是靜態的」這個本質落差。

更現實的問題是 CI/CD 流水線:每個 PR 想開一個 preview URL,難道也要在 pipeline 裡 ssh 進機器寫 conf?這不該是 2026 年的 DevOps 該做的事。即便用 Ansible 去管 nginx conf,也只是把改 conf 的動作從手動換成 declarative,整個 reload 流程依然跟容器事件脫鉤。

二、nginx-proxy 怎麼運作

整個專案其實是兩個元件組合而成:一個是真正提供 HTTP 服務的 Nginx,另一個是叫做 docker-gen 的工具。docker-gen 才是核心,它的歷史比 nginx-proxy 更早,原本就是一個「把 docker 容器狀態渲染成任意配置檔」的通用工具,nginx-proxy 只是它最知名的應用場景。

2.1 docker-gen 監聽容器事件

docker-gen 的工作很單純:把它指向 /var/run/docker.sock,它就會持續監聽 Docker daemon 發出的事件流(container start / stop / die / health-status / ...)。每當事件觸發,它把當下所有容器的 metadata(環境變數、labels、暴露的 port、所屬網路、IP)灌進一份 Go template,渲染出新的 nginx.conf

這個 template 才是 nginx-proxy 真正的「魔法」所在。模板會掃過每個容器,挑出帶有 VIRTUAL_HOST 環境變數的那些,依照 host name 分群,產生對應的 serverupstream 區塊。把同一個 VIRTUAL_HOST 對應到多個容器時,nginx 會自動把它們放進同一個 upstream 用 round-robin 分流——擴容變成「再多開一個 container 就好」,conf 完全不用碰。

支援的環境變數比想像中豐富:

環境變數 用途
VIRTUAL_HOST 對外的 domain,可逗號分隔多個
VIRTUAL_PORT 後端容器 listen 的 port,沒填時自動偵測 expose
VIRTUAL_PATH 把容器掛到指定 URL path(/api/),支援 regex
VIRTUAL_PROTO 後端協議:http / https / uwsgi / fastcgi
VIRTUAL_DEST 反代時改寫 path,/ 表示剝掉 VIRTUAL_PATH
HTTPS_METHOD 是否強制 redirect(redirect / noredirect / nohttps / nohttp
HSTS Strict-Transport-Security header 內容

debug 時最常用的兩招:

# 看渲染後的真正 nginx.conf(包含所有 include)
docker exec nginx-proxy nginx -T

# 看 docker-gen 即時的容器狀態
docker exec nginx-proxy curl -s localhost/nginx-proxy-debug

-T 出來的 conf 沒問題、實際請求卻 502 時,幾乎都是 docker network 沒共用,或 VIRTUAL_PORT 跟容器實際 listen 的 port 對不上。

2.2 SIGHUP reload 機制

模板渲染完不會直接 kill nginx 重來——那會中斷現有連線。docker-gen 改用 SIGHUP 通知 nginx 進程:nginx 收到後會 fork 新的 worker 載入新 conf,舊 worker 處理完手上的請求才退出。對線上連線來說是無感的優雅切換,這個機制是 nginx 從 1.x 時代就有的設計,nginx-proxy 沒有重新發明任何東西,只是把觸發訊號從「人手 kill -HUP」換成「容器事件自動觸發」。

實作上有兩種佈署方式。一種是「all-in-one」,nginx-proxy 官方 image 把 nginx 跟 docker-gen 包在同一個容器裡,預設就跑著 supervisor 同時管兩個 process。另一種是「分離模式」,docker-gen 跑單獨容器,透過 -notify-sighup nginx 參數對 nginx 容器發訊號,適合需要客製 nginx image 的場景,例如要塞自家編譯的 ngx_brotli、ngx_http_geoip2_module 之類的第三方模組。

我自己跑的是 all-in-one,省事,而且絕大多數情境下夠用。要客製 nginx 行為直接掛 vhost.d 就能解決,沒必要拆。真的要拆的話官方文件有完整範例,三個容器(nginx、docker-gen、acme-companion)各自獨立、用 volumes_from 串起來。

2.3 與 Traefik 的差異

很多人會問:「現在不是都用 Traefik 了嗎?」Traefik 確實是更現代的方案,但兩者目標不太一樣。

面向 nginx-proxy Traefik
路由產生方式 docker-gen 渲染 nginx.conf Traefik 內建 dynamic config
配置介面 容器環境變數 容器 labels 或檔案
觀察性 標準 nginx access log 內建 dashboard / metrics
擴展性 Go template 客製,學習成本中 middleware plugin 系統
後端引擎 nginx,效能與生態成熟 Go 自寫 HTTP server
適合場景 單機 / 小規模 swarm,重視 nginx 熟悉度 多 provider(k8s + docker + consul)

簡單講:如果機器上跑的就是「一台 Linux 加幾十個容器」,nginx-proxy 的學習曲線跟維運成本都比較友善;要是已經在 k8s 上、或同時要對接 Consul / etcd 做服務發現,Traefik 才會發揮價值。另一個 nginx-proxy 沒被取代的理由是:很多公司內部已經有大量 nginx 配置經驗、log 分析工具、WAF 規則都圍繞 nginx 寫,換 Traefik 等於整套生態要重建。

三、快速啟動

3.1 單一 docker run

最小可運作版本只需要一行:

docker run --detach \
    --name nginx-proxy \
    --publish 80:80 \
    --volume /var/run/docker.sock:/tmp/docker.sock:ro \
    nginxproxy/nginx-proxy:1.10

幾個關鍵點:

  • :ro 是必要的安全習慣,nginx-proxy 只需要讀 socket,不該有寫入權限
  • 鎖版號用 1.10,不要用 latest(官方 docs 也明確警告過)
  • Alpine 版叫 1.10-alpine,image 小一些但邊緣案例少測試
  • 想用 IPv6 的話加 -e ENABLE_IPV6=true,記得 docker daemon 也要先打開 IPv6

接著任何想被代理的容器只要帶上 VIRTUAL_HOST

docker run --detach \
    --name myapp \
    --expose 3000 \
    --env VIRTUAL_HOST=app.example.com \
    your-app-image

注意是 --expose 不是 -p-p 會把 port 直接映射到宿主機,反而繞過了 nginx-proxy;--expose 只是宣告容器內哪個 port 在 listen,給同一個 docker 網路裡的其他容器用。這是新手最常踩的坑,沒有之一。

3.2 docker-compose 完整範例

實務上我都用 compose 管理,配置可讀性高也好版控。下面這份是搭配 acme-companion 一起跑的完整範例:

version: "3.8"

services:
  nginx-proxy:
    image: nginxproxy/nginx-proxy:1.10
    container_name: nginx-proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - certs:/etc/nginx/certs
      - vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
    networks:
      - proxy
    restart: always

  acme-companion:
    image: nginxproxy/acme-companion
    container_name: nginx-proxy-acme
    volumes_from:
      - nginx-proxy
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - acme:/etc/acme.sh
    environment:
      - [email protected]
    networks:
      - proxy
    restart: always

  myapp:
    image: nginx:alpine
    container_name: myapp
    expose:
      - "80"
    environment:
      - VIRTUAL_HOST=app.example.com
      - LETSENCRYPT_HOST=app.example.com
    networks:
      - proxy
    restart: always

networks:
  proxy:
    name: proxy

volumes:
  certs:
  vhost:
  html:
  acme:

docker compose up -d 後,DNS 把 app.example.com 指到這台機器,第一次請求時 acme-companion 會去 Let's Encrypt 申請憑證、寫進 certs volume、SIGHUP 通知 nginx 重新載入,整個流程不到 1 分鐘。

四、進階配置

4.1 自動 HTTPS(Let's Encrypt companion)

acme-companion 是 nginx-proxy 的姐妹專案(前身叫 letsencrypt-nginx-proxy-companion),專門處理憑證生命週期。它的工作流程:

  1. 監聽 docker 事件,找出有 LETSENCRYPT_HOST 的容器
  2. 用 acme.sh 跑 HTTP-01 challenge(透過共用的 /usr/share/nginx/html 目錄)
  3. 拿到憑證後寫進 /etc/nginx/certs/<domain>.crt.key
  4. 對 nginx-proxy 容器發 SIGHUP 觸發 reload
  5. 內建 cron 每天檢查到期日,剩 30 天內自動續期

需要的環境變數:

  • DEFAULT_EMAIL(acme-companion 自己的容器)
  • LETSENCRYPT_HOST(每個應用容器都要設,通常等於 VIRTUAL_HOST
  • LETSENCRYPT_EMAIL(可選,覆蓋 DEFAULT_EMAIL)

支援的 ACME provider 不只 Let's Encrypt,也能切到 ZeroSSL 或 BuyPass。Let's Encrypt 有 rate limit(每週同一個 registered domain 50 張),對需要大量子域名的服務(例如 PR preview 環境)建議改用 ZeroSSL 或申請 wildcard 憑證自己掛。Wildcard 憑證沒辦法用 HTTP-01 challenge 申請,需要 DNS-01,acme-companion 也支援,但要在容器裡塞 DNS provider 的 API token,配置複雜度高一些。

4.2 多 host 與 path-based routing

同一個容器可以對應多個 host:

environment:
  - VIRTUAL_HOST=app.example.com,api.example.com

也可以反過來,讓不同 path 路由到不同容器:

# 容器 A:API
environment:
  - VIRTUAL_HOST=example.com
  - VIRTUAL_PATH=/api/

# 容器 B:前端
environment:
  - VIRTUAL_HOST=example.com
  - VIRTUAL_PATH=/

VIRTUAL_PATH 也支援 regex:~^/(app1|app2)/ 這種寫法可以接住多個前綴。要把 /api/ prefix 在送到後端時剝掉,就加 VIRTUAL_DEST=/。這個機制讓單一 domain 拆成多個微服務變得很容易,而且不需要前端 router 也不需要 BFF。

4.3 WebSocket / load balancing

WebSocket 不需要特別配置,nginx-proxy 預設模板會處理 Connection: UpgradeUpgrade header,瀏覽器跑 socket.io 或原生 WebSocket 直接通。長連線的 timeout 預設值偏短(60 秒),跑視訊串流或 SSE 的話建議在 vhost.d 客製:

proxy_read_timeout 7d;
proxy_send_timeout 7d;

同一個 VIRTUAL_HOST 對應多個容器時自動 round-robin。要改 hash by IP 或 least_conn,可以加 label:

labels:
  com.github.nginx-proxy.nginx-proxy.loadbalance: "hash $remote_addr;"

預設加上的 X-Forwarded-* 系列 header 也很完整:X-Real-IPX-Forwarded-ForX-Forwarded-ProtoX-Forwarded-HostX-Forwarded-PortProxy header 永遠被移除,避免 httpoxy 漏洞。

4.4 自訂 Nginx 模板

預設模板能滿足 80% 的需求,但碰到特殊情境(例如要塞自訂 security header、要改 client_max_body_size、要加 rate limit)就需要客製:

  • 單一 vhost 的 server 段:在 vhost.d volume 放一個檔名等於 <VIRTUAL_HOST> 的檔案,例如 vhost.d/app.example.com,內容會被 include 進該 host 的 server block
  • 單一 vhost 的 location 段:檔名加 _location 後綴,例如 vhost.d/app.example.com_location
  • 全站預設:檔名 defaultdefault_location
  • proxy-wide:mount 一個 .conf/etc/nginx/conf.d/

例如要為某個 host 加 client_max_body_size:

echo 'client_max_body_size 100M;' > vhost.d/upload.example.com
docker exec nginx-proxy nginx -s reload

要更深入的話,整份 nginx.tmpl 也可以 override 掛進容器,但這就需要看懂 docker-gen 的 template 語法(Go template 加上一些自訂 function 像 groupByMultiwhere),學習曲線陡很多,除非要做大規模客製,不然不建議走這條路。

五、實測:5 分鐘加新服務

跑一次完整流程感受一下落差。我手上有台機器已經跑著上面那份 compose,現在要加一個 Gitea 服務。

# 加到既有 compose
gitea:
  image: gitea/gitea:1.21
  container_name: gitea
  expose:
    - "3000"
  environment:
    - VIRTUAL_HOST=git.example.com
    - VIRTUAL_PORT=3000
    - LETSENCRYPT_HOST=git.example.com
  volumes:
    - gitea-data:/data
  networks:
    - proxy
  restart: always

然後:

# DNS:git.example.com A record 指向機器 IP(事前做好)
docker compose up -d gitea

# 看 nginx-proxy 自動 reload
docker logs -f nginx-proxy --tail 20
# 應該看到:Received event start for container ...
# Received signal: hangup
# Generated nginx config

# 看 acme-companion 申請憑證
docker logs -f nginx-proxy-acme --tail 30
# Creating/renewal of git.example.com certificates...
# Reloading nginx proxy

docker compose uphttps://git.example.com 真的能開,整個過程確實 3 分鐘左右。對比過去手動 nginx + certbot 的流程,省下的每一分鐘都是真的。要拆掉 service 也一樣輕鬆,docker compose rm -sf gitea 之後 nginx-proxy 會收到 die 事件、自動把 git.example.com 從 conf 移除。

六、vs. Traefik / Caddy / Token Gateway

把市面上主流的 reverse proxy 工具放在一起對比一下:

工具 配置來源 HTTPS 自動化 適合規模 學習成本
nginx-proxy docker env / labels acme-companion(外掛) 單機到中型 低(會 nginx 就行)
Traefik docker labels / k8s ingress 內建 ACME 中到大型,多 provider 中(label 系統需熟悉)
Caddy Caddyfile 內建(自動 on by default) 小到中型 極低(語法極簡)
HAProxy hapi-runtime API 需搭配 certbot 大型,重視效能

Caddy 的「預設開 HTTPS」確實香,但要綁 docker 動態服務需要額外的 plugin(caddy-docker-proxy),生態不如 nginx-proxy 跟 Traefik 成熟。HAProxy 是另一個世界,企業內網跟金融場景比較常見,配置複雜度高一個量級。

選擇上:「我跑 docker,沒有 k8s,喜歡 nginx 慣用的 conf 思維」就 nginx-proxy;「我有 k8s 或多 cluster」就 Traefik;「我只想開三個小服務、connection 量小」就 Caddy。如果手上機器以後可能會搬到 k8s,倒是建議直接學 Traefik 一勞永逸,遷移時還能保留 label 配置。

七、適合與不適合的場景

不是每個場景都該用 nginx-proxy。整理一下我的判斷:

適合

  • 個人 / 小團隊在 1~3 台機器上跑多個容器,每個都要獨立 domain
  • CI/CD 動態 preview 環境(PR 一開就有對應 URL,PR 一關 URL 自動消失)
  • 自架服務站台(Gitea、Mattermost、Vaultwarden、wiki...)
  • 微服務內部反代,不需要複雜的 traffic shaping
  • 對 nginx 設定熟悉、想保留客製空間的團隊

不適合

  • 已經在 Kubernetes 上,那直接用 ingress controller(nginx-ingress、Traefik)才正解
  • 需要金絲雀發布、藍綠部署、weighted routing 等進階流量控制
  • 後端不是 docker 容器(VM、bare metal、雲端 serverless)
  • 多機器 swarm 雖然能跑,但運維成本未必比 Traefik 低
  • 對 nginx 完全陌生、且不想學 Go template 的團隊

下面這張圖整理三個元件的互動關係:

nginx-proxy + acme-companion 自動化反向代理架構

整個系統的事件流就是:容器啟動 → docker-gen 收到 → 渲染 conf → SIGHUP → nginx 重載;憑證需求 → acme-companion 跑 challenge → 寫進共用 volume → SIGHUP → nginx 帶上新憑證。沒有外部 control plane,沒有 etcd,沒有 service mesh,純粹是兩個容器加幾個 volume,這個極簡的 architecture 也是它在小規模場景能跑得這麼穩的原因。

7.1 常見踩坑清單

整理這幾年用下來看過的常見問題,給之後要採用的人一個快速查找表:

  • 502 Bad Gateway:九成是 docker network 沒共用,或者 VIRTUAL_PORT 寫錯。先 docker exec nginx-proxy nginx -T 看 upstream 的 IP,再 docker exec myapp curl localhost:<port> 確認後端真的有 listen。
  • 憑證一直發不出來:Let's Encrypt 走 HTTP-01,需要 80 port 對外開放、DNS 已正確指向、防火牆放行。第一次失敗會被退到 staging,看 acme-companion log 有沒有 Account creation 那段成功訊息。
  • 每次 reload 連線中斷:通常是 nginx worker_shutdown_timeout 太短。預設 nginx 行為是 SIGHUP 時優雅切換,這個問題比較少見,遇到的話檢查是不是有 process 異常 panic。
  • swarm 模式抓不到 service:原生 nginx-proxy 預設模式是讀 docker daemon 的 container 列表,swarm service 要用 nginxproxy/docker-gen 搭 swarm 專用模式,或者改用 Traefik,這個情境 nginx-proxy 並不順手。
  • 一台機器跑幾百個 vhost:nginx 自己沒問題,但 docker-gen 每次事件都會重新渲染整份 conf,容器數量大時會看到明顯的 reload 延遲。對策是把不常變動的服務用標準 vhost.d 寫死,把動態的部分留給 docker-gen。

7.2 觀察與監控

跑起來之後別忘了配 log 跟 metrics。nginx-proxy 內建會把 access log 寫到 stdout,配合 docker logging driver 直接送進 Loki / ELK 都可以。要更深入的指標可以考慮:

  • nginxinc/nginx-prometheus-exporter 抓 stub_status,掛在同一個 docker network 即可
  • 在 vhost.d 加上 log_format 帶上 $upstream_response_time$upstream_addr,方便排查慢 upstream
  • acme-companion 的 log 訂閱起來,憑證失敗第一時間能收到告警

八、小結

nginx-proxy 的價值在於它把「dynamic Docker topology」跟「靜態 nginx.conf」這兩個本質衝突的東西用 docker-gen 接起來,而且做法夠樸素——不重新發明 reverse proxy,只是把現有的 nginx 自動化掉。對中小規模、單機或少量機器的場景,這個取捨非常划算,大型場景才需要 Traefik 或 ingress controller 那層額外的複雜度。

實際在用的時候有幾點小心:版本鎖死(不要追 latest)、acme.sh 的 rate limit、exposeports 別搞混、共享 docker network 別漏設、要 reload 之前先用 nginx -T 看一下渲染結果。把這幾個踩過一次後,後續加新服務真的就是「寫四行 yaml 然後 up -d」的事。

下次想多開一個服務之前,先想想有沒有必要再 ssh 進去寫 conf。如果答案是「沒必要」,那 nginx-proxy 就是答案。

參考資料

分享這篇
X LinkedIn Facebook Hacker News Reddit

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料