跳至主要内容

8.2 利用 Traefik 部署自己的映像檔儲存庫

購買完網域後,部署自己的映像檔儲存庫之前,我想介紹這一個非常好用的 反向代理伺服器,也是我最近不論是在個人的 Side Project 或是公司的專案都很常使用的工具。

原因是其對於 Docker 的支援度之高,基本上可以說是無腦使用,用起來非常的舒服。

什麼是反向代理伺服器?

我們可以看到下方的圖片:

反向代理伺服器示意圖

當我們透過網際網路要存取某個 Web 應用程式的服務時,會先經過一個守門員叫做 反向代理伺服器,將由它來幫我們去拿取服務的內容,而使用這個服務有什麼好處呢?

第一個,內容的 Cache,實現網站加速,反向代理伺服器可以檢視每一次的請求,並且設定 Cache 的機制,讓我們的 App 伺服器不需要每次都和資料庫做請求,大幅地降低 App 伺服器的負擔。

第二個,流量清洗,杜絕惡意攻擊,可以透過觀察單一守門員的紀錄,來整理出某些惡意的請求,並且進行封鎖,反向代理伺服器也支援白名單的功能,當我們發現某些請求過度頻繁時,可以直接封鎖來自該 IP 的請求某段時間。

第三個,隱藏IP位置,避免遭受攻擊,有一個守門員擋在最前面,在後面的所有服務都不需要公開 IP 位置到網際網路之中,等於是整個龐大的 Web 應用程式,只公開反向代理伺服器的 IP 位置以及 Port,這樣可以大幅度地降低被攻擊的風險。

第四個,負載平衡,避免伺服器過載,反向代理伺服器都具備基本的負載平衡功能,也就是其可以分配請求到比較閒的 App 伺服器,避免單一伺服器的工作負擔太重導致伺服器崩潰,放在 Docker 的世界中,Traefik 可以平均分配流量到同一個 Service 的容器中,非常好用。

在介紹完 反向代理伺服器 的好處後,我們直接開始使用 Traefik 部署自己的映像檔儲存庫吧,關於 Traefik 的詳細使用方式, Traefik 官方的手冊上都有非常詳盡的文件說明,本書只會針對部署流程上會使用到的參數以及指令解釋,不會詳細探討 Traefik 如何做到這些事情。

先釐清應用程式的架構

在部署前,可以先用簡單的紙筆記錄一下整個服務會需要使用到的映像檔,以最簡單地部署映像檔儲存庫來說,架構會像是下方的圖片一樣:

基礎架構示意圖

但在 Docker 映像檔篇 有提過,官方的映像檔儲存庫是沒有 UI 介面的,而我們可以使用開源的儲存庫 UI 一起加入這個架構之中,就會變成下圖:

加入 UI 介面後的架構

接著就可以建立 docker-compose.yml 並開始撰寫,準備部署這整個應用程式。

這邊因為映像檔儲存庫並不需要應付過多的請求,我們就先使用單台的伺服器搭配 Docker Compose 進行單體式部署,而後面的章節將會使用 Docker Swarm 來部署前後端分離的應用程式。

首先是 Traefik 服務的建立,如下面的檔案所示:

# docker-compose.yml

version: '3.9'

x-networks: &network
networks:
- registry

x-restart: &restart-always
restart: always

services:
proxy:
image: traefik:v2.8
container_name: traefik
<<: *network
<<: *restart-always
ports:
- 80:80
- 443:443
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./acme.json:/acme.json:rw
command:
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entrypoints.scheme=https
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --certificatesresolvers.letencrypt=true
- --certificatesresolvers.letencrypt.acme.httpchallenge=true
- --certificatesresolvers.letencrypt.acme.email=robert@5xcampus.com
- --certificatesresolvers.letencrypt.acme.storage=acme.json
- --certificatesresolvers.letencrypt.acme.httpchallenge.entrypoint=web

networks:
registry:
external: true

撇除下方關於 Traefik 指令的輸入,其餘的部分都是在 Docker Compose 篇 有提過的參數,有幾個有疑慮的部分,我會一個一個解釋。

在 volumes 參數中,/var/run/docker.sock:/var/run/docker.sock:ro 這段,代表的是 Traefik 也需要監聽 docker.sock 這個 Unix Socket 上的事件,來掌握同一個虛擬網路中是否有容器被建立或是或是移除,而最後面的 :ro 則代表了 read only,也就是把伺服器的 docker.sock 交給 Traefik 去監聽,但他只能讀取資訊,而不能夠修改內容。

