Blog Details

Linux 作業系統 cutexyz > Blog > 伺服器管理 > 伺服器管理必學 100 招 > 伺服器管理必學 100 招|Part 9:Docker 容器化運維實戰

伺服器管理必學 100 招|Part 9:Docker 容器化運維實戰

前言
「在我的本機明明可以跑,為什麼放到伺服器上就壞了?」容器化技術徹底解決了環境相依性的難題,但當 Docker 進入生產伺服器時,若缺乏妥善治理,隨之而來的往往是硬碟被無窮日誌塞爆、Volume 權限衝突、孤兒容器佔用記憶體等新噩夢。本篇精選 10 個核心 Docker 運維技巧,帶你掌握生產等級的日誌截斷、資源邊界限縮、安全健康檢查與平滑滾動更新。

技巧 081:別讓容器日誌吃滿硬碟!全域配置 json-file 日誌大小限制

  • 痛點場景:
    Docker 預設的日誌驅動(json-file)沒有設定任何大小上限!一個高流量或狂印 Log 的容器,會在 /var/lib/docker/containers/ 底下悄悄長出一個 50GB 的 *-json.log,無預警擠爆宿主機硬碟。
  • 解決方案:
    在 /etc/docker/daemon.json 中配置全域日誌輪替上限:
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "50m",
    "max-file": "3"
  }
}
  • 重新載入 Docker 守護進程使其生效:
sudo systemctl reload docker
  • 這會強制每個容器最多只保留 3 個 50MB 的日誌檔,單一容器日誌消耗絕對不超過 150MB。
  • 專家提醒:
    修改 daemon.json 僅對後續新建的容器生效;既有容器需手動重建(docker compose down && docker compose up -d)。

技巧 082:告別幽靈資料!徹底理解 Bind Mount 與 Named Volume 的權限差異

  • 痛點場景:
    使用 Bind Mount(如 -v /host/app:/app)將宿主機目錄掛載進容器時,容器內寫入檔案導致擁有者變成 root,宿主機一般使用者無權修改;或是相反地,容器內程式因 Permission denied 直接啟動失敗。
  • 解決方案:
    生產資料庫與儲存優先使用 Named Volume:由 Docker 統一託管權限與掛載點,性能更好且跨平台一致。
services:
  db:
    image: mariadb:10.11
    volumes:
      - db_data:/var/lib/mysql
volumes:
  db_data:
  • 核心觀察若必須使用 Bind Mount,強制對齊 UID/GID:指標:
# 取得宿主機當前使用者的 UID 與 GID
id -u; id -g
  • 在啟動時指定執行身分:
docker run -u $(id -u):$(id -g) -v /data:/data myimage
  • 專家提醒:
    Named Volume 的實體資料存放在 /var/lib/docker/volumes/,定期備份伺服器時千萬別漏了這個目錄。

技巧 083:孤兒與垃圾清道夫!善用 docker system prune 安全回收空間

  • 痛點場景:
    伺服器頻繁進行 CI/CD 部署後,殘留大量虛懸映像檔(<none>:<none> 標籤)、停止的舊容器與未掛載的孤立 Volume,霸佔數十 GB 的磁碟空間。
  • 解決方案:
    使用系統清理指令釋放無用資源:
# 1. 檢視各類 Docker 資源所佔用的真實磁碟大小
docker system df

# 2. 安全清理:刪除所有停止的容器、未被使用的網路與懸空映像檔(Dangling Images)
docker system prune -f

# 3. 深度大掃除(小心使用!會連同目前未被任何運行中容器使用的所有映像檔一併刪除)
docker system prune -a --volumes -f
  • 專家提醒:
    加上 --volumes 參數會將「沒有任何容器綁定的 Named Volume」連根拔除,若你的停止容器中包含尚未掛載的資料庫 Volume,請務必先確認再執行!

技巧 084:容器也有邊界!在 Compose 嚴格限制 CPU 與 Memory 配額

  • 痛點場景:
    容器內部應用程式發生記憶體洩漏(Memory Leak),無限制吞噬宿主機實體記憶體,最後觸發系統 OOM Killer 把宿主機核心進程或其他關鍵服務殺死。
  • 解決方案:
    在 docker-compose.yml 中宣告資源配額上限(Deploy Resources):
services:
  web:
    image: my-node-app:latest
    deploy:
      resources:
        limits:
          cpus: '1.5'        # 最多只能使用 1.5 顆 CPU 核心
          memory: 1024M      # 記憶體上限 1GB,超標時僅該容器重啟
        reservations:
          memory: 256M       # 啟動時至少預留保證 256MB
  • 專家提醒:
    若使用原生 docker run 指令,可直接帶上 --memory="1g" --cpus="1.5" 參數,防止單一黑天鵝容器拖垮整台伺服器。

技巧 085:真假在線!配置容器內建健康檢查(HEALTHCHECK)

  • 痛點場景:
    容器內的 Node.js 或 Python 程式已經死鎖(Deadlock)或拋出 500 錯誤,但因 Process 本身尚未結束,Docker 依然判定容器為 Up (healthy),上層反向代理繼續把流量導進去。
  • 解決方案:
    在 Dockerfile 或 docker-compose.yml 中加入健康檢查探針:
