Nginx是一個免費開源且穩定高效的Web伺服器程式,擁有反向代理以及負載平衡的功能,經常作為最前端的伺服器。



安裝Nginx伺服器

要在Ubuntu Server上安裝Nginx伺服器,可以直接在終端機中執行以下指令:

sudo apt install nginx

ubuntu-server-nginx

Nginx預設會啟用HTTP,TCP連接埠為HTTP預設的80,可以使用以下指令來查看Nginx是否有確實安裝成功。

sudo ss -tlnp | grep nginx

ss指令可以顯示出Socket相關的資訊。-l參數可以只顯示正在監聽中的連線。若使用ss指令時都沒給任何參數的話,會忽略掉監聽中的連線。-t參數可以只顯示TCP連線。-n參數可以讓連接埠數字直接被輸出,而不是用一個名稱代替。-p參數可以顯示佔用連線的行程。

ubuntu-server-nginx

如上圖,如果有看到TCP有在監聽連接埠80,就表示Nginx安裝成功了!

注意,Nginx之所以會去監聽任意網路介面上的80連接埠,是因為預設的設定檔(/etc/nginx/sites-available/default)中,有設一個會去監聽任意網路介面,且連接埠為80的「虛擬主機」(Virtual Host)。有關於「虛擬主機」,會在本篇文章之後做介紹。

設定Nginx

Nginx的主設定檔為/etc/nginx/nginx.conf。它預設的模樣長成這樣:

ubuntu-server-nginx

Nginx的設定有分層級(區塊),在最頂層比較重要的設定項目(命令)有:

  • worker_processes:設定Nginx要有幾個「工人」(Worker)。一個工人就是一個行程,會透過非同步I/O和執行緒池(Thread Pool)處理多個連線。工人的數量通常會設定為處理器(邏輯處理器)的數量,可以小於但不建議超過。預設的auto即表示要使用處理器(邏輯處理器)的數量作為工人的數量。
  • user:設定執行Nginx工人行程的擁有者和群組。不過如果只有指定使用者,則表示要使用與使用者相同名稱的群組。預設的擁有者和群組為www-data
  • worker_rlimit_nofile:設定每個Nginx工人行程最多同時可以開啟幾個檔案描述符(File Descriptor,簡稱FD)。關於這個設定的用途很模糊,我們可用ulimit -Sn指令和ulimit -Hn指令來查看每個行程的FD數量的最大限制,前者為超過會有警告的軟限制,後者為超過就不給用的硬限制,設定worker_rlimit_nofile應該是要讓工人行程可以正常地突破軟限制。worker_rlimit_nofile常被設為每個工人行程的最大連線數(即worker_connections,等等會介紹)再乘上2,或者直接設為65535
  • include:在這個命令的位置插入指定路徑(可用星號*表示任意檔名)的Nginx設定檔。include命令可以被用在任意區塊中。
  • load_module:動態載入模組。/etc/nginx/modules-available目錄中存放著能被載入的模組的設定檔,如果要載入模組,可以會將其設定檔的符號連結(Symbolic Link)放置在/etc/nginx/modules-enabled目錄中。至於靜態模組,可以使用nginx -V 2>&1 | tr -- - '\n' | grep module指令來查看。沒有用到的模組就別動態載入了,會多吃記憶體。
  • error_log:設定錯誤日誌的存放位置,若第一個參數傳入檔案路徑,則可以存到檔案系統中。第二個參數可以控制錯誤日誌的訊息最低等級(重要度),預設是error,可用的等級依照重要度排序有infonoticewarnerrorcritalertemerg。若設定為error,表示要存放errorcritalertemerg等級的訊息。如果沒有在Nginx的設定頂層中設定error_log,Ubuntu Server預設的Nginx錯誤日誌會存放在/var/log/nginx/error.logerror_log命令還可以(不限於)被用在httpserverlocation區塊中。
  • eventsevents區塊用來設定處理連線的方式。
  • httphttp區塊用來設定HTTP,包括虛擬主機。http區塊最多只能有一個。

如果用了error_log命令把日誌檔案的路徑設在Linux發行版的預設日誌目錄下的nginx目錄之外或者檔名不是以.log結尾,要記得去調整logrotate的設定檔(一般會在/etc/logrotate.d/nginx,如果沒有就自行建立吧),讓它可以正確地去對Nginx日誌檔案做「輪替(rotate)」的動作,避免日誌檔案把伺服器的硬碟空間塞滿了!

events區塊比較重要的設定項目(命令)有:

  • worker_connections:設定每個工人行程的最大連線數,包含對客戶端和對被反向代理的伺服器等所建立的連線。這個值如果設定太大,雖然可以讓伺服器同時處理多筆連線,但會佔用很多的記憶體,而且每筆連線的處理速度也會變得很慢,甚至會導致開啟檔案描述符的額度不足;如果設得太小,不用說,會讓很多連線無法被建立。預設的最大連線數為768,如果不設定會是512。如果您的伺服器在上線之後觀察CPU和記憶體的使用率,覺得還有餘力可以再處理更多連線的話,可以逐步調高worker_connections
  • multi_accept:是否讓Nginx工人行程一次接受所有連線。不設定的話是off,Nginx會一個一個選擇它認為比較好(比較閒?)的工人行程去接受連線;設成on的話,Nginx會直接選一個工人行程去接受所有連線。至於哪種比較快似乎不一定,請自行嘗試吧!

