跳至主要内容

8.4 部署前後端分離應用程式

接著一樣使用 Traefik 搭配剛學完的 Docker Swarm 來部署前後端分離的應用程式!

而相同的方式,透過畫圖的方式把整個服務的架構給畫出來,雖然這個架構圖在 Docker Compose 篇 就有出現過,但我們還是可以把 Traefik 也加上去。

應用程式架構圖

這次採用的是 Docker Swarm 的模式,所以在有些設定上會稍顯不同。

這邊的 YAML 檔案要叫什麼都可以,我自己還是習慣叫做 docker-compose.yml ( 這邊要記得換到不同台的伺服器,同一台其實也可以,但就不要叫做 docker-compose.yml 而已 )

按照慣例還是需要建立一個 Traefik 的反向代理伺服器:

# docker-compose.yml

version: '3.9'

services:
proxy:
image: traefik:v2.8
networks:
- frontend
- backend
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
- --providers.docker.swarmmode=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
deploy:
replicas: 1
restart_policy:
condition: on-failure
placement:
constraints: [node.id == 您的固定節點 ID]

networks:
frontend:
external: true
backend:
external: true

這裡關於 Traefik 還有個稍微不一樣的地方就是下方這段指令,就是告訴 Traefik 這個是一個 Swarm 的模式。

--providers.docker.swarmmode=true

還有一個要注意的地方,就是這裡的 network 要記得用 --driver overlay 的方式來建立,因為現在已經是在 Swarm 的模式內了!

root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# docker network create --driver overlay frontend
j7bjkvvpljxx

root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# docker network create --driver overlay backend
8haow1vp9xq0

接著使用學過的 docker stack deploy 的方式來先送出 Traefik 這個服務。

root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# docker stack deploy --compose-file docker-compose.yml todo
Creating service todo_proxy

接著用 docker service log --follow 的方式來確認 Traefik 有確實的運作。

root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# docker service logs --follow todo_proxy
todo_proxy.1.azgrwa554qor@ubuntu-s-1vcpu-512mb-10gb-sgp1-03 | time="2022-10-13T16:13:50Z" level=info msg="Configuration loaded from flags."

接著再來運作資料庫以及 Redis 的服務,這在 Swarm 模式下就相對的單純,因為在 Docker Swarm 篇 我們就有提過,像這種需要儲存資料的服務,只能強迫其運作在某個節點,或是利用外部服務。

# docker-compose.yml

version: '3.9'

services:
proxy:
...
redis:
image: redis:7-alpine
networks:
- backend
volumes:
- redis-data:/data
deploy:
replicas: 1
restart_policy:
condition: on-failure
labels:
- traefik.http.services.redis.loadbalancer.server.port=6379
placement:
constraints: [node.id == 您的固定節點 ID]

database:
image: postgres:14-alpine
networks:
- backend
volumes:
- database-data:/data
environment:
- POSTGRES_PASSWORD=您的資料庫密碼
deploy:
replicas: 1
restart_policy:
condition: on-failure
labels:
- traefik.http.services.postgres.loadbalancer.server.port=5432
placement:
constraints: [node.id == 您的固定節點 ID]

volumes:
redis-data:
database-data:

networks:
...
backend:
external: true

接著一樣透過 docker stack deploy 的方式來部署 Redis 以及 PostgreSQL 兩個服務。

root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# docker stack deploy --compose-file docker-compose.yml todo <- 不換行
Creating service todo_database
Updating service todo_proxy (id: tvc5zjt4f88aziswhk3blnkzw)
Creating service todo_redis

接著養成好習慣,自己用 docker service logs 的方式確認服務的運作情況。

接下來都沒問題後,我們就要來部署後端的 API 應用程式了!

# docker-compose.yml

version: '3.9'

