反向代理 - 在 Docker 環境建置
你可能知道反向代理,但你試過在 Docker 做嗎
反向代理的基本說明,以及在 Docker 容器環境中如何建置反向代理
反向代理說明
不同實體主機
使用 nginx proxy-pass
目標
- 使用者造訪指定網頁路徑
/new_path
或是整個跳轉/
- 設定測試機 nginx proxy_pass 使其跳轉到其他主機專案相同路徑下
- 確認使用者 IP 沒有被變成為代理伺服器的 IP
- 確認在代理目標的專案可以讀取 Cookie
- 確認 Post 請求的 payload 也會被轉送過去
設定 nginx config
第一台主機的 nginx 設定
# 把所有前往 domain/new_path 的請求反向代理到另一台主機上
location /new_path {
# 要代理到哪個網址
proxy_pass http://laravel55.test.com;
# 傳送使用者真實 IP 到目標主機
proxy_set_header X-Real-IP $remote_addr;
# 設定伺服器為代理前的伺服器,否則 HTTP_HOST 會變成 proxy 那台代理伺服器的位址
proxy_set_header Host $host;
# 設定 cookie 轉發
proxy_set_header Cookie $http_cookie;
# 用來確認最初的用戶端原始 ip 位置
proxy_set_header X-Forwarded-Host $host;
# 記錄從使用者端出來經過的每個代理 X-Forwarded-For: <client>, <proxy1>, <proxy2>
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 識別網路連線協定
proxy_set_header X-Forwarded-Proto $scheme;
# 隱藏伺服器資訊
proxy_hide_header Server;
proxy_hide_header X-Powered-By;
# 因為自行加大 header 所以把空間也加大
proxy_headers_hash_bucket_size 128;
proxy_headers_hash_max_size 512;
}
第二台主機的 nginx 設定
server {
listen 80;
server_name ray200;
root /home/toc/www/laravel55/public;
access_log /var/log/nginx/www/laravel55.access.log;
error_log /var/log/nginx/www/laravel55.error.log;
index index.php;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~* \.(jpg|jpeg|gif|css|png|js|ico|html|svg|ttf|woff|woff2|eotD)$ {
access_log off;
expires max;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# php-fpm 的位置
fastcgi_pass 127.0.0.1:9000;
}
}
注意事項
server_name
必須要是 proxy_pass 過來那台的 domain
如果不這樣設定,proxy_pass 過來會進到預設的 server_name 下- 在程式中判斷真實 IP 的方法要改成讀讀取 header 中的
x-real-ip
才是真實使用者 ip 位置
確認 cookie 可以讀取
如果設定 nginx 有設定proxy_set_header Cookie $http_cookie;
那dump($_COOKIE);
可以取得 cookie,但是 Laravel 內建的request()->cookie();
卻沒有取到值
因為 Laravel 預設的 web middleware \App\Http\Middleware\EncryptCookies::class, 中
要求 cookie 是加密過後的,如果沒有加密當然就讀不到
解決方案一個是在 class EncryptCookies
中的$except
加入要排除的 cookie 名稱
另一個就是直接把 middleware 註解掉,解決發生問題的 code
使用 Docker
會點進這篇文章應該是比較想知道這部分
架構示意圖
參考資料:How to set up NGINX Docker Reverse Proxy?
步驟
1. 手動建立 network
反向代理會根據載入的設定檔把請求導向到指定的容器名稱內
故代理的目標容器必須在同一網路內,才能直接使用容器名稱做代理目標
因為不想讓 network 前面帶上 prefix,還有避免依賴,所以手動建立 network
docker network create nginx-network
這個情境屬於來自外部的 nginx-proxy,要多給一個external network
有需要連接到這個 network 裡面的服務都要設定
networks:
default:
external:
name: nginx-network
如此就可以讓不同 docker-compose.yml 中的服務互相溝通:官方說明
2. 建立測試用的服務
因為懶得建太複雜的服務,所以抓現成的「portainer」服務來做為 proxy 的目標
只是為了要有一個只要 docker-compose up 就可以運行的服務,也可以換成自己比較熟的映像檔
為了測試從 80 port poxy 到其他 port,所以這邊開在 8080 port
服務啟動之後可以先去localhost:8080
查看 portainer 服務有沒有啟動
version: '3'
networks:
default:
external:
name: nginx-network
services:
portainer:
container_name: nginx_test_portainer
image: portainer/portainer-ce
command: -H unix:///var/run/docker.sock
restart: always
ports:
- 8080:9000
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
volumes:
portainer_data:
3. Nginx 的 proxy 設定
3.1 設定docker-compose.yml
version: '3'
networks:
default:
external:
name: nginx-network
services:
nginx:
container_name: test_nginx
image: nginx:1.18
ports:
- "80:80"
- "443:443"
volumes:
- ./conf.d/:/etc/nginx/conf.d/
- ./ssl/:/ssl/
3.2 設定 mount 進去給反向代理伺服器使用的 nginx config
可以看到底下設定中,我們把所有發往「test.example」網址的請求都轉到
http://nginx_test_portainer:9000
在容器中使用的模式是
http://{container_name}:{port}
可以看到「container_name」對應了上面建立測試服務
時候使用的容器名稱
而「port」對應到了容器的內部 port!
這邊特別把上面的容器內外使用的 port 區隔開來顯示差別,我們代理的目標是內部的 9000 port
而不是對外的8080
port
server {
listen 80;
listen [::]:80;
server_name test.example;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name test.example;
ssl_certificate /ssl/ssl.crt;
ssl_certificate_key /ssl/ssl.key;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
location / {
proxy_set_header Host $host;
proxy_set_header Cookie $http_cookie;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_pass http://my_portainer:9000;
}
}
要特別注意的是,這邊 proxy_pass 目標網址中的http://
不可以省略!
如果轉址過去的網址有自己的 https 證書驗證(例如另一個 nginx 環境),則必須改成https://
開頭
參考資料:What value should I use for Nginx proxy_pass running in Docker?
3.3 編輯etc/hosts
127.0.0.1 test.example
接著啟動 nginx 服務,用瀏覽器開啟剛剛設定的test.example
正確設定的話,監聽 80 port 的 nginx 服務應該會把所有目標是 test.example
的請求
利用 proxy_pass 送到 portainer 容器的指定 port,所以畫面上會顯示 portainer 服務的畫面
要在所有服務都啟動之後再啟動反向代理服務,否則會報錯指出被代理的容器目標不存在
注意事項
如果要把全部沒有匹配到的server_name
請求統一處理
可以在指定的 nginx config 加上 default_server
定義
如果沒有定義 default_server,則第一個 server 會被預設成 default server
server {
listen 8080 ssl default_server;
listen [::]:8080 ssl default_server;
ssl_certificate /ssl/ssl.crt;
ssl_certificate_key /ssl/ssl.key;
# Virtual Host Domain
server_name _;
# where code is
root /var/project/dev/www/my_backend/public;
index index.php index.html index.htm;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
try_files $uri /index.php =404;
fastcgi_pass dev_php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}
因為要處理沒有被其他 server_name 捕獲的請求
所以 server_name 就不是什麼重點,可以用「_
, __
, ___
」都可以
這個情境會發生在當前端的 API 設定檔使用的路徑網址是指定後端容器名稱的時候
因為不是使用網址經過反向代理轉發,而是直接將請求發至容器
而這個 nginx 容器中又有多個 server config 設定
所以 nginx 沒有「server_name」可以判斷要使用哪個 config 設定
案例說明
同一個 nginx 容器(dev_nginx) 裡面有多個 server config
- alex_backend
- john_backend
- dev_backend
如果現在有一個在同樣 docker network 的前端容器要打 API 到 dev_backend
他會在設定檔指定網址是 dev_nginx
這時候問題就來了,當 dev_nginx 收到這個請求會不知道是發給誰的
如果沒有設定 default_server,就會把請求派發給第一個 server,也就是 alex_backend
這不是我們要的結果
所以會把「dev_backend」設定為「default_server」
如此之後,只要是送進這個容器的請求
若沒有被其他設定檔捕獲,就會被送進 dev_backend 處理