📋 目錄
辛苦寫好的 API,在自己電腦上跑得好好的,部署到伺服器卻冒出「Node 版本不一樣」、「npm install 失敗」、「port 被佔用」等一堆問題?Docker 把你的應用連同整個執行環境一起打包,無論在哪裡開機都長得一樣。這篇整理 Docker 的核心概念與最基本的部署流程。
前言:Docker 解決了什麼問題?
傳統部署的痛:開發環境與生產環境不一致。
你在 Mac M 系列上開發,用的是 Node.js 22。伺服器是 Ubuntu 20.04,裝的是 Node.js 18。同事的 Windows 環境又是另一套。你在自己機器上 npm install 裝了 200 個套件,他機器上裝了 190 個——功能一樣,但差了 10 個小版本,有時候就出事。
Docker 的解決思路:把「應用程式」和「執行環境」打包成一個可攜的「容器」。這個容器在哪裡開就長什麼樣,不會因為主機環境不同而出問題。
Docker 核心概念
容器(Container)vs 映像檔(Image)
- Image(映像檔):只讀的範本,相當於「類別」(Class)。描述了要怎麼組裝一個服務。
- Container(容器):Image 的執行個體,相當於「物件」(Object)。是真正在跑的服務。
你可以從同一個 Image 啟動多個 Container,它们完全獨立互不影響。
基本指令
# 從 Docker Hub 拉一個現成的 Image 下來
docker pull node:22-alpine
# 看看目前主機上有哪些 Image
docker images
# 用 node:22-alpine 啟動一個容器,進入互動模式
docker run -it node:22-alpine sh
# 進去之後可以用 node -v 確認版本
# 離開互動模式
exit
# 看目前有哪些容器在跑
docker ps
# 看所有容器(包括已停止的)
docker ps -a
# 刪除一個容器
docker rm <container_id>
Dockerfile:把你的應用包成 Image
這才是真正的重點——用 Dockerfile 描述你要怎麼打包自己的應用。
假設你有一個 Express API 專案,結構如下:
my-api/
├── package.json
├── src/
│ └── index.ts
└── tsconfig.json
你的 Dockerfile 會長這樣:
# Dockerfile
# 第一步:使用官方 Node.js Image 當作基礎
# alpine 版本體積最小(約 150MB vs 1GB+)
FROM node:22-alpine
# 在 Image 裡建立一個工作目錄
WORKDIR /app
# 把 package.json 先複製進去(為什麼先做這步?見下方解釋)
COPY package*.json ./
# 安裝依賴
RUN npm ci --only=production
# 把剩下的程式碼複製進去
COPY . .
# 編譯 TypeScript(如果有的話)
RUN npm run build
# 對外暴露的 port
EXPOSE 3000
# 啟動指令
CMD ["node", "dist/index.js"]
為什麼要先複製 package.json 再 npm install?
這個順序很重要。Docker 的 Image 層級(layer)會緩存,當你改動了 .gitignore 之類的小檔案,Docker 可以直接用之前 cache 的 node_modules,不需要重新下載所有套件。如果把 npm install 放在整個 COPY . . 後面,每次改 code 都會觸發重新安裝。
# ✅ 好的順序:code 變動不會觸發 npm install
COPY package*.json ./
RUN npm ci
COPY . .
# ❌ 壞的順序:每次改 code 都要重新下載所有套件
COPY . .
RUN npm ci
多階段建構(Multi-stage Build)
上述 Dockerfile 的問題是:最終 Image 會包含 source code、編譯器、和所有 development dependencies。實際上,生產環境根本不需要這些東西。
多階段建構可以讓最終 Image 只包含編譯好的產物:
# 第一階段:建構
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 第二階段:實際執行的 Image,乾淨最小
FROM node:22-alpine AS runner
WORKDIR /app
# 只複製第一階段建出來的產物
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
EXPOSE 3000
CMD ["node", "dist/index.js"]
最終 Image 從可能 1GB+ 降到 150MB 左右。
docker build 與 docker run
# 在專案根目錄建立 Image
# -t 是給 Image 命名,格式是 name:tag
docker build -t my-api:1.0.0 .
# 看看 Image 是否建立成功
docker images
# 啟動容器
# -d:在背景執行(detached)
# -p 3000:3000:把主機的 3000 port 對應到容器的 3000 port
docker run -d -p 3000:3000 --name my-api-container my-api:1.0.0
# 看看容器是否正常啟動
docker logs my-api-container
# 停止容器
docker stop my-api-container
# 刪除容器
docker rm my-api-container
# 刪除 Image
docker rmi my-api:1.0.0
Docker Compose:管理多個容器
真實應用很少只有一個服務——你可能有 API 服務、資料庫、快取服務。多個容器要用 Docker Compose 統一管理。
在專案根目錄建立 docker-compose.yml:
# docker-compose.yml
services:
# API 服務
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://db:5432/myapp
depends_on:
- db
restart: unless-stopped
# PostgreSQL 資料庫
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: admin
POSTGRES_PASSWORD: mysecretpassword
volumes:
# 把資料庫資料存在主機目錄,避免容器刪除後資料消失
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
postgres_data:
# 啟動所有服務
docker compose up -d
# 看服務狀態
docker compose ps
# 看所有 logs
docker compose logs -f
# 停止所有服務
docker compose down
# 停止並刪除資料卷(乾淨重來)
docker compose down -v
開發環境 vs 生產環境
開發環境
# docker-compose.dev.yml
services:
api:
build:
context: .
target: builder # 使用完整建構階段,保留 source code 和 devDependencies
ports:
- "3000:3000"
volumes:
- ./src:/app/src # 熱重載:程式碼改動即時生效
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://admin:mysecretpassword@db:5432/myapp
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: admin
POSTGRES_PASSWORD: mysecretpassword
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
# 用開發版本啟動
docker compose -f docker-compose.dev.yml up -d
生產環境
# 用正式版本啟動
docker compose -f docker-compose.yml up -d --build
常見問題
Q:Docker 會讓我的應用變慢嗎?
A:不會。Docker 容器直接跑在主機的作業系統上,沒有 VM 的效能損耗。少數情況下網路層會有微幅 overhead,但對多數網頁應用來說感受不到差異。
Q:Docker 和 VM(虛擬機)的差別是什麼?
A:VM 需要虛擬化整個硬體 + 作業系統,體積大(GB 等級)且啟動慢(分鐘)。Docker 容器共享主機 OS kernel,體積小(MB 等級)且啟動快(秒)。簡單比喻:VM 是把一間公寓連骨架一起租給你,Docker 是把公寓裡的一個房間租給你。
Q:生產環境用 Docker 夠嗎?
A:對小型專案和初創公司來說,Docker Compose + 一台 VPS 已經足夠。當流量變大、需要水平擴展(多台伺服器)時,會需要 Kubernetes(K8s)這類容器編排工具,但那已經是另一個層次的議題了。
Q:如何確保生產環境的容器不會被駭客入侵後直接取得主機權限?
A:執行容器時使用非 root 帳號、啟用 read-only root filesystem、限制容器的能力(capabilities)。這些是進階安全設定,可以在 Dockerfile 和 docker-compose.yml 中逐步加上。
總結:Docker 是現代部署的基礎建設
Docker 的核心價值只有一個:讓「它在我機器上能跑」變成「它在任何地方都能跑」。
這篇文章涵蓋的範圍:
- Docker Image 和 Container 的基本概念
- 如何寫 Dockerfile 把你的應用打包
- 多階段建構:讓 Image 更小更安全
- 用 Docker Compose 管理多個服務
- 開發環境與生產環境的分離
下一步,可以把之前 Prisma + Express 做的 CRUD API 包成 Docker 容器,然後部署到一台便宜的 VPS 上——親手完成一次部署經驗,比看十篇文章都更有用。
本篇是「前端工程師後端入門」系列第四篇。系列前三篇:《從 Node.js 原生 HTTP 到 Express 框架》、《RESTful API 設計:前端視角的 API 思維》、《Prisma ORM:前端工程師的第一堂資料庫課》。