services:
proxy:
...
redis:
...
database:
...
api:
image: robeeerto/todo-list-api:production
env_file:
- ./.env
networks:
- backend
deploy:
replicas: 3
restart_policy:
condition: on-failure
labels:
- traefik.enable=true
- traefik.http.routers.api-http.entrypoints=web
- traefik.http.routers.api-https.entrypoints=websecure
- traefik.http.routers.api-http.rule=Host(`您購買的網域`)
- traefik.http.routers.api-https.rule=Host(`您購買的網域`)
- traefik.http.routers.api-https.tls=true
- traefik.http.routers.api-https.tls.certresolver=letencrypt
- traefik.http.middlewares.https-only.redirectscheme.scheme=https
- traefik.http.routers.api-http.middlewares=https-only
- traefik.http.routers.api-https.service=api
- traefik.http.services.api.loadbalancer.server.port=3000

volumes:
...

networks:
frontend:
external: true
backend:
external: true

這邊有用到 --env-file 來傳入環境變數,使用的方式就是把本地端真實存在的檔案放入服務內,下方為環境變數的內容:

# .env

DB_HOST=database <- services 的名字
DB_USER=postgres <- postgres 的預設使用者名稱
DB_PORT=5432 <- postgres 的預設 port
DB_PASSWORD=您傳入 postgres 的密碼
RAILS_ENV=production
REDIS_HOST=redis <- services 的名字
CABLE_REDIS_URL=redis://redis:6379 <- redis 的路徑

其實也可以用 config 的方式來放到指定的路徑並形成 .env 的檔案,但這都端看自己設計的應用程式是用什麼樣的方式來讀取環境變數,而用 --env-file 算是最簡單的方式,會讓服務在啟動時就擁有環境變數。

接著透過 docker stack deploy 的方式來部署 API 的服務。

root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# docker stack deploy --compose-file docker-compose.yml todo <- 不換行
Updating service todo_database (id: qi3jx1cr19qn1r5b9o52y4gt2)
Updating service todo_proxy (id: tvc5zjt4f88aziswhk3blnkzw)
Updating service todo_redis (id: woxm7k993ak9k5t8sz1igoqo0)
Creating service todo_api

一樣透過 docker service logs 的方式確認服務的運作是否正常,接著就可以透過任何請求工具 ( Postman / curl / Thunder client 等等 ) 去確認建立起來的 API 服務是不是有用,可以使用 GET 方法去請求 **https://您自己的網域/events**,會得到 Status 200 OK。

再來就是最後一步部署前端畫面,如下面所示:

# docker-compose.yml

version: '3.9'

services:
proxy:
...
redis:
...
database:
...
api:
...
ui:
image: robeeerto/todo-list-ui:production
configs:
- source: ui-env
target: /app/.env.local
environment:
- API_HOST=api
networks:
- frontend
- backend
deploy:
replicas: 1
restart_policy:
condition: on-failure
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.routers.ui-https.tls.certresolver=letencrypt
- traefik.http.middlewares.https-only.redirectscheme.scheme=https
- traefik.http.routers.ui-http.middlewares=https-only
- traefik.http.routers.ui-https.service=ui
- traefik.http.services.ui.loadbalancer.server.port=3001

configs:
ui-env:
external: true

volumes:
...

networks:
...

這邊就有實際的使用到 config 物件來傳入環境變數,其實一樣可以用 --env-file 的方式,但基於範例可以看到越多越好,就一部分改成用 config 的方式,因為 external 的設定關係,需要先建立 config 物件。

下方為 UI 服務的環境變數:

# .env.local

NEXT_PUBLIC_CABLE_URL=wss://您的 API 網域名稱/cable
NEXT_PUBLIC_API=https://您的 API 網域名稱

接著就是建立 config 物件:

root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# docker config create ./.env.local ui-env
r8b90ssgn8ee141tskvyx8oct

最後還是透過 docker stack deploy 的方式來部署 API 的服務。