http區塊中已有預設許多幾乎必定得啟用的功能(如讓最後一個封包不管大小就直接送出的tcp_nodelay、讓封包可以在資料集滿之後再送出的tcp_nopush、不進行中介直接發送檔案的sendfile、壓縮回應加快傳輸速度的gzip、HTTP加密連線用的ssl_protocolsssl_prefer_server_ciphers),其它比較重要的設定項目(命令)有:

  • log_format:建立日誌的格式。第一個參數設定要建立的格式名稱,第二個參數和第三個參數用來設定格式(有點複雜就不在這裡提了)。
  • access_log:設定存取日誌的存放位置,若第一個參數傳入檔案路徑,則可以存到檔案系統中;若傳入off則該區塊下不去記錄存取動作。第二個參數傳入要套用的日誌格式名稱(用上面提到的log_format命令來建立),預設是Nginx內建的combined格式。access_log支援Gzip,但我們已有logrotate,所以不需要去管它。access_log命令還可以被用在serverlocation區塊中。
  • server_tokens:設定是否要將Nginx的版本資訊代入回應的HTTP標頭中(於Server欄位),以及是否要在顯示Nginx預設的錯誤頁面時也顯示Nginx的版本資訊。不設定的話是on。建議設成off
  • keepalive_timeout:設定Keep-alive連線的逾時時間(秒)。預設值為65;不設定的話是75。還可以被用在serverlocation區塊中。
  • types_hash_max_size:設定一個Hash Table的最大空間(位元組)。預設是2048,不設定的話是1024。還可以被用在serverlocation區塊中。
  • types_hash_bucket_size:設定一組Hash資料的最大空間(位元組)。不設定的話是64。還可以被用在serverlocation區塊中。
  • server_names_hash_max_size:設定用來存放伺服器名稱的Hash Table的最大空間(位元組)。不設定的話是512
  • server_names_hash_bucket_size:設定一個伺服器名稱的最大空間(位元組)。不設定的話會是CPU的快取塊(Cache Line)大小,通常是32或是64。這個設定值直接影響著能使用的伺服器名稱的最大長度。
  • default_type:設定預設的Mime類型。預設是application/octet-stream,不設定的話是text/plain。還可以被用在serverlocation區塊中。
  • client_header_buffer_sizelarge_client_header_buffers:用來設定客戶端請求(request)中標頭的緩衝空間(位元組),如果小的不夠就建大的,大的不夠的話就會回傳HTTP 400狀態(Bad Request)。不設定的話是10248192。它們還可以被用在server區塊中。
  • client_body_buffer_size:設定客戶端請求中主體的緩衝空間(位元組),如果不夠大的話就會暫存成檔案。不設定的話,32位元的系統是8192;64位元的系統是16384。還可以被用在serverlocation區塊中。如果記憶體夠大,這個值可以設高一點。
  • client_max_body_size:設定客戶端請求中主體的最大大小(位元組),不設定的話是1m。還可以被用在serverlocation區塊中。若是需要讓客戶端上傳檔案或是傳送可能會有比較多內容的訊息(例如長篇部落格文章),就會很需要調整這個設定。
  • types:設定檔案副檔名對應的Mime類型。/etc/nginx/mime.types檔案是預設的對應表。types命令還可以被用在serverlocation區塊中。
  • open_file_cache:是否快取檔案的FD、大小、修改時間、有無存在等。不設定的話是off(不快取)。第一個參數要設定max,也就是最大快取數量,並非愈大愈好,愈大會需要愈多的記憶體和搜尋時間,可以先從max=1000開始嘗試;第二個參數要設定inactive,也就是快取的持續時間(秒),不設定的話是inactive=60open_file_cache命令還可以被用在serverlocation區塊中。
  • open_file_cache_min_uses:設定快取的檔案在inactive的時間內,最少要被存取幾次才不會被踢出快取。不設定的話是1。還可以被用在serverlocation區塊中。
  • open_file_cache_errors:是否快取讀取失敗的檔案。不設定的話是off(不快取)。一般情況下這個功能不需要被開啟。還可以被用在serverlocation區塊中。
  • open_file_cache_valid:間隔多久(秒)後去驗證快取的資料之正確性。不設定的話是60。還可以被用在serverlocation區塊中。
  • charset:設定純文字格式的編碼方式。參考這篇文章
  • server:定義虛擬主機。可以使用多個server區塊來定義多個虛擬主機。
  • upstream:定義上游伺服器的群組,用來做負載平衡。可以使用多個upstream區塊來定義多個上游伺服器群組。

位元組若有km單位表示要再乘上210、220;秒若有mhd單位表示要再乘上60、3600、86400。

關於types,還可以再加上以下內容到對應表中(加入之前先檢查是否已存在):

    application/font-woff2                  woff2;
    application/wasm                        wasm;