./acme.json:/acme.json:rw 這段參數,則是 Traefik 一個非常厲害的功能,就是它會替您自動申請 SSL 的憑證,也就是在上網時,安全的網站旁邊都會有一個小鎖頭;而這個 acme.json 檔案,需要自己手動建立,並且將其權限設定為 600 來讓 Traefik 能夠寫入憑證資訊,結尾的 :rw 則是 read & write 都可以的意思。

root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# touch acme.json
root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# chmod 600 acme.json

Traefik 指令解釋

建立一個 entrypoint 叫做 『 web 』並且給予其 port 為 80。

--entrypoints.web.address=:80

建立一個 entrypoint 叫做 『 websecure 』並且給予其 port 為 443。

--entrypoints.websecure.address=:443

此行作用為將 http 協定自動轉至 https,也就是從 80 轉到 443。

--entrypoints.web.http.redirections.entrypoints.scheme=https

因為 Traefik 提供的支援不是只有 Docker,故在此處我們需要讓它知道提供服務的平台式 Docker。

--providers.docker=true

前面有提過 Traefik 會透過監聽 docker.sock 來檢查有沒有服務被建立,而這行的目的在於告訴 Traefik,有需要被 Traefik 接受的服務我們會自己加入參數,而不需要 Traefik 自動追蹤。

這麼做的好處在於,有時候我們並不需要所有的服務都被 Traefik 給追蹤,而我們只需要在想被追蹤的服務上加入 --traefik.enable=true 即可,後面會有示範。

--providers.docker.exposedbydefault=false

Let's Encrypt 是一個提供免費 SSL 憑證的網站,而這邊告訴 Traefik 我們要使用它。

--certificatesresolvers.letencrypt=true

Traefik 主要提供了三種不同的 SSL 憑證申請,這邊就是告訴 Traefik 我要走 httpchallenge 的憑證申請,至於不同的申請方式,大家都可以自己到 Traefik 的官方文件上找到。

--certificatesresolvers.letencrypt.acme.httpchallenge=true

這邊就是申請 SSL 憑證時需要附上的 Email,當憑證快到期時,就會發送 Email 通知您。

--certificatesresolvers.letencrypt.acme.email=robert@5xcampus.com

這行則是告訴 Traefik 説,關於 Let's Encrypt 的憑證儲存檔案,是 acme.json 這個檔案,而這個檔案我們已經預先建立好,並且用 volume 的方式放到容器內。

--certificatesresolvers.letencrypt.acme.storage=acme.json

這行則是告訴 Let's Encrypt 的 SSL 憑證申請的入口是走 『 web 』也就是 80 port 的入口。

--certificatesresolvers.letencrypt.acme.httpchallenge.entrypoint=web

這些都準備好的話,我們就可以先啟動 Traefik 這個服務了。

root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# docker compose up --detach <- 不換行
[+] Running 1/1
⠿ Container traefik Started 1.1s

接著透過伺服器的 IP 位置進入,會變成 404 page not found 的畫面,代表 Traefik 其實有成功的建立,只是目前還沒有任何服務啟動,所以沒有任何內容可以回應。

接著我們在 docker-compose.yml 的 service 內繼續加入映像檔儲存庫的服務。

# docker-compose.yml

version: '3.9'

x-networks: &network
networks:
- registry

x-restart: &restart-always
restart: always

services:
proxy:
...
registry:
image: registry:latest
container_name: registry
<<: *network
<<: *restart-always
volumes:
- registry-data:/var/lib/registry
labels:
- traefik.enable=true
- traefik.http.routers.registry-http.entrypoints=web
- traefik.http.routers.registry-https.entrypoints=websecure
- traefik.http.routers.registry-http.rule=Host(`您購買的網域`)
- traefik.http.routers.registry-https.rule=Host(`您購買的網域`)
- traefik.http.routers.registry-https.tls=true
- traefik.http.routers.registry-https.tls.certresolver=letencrypt
- traefik.http.middlewares.https-only.redirectscheme.scheme=https
- traefik.http.routers.registry-http.middlewares=https-only
- traefik.http.routers.registry-https.service=registry
- traefik.http.services.registry.loadbalancer.server.port=5000
- traefik.docker.network=registry

