iT邦幫忙

2026 iThome 鐵人賽

DAY 25
0
佛心分享-IT 人自學之術

出發吧!後端菜鳥:30 天的後端學習紀錄系列 第 25 篇

Day 25|Node.js + PostgreSQL 一次啟動:Docker Compose

  • 分享至 

  • xImage
  •  

前言

前面已經陸續把 Node.js、Express、PostgreSQL 和 Docker 串了起來,到了這個階段,專案開始同時需要 API 和資料庫兩個服務。單獨啟動一個 Container 時,設定還算容易處理,但當服務開始增加,每個 Container 都有自己的 Image、Port、環境變數和資料保存方式,開發時就會需要記住越來越多指令與設定。

Docker Compose 可以處理這種情況。它可以把一個專案需要的多個服務,以及這些服務相關的設定集中放在同一份 compose.yaml 裡。之後啟動專案時,不需要再一個一個建立 Container,只要執行 Compose 指令,就能按照設定把整個開發環境建立起來。

今天就延續前面的 API 和 PostgreSQL 專案,看看 Compose 怎麼把這兩個服務放在一起,以及它們之間的 Network、Volume 和環境設定又是怎麼配合的。

為什麼需要 Compose?

假設現在的專案有一個 Express API,另外還需要一個 PostgreSQL 資料庫。如果完全使用 Docker 指令處理,就需要分別建立兩個 Container,還要記住各自使用什麼 Image、Port 怎麼設定、資料庫需要哪些環境變數,以及資料要怎麼保存。當服務只有兩個時可能還不算太困難,但之後加入 Redis 或其他服務,手動管理就會變得越來越繁瑣。

Compose 的做法,是把這些服務和設定集中寫進 compose.yaml,讓專案本身就保留一份完整的環境設定。先從最基本的結構開始,可以寫成下面這樣:

services:
  api:
    build: .

  db:
    image: postgres:17

services 裡面定義的就是目前這個專案需要的服務,而現在有 api 和 db 兩個服務。api 會使用目前專案的 Dockerfile 建立 API Image,db 則直接使用 PostgreSQL 官方提供的 Image。

設定完成後,只要執行下面的指令,Compose 就會依照這份設定建立並啟動服務:

docker compose up

原本需要分別處理不同 Container 的工作,現在可以集中交給 Compose 管理,之後即使服務數量增加,也只需要修改同一份設定檔。

API Container

前面已經準備好的 Dockerfile,現在可以交給 Compose 使用。這份 Dockerfile 負責描述 API Image 要使用什麼 Node.js 環境、如何安裝套件,以及 Container 啟動時要執行什麼指令。

FROM node:22

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD ["npm", "run", "dev"]

接著在 compose.yaml 裡使用 build 來建立 API,並透過 ports 把本機和 Container 的 Port 接起來:

services:
  api:
    build: .
    ports:
      - "3000:3000"

build: . 代表使用目前資料夾裡的 Dockerfile 建立 Image,而 3000:3000 則代表把本機的 3000 Port 對應到 Container 裡的 3000 Port。完成設定後,就可以透過下面的網址從本機存取 Container 裡正在執行的 API:

http://localhost:3000

這裡左邊的 3000 是電腦本機使用的 Port,右邊的 3000 則是 API 在 Container 裡監聽的 Port。Docker 會負責把兩邊接起來,所以瀏覽器才能透過 localhost:3000 找到 Container 裡的 Express。

PostgreSQL Container

API 有了之後,再把 PostgreSQL 加進同一份 Compose 設定。PostgreSQL 已經有官方 Image,所以不需要另外寫 Dockerfile,只要指定要使用的 Image 就可以。

services:
  api:
    build: .
    ports:
      - "3000:3000"

  db:
    image: postgres:17
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: notes