關於Gzip還有以下幾個比較重要的設定項目(命令):

  • gzip_comp_level:設定Gzip的壓縮程度,數值範圍是1~9,數值愈高壓縮程度愈高(需要更多的CPU運算和時間)。不設定的話是1。設定1就有明顯的效益,但如果想要減少更多的網路流量,可以再設高一點(建議6以下)。gzip_comp_level命令還可以被用在serverlocation區塊中。
  • gzip_min_length:設定需要被Gzip壓縮的最小資料量(位元組)。不設定的話是20。建議可以設高一點(例如100),增加Nginx對小資料的反應速度,並減少CPU使用率。gzip_min_length命令還可以被用在serverlocation區塊中。
  • gzip_proxied:設定從被反向代理的伺服器拿到的資料要如何進行Gzip壓縮。不設定的話是off(不壓縮)。其它的設定值根據判斷HTTP回應的標頭來決定要不要壓縮,有expiredno-cacheno-storeprivateno_last_modifiedno_etagauth,可用空格隔開多個。如果都要壓縮,就設成anygzip_proxied命令還可以被用在serverlocation區塊中。
  • gzip_vary:設定是否在有使用Gzip壓縮的回應中添加Vary: Accept-Encoding標頭。不設定的話是off(不添加)。如果有使用CDN的話,這個項目一定要改成on,為了要讓CDN根據客戶端請求中的Accept-Encoding來決定要回傳的回應是要有用Gzip壓縮過的版本的還是沒有壓縮過的版本。gzip_vary命令還可以被用在serverlocation區塊中。
  • gzip_types:設定可以被Gzip壓縮的MIME類型,用空格隔開多個,可以只用一個星號*來表示的任意的MIME類型。不設定的話是text/htmlgzip_types命令還可以被用在serverlocation區塊中。
  • gzip_disable:利用正規表示式排除不使用Gzip的User Agent,可用空格隔開多個。填寫msie6可用來排除不能正常支援Gzip壓縮的IE(IE4、IE5和部份IE6)。不建議使用這個設定項目來排除IE,因為IE很少人用了,沒有必要為了它在每次處理HTTP請求時都去耗費CPU資源來判斷User-Agent標頭。gzip_disable命令還可以被用在serverlocation區塊中。

關於gzip_types,可以參考以下設定:

gzip_types text/plain text/css text/xml application/xml application/atom+xml application/rss+xml application/xhtml+xml application/json application/msgpack application/javascript application/wasm image/svg+xml image/bmp image/x-ms-bmp image/x-icon font/opentype font/ttf