networks:
registry:
external: true

volumes:
registry-data:
external: true

這次我們專注在 labels 內的參數,對於 Traefik 的使用方式,是在容器上貼標籤,來幫助 Traefik 了解要對此服務執行何種功能。

這行指令的意思是告訴 Traefik 這個容器是需要被追蹤的,呼應到上一段介紹 Traefik 設定時,有提到過。

- traefik.enable=true

下面兩行指令中的 registry-http 以及 registry-https 是可以替換的命名,主要的用途是告訴 Traefik 這個服務的 registry-http 是走 web 這個 entrypoints,也就是走 80 port,而 registry-https 則是走 websecure 這個 entrypoints,也就是 443 port。

- traefik.http.routers.registry-http.entrypoints=web
- traefik.http.routers.registry-https.entrypoints=websecure

下面兩行則是延續上一段,告訴 Traefik 關於 registry-http 以及 registry-https 這兩個 router 的規則,後方可以填入自己購買的網域,也可以使用 subdomain,像是 registry-core.qqqaaazzz.online 這樣的網址也是可以的。

- traefik.http.routers.registry-http.rule=Host(`您購買的網域`)
- traefik.http.routers.registry-https.rule=Host(`您購買的網域`)

但我們需要先到管理 DNS 的網站設定網域的 A Record 指向目前在部署的這台機器,以 Cloudflare 為例:

指向 DigitalOcean 伺服器的 IP 位置

下面則是告訴 Traefik 我們有一個 middleware 叫做 https-only,並且我們把它掛在了 registry-http 這個 router 前面,意思是當我們今天通過 80 port 走 http 協定時,會強制幫我們轉到 https 協定的意思,這是屬於 Traefik 內建的 middleware,還有很多很有趣的功能,都可以自己去玩玩看。

- traefik.http.middlewares.https-only.redirectscheme.scheme=https
- traefik.http.routers.registry-http.middlewares=https-only

下面兩行指令的意思代表 registry-https 這個 router 因為是 443 port,所以預設是執行 https 協定,所以需要 SSL 的憑證,而這邊所用的憑證頒發則是一開始在 Traefik 就設定好的 letencrypt。

- traefik.http.routers.registry-https.tls=true
- traefik.http.routers.registry-https.tls.certresolver=letencrypt

從第一行開始解釋,我們替 registry-https 這個 router 命名了一個叫做 registry 的 service。

而第二行則告訴 Traefik registry 這個服務的 port 開在 5000。

最後一行則是告知 Traefik Docker 的虛擬網路名稱。

- traefik.http.routers.registry-https.service=registry
- traefik.http.services.registry.loadbalancer.server.port=5000
- traefik.docker.network=registry

接著我們就可以再次輸入 docker compose up --detach,Docker 就會自動在運行映像檔儲存庫的服務。

root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# docker compose up --detach <- 不換行
[+] Running 1/1
⠿ Container registry Started 1.1s

接著當我們直接連接 IP 位置時,就會自動觸發 Traefik 替填入的網域名稱註冊 SSL 憑證,也就是當我們去 cat acme.json 時,裡面會充滿了代表憑證的密鑰。

而這個時候我們就成功部署了屬於我們的自己的映像檔,可以隨便找一個本機的映像檔,並且將其重新 tag 成 『 您的網域名稱/映像檔名稱:tag 』,如下方示範,並且把它推出去!

$ docker image tag todo-list registry-core.qqqaaazzz.online/todo-list

$ docker image push registry-core.qqqaaazzz.online/todo-list:latest
The push refers to repository [registry-core.qqqaaazzz.online/todo-list]
33dac495015d: Pushed
aad85cda03d4: Pushed
5db4753ceee7: Pushed
f089e986c59a: Pushed
42335e5f5f2a: Pushed
36afbd63eabe: Pushed
2a2946ba46e3: Pushed
994393dc58e7: Pushed
latest: digest: sha256:05d683f0d5da346ccb7e1048aa030e99728b3c3e4947c7c0472f0fbe9171c73a size: 1992

推上之後,我們也能夠透過請求 API 的方式來確認回應 ( 像 Docker 映像檔篇 學到的請求方式 ),對著 https://registry-core.qqqaaazzz.online/v2/_catalog ( 您自己的網域 ) 做出 GET 請求,可以看到回應 Status: 200 OK,以下回應的內容如下方所示:

{
"repositories": [
"todo-list"
]
}

接著我們會發現目前的映像檔儲存庫沒有帳號密碼的保護,接著透過閱讀官方文件可以如何替儲存庫加上帳號密碼的功能,建立一個 auth 的資料夾,並且透過 httpd:2 這個映像檔建立 user 以及 password 的帳號密碼。

root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# mkdir auth
root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# docker container run --entrypoint htpasswd httpd:2 -Bbn user password > auth/htpasswd
# user 可以替換成您要的帳號
# password 可以替換成您要的密碼

接著我們先停掉映像檔儲存庫的容器。

root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# docker container rm --force registry
registry

接著修改 docker-compose.yml 成下面的格式。

# docker-compose.yml

version: '3.9'

x-networks: &network
networks:
- registry

x-restart: &restart-always
restart: always

services:
proxy:
...
registry:
image: registry:latest
container_name: registry
<<: *network
<<: *restart-always
volumes:
- registry-data:/var/lib/registry
- ./auth:/auth <- 新增
environment:
- REGISTRY_AUTH=htpasswd <- 新增
- REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm <- 新增
- REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd <- 新增
labels:
- traefik.enable=true
- traefik.http.routers.registry-http.entrypoints=web
- traefik.http.routers.registry-https.entrypoints=websecure
- traefik.http.routers.registry-http.rule=Host(`您購買的網域`)
- traefik.http.routers.registry-https.rule=Host(`您購買的網域`)
- traefik.http.routers.registry-https.tls=true
- traefik.http.routers.registry-https.tls.certresolver=letencrypt
- traefik.http.routers.registry-https.service=registry
- traefik.http.services.registry.loadbalancer.server.port=5000
- traefik.docker.network=registry

networks:
registry:
external: true

volumes:
registry-data:
external: true

接著我們再次啟動。

root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# docker compose up --detach <- 不換行
[+] Running 2/2
⠿ Container registry Started 2.0s
⠿ Container traefik Running 0.0s

並且在本機試試看推送映像檔是否需要帳號密碼。

$ docker image push registry-core.qqqaaazzz.online/todo-list:v2                                                                                                   
The push refers to repository [registry-core.qqqaaazzz.online/todo-list]
33dac495015d: Preparing
aad85cda03d4: Preparing
5db4753ceee7: Preparing
f089e986c59a: Preparing
42335e5f5f2a: Preparing
36afbd63eabe: Waiting
2a2946ba46e3: Preparing
994393dc58e7: Preparing
no basic auth credentials <- 被擋下來

接著我們就用之前學過的 docker login 『 您的映像檔網域 』 來登入。

$ docker login https://registry-core.qqqaaazzz.online                                                                                                           
Username: user
Password:
Login Succeeded

接著就可以正常的推送映像檔了,接著就要來處理 UI 的部分,這時候雖然有映像檔儲存庫,但沒有畫面;其實 GitHub 上面有許多開源的映像檔儲存庫 UI,每一個都非常漂亮,這邊我們選用的是 quiq/docker-registry-ui:0.9.4 這個映像檔作為 UI 呈現,我們一樣要把它加入 docker-compose.yml 檔案內,並接受 Traefik 的掌控!

# docker-compose.yml

version: '3.9'

x-networks: &network
networks:
- registry

x-restart: &restart-always
restart: always

services:
proxy:
...
registry:
...
ui:
image: quiq/docker-registry-ui:0.9.4
container_name: ui
<<: *restart-always
<<: *network
environment:
- TZ=Asia/Taipei
volumes:
- ./config.yml:/opt/config.yml:ro
labels:
- traefik.enable=true
- traefik.http.routers.ui-http.entrypoints=web
- traefik.http.routers.ui-https.entrypoints=websecure
- traefik.http.routers.ui-http.rule=Host(`您購買的網域`)
- traefik.http.routers.ui-https.rule=Host(`您購買的網域`)
- traefik.http.routers.ui-https.tls=true
- traefik.http.middlewares.https-only.redirectscheme.scheme=https
- traefik.http.routers.ui-http.middlewares=https-only
- traefik.http.routers.ui-https.tls.certresolver=letencrypt
- traefik.http.routers.ui-https.service=ui
- traefik.http.services.ui.loadbalancer.server.port=8000
- traefik.docker.network=registry

