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 一起加入這個架構之中,就會變成下圖:
接著就可以建立 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 為例:
下面則是告訴 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 這個映像檔,是我們剛剛嘗試推送映像檔時推上去的。
恭喜你!利用前面所有和 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,一起來實戰練習吧!