今天的重頭戲是把 EC2 裡的 Node.js 容器 → Private RDS 這一段接起來,讓程式真的查一次資料庫
動手前,EC2 要能正常執行前篇的容器,私有 PostgreSQL RDS 也要是 Available。如果練習後已刪掉資料庫,先依前面第十八天的 RDS 來重建,這次必須要用新 Endpoint
先把這兩個位置畫一下。DBeaver 在自己的電腦,所以我們透過 SSH,請 EC2 幫忙轉送到 RDS
那現在 Node.js 已經在 EC2 裡,就可以從那裡連 RDS
也提醒下同一個資料庫,發出連線的程式卻換了。DBeaver 存過的帳密、SSL 設定,不會自動跑進 Container 裡哦
圖中 RDS 仍留在 Private Subnet,Public access 選 No。DB Subnet Group 需要跨至少兩個 AZ 的子網路
回 AWS 網頁,區域切到東京,進入 RDS → Databases → 點自己的 DB 名稱 → Connectivity & security。新版畫面會先看到 Connect using,選 Endpoints,再展開下面的 Additional configurations:
| 畫面上的欄位 | 這次要看什麼? |
|---|---|
| VPC | 與執行 API 的 EC2 是同一個 VPC,核對 vpc-... ID |
| Publicly accessible | 維持 No |
| Endpoint/Port | 複製 Endpoint,Port 是 5432 |
| VPC security groups | 點進 DB 使用的 SG,看它的 Inbound rules |
DB 的 Inbound 要允許 PostgreSQL / TCP 5432,Source 是 EC2 實際掛載的 Web SG ID。不確定是哪一個就回到 EC2 → Instances → 選主機 → Security 來對照。同時 EC2 的 Outbound 也要允許連到資料庫
下面是前面建立 RDS 的過程,搬到 EC2 後,Public access 一樣保留 No,不用為了讓程式連線而改成 Yes
同 VPC 的這段私有連線不需要新增 NAT Gateway。因為已經沿用前面設定的路由與 NACL;如果你改過,排錯時也請核對下~
剛才複製的 Endpoint,就是程式要用的 DB_HOST。只填主機名稱,不加 https:// 或 :5432,Port 已經有在程式裡另外設定
假設你沿用本機開發設定,Host 還寫 localhost,它會找到誰?
對 Container 來說,那是它自己本身,所以當然找不到另一邊的 RDS。所以這裡要填 RDS Endpoint,也別把它換成當下查到的 IP,因為之後 AWS 維護或切換,IP 可能會變
先回到自己電腦的 demo-api 程式資料夾。延續前面的範例,執行:
npm init -y
npm install pg
假尺已經有 package.json,安裝 pg 就好。這個套件負責讓 Node.js 與 PostgreSQL 溝通
接著在 server.cjs 旁新增 db.cjs,把資料庫連線放在這裡。你會看到程式從 process.env 讀取 Host、帳密,這些值等部署時再提供,先不用把私人資料填進程式碼:
// db.cjs
const fs = require('node:fs');
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST,
port: 5432,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
connectionTimeoutMillis: 5000,
query_timeout: 5000,
ssl: {
ca: fs.readFileSync(process.env.DB_CA_FILE, 'utf8'),
rejectUnauthorized: true,
},
});
pool.on('error', (error) => {
console.error('idle database connection failed', error.code || 'NO_ERROR_CODE');
});
module.exports = pool;
有了連線設定,接下來就要讓 API 用它查資料。我們新增 /ready,收到請求後執行 SELECT 1,等資料庫回覆再回傳結果
先用這張圖看差別:上面的 /health 直接回應
下面的 /ready 也多走了右邊的資料庫這一段
接下來在自己電腦,把 server.cjs 換成下面完整版本:
const http = require('node:http');
const pool = require('./db.cjs');
http.createServer(async (req, res) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (req.method === 'GET' && req.url === '/ready') {
try {
await pool.query('SELECT 1');
res.writeHead(200);
res.end(JSON.stringify({ status: 'ready' }));
} catch (error) {
console.error('database check failed', error.code || 'NO_ERROR_CODE');
res.writeHead(503);
res.end(JSON.stringify({ status: 'not ready' }));
}
return;
}
const ok = req.method === 'GET' && req.url === '/health';
res.writeHead(ok ? 200 : 404);
res.end(JSON.stringify(ok ? { status: 'ok' } : { error: 'not found' }));
}).listen(8080, '0.0.0.0', () => console.log('API listening on 8080'));
關鍵就在 await pool.query('SELECT 1'),等查詢完成才往下執行。如果連線或查詢失敗,就進 catch,回傳失敗結果
所以 /ready 是我們自己寫出來的功能,後面讓 Docker 做健康檢查時,也會用到它~
程式改好了,但 EC2 跑的還是前篇那份 Image,要重新打包才會有剛才加的功能
先更新 Dockerfile。前一版只帶 server.cjs,現在多了 db.cjs 和 pg 套件,也要一起放進去:
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY server.cjs db.cjs ./
USER node
EXPOSE 8080
CMD ["node", "server.cjs"]
其中 npm ci 會照 package-lock.json 安裝套件,這個檔案記得保留。.dockerignore 沿用前篇,密碼與 .pem 都不放進 Image
在自己電腦的程式資料夾打包並上傳新版。沿用第 22 天的 ECR 登入方式與推送權限
docker buildx build --platform linux/arm64 --load -t demo-api:v2 .
docker tag demo-api:v2 "YOUR_REPOSITORY_URI:v2"
docker push "YOUR_REPOSITORY_URI:v2"
YOUR_REPOSITORY_URI 整段要換掉,例如換成自己 ECR 的 帳號ID.dkr.ecr.ap-northeast-1.amazonaws.com/demo-api。這三行在 PowerShell 也可以使用;如果 v2 已存在,就換未使用的新 Tag,後面的 Compose 也一起改
前面的 db.cjs 沒有直接寫帳密,而是用 process.env.DB_HOST 這些環境變數來讀取。所以 Image 上傳後,我們還要把這些值交給 EC2 裡的容器
接下來切回 SSH 登入 EC2 的 deployer 視窗,進入前篇的部署資料夾:
cd ~/demo-api-deploy
nano .env.app
填入自己的資料庫設定:
DB_HOST=自己的RDS端點
DB_NAME=自己的資料庫名稱
DB_USER=已存在的練習資料庫帳號
DB_PASSWORD='自己的資料庫密碼'
DB_USER 和 DB_PASSWORD 可以沿用 DBeaver 測通的 PostgreSQL 帳密
DB_NAME 則看 DBeaver 的 Database 欄位。如果之前填的是 postgres,這裡也填 postgres。別填成 AWS 網頁上的 DB instance identifier,那是 RDS 這台資源的名稱
密碼外面的單引號,能保留 $ 等字元;若密碼本身包含單引號,再依 Compose env 檔格式 處理
按 Ctrl+O、Enter、Ctrl+X 儲存離開,再執行:
chmod 600 .env.app
這是限制其他一般使用者讀寫這份檔案。帳密依然是明文,所以 .env.app 不要提交 Git,也不要放進截圖
還差一步,要在 Compose 指定 env_file。它會把 .env.app 裡的設定交給容器,程式才能透過 process.env 讀到
Image 負責帶程式與套件,資料庫位置與帳密則在部署時提供。之後換資料庫,改設定就好,不必為了帳密重新打包 Image