這裡的 db 是 PostgreSQL 服務的名稱,而 image: postgres:17 代表使用 PostgreSQL 17 的 Image。environment 則用來提供 PostgreSQL 啟動時需要的基本設定,包括資料庫使用者、密碼和建立的資料庫名稱。

現在 Compose 已經知道這個專案需要兩個服務,一個負責執行 Express API,另一個負責提供 PostgreSQL 資料庫。接下來就要處理兩個服務之間怎麼互相找到對方。

Network

當 API 和 PostgreSQL 都由同一個 Compose 專案管理時,Compose 會建立一個可以讓服務互相溝通的 Network。因此 API 連 PostgreSQL 時,可以直接使用 PostgreSQL 的服務名稱來建立連線。

目前 PostgreSQL 的服務名稱是 db,所以資料庫連線資訊可以寫成下面這樣:

postgresql://postgres:password@db:5432/notes

其中的 db:5432 代表連到 db 這個服務所提供的 PostgreSQL,而 db 這個名稱就是在 compose.yaml 裡定義的服務名稱。這也是為什麼在 Docker 裡跑 API 時,資料庫連線通常不會再寫成 localhost:5432。

當 API 和 PostgreSQL 都跑在 Docker 裡時,localhost 指的是 API 自己所在的 Container,因此 API 如果要找到另一個資料庫 Container,就需要使用 Compose 裡的服務名稱。這樣服務之間就能透過同一個 Network 互相找到,也不需要自己處理 Container 的 IP 位址。

Volume

服務之間可以正常溝通之後,還有一件事情需要處理,那就是 PostgreSQL 的資料要怎麼保存。Container 本身可以被停止、移除甚至重新建立,如果資料只存在 Container 裡,重新建立資料庫環境時就可能失去原本的內容。

因此 PostgreSQL 通常會搭配 Docker Volume,讓資料能夠獨立於 Container 保存。Compose 可以直接在設定檔裡宣告 Volume,並把它掛載到 PostgreSQL 存放資料的位置:

services:
  api:
    build: .
    ports:
      - "3000:3000"

  db:
    image: postgres:17
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: notes
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

這裡左邊的 postgres_data 是 Docker Volume,右邊的 /var/lib/postgresql/data 則是 PostgreSQL Container 裡存放資料的位置。透過這個設定,資料會保存到 Volume 裡,因此就算之後重新建立 db Container,只要這個 Volume 還存在,原本的資料就可以繼續使用。

也可以把 Container 和 Volume 想成兩個不同的角色,Container 負責提供可以執行程式的環境,Volume 則專門拿來保存需要長期留下來的資料。這也是資料庫通常需要搭配 Volume 的原因。

另外要注意,執行 docker compose down 時,Compose 會移除它建立的 Container,但預設不會刪除命名 Volume,所以 PostgreSQL 的資料通常還會保留。如果真的要連 Volume 一起刪除,才需要額外使用 docker compose down -v。

Environment Variables

現在 API 和 PostgreSQL 已經可以一起運作,但前面的設定還有一個問題,就是資料庫密碼直接寫在 compose.yaml 裡面。自己練習時這樣做很直觀,但當專案需要提交 Git、放進 GitHub,或交給其他人使用時,資料庫密碼、JWT Secret 和 API Key 這類敏感資訊都不適合直接寫在專案設定裡。

這時候就會用到 Environment Variables。它的概念很簡單,就是讓程式需要的設定從外部環境傳進去,Compose 可以先寫好「這個服務需要哪些變數」,真正的值則在啟動時提供。

例如目前的設定可以改成:

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: ${DATABASE_URL}
      JWT_SECRET: ${JWT_SECRET}

  db:
    image: postgres:17
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

像 ${DATABASE_URL} 和 ${JWT_SECRET} 這種寫法,代表 Compose 在啟動服務時,會從環境設定取得對應的實際值。這樣 compose.yaml 裡面只需要保留變數名稱,就不需要直接放進真正的密碼或 Secret。