networks:
registry:
external: true

volumes:
registry-data:
external: true

可以看到 volumes 的部分,有掛載一個 config.yml 檔案到容器內,所以我們要先建立一個 config.yml 給這個 UI 做設定,詳細的內容每一個 UI 都不太一樣,可以自己找自己喜歡的,但不外乎就是要連接上您的映像檔儲存庫,這邊附上最簡單的設定檔。

root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# touch config.yml

# 以下為檔案內容
listen_addr: 0.0.0.0:8000
base_path: /
registry_url: http://registry:5000
registry_username: user
registry_password: password
cache_refresh_interval: 0
debug: false

接著不要忘了一樣要到 DNS 的管理處設定新的 A Record 到伺服器的 IP 位置喔!接著我們就可以透過 docker compose up --detach 的方式建立起 UI 的介面。

接著在瀏覽器上輸入您設定在 Traefik 上的 Host,就能看到下面的畫面,可以看到畫面中有 todo-list 這個映像檔,是我們剛剛嘗試推送映像檔時推上去的。

映像檔 UI 介面

恭喜你!利用前面所有和 Docker 有關的基本技術以及使用 Docker Compose 串連所有的服務並部署了第一個應用程式!而且還擁有了自己的映像檔儲存庫!

下面為整個應用程式的 docker-compose.yml 檔案:

version: '3.9'

x-networks: &network
networks:
- registry

x-restart: &restart-always
restart: always

services:
proxy:
image: traefik:v2.8
container_name: traefik
<<: *network
<<: *restart-always
ports:
- 80:80
- 443:443
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./acme.json:/acme.json:rw
command:
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --entrypoints.web.http.redirections.entrypoint.scheme=https
- --providers.docker=true
- --providers.docker.exposedbydefault=false
- --certificatesresolvers.letencrypt=true
- --certificatesresolvers.letencrypt.acme.httpchallenge=true
- --certificatesresolvers.letencrypt.acme.email=robert@5xcampus.com
- --certificatesresolvers.letencrypt.acme.storage=acme.json
- --certificatesresolvers.letencrypt.acme.httpchallenge.entrypoint=web

registry:
image: registry:latest
container_name: registry
<<: *network
<<: *restart-always
volumes:
- registry-data:/var/lib/registry
- ./auth:/auth
environment:
- REGISTRY_AUTH=htpasswd
- REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm
- REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd
labels:
- traefik.enable=true
- traefik.http.routers.registry-http.entrypoints=web
- traefik.http.routers.registry-https.entrypoints=websecure
- traefik.http.routers.registry-http.rule=Host(`registry-core.qqqaaazzz.online`)
- traefik.http.routers.registry-https.rule=Host(`registry-core.qqqaaazzz.online`)
- traefik.http.routers.registry-https.tls=true
- traefik.http.routers.registry-https.tls.certresolver=letencrypt
- traefik.http.routers.registry-https.service=registry
- traefik.http.services.registry.loadbalancer.server.port=5000
- traefik.docker.network=registry

ui:
image: quiq/docker-registry-ui:0.9.4
container_name: ui
<<: *restart-always
<<: *network
environment:
- TZ=Asia/Taipei
volumes:
- ./config.yml:/opt/config.yml:ro
labels:
- traefik.enable=true
- traefik.http.routers.ui-http.entrypoints=web
- traefik.http.routers.ui-https.entrypoints=websecure
- traefik.http.routers.ui-http.rule=Host(`registry.qqqaaazzz.online`)
- traefik.http.routers.ui-https.rule=Host(`registry.qqqaaazzz.online`)
- traefik.http.middlewares.https-only.redirectscheme.scheme=https
- traefik.http.routers.ui-http.middlewares=https-only
- traefik.http.routers.ui-https.tls=true
- traefik.http.routers.ui-https.tls.certresolver=letencrypt
- traefik.http.routers.ui-https.service=ui
- traefik.http.services.ui.loadbalancer.server.port=8000
- traefik.docker.network=registry

networks:
registry:
external: true

volumes:
registry-data:
external: true

接著我們下一個小節我們將部署在 Docker Compose 篇 有拿來當範例的前後端分離應用程式,而且將使用 Docker Swarm,一起來實戰練習吧!