以 Docker Compose 建立 Node.js 全端開發環境(六) — Image 發布

Toki Lee
15 min readMar 31, 2023

在上個章節我們成功在本地端打包 Image ,接下來我將會提供個簡易的方式來發布 Image。

而在 Registry 的選擇上相當多元,除了 docker 官方之外,在 GitHub、 GitLab 上也提供了各式各樣的 Registry 服務,當然你也可以自己架設私人的 Registry,而這章節我就是要在 GitHub 提供的 Registry 上發布 Image 。

上個章節結尾:

這個章節目標:

Github Action

因為在上個章節我們已經把 Image 打包的過程完成了,所以這個章節我們只要寫好 Github Action 的部分就好,首先我們先在專案目錄下新增 .github/workflows/proj6-publish-images.yml 這個檔案,在這個目錄下的任何 yml 檔案將會按照裡面所寫的規則在正確的時機點執行 Action:

#.github/workflows/proj6-publish-images.yml
name: Create and publish a Docker image

# 設定 Action 在 proj6 的 tag 被推到倉庫時執行
on:
push:
tags:
- "proj6*"

# 設定環境變數
env:
DOCKER_REGISTRY: ghcr.io
DOCKER_USERNAME: ${{ github.actor }}
DOCKER_PASSWORD: ${{ secrets.GITHUB_TOKEN }}

jobs:
# 加入一個用來打包與發佈 Image 的 Job
build-and-push-docker-image:
# 指定 runner 的系統環境
runs-on: ubuntu-latest

# 使用 strategy 產生兩個 job 個別打包 frontend 與 backend
strategy:
matrix:
docker-config:
- dockerfile: my-project-6/apps/backend/docker/Dockerfile
name: backend
context: my-project-6/apps/backend
- dockerfile: my-project-6/apps/frontend/docker/Dockerfile
name: frontend
context: my-project-6/apps/frontend

# 允許 github action 讀取倉庫內容,並允許寫入 packages
permissions:
contents: read
packages: write

# job 中逐個要執行的任務
steps:
# 簽出倉庫的程式碼
- name: Checkout repository
uses: actions/checkout@v3

# 登入 github 的 registry 以便等等可以 push 打包完的 image
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ env.DOCKER_USERNAME }}
password: ${{ env.DOCKER_PASSWORD }}

# 計算等等需要在發布時要用的 meta 資訊
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.DOCKER_REGISTRY }}/${{ github.repository }}-${{ matrix.docker-config.name }}