這裡也可以先分清楚兩個容易混在一起的概念。Environment Variables 是「把設定傳進程式的方式」,.env 則是本機開發時很常拿來整理這些設定值的檔案,兩者並不是完全相同的東西。

.env

在本機開發時,可以建立一份 .env,把目前 Compose 需要的環境設定集中放在裡面:

POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
POSTGRES_DB=notes

DATABASE_URL=postgresql://postgres:password@db:5432/notes

JWT_SECRET=my-secret-key

這樣 compose.yaml 主要負責描述服務需要哪些設定,而 .env 則提供這些設定實際使用的值。Compose 看到 ${DATABASE_URL} 時,就可以使用 .env 裡對應的內容進行替換。

不過要注意的是,.env 裡面仍然可能包含真正的密碼和 Secret,所以把敏感資訊移到 .env,並不代表這些資訊就可以直接公開。一般情況下,會把 .env 加進 .gitignore,避免不小心提交到 Git:

.env

同時可以準備一份 .env.example,只留下需要哪些設定的資訊,讓其他人知道啟動專案前需要準備什麼:

POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DB=notes

DATABASE_URL=
JWT_SECRET=

這樣真正的敏感資訊留在自己的開發環境裡,而專案中仍然有一份清楚的設定範例可以參考。

把整個專案串起來

前面的設定都完成之後,就可以把整個專案整理成一份完整的 compose.yaml。這份設定會同時管理 API、PostgreSQL、Port、環境變數和 PostgreSQL 的 Volume。

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: ${DATABASE_URL}
      JWT_SECRET: ${JWT_SECRET}

  db:
    image: postgres:17
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB}
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

再搭配 .env 提供實際的環境設定:

POSTGRES_USER=postgres
POSTGRES_PASSWORD=password
POSTGRES_DB=notes

DATABASE_URL=postgresql://postgres:password@db:5432/notes

JWT_SECRET=my-secret-key

設定完成之後,啟動整個專案只需要執行:

docker compose up

如果希望 Container 在背景執行,也可以使用:

docker compose up -d

想查看目前 Compose 管理的服務,可以使用:

docker compose ps

完成開發後,如果只是想停止並移除目前建立的 Container,可以使用:

docker compose down

前面設定的 Volume 會繼續保留,因此下次重新執行 docker compose up 時,PostgreSQL 仍然可以使用原本保存的資料。

現在回頭看整個架構,就可以比較清楚地理解 Compose 在這個專案裡扮演的角色:

Docker Compose
│
├── API Container
│   └── Express
│
└── PostgreSQL Container
    └── Database
            │
          Volume

API ↔ PostgreSQL
       │
     Network

Environment Variables
       │
      .env

Compose 把原本分開處理的服務放到同一個環境裡,API 和 PostgreSQL 各自使用自己的 Container,兩個服務透過 Network 溝通,資料庫透過 Volume 保存資料,而環境設定則交給 Environment Variables 和 .env 管理。

結語

前面學 Docker 時,比較像是在學會怎麼建立和執行一個 Container。到了 Compose,開始可以從整個專案的角度去思考開發環境,API 和 PostgreSQL 可以一起管理,服務之間的連線方式、資料保存方式以及環境設定也都有了比較清楚的安排。

這樣做最大的好處,是把原本散落在不同 Docker 指令裡的設定集中到 compose.yaml,之後即使專案加入其他服務,也可以繼續沿著同樣的方式擴充。

最後啟動整個開發環境時,只需要:

docker compose up

原本需要分開管理的 API 和 PostgreSQL,現在已經可以組成一個完整的開發環境。對目前這個後端專案來說,Docker 也不再只是拿來「跑一個 Container」,而是開始成為管理整個開發環境的一部分。


上一篇
Day 24|我的電腦可以跑,為什麼你的不行?Docker 入門
系列文
出發吧!後端菜鳥:30 天的後端學習紀錄 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言