在 EC2 的 ~/demo-api-deploy,用 nano compose.yaml 編輯。Image 換成自己剛推的 v2,整份內容如下:
services:
api:
image: YOUR_ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/demo-api:v2
platform: linux/arm64
ports:
- "127.0.0.1:8080:8080"
restart: unless-stopped
env_file:
- .env.app
healthcheck:
test:
- CMD
- node
- -e
- "fetch('http://127.0.0.1:8080/ready').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
interval: 30s
timeout: 10s
retries: 3
start_period: 15s
logging:
driver: local
options:
max-size: "10m"
max-file: "3"
image 要填自己的完整 ECR 位址,Repository 名稱和 Tag 都要對上。env_file 指向剛才的 .env.app,只在主機上存好檔案還不夠,要加這行才會交給容器(Compose 環境變數)
8080 沿用前篇,只讓 EC2 自己連入;logging 則把紀錄限制為每份 10 MB、保留 3 份,避免一直堆在主機裡
前面已經寫好 /ready,可以讓 Docker 定期呼叫它,就不用每次自己打指令確認資料庫了
test 那行用容器裡的 Node.js 發出請求,成功就以 0 結束檢查,失敗則用 1。下面幾個設定決定多久檢查一次:
| 設定 | 這次的意思 |
|---|---|
interval: 30s |
每隔 30 秒檢查一次 |
timeout: 10s |
一次檢查最多等 10 秒 |
retries: 3 |
連續失敗 3 次,標記為 unhealthy |
start_period: 15s |
啟動初期先給 15 秒寬限 |
寬限期間仍會檢查;如果已成功過,後面的失敗就會開始計數~
把 compose.yaml 儲存後,在 EC2 的部署資料夾執行:
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps
第一行檢查格式,沒出現錯誤再繼續。接著下載新版 Image、啟動容器,最後看執行狀態。如果 pull 顯示 ECR 登入過期,就沿用前篇 EC2 Role 的登入方式,重新登入再拉取
這次有換 Image、加環境變數,要用 up -d 套用。只打 restart,不會把新設定帶進去
剛啟動可能會看到 health: starting,通過檢查後才會變成 healthy。我們也自己查一次:
curl -i http://127.0.0.1:8080/ready
docker compose logs --tail=50 api
其他問題可以先對照這張圖:

| 紀錄最後的代碼 | 先查哪裡? |
|---|---|
ENOTFOUND |
Endpoint 是否拼錯,DNS 能否解析 |
28P01 |
DB 帳號與密碼 |
3D000 |
DB_NAME 指定的資料庫是否存在 |
ETIMEDOUT/ECONNREFUSED |
Endpoint、5432、RDS 狀態、SG 與網路規則 |
NO_ERROR_CODE |
錯誤沒有提供代碼,先沿上圖核對;可能是連線或查詢逾時 |
今天接好的是 EC2 裡的程式 → 私有 RDS。接下來寫會員、訂單 API,就能沿用 Endpoint、帳密與 Compose 提供設定的方式
下一篇再往前接,用 DNS 與 Nginx 讓外面的請求找到這個 Container,我們下篇見 :D