# build docker 並且發佈到指定的 registry
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: ${{ matrix.docker-config.context }}
file: ${{ matrix.docker-config.dockerfile }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

因為我這個專案的範例是一個個放在倉庫中,所以這邊是針對 proj6 這個 tag 來驅動,並且針對 my-project-6 目錄下的內容進行打包,所以要看這個檔案的話,可能要回到上個目錄下才找得到。

大部分都蠻容易理解的,這邊只針對幾個部分說明:

  1. 因為我們選擇的是 Github 的 Registry,所以採用 ghcr.io ,如果你想發布到其他的 Registry 的話,就可以直接修改我們在這邊設定的三個環境變數:DOCKER_REGISTRYDOCKER_USERNAMEDOCKER_PASSWORD。
  2. 因為打包與發佈 Image 的過程在 frontend 與 backend 是相同的,所以會使用 Strategy 的 Matrix 來產生兩個相同的 job,以官方的範例來簡單解釋下 Matrix 的部分:
Github 官方說明截圖

我們逐步分解來看:

strategy:
matrix:
fruit: [apple, pear]
animal: [cat, dog]

# =>
# 1. { fruit: apple, animal: cat }
# 2. { fruit: apple, animal: dog }
# 3. { fruit: pear, animal: cat }
# 4. { fruit: pear, animal: dog }

Matrix 的基本功能來說就是矩陣,目前兩個維度,各有兩個值,產生的 job 數量就是 2 x 2,得到四個 jobs。那再來 include 的功能呢?

include 將會獨立於原本的 Matrix 所設定的變數,他用來擴展 job 的內容,原始的矩陣值不能取代,若沒能找到能擴展的組合,他將會創建新的組合,接下來逐步的結果如下:

strategy:
matrix:
fruit: [apple, pear]
animal: [cat, dog]
include:
- color: green

# =>
# 因為 color 不是原本 matrix 的 key 值,所以可以擴展每個組合

# 1. { fruit: apple, animal: cat, color: green }
# 2. { fruit: apple, animal: dog, color: green }
# 3. { fruit: pear, animal: cat, color: green }
# 4. { fruit: pear, animal: dog, color: green }

# ---------

strategy:
matrix:
fruit: [apple, pear]
animal: [cat, dog]
include:
- color: green
- color: pink
animal: cat

# =>
# 首先, color 並不是原始的矩陣內容,所以可以被覆蓋,而上一個結果來看,
# 因為 animal 是 cat,所以我們抓出 1 和 3 來覆蓋。

# 1. { fruit: apple, animal: cat, color: pink }
# 2. { fruit: apple, animal: dog, color: green }
# 3. { fruit: pear, animal: cat, color: pink }
# 4. { fruit: pear, animal: dog, color: green }

# ---------

strategy:
matrix:
fruit: [apple, pear]
animal: [cat, dog]
include:
- color: green
- color: pink
animal: cat
- fruit: apple
shape: circle

# =>
# 因為 shape 不是原本矩陣的內容所以可以擴展。
# 而因為 fruit 是 apple,所以我們抓出 1 和 2來擴展。

# 1. { fruit: apple, animal: cat, color: pink, shape: circle }
# 2. { fruit: apple, animal: dog, color: green, shape: circle }
# 3. { fruit: pear, animal: cat, color: pink }
# 4. { fruit: pear, animal: dog, color: green }

# ---------

strategy:
matrix:
fruit: [apple, pear]
animal: [cat, dog]
include:
- color: green
- color: pink
animal: cat
- fruit: apple
shape: circle
- fruit: banana

# =>
# 因為 fruit 是原本矩陣的內容不能覆蓋,所以增添新的一個組合 5。

# 1. { fruit: apple, animal: cat, color: pink, shape: circle }
# 2. { fruit: apple, animal: dog, color: green, shape: circle }
# 3. { fruit: pear, animal: cat, color: pink }
# 4. { fruit: pear, animal: dog, color: green }
# 5. { fruit: banana }

# ---------

strategy:
matrix:
fruit: [apple, pear]
animal: [cat, dog]
include:
- color: green
- color: pink
animal: cat
- fruit: apple
shape: circle
- fruit: banana
- fruit: banana
animal: cat

# =>
# 因為 fruit 和 animal 是原本矩陣的內容不能覆蓋,所以增添新的一個組合 6。

# 1. { fruit: apple, animal: cat, color: pink, shape: circle }
# 2. { fruit: apple, animal: dog, color: green, shape: circle }
# 3. { fruit: pear, animal: cat, color: pink }
# 4. { fruit: pear, animal: dog, color: green }
# 5. { fruit: banana }
# 6. {fruit: banana, animal: cat}

大概是這樣分解,其實這部分官方就有逐步說明了,只是也順便詳細的說明下而已,內容請參考:

按照上面的邏輯,我們現在可以知道,因為這邊使用 docker-config 提供了兩種組合,所以才會產生兩個 job 個別打包發布前端與後端的 Image。

3. 在最後一個 step 中,我們寫了這樣的內容:

- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: ${{ matrix.docker-config.context }}
file: ${{ matrix.docker-config.dockerfile }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

其實就跟我們使用 docker build 一樣,因為我們先前在 matrix 中指定好了變數,所以可以在這邊使用到,而這邊幾個參數:

  • context:指的是在哪個目錄進行 build
  • file:使用哪個 Dockerfile
  • push:如果為 true 的話就會推送到剛剛 login 的 Registry
  • tags 與 labels:這兩者則是在前面 docker/metadata-action@v4 產生好了。

4. 最後再稍微說明下,在上面有用到這三個變數:

  • github.actor
  • github.repository
  • secrets.GITHUB_TOKEN

分別是使用者的帳號、倉庫名稱、和用於驗證身份的 Token ,而這些變數都是 Github Action 自帶的,並不是需要自行設定的,而因為這此使用到的 Registry 就是 Github 本身的 Registry,自然使用到你自己的帳號就能登入。

我認為這算是使用 Github 與 Gitlab 提供的 Registry 方便的地方,你不需要產生多餘的一些 Token 或 Key ,對於想嘗試看看的新手,算是挺好入門。

發布 Image

如果以上的部分都完成了,當推送 proj6 這個 Tag 上去時,就會開始進行 Action 並最終打包成以下的結果,並且都是 proj6 的 tag:

而與前幾個章節類似,只是這次我們把 Image 都改為剛剛我們發布的 Image ,在目錄下新增個docker-compose.prod.yml

# docker-compose.prod.yml
version: '3'

volumes:
postgres-store:
services:
postgres:
image: postgres
container_name: prod-my-project-postgres
environment:
- POSTGRES_DB=$DATABASE_NAME
- POSTGRES_USER=$DATABASE_USERNAME
- POSTGRES_PASSWORD=$DATABASE_PASSWORD
volumes:
- postgres-store:/var/lib/postgresql/data
backend:
image: ghcr.io/tokileecy/blog-post-docker-compose-nodejs-backend:proj6
user: 1000:1000
container_name: 'prod-my-project-backend'
restart: unless-stopped
environment:
- NODE_OPTIONS=--max_old_space_size=2048
- ALLOW_CORS_ORIGIN=$ALLOW_CORS_ORIGIN
- DATABASE_URL=postgresql://$DATABASE_USERNAME:$DATABASE_PASSWORD@postgres:5432/$DATABASE_NAME?connect_timeout=300
ports:
- 8081:8081
depends_on:
- postgres
frontend:
image: ghcr.io/tokileecy/blog-post-docker-compose-nodejs-frontend:proj6
container_name: 'prod-my-project-frontend'
restart: unless-stopped
environment:
- NODE_OPTIONS=--max_old_space_size=2048
- REACT_APP_PUBLIC_BACKEND_URL=$PUBLIC_BACKEND_URL
ports:
- 3000:80

如果想確認結果,就對剛新增好的 prod執行 docker compose -f docker-compose.prod.yml up 就會從 ghcr.io pull 下你發布的 Image 並執行起來了。

總結

這個章節簡單的介紹了如何發布 Image 到 Github 上的 packages,其實主要的內容反而是在說明 Matrix 那部分,由於 Github Action 的工具非常多,基本上想要的內容在 Marketplace 都能找到,也蠻多官方的 Action,只要知道如何使用這些 Action 很多事情都能輕鬆完成。

--

--

Toki Lee

沒有技術上不可行,只是時間上做不到⋯