services:
  api:
    image: my-api:latest
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"]
      interval: 30s    # 每 30 秒檢測一次
      timeout: 5s      # 逾時時間 5 秒
      retries: 3       # 連續失敗 3 次標記為 unhealthy
      start_period: 15s # 啟動緩衝期(防止啟動慢被誤殺)
  • 專家提醒:
    配合 Docker Swarm 或 Auto-heal 容器(如 willfarrell/autoheal

技巧 086:網路隔離防穿透!使用自訂 Bridge Network 切割架構邊界

  • 痛點場景:
    所有容器全部掛在預設的 bridge 網路上,導致任何一個容器只要被植入後門,即可在同一網段內直接掃描存取其他不應對外的內部資料庫或快取服務。
  • 解決方案:
    在 Compose 檔案中切割前後端獨立網路,資料庫不暴露連接埠給宿主機,僅在內網互通:
services:
  frontend:
    image: nginx:alpine
    networks:
      - web-tier
      - internal-tier

  database:
    image: mariadb:10.11
    # 不寫 ports: - "3306:3306",杜絕宿主機與外網直接存取
    networks:
      - internal-tier

networks:
  web-tier:
  internal-tier:
  • 專家提醒:
    自訂 Bridge Network 內建 DNS 服務發現功能,容器間可直接透過服務名稱(如 http://database:3306)互相解析,嚴禁在設定檔中寫死容器的動態 IP。

技巧 087:拒絕 root 身分跑容器!Dockerfile 最佳非特權實踐

  • 痛點場景:
    多數人直接在容器內以預設的 root 執行應用,若應用程式存在遠端程式碼執行(RCE)漏洞,攻擊者可能藉由容器逃逸(Container Escape)直接取得宿主機 root 權限。
  • 解決方案:
    在 Dockerfile 打包末尾建立非特權專用帳號,並切換身分:
FROM node:20-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .

# 建立專用 appuser,並轉移檔案擁有權
RUN addgroup -S appgroup && adduser -S appuser -G appgroup && \
    chown -R appuser:appgroup /app

# 切換為非特權帳號執行
USER appuser

EXPOSE 3000
CMD ["node", "server.js"]
  • 專家提醒:
    Alpine 映像檔自帶許多輕量使用者群組;切換 USER 之後,若應用程式需要綁定低位連接埠(如 Port 80),請改綁高位埠(如 3000 或 8080),再由外部 Nginx 轉導。

技巧 088:映像檔極致瘦身!多階段建置(Multi-Stage Builds)

  • 痛點場景:
    打包 Go 或前端 React/Vue 專案時,把整套編譯器、SDK、Git 與龐大的源碼全部包進最終 Image,導致映像檔高達 1.5GB,下載耗時且存在大量安全漏洞。
  • 解決方案:
    採用多階段建置,僅將編譯後的二進制或靜態檔案帶入最終發行版:
# 第一階段:編譯環境(Builder)
FROM golang:1.22-alpine AS builder
WORKDIR /build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .

# 第二階段:極致精簡運行環境(Runner)
FROM scratch
WORKDIR /app
# 從 builder 階段僅拷貝編譯成果與憑證
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /build/server /app/server

EXPOSE 8080
ENTRYPOINT ["/app/server"]
  • 專家提醒:
    使用 scratch 或 alpine 作為最終層,能將原本數百 MB 的映像檔瞬間壓縮至 15MB 以內,同時因沒有 Shell 環境,大幅提高攻擊者的利用難度。

技巧 089:秒查容器內存與死因!排查 Exit Code 與 OOM 終端實戰

  • 痛點場景:
    容器無預警重開機或突然消失,只顯示 Exited (137),不知道是因為記憶體超標被殺,還是應用程式拋錯崩潰。
  • 解決方案:
    • 解讀 Exit Code 密碼:
      • Exit Code 137:通常是 128 + 9 (SIGKILL),90% 是觸發了宿主機或 Docker 的 OOM(Out Of Memory)。
      • Exit Code 143:128 + 15 (SIGTERM),代表容器接收到正常的停止訊號。
    • 調閱容器的死亡報告:
# 檢查容器是否曾被 OOM Killer 終結
docker inspect <CONTAINER_ID> --format '{{json .State}}' | grep -i "oomkilled"
# 輸出中若 "OOMKilled": true,代表記憶體確實被榨乾
  • 即時檢視所有運作中容器的記憶體與 CPU 即時佔用率:
docker stats --no-stream
  • 專家提醒:
    排查已崩潰容器的最後遺言時,使用 docker logs --tail 100 <CONTAINER_ID> 往往能看到程式被殺前的最後一行 Traceback。

技巧 090:零停機發布!Compose 平滑更新與 Rollback 策略

  • 痛點場景:
    執行更新時先 docker compose down 再 up,在拉取映像檔與啟動應用的 30 秒至 1 分鐘內,所有使用者造訪全數中斷。
  • 解決方案:
    在不使用複雜 K8s 的前提下,利用 Compose 實現近乎無感的平滑替換:
# 1. 預先拉取新版映像檔(不影響正在運行的容器)
docker compose pull web

# 2. 以新映像檔直接重建並在背景啟動(Docker 會在新容器就緒後切換流量)
docker compose up -d --no-deps --build web
  • 若需確保秒級無縫切換,可在反向代理層配合 Blue-Green 雙藍綠容器切換,待綠版健康檢查通過後,再以 nginx -s reload 將轉導切換到新 Port。
  • 專家提醒:
    平時務必為映像檔打上具體的版本號 Tag(如 myapp:v1.2.4 或 Commit SHA),切忌在生產環境全域依賴 latest,否則在發生事故時將完全無法做一鍵回滾(Rollback)。