Docker部署DevOps容器化後端開發前端教學

Docker 部署教學:前端工程師的容器化入門指南

如何把 Express API 部署上線?Docker 完整教學含 Dockerfile 寫法、多階段建構、Docker Compose。讓你的應用無論在哪都能穩定執行。

· 6 分鐘閱讀

辛苦寫好的 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:前端工程師的第一堂資料庫課》。