root@ubuntu-s-1vcpu-512mb-10gb-sgp1-01:~# docker stack deploy --compose-file docker-compose.yml todo <- 不換行
Updating service todo_database (id: qi3jx1cr19qn1r5b9o52y4gt2)
Updating service todo_proxy (id: tvc5zjt4f88aziswhk3blnkzw)
Updating service todo_redis (id: woxm7k993ak9k5t8sz1igoqo0)
Updating service todo_api (id: xkj7it9h2u6n0ifad4o0pkftj)
Creating service todo_ui

確認服務正常啟動後,就是打開瀏覽器輸入您註冊的網域,就會看到下面的新增代辦事項畫面:

網站建立成功!

點擊新增按鈕可以新增代辦事項,而且可以看到每一個待辦事項新增時都來自不同的容器 ( 來源 DNS 為容器的 ID )。

新增代辦事項

有興趣的人可以打開兩個的瀏覽器視窗,感受一下搭配 Redis 建立的 WebSocket 即時傳訊的功能。

恭喜您從完全不懂 Docker 到分別使用 Docker Compose 以及 Docker Swarm 部署了應用程式!

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

# docker-compose.yml

version: '3.9'

services:
proxy:
image: traefik:v2.8
networks:
- frontend
- backend
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
- --providers.docker.swarmmode=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
deploy:
replicas: 1
restart_policy:
condition: on-failure
placement:
constraints: [node.id == 您的固定節點 ID]

redis:
image: redis:7-alpine
networks:
- backend
volumes:
- redis-data:/data
deploy:
replicas: 1
restart_policy:
condition: on-failure
labels:
- traefik.http.services.redis.loadbalancer.server.port=6379
placement:
constraints: [node.id == 您的固定節點 ID]

database:
image: postgres:14-alpine
networks:
- backend
volumes:
- database-data:/data
environment:
- POSTGRES_PASSWORD=robert
deploy:
replicas: 1
restart_policy:
condition: on-failure
labels:
- traefik.http.services.postgres.loadbalancer.server.port=5432
placement:
constraints: [node.id == 您的固定節點 ID]

api:
image: robeeerto/todo-list-api:production
env_file:
- ./.env
networks:
- backend
deploy:
replicas: 3
restart_policy:
condition: on-failure
labels:
- traefik.enable=true
- traefik.http.routers.api-http.entrypoints=web
- traefik.http.routers.api-https.entrypoints=websecure
- traefik.http.routers.api-http.rule=Host(`您購買的網域`)
- traefik.http.routers.api-https.rule=Host(`您購買的網域`)
- traefik.http.routers.api-https.tls=true
- traefik.http.routers.api-https.tls.certresolver=letencrypt
- traefik.http.middlewares.https-only.redirectscheme.scheme=https
- traefik.http.routers.api-http.middlewares=https-only
- traefik.http.routers.api-https.service=api
- traefik.http.services.api.loadbalancer.server.port=3000

ui:
image: robeeerto/todo-list-ui:production
configs:
- source: ui-env
target: /app/.env.local
environment:
- API_HOST=api
networks:
- frontend
- backend
deploy:
replicas: 1
restart_policy:
condition: on-failure
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.routers.ui-https.tls.certresolver=letencrypt
- traefik.http.middlewares.https-only.redirectscheme.scheme=https
- traefik.http.routers.ui-http.middlewares=https-only
- traefik.http.routers.ui-https.service=ui
- traefik.http.services.ui.loadbalancer.server.port=3001

configs:
ui-env:
external: true

volumes:
database-data:
redis-data:

networks:
frontend:
external: true
backend:
external: true

關於 Traefik 的設定其實都可以沿用,所以在這個章節就沒有多提,因為都和 8-2 利用 Traefik 部署自己的映像檔儲存庫 時相同。

在 Docker 的世界中還有很多資訊等待大家去發掘,這本書最主要的目的是希望新手能夠快速的使用 Docker 來部署應用程式,並且理解最基礎的概念,至於如何用更高規格的方式來維護一個應用程式,則是在學會最基本的部署後要繼續努力的課題,希望大家都能夠在開發和維運的路上學習到更多變得更厲害!