由於虛擬主機的設定比較繁瑣,通常寫在其它的設定檔中,再於主設定檔中使用include命令來引入。冗長但可以由各個虛擬主機共同使用的設定(例如用map命令建立的Hash Table),可以被放在/etc/nginx/conf.d目錄中;而由虛擬主機各別的設定,可以被放在/etc/nginx/sites-available目錄中,而要上線的虛擬主機,可以將其設定檔的符號連結放置在/etc/nginx/sites-enabled目錄中。一般來說,被放在/etc/nginx/sites-enabled/etc/nginx/sites-available目錄中的設定檔,都是直接由server {開頭(但不限於只使用一個server區塊),而設定檔的檔名,則會和該虛擬主機直接相關,方便辨認。

舉例來說,若我們想要讓我們的網站1架設在1.magiclen.org,網站2架設在2.magiclen.org網域上,那麼在/etc/nginx/sites-available目錄中,就可以建立出兩個設定檔,檔名分別為1.magiclen.org.conf2.magiclen.org.conf。若要讓這兩個網站上線,就用sudo ln -s /etc/nginx/sites-available/*.magiclen.org.conf /etc/nginx/sites-enabled指令來建立這兩個設定檔的符號連結到/etc/nginx/sites-enabled目錄中。

設定虛擬主機

在一台實體主機上安裝Nginx伺服器,可以藉由使用多個server區塊,將不同的網站掛上不同的網域、IP或是連接埠。簡單來說,一台實體主機可以建立出多個網站,就像是弄出了多台虛擬主機一樣,當然這邊完全沒有任何像KVM、Docker等虛擬化技術(Virtualization)存在,應該是因為外行人通常會認為一台主機只能使用一組網域和IP來架設一個網站,才如此命名(似乎是從Apache伺服器開始的)。

也就是說,server區塊的根本,是Nginx要依照從客戶端來的HTTP請求,來區分要使用哪個server區塊,進而提供不同的網站內容。區分的方式有兩步驟,第一步是看IP(網路介面的IP)和連接埠,第二步是看HTTP標頭中的Host欄位。

listen命令設定要監聽的網路介面和連接埠

listen命令直接影響著Nginx要去監聽哪幾個網路介面以及哪幾個連接埠。不設定的話是*:80,也就是監聽所有網路介面(IPv4)的80連接埠。

由於listen命令的設定方式挺多樣的,以下就直接以常用的例子來說明。

server {
    listen *:8000;

    ...
}

以上設定,表示要監聽所有網路介面(IPv4)的8000連接埠。

server {
    listen 8000;

    ...
}

以上設定,也是表示要監聽所有網路介面(IPv4)的8000連接埠。

server {
    listen 127.0.0.1:8000;
    listen 10.0.2.15:80;

    ...
}

以上設定,表示要監聽127.0.0.1(loopback)網路介面的8000連接埠和10.0.2.15網路介面的80連接埠。

server {
    listen [::]:8000;

    ...
}

以上設定,表示要監聽所有網路介面(IPv6)的8000連接埠。

server {
    listen *:443 ssl http2;

    ...
}

以上設定,表示要監聽所有網路介面(IPv4)的443連接埠,並且使用SSL模式(HTTPS)及啟用HTTP/2協定(可向下相容HTTP/1.x)。

SSL模式還需要搭配ssl_certificatessl_certificate_keyssl_dhparam命令,用於http區塊或是server區塊。可以參考這篇文章來申請免費的SSL證書。

server_name命令設定HTTP請求中的Host標頭欄位需要擁有的值

當客戶端透過網域名稱和HTTP/HTTPS協定來發送請求時,客戶端軟體會先查找網域名稱對應的IP位址,接著將網域名稱代進請求中的Host標頭欄位中,再將請求發送到該IP位址對應的主機。Nginx的server_name命令就是要用來判斷客戶端請求中的Host標頭欄位是否能夠與之匹配,可用空格隔開多個。server_name的匹配是不分大小寫的(case-insensitive)。

如果不設定server_name,就會是空字串"",表示不去匹配Host標頭欄位。server_name命令可以使用星號*來進行wildcard的匹配,也可以以~字元為開頭,使用正規表示式進行匹配,例如~^www\d+\.magiclen\.org$,就可以去匹配www1.magiclen.orgwww10.magiclen.org

在IP與連接埠都相同的情況下,server區塊的選擇是根據server_name的設定方式來決定的,順序如下:

  • Host標頭欄位值完全與server_name一致。
  • 最長且星號*在開頭的wildcard。(例如*.magiclen.org*.org,都與blog.magiclen.org匹配,就會選擇*.magiclen.org)
  • 最長且星號*在結尾的wildcard。(例如blog.magiclen.*blog.*,都與blog.magiclen.org匹配,就會選擇blog.magiclen.*)
  • 第一個匹配到的正規表示式。

server_name均無法完成匹配,就會選擇該IP與連接埠的第一個server區塊。如果想要使用其它的server區塊作為server_name均無匹配到時的預設server區塊,那就要在listen命令加上default_server參數,如下:

server {
    listen 80;

    server_name 1.magiclen.org;

    ...
}

server {
    listen 80 default_server;

    server_name 2.magiclen.org;

    ...
}

以上設定,請求中的Host標頭欄位只能是1.magiclen.org,才可以選擇到第一個server區塊。

其它比較重要的設定項目
  • root:設定網站根目錄。不設定的話,Ubuntu Server預設會使用/var/www/htmlroot命令還可以被用在httplocation區塊中。
  • index:當請求目標是目錄時,要對應到該目錄底下的哪個檔案。可用空格隔開多個檔案,檔案尋找順序是從左到右。若以/開頭,表示要從網站根目錄尋找檔案,通常會用在最後一個,例如index.html /no-index.html。不設定的話是index.html。要注意的是,這個命令是透過內部轉址來動作(會被location區塊處理),並不是直接去存取檔案。index命令還可以被用在httplocation區塊中。
  • error_page:設定當要回傳300以上的HTTP狀態碼時,要如何進行轉址(例如轉到特定的錯誤網頁)。前面幾個參數是要設定的狀態碼,最後一個參數是轉址的路徑(以/開頭的話是內部轉址,以其它協定為開頭的話就是外部轉址)。倒數第二個參數也可以加上=狀態碼,來改變回應的狀態碼。error_page命令還可以被用在httplocation區塊中。
  • locationlocation區塊可以針對不同的請求的路徑做設定,所以一個server區塊中可以有多個location區塊。
location區塊

location命令的設定方式挺多樣的,以下就直接以常用的例子來說明。

location = / {
    ...
}

以上的location區塊可以處理/請求。=就是讓location命令的第三個參數要與請求的路徑「完全匹配」的意思。

location / {
    ...
}

以上的location區塊可以處理以/為開頭的請求。

location /foo {
    ...
}

以上的location區塊可以處理以/foo為開頭的請求,區分大小寫(case-sensitive),包括/foobar/foo/bar。不過若是在macOS或是Windwos等路徑不區分大小寫的作業系統中架設Nginx伺服器,這個方式就不分大小寫。

location ^~ /foo {
    ...
}

以上的location區塊可以處理以/foo為開頭的請求,區分大小寫(case-sensitive),包括/foobar/foo/bar。不過若是在macOS或是Windwos等路徑不區分大小寫的作業系統中架設Nginx伺服器,這個方式就不分大小寫。這功能看起來和不寫^~時一樣,但優先順序有差(稍候會提到)。

location ~ /foo {
    ...
}

以上的location區塊可以處理請求路徑中含有/foo的請求,區分大小寫,包括/foo/foobar/foo/bar/bar/foo

location ~ \.(jpg|gif)$ {
    ...
}

以上的location區塊可以處理以.jpg或是.gif為結尾的請求,區分大小寫,包括/foo.jpg/bar.gif~就是讓location命令的第三個參數變成區分大小寫的正規表示式。

location ~* \.(jpg|gif)$ {
    ...
}

以上的location區塊可以處理以.jpg或是.gif為結尾的請求,不區分大小寫,包括/foo.JPG/bar.GIF~*就是讓location命令的第三個參數變成不區分大小寫的正規表示式。

請求路徑在匹配時,最先會去找有用=修飾的location區塊,如果有找到能匹配的location區塊,就會直接去用它。接下來會從有^~修飾和完全無修飾的location區塊來進行路徑前綴的匹配,並找出能夠匹配到最長前綴的那個location區塊,看它是否有用^~修飾,有的話就會去用它,沒有的話就暫時當這個location區塊不存在,繼續從有用~修飾的location區塊進行正規表示式的匹配。若都找不到能匹配的用~修飾的location區塊,就繼續再從有用~*修飾的location區塊進行正規表示式的匹配。而使用正規表示式來匹配時,最先被匹配成功的location區塊會被使用。

如果正規表示式都匹配失敗,就會使用先前找出的那個能夠匹配到最長前綴但完全無修飾的location區塊。

所以在撰寫location命令的時候,路徑的匹配順序是很重要的,它會一定程度地影響網站的反應速度。

再來要介紹幾個在location區塊中比較重要的命令:

  • auth_basic:設定HTTP基本認證(帳密認證)。參考這篇文章
  • deny:禁止指定的客戶端IP或客戶端IP所在CIDR進行訪問。設定成all表示要全部禁止,通常會於deny all;前再搭配allow來用。deny命令還可以被用在httpserver區塊中。
  • allow:允與指定的客戶端IP或客戶端IP所在CIDR進行訪問,以免其因之後的deny命令而被禁止存取。還可以被用在httpserver區塊中。
  • expires:設定HTTP回應標頭中,Cache-Control欄位的max-age參數,即資源的快取時間(秒),可以讓客戶端不會為了還在快取中的資源發送請求。或者可以設定為負數,表示要在HTTP回應標頭中明確加上Cache-Control: no-cache,讓客戶端至少在使用快取的資源時會發送請求來驗證。不設定的話是off(不使用Cache-Control標頭欄位)。expires命令還可以被用在server區塊中。
  • try_filestry_files命令的前幾個參數要傳入檔案或目錄路徑,最後一個參數是轉址的路徑。Nginx會從前幾個參數由左到右去判斷檔案或目錄存不存在,如果存在就回應該檔案或目錄;如果都不存在,就會使用最後一個參數來轉址。最後一個參數也可以是=狀態碼,表示要用指定的狀態碼來回應,而不去轉址。try_files命令還可以被用在server區塊中。

ifreturn等流程控制的命令和變數(variable)也常被用於location區塊甚至是server區塊中。if命令用來進行條件判斷,來決定要伺服器在處理請求時走哪個流程;return命令的用法可以參考這篇文章

最簡單的if判斷方式如下:

if ($var) {
    ...
}

以上設定,若$var不為空字串""或是"0"if區塊內的命令就會被執行。

if命令也可以進行如location命令的完全匹配和正規表示式匹配的動作,如下:

if ($var = 123) {
    ...
}

if ($var ~ ^(1|2)3$) {
    ...
}

if ($var ~* abc) {
    ...
}

加上!表示要將判斷結果反向。如下:

if ($var != 123) {
    ...
}

if ($var !~ ^(1|2)3$) {
    ...
}

if ($var !~* abc) {
    ...
}

if命令還支援如Linux系統的test命令那樣的判斷,能判斷檔案是否存在(-f)、目錄是否存在(-d)等。如下:

if (-f $var) {
    ...
}

if (!-d $var) {
    ...
}

至於Nginx的變數,常用的有以下幾個:

  • $uri:請求的路徑,已被正規化,僅包含路徑。
  • $request_uri:原始的請求路徑,包含查詢(query)。
  • $scheme:請求使用的通訊協定,http或是https
  • $remote_addr:客戶端的IP位址。
  • $server_addr:請求是從哪個IP位址的網路介面進來的。
  • $request_method:請求方法。例如GETPOST
  • $http_xxx:請求標頭中的xxx欄位。例如User-Agent標頭欄位就是$http_user_agent
  • $cookie_xxx:請求所帶的Cookie中的xxx欄位。
  • $server_nameserver_name命令的第一個參數值。
  • $host:若$http_host存在,則$host$http_host的去除連接埠外加轉小寫的結果。若$http_host不存在,則$host的值為$server_name

location區塊中也可以再加入location區塊。我們甚至還可以直接對location區塊「取名字」。例如:

location @home {
    expires 1d;
    rewrite ^ /index.html;
}

如此一來,在填寫轉址的路徑時,可以直接使用這個名稱,例如:

server {
    error_page 404 @home;

    location @home {
        rewrite ^ /index.html;
    }
}
正規表示式的利用

server_name命令是使用正規表示式時,可以將正規表示式的群組內容保留給之後的命令來使用,例如:

server {
    server_name ~^(www\.)?(?<domain>.+)$;

    location / {
        root /sites/$domain;
    }
}

location命令也是一樣,例如:

location ~ ^/api/v(?<number>\d+) {
    root /api/$number;
}
反向代理(Reverse Proxy)

location區塊中,利用proxy_pass命令,可以讓Nginx把請求透過URI,交給其它的伺服器來處理。用法如下:

server {
    error_page 404 = @fallback;

    location @fallback {
        proxy_pass http://127.0.0.1:3000;
    }
}

以上設定,當Nginx伺服器遇到無對應檔案的請求時,就會把請求交給127.0.0.1:3000這個另外的伺服器來處理。這邊error_page命令之所以會多用一個=,是為了要讓另外的伺服器可改變原先的HTTP狀態。

另外,為了讓被代理的伺服器工作正常,我們也會需要再傳遞請求的時候,去修改請求的標頭欄位,以保留客戶端的IP位址以及Host欄位等。可以直接引用/etc/nginx/proxy_params設定檔來完成這件事。如下:

server {
    error_page 404 = @fallback;

    location @fallback {
        proxy_pass http://127.0.0.1:3000;
        include proxy_params;
    }
}

為了避免被代理的伺服器也在進行HTTP回應時去做Gzip等壓縮的動作,可以考慮在/etc/nginx/proxy_params設定檔中加上:

proxy_set_header Accept-Encoding "";

一般來說,被代理的伺服器和Nginx伺服器的距離並不會太遠(或許根本就在同一台電腦上),所以Nginx在與被代理的伺服器連線時不會啟動HTTP的Keep-Alive機制,甚至Nginx還會自動在請求的標頭中再添加Connection: close,來明確關閉Keep-Alive,避免Keep-Alive連線長時間存在而佔用記憶體。如果還是想啟動Keep-Alive機制的話,可以參考下一小節的作法。

而若被代理的伺服器和Nginx伺服器是在同一個Ubuntu Server作業系統(或是同一個Unix-like作業系統)中,我們可以優先考慮使用Unix Domain Socket(簡稱UDS,或稱IPC Socket)來實現不同伺服器行程間的通訊,比起用TCP Socket透過loopback網路介面來連線,有著更低的延遲(latency)並和更高的吞吐量(throughput)。若UDS的檔案路徑是/path/to/socket,則Nginx的設定方式如下:

server {
    error_page 404 = @fallback;

    location @fallback {
        proxy_pass http://unix:/path/to/socket;
        include proxy_params;
    }
}

使用UDS時要注意Nginx必須要對UDS的檔案擁有讀取和寫入的權限。

值得注意的是,當location區塊會將請求「pass」給其它服務來處理時(即不限於使用proxy_pass命令),且它是以=進行完全匹配,或是指定路徑前綴來匹配的話,如果匹配樣本最後是以/來結尾,則等同自動再加上一個完全匹配路徑無/結尾的location區塊,用以進行301轉址,將無/結尾的路徑轉成有/結尾的路徑。什麼意思呢?例如以下這個設定:

server {
    location /foo/ {
        proxy_pass http://unix:/path/to/socket;
        include proxy_params;
    }
}

等同於:

server {
    location = /foo {
        return 301 /foo/;
    }

    location /foo/ {
        proxy_pass http://unix:/path/to/socket;
        include proxy_params;
    }
}

不過如果「pass」命令是寫在if區塊內,就不會自動新增301轉址的location區塊。

另外,使用了proxy_pass命令把請求給其它伺服器來處理的location區塊,若沒有「取名」或是沒有使用正規表示式來匹配路徑的話,則在使用proxy_pass命令時,可以在URI加上路徑,以改變請求的路徑。例如:

server {
    location /foo/ {
        proxy_pass http://unix:/path/to/socket:/bar/;
        include proxy_params;
    }
}

以上設定,可以把路徑為/foo,或是以/foo/為開頭的請求,在交給其它伺服器來處理時,將請求路徑的/foo/部份以/bar/來取代。注意由於這邊是使用UDS來通訊,在UDS檔案路徑與請求路徑間,需要使用冒號:隔開。

另外有些特定的伺服器程式可能會需要大量的執行時間,如果要讓該程式也能夠正常把結果回傳給Nginx,就要在Nginx設定檔中使用proxy_read_timeoutproxy_send_timeout命令來提高伺服器程式讀取和回應資料的間隔之逾時的時間。proxy_read_timeoutproxy_send_timeout命令可以被用在httpserverlocation區塊中,不設定的話是60s(60秒)。

反向代理的快取

Nginx在接收被代理的伺服器回應的資料時,可以將其快取成檔案,這樣下次如果又有一樣的請求,Nginx就可以直接從檔案系統中撈出來回應,不必再轉送給被代理的伺服器處理。

http區塊中使用proxy_cache_path命令,可以建立檔案快取存放的目錄,以及用以儲存鍵值(Key)的「Zone」。例如:

http {
    proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

    ...
}

以上設定,可以建立出一個名為my_cache的「Zone」,擁有10MiB的大小的共享記憶體空間,用以儲存被快取的資源的鍵值雜湊值以及請求次數。而被快取的資源則會儲存在檔案系統中的/path/to/cache目錄底下,另外擁有兩層子目錄。/path/to/cache目錄不需要事先被建立出來,因為它會在Nginx載入設定檔時被建立,但/path/to目錄要事先被建立。

levels最多可以擁有三層,值的格式為x[:y[:z]]x表示第一層子目錄的檔名長度,y表示第二層子目錄的檔名長度,z表示第三層子目錄的檔名長度。檔名是從快取的鍵值經過MD5雜湊值的十六進制(HEX)字串中,於尾端開始取固定長度的子字串來使用。舉例來說,若levels設為1:2,快取的鍵值經MD5雜湊後的十六進制字串為b7f54b2df7773722d382f4809d65029c,則這個快取檔案的存放路徑為c/29/b7f54b2df7773722d382f4809d65029c

max_size=10g用來限制/path/to/cache目錄底下的快取總大小最多只能有10GiB,超過的話就會從最久沒被使用的快取開始刪除。

inactive=60m用來設定快取在60分鐘內如果都沒被使用,就將其刪除。use_temp_path=off用來設定要快取的檔案不要事先存到proxy_temp_path再搬進快取(在無開啟反向代理快取時,被代理的伺服器所回應的資料會先被暫存至預設的或是指定的proxy_temp_path,在有啟用快取的情況下,實在沒必要再做這個動作)。

在想要啟用反向代理快取的location區塊,使用proxy_cache命令,就可以讓Nginx進行反向代理快取了。proxy_cache命令的參數要傳入一個「Zone」的名稱,例如:

http {
    proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

    server {
        error_page 404 = @fallback;

        location @fallback {
            proxy_cache_revalidate on;
            proxy_cache my_cache;
            proxy_pass http://unix:/path/to/socket;
            include proxy_params;
        }
    }
}

以上設定另外還啟用了proxy_cache_revalidate,可以讓Nginx在處理過期快取的時候去嘗試向被代理的伺服器確認快取的有效性,而不是直接讓被代理的伺服器重新處理請求。

利用proxy_cache_use_stale命令,當被反向代理的伺服器無法正常處理請求(有錯誤或是逾時),或是回應了如500 (Internal Server Error)的錯誤狀態碼時,Nginx還可以把上一次快取到但是過期的結果回應給客戶端。例如:

http {
    proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

    server {
        error_page 404 = @fallback;

        location @fallback {
            proxy_cache_use_stale error timeout invalid_header http_500 http_502 http_503 http_504;
            proxy_cache_revalidate on;
            proxy_cache my_cache;
            proxy_pass http://unix:/path/to/socket;
            include proxy_params;
        }
    }
}

proxy_cache_use_stale命令也可以在參數加上updating,讓快取正在被更新時,也依然去使用過期的快取。而若加了updating,則應該要再啟用proxy_cache_background_update,讓Nginx可以在背景更新過期的快取。例如:

http {
    proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

    server {
        error_page 404 = @fallback;

        location @fallback {
            proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
            proxy_cache_background_update on;
            proxy_cache_revalidate on;
            proxy_cache my_cache;
            proxy_pass http://unix:/path/to/socket;
            include proxy_params;
        }
    }
}

另外還有proxy_cache_min_uses命令可以設定資源至少要被請求幾次才會真的快取到檔案系統中。不設定的話是1。增加這個值表示要讓更頻繁被存取的資源更能留在快取中。例如:

http {
    proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

    server {
        error_page 404 = @fallback;

        location @fallback {
            proxy_cache_min_uses 3;
            proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
            proxy_cache_background_update on;
            proxy_cache_revalidate on;
            proxy_cache my_cache;
            proxy_pass http://unix:/path/to/socket;
            include proxy_params;
        }
    }
}

對於沒有在HTTP標頭內設定快取的回應,我們也可以利用proxy_cache_valid命令來快取它,不過不是很建議這樣做。proxy_cache_valid命令的前幾個參數是HTTP狀態碼,最後一個參數是要快取的時間(秒),即表示要對HTTP狀態碼為前幾個參數的回應做快取。如果只有傳入一個時間給proxy_cache_valid命令,則會套用給200、301和302狀態碼;如果狀態碼是傳入any,即代表所有狀態碼。例如:

http {
    proxy_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;

    server {
        error_page 404 = @fallback;

        location @fallback {
            proxy_cache_valid 200 60m;
            proxy_cache_min_uses 3;
            proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;
            proxy_cache_background_update on;
            proxy_cache_revalidate on;
            proxy_cache my_cache;
            proxy_pass http://unix:/path/to/socket;
            include proxy_params;
        }
    }
}

若想控制啟用快取的時機以及重新整理(refresh)快取的時機,可以利用proxy_cache_bypassproxy_no_cache命令。這兩個命令都可以傳入多個字串,以空格隔開。若傳入proxy_cache_bypass命令的字串有至少一個不是空字串""或是"0",則Nginx就不會使用快取中的資料來回應,但還是會把新拿到的回應存入快取中;若傳入proxy_no_cache命令的字串有至少一個不是空字串""或是"0",Nginx還是可以使用快取中的資料來回應,但如果需要去向被代理的伺服器拿新的回應,該回應就不會被存入快取中。

如果要刪除快取,可以直接刪除檔案系統中,存放快取資料的目錄下的檔案以及目錄。舉例來說,如果想刪除所有快取,可以執行以下指令(請小心使用,有誤刪到其它檔案的危險性):

sudo sh -c 'rm -rf /data/cache/*'

快取用的鍵值可以透過proxy_cache_key命令來設定,預設類似是:

proxy_cache_key $scheme$proxy_host$request_uri;

如果是用HTTP標頭欄位的訊息來辨識不同的客戶端,就可能要改用如下的設定(以Authentication標頭來舉例):

proxy_cache_key $scheme$request_uri$http_authentication;
反向代理的負載平衡(Load Balancing)

上面介紹的反向代理,是直接透過URI去連結某個被代理的伺服器。其實我們可以在http區塊下,透過upstream命令建立出「被代理的伺服器的群組」,讓Nginx能夠「選擇」要把請求交給這群組中的哪個伺服器處理。Nginx要做的事其實就是負責接收客戶訂單,交給「被代理的伺服器的群組」中的「上游廠商」進行生產,再把成品拿回來出貨給客戶,這也就是要稱這些伺服器為upstream(上游)的原因了。

Nginx的負載平衡方法(非Nginx Plus)主要有以下幾種:

  • 循環(Round-robin):輪詢那些在上游群組中的伺服器,並依照權重來決定一個伺服器要處理幾個請求後才會換下一個。
  • 最小連接(Least-connected):選擇目前連接數最少的伺服器來用。如果有多種選擇,就使用上面的循環(Round-robin)方式來從中再挑。
  • IP雜湊(IP-hash):根據客戶端的IP來決定要用哪個伺服器,可以保證同一個IP選到同一個伺服器。

如果選到的上游伺服器請求處理失敗,就繼續找可用的下一個伺服器。如果都處理失敗,客戶端就會取得最後一次處理的結果。

以下是一個簡單的負載平衡範例:

http {
    upstream myapp1 {
        server srv1.example.com;
        server srv2.example.com;
        server unix:/path/to/socket;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://myapp1;
            include proxy_params;
        }
    }
}

由於以上設定並未在名為myapp1upstream區塊內定義Nginx要用哪種負載平衡方法,因此myapp1會使用預設的循環(Round-robin)方法。另外,若是原本在proxy_pass命令有傳入URI路徑的話,改成用upstream區塊時,依然可以在proxy_pass命令傳入路徑。

還可以進行如下更進階的設定:

http {
    upstream myapp1 {
        server srv1.example.com weight=5;
        server srv2.example.com max_fails=3 fail_timeout=30;
        server unix:/path/to/socket backup;
        server srv3.example.com down;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://myapp1;
            include proxy_params;
        }
    }
}

weight用來設定權重,也就是設定這個伺服器要處理幾次的請求才換下一個伺服器。不設定的話是weight=1

max_failsfail_timeout用來設定伺服器在某段時間(秒)內處理請求超過多少次後,就將它暫時禁用某段時間。不設定的話是max_fails=1 fail_timeout=10

backup用來指定作為備援的伺服器,備援伺服器只有在其它非備援的伺服器都被禁用時,才會被使用。down用來禁用指定的伺服器。

若在upstream區塊使用了least_conn命令,則會使用最小連接(Least-connected)作為負載平衡方法。如下:

http {
    upstream myapp1 {
        least_conn;
        server srv1.example.com;
        server srv2.example.com;
        server srv3.example.com;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://myapp1;
            include proxy_params;
        }
    }
}

若在upstream區塊使用了ip_hash命令,則會使用IP雜湊(IP-hash)作為負載平衡方法。如下:

http {
    upstream myapp1 {
        ip_hash;
        server srv1.example.com;
        server srv2.example.com;
        server srv3.example.com;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://myapp1;
            include proxy_params;
        }
    }
}

upstream區塊使用keepalive命令,可以設定對每個上游伺服器的最大Keep-Alive的連接數。另外還需要使用proxy_http_version命令將HTTP版本設成1.1(不設定的話是1.0),並用proxy_set_header命令將Connection: close標頭欄位移除。如下:

http {
    upstream myapp1 {
        keepalive 32;
        server srv1.example.com;
        server srv2.example.com;
        server srv3.example.com;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://myapp1;
            include proxy_params;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
        }
    }
}

套用Nginx的新設定

改寫Nginx設定檔並存檔後,新設定並不會立即套用給正在運行中的Nginx伺服器。

要讓Nginx伺服器重新讀取設定檔,最好先用以下指令檢查設定檔是否無誤,避免Nginx伺服器因為設定檔錯誤而中斷服務。

sudo nginx -t

ubuntu-server-nginx

接著再執行以下指令來重新載入Nginx的設定檔:

sudo nginx -s reload

ubuntu-server-nginx

網站根目錄的權限處理

Ubuntu Server的Nginx伺服器預設會使用/var/www/html作為網站根目錄,而這個/var/www/html目錄預設的擁有者和群組都是root,權限為0755(drwxr-xr-x)。

其實我們可以將網站根目錄的擁有者和群組設為Nginx工人行程用的擁有者和群組,甚至替該目錄加上SGID,讓這個目錄中底下被建立出來的檔案和目錄的群組,都與網站根目錄的群組一樣。若Nginx工人行程的擁有者和群組都是www-data,則可以使用如下的指令來修改網站根目錄的權限。

sudo chown www-data:www-data /var/www/html
sudo chmod 2775 /var/www/html

然後把我們常用的Linux一般使用者也加進Nginx工人行程用的群組中,指令如下:

sudo usermod -a -G www-data $USER

如此一來要維護網站根目錄下的檔案就會變得很方便了!