身為一個天天在跟容器打交道的人
我覺得Docker是每個搞後端或運維的人都應該要會的東西
今天就來從頭介紹一下Docker到底是什麼以及怎麼用吧
# 前言
如果你有在自架伺服器或是搞後端
應該多少都聽過Docker這個名字
但每次聽別人講什麼容器化、映像檔、Docker Compose
可能還是會覺得有點抽象
這篇文章會從最基本的概念開始
一路講到你可以自己寫Dockerfile和docker-compose.yml
目標是讓你看完之後可以自己把一個服務容器化並跑起來
# 先備條件
- 你會用終端機(CLI)
- 你有一台Linux、MacOS或Windows的電腦
- 你知道什麼是伺服器(至少概念上知道)
- 你的腦袋是清醒的
# Docker是什麼
# 虛擬機 vs 容器
在Docker出現之前
如果你想要在一台機器上跑多個獨立的服務
通常的做法是開虛擬機(VM)
每台VM都有自己完整的作業系統、核心、驅動
很安全很隔離 但也很肥很慢
Docker的做法不一樣
它不會去虛擬化整個作業系統
而是利用Linux核心的namespace和cgroup機制
把程式跑在一個隔離的環境裡 但共用同一個核心
所以啟動快、佔資源少、部署方便
簡單講
VM是幫你蓋一棟新房子 裡面有自己的水電瓦斯
Container是在同一棟大樓裡隔出一間套房 共用水電但有自己的門鎖
# 核心概念
在開始之前 先認識幾個名詞
| 名詞 | 說明 |
|---|---|
| Image(映像檔) | 一個唯讀的模板,包含了跑起一個服務所需的所有東西 |
| Container(容器) | 由Image啟動的一個實例,可以理解為一個在跑的程式 |
| Dockerfile | 一份描述如何建構Image的腳本 |
| Docker Compose | 用YAML定義多個容器的編排工具 |
| Registry | 存放Image的倉庫,最常見的是Docker Hub |
| Volume | 資料卷,讓容器可以持久化資料 |
# 安裝Docker
# Linux
大部分的Linux發行版都可以用官方的安裝腳本一鍵搞定
curl -fsSL https://get.docker.com | sh
裝完之後把自己加進docker群組 這樣就不用每次都sudo
sudo usermod -aG docker $USER
加完群組之後要重新登入才會生效
你可以用newgrp docker暫時切換 或者直接重開終端
# MacOS
MacOS的話直接去Docker官網下載Docker Desktop
安裝完就可以用了 它會自帶一個Linux VM在背景跑
如果你是Apple Silicon(M系列晶片)
Docker Desktop會自動幫你處理arm64的相容性
但有些Image可能只有amd64版本 跑起來會透過Rosetta轉譯 效能會差一點
# Windows
Windows的話一樣去官網下載Docker Desktop
不過你需要先啟用WSL2
wsl --install
裝完重開機 再裝Docker Desktop就可以了
# 驗證安裝
不管哪個系統 裝完之後跑一下這個確認有沒有裝好
docker --version
docker run hello-world
如果看到一坨Hello from Docker!的訊息就代表成功了
# 基本操作
# 拉取Image
# 從Docker Hub拉取一個Image
docker pull nginx
# 拉取指定版本
docker pull nginx:1.25
# 看看本地有哪些Image
docker images
# 啟動Container
# 最基本的啟動方式
docker run nginx
# 背景執行 + 端口映射
docker run -d -p 8080:80 nginx
# 給容器取名字
docker run -d -p 8080:80 --name my-nginx nginx
解釋一下參數
| 參數 | 說明 |
|---|---|
-d |
背景執行(detach) |
-p 8080:80 |
把主機的8080 port映射到容器的80 port |
--name |
給容器取名字 不然Docker會隨機生成一個 |
跑完之後打開瀏覽器連http://localhost:8080
應該就能看到Nginx的預設頁面了
# 管理Container
# 列出正在跑的容器
docker ps
# 列出所有容器(包含已停止的)
docker ps -a
# 停止容器
docker stop my-nginx
# 啟動已停止的容器
docker start my-nginx
# 刪除容器(要先停止)
docker rm my-nginx
# 強制刪除(不用先停止)
docker rm -f my-nginx
# 進入Container
有時候你需要進到容器裡面看看到底發生了什麼事
# 進入容器的shell
docker exec -it my-nginx bash
# 如果容器沒有bash 可以試試sh
docker exec -it my-nginx sh
-it是-i(interactive)加-t(tty)的縮寫
簡單講就是讓你可以跟容器互動
# 看Log
# 看容器的log
docker logs my-nginx
# 持續追蹤log(像tail -f一樣)
docker logs -f my-nginx
# 只看最後100行
docker logs --tail 100 my-nginx
# Dockerfile
到目前為止我們都是用別人做好的Image
但如果你要把自己的程式容器化 就需要自己寫Dockerfile
# 基本結構
# 基底Image
FROM node:20-slim
# 設定工作目錄
WORKDIR /app
# 複製檔案
COPY package.json package-lock.json ./
# 執行指令
RUN npm ci --production
# 複製其餘程式碼
COPY . .
# 暴露端口
EXPOSE 3000
# 啟動指令
CMD ["node", "server.js"]
# 常用指令說明
| 指令 | 說明 |
|---|---|
FROM |
指定基底Image |
WORKDIR |
設定工作目錄 之後的指令都會在這個目錄下執行 |
COPY |
複製檔案到容器裡 |
RUN |
在build時執行指令(裝套件之類的) |
EXPOSE |
宣告容器要監聽的port(只是文件化 不會自動映射) |
CMD |
容器啟動時要執行的指令 |
ENV |
設定環境變數 |
ARG |
設定build時的參數 |
# Build Image
# 在有Dockerfile的目錄下
docker build -t my-app:latest .
# -t 是tag的意思 給你的Image取名字和版本
# .dockerignore
跟.gitignore一樣的概念
告訴Docker在COPY的時候要忽略哪些檔案
node_modules
.git
.env
*.md
一定要記得加.dockerignore
不然你COPY的時候會把node_modules之類的垃圾一起複製進去
不僅build變慢 Image也會變超肥
# Multi-stage Build
如果你的程式需要編譯(像TypeScript、Go之類的)
可以用multi-stage build來減少最終Image的大小
# Stage 1: Build
FROM node:20-slim AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]
第一個stage負責編譯 第二個stage只複製編譯完的結果
這樣最終的Image就不會包含devDependencies和原始碼
# Docker Compose
當你的服務不只一個容器的時候
一個一個docker run會瘋掉
Docker Compose就是來解決這個問題的
# 基本結構
services:
web:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DB_HOST=db
depends_on:
- db
restart: unless-stopped
db:
image: postgres:16
volumes:
- db_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=mysecretpassword
- POSTGRES_DB=myapp
volumes:
db_data:
# 常用指令
# 啟動所有服務(背景執行)
docker compose up -d
# 停止所有服務
docker compose down
# 停止並刪除volume(小心資料會不見)
docker compose down -v
# 重新build並啟動
docker compose up -d --build
# 看log
docker compose logs -f
# 只看某個服務的log
docker compose logs -f web
# Volume
Volume是Docker用來持久化資料的機制
如果你不掛Volume 容器一刪資料就沒了
services:
db:
image: postgres:16
volumes:
# Named volume(Docker管理)
- db_data:/var/lib/postgresql/data
# Bind mount(直接掛主機目錄)
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
volumes:
db_data:
Named volume和bind mount的差別在於
Named volume是Docker自己管理的 放在/var/lib/docker/volumes/底下
Bind mount是直接把主機的目錄映射進容器
一般來說 資料庫之類的用named volume 設定檔之類的用bind mount
# Network
Docker Compose預設會建立一個bridge network
同一個compose檔案裡的服務可以用服務名稱互相連線
也就是為什麼上面的DB_HOST可以直接寫db
如果你需要讓不同compose檔案的容器互相溝通
可以自己建立外部network
services:
web:
networks:
- shared
networks:
shared:
external: true
# 先建立外部network
docker network create shared
# 常用技巧
# 清理垃圾
Docker用久了會堆積一堆沒在用的Image、Container、Volume
定期清一下
# 清除所有沒在用的東西(小心使用)
docker system prune -a
# 只清沒在用的Image
docker image prune -a
# 只清沒在用的Volume
docker volume prune
# 看看Docker佔了多少空間
docker system df
# 健康檢查
services:
web:
build: .
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
加了healthcheck之後 Docker會定期去戳你的服務確認它還活著
搭配restart: unless-stopped可以讓掛掉的服務自動重啟
# 環境變數管理
不要把密碼直接寫在docker-compose.yml裡面
用.env檔案管理
# .env
POSTGRES_PASSWORD=mysecretpassword
DB_NAME=myapp
services:
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${DB_NAME}
Docker Compose會自動讀取同目錄下的.env檔案
記得把.env加進.gitignore
不要把密碼推上去 這是常識
# 結語
Docker這東西說難不難 說簡單也沒那麼簡單
但只要搞懂Image、Container、Volume、Network這幾個核心概念
剩下的就是看文件然後多用就會了
我自己從開始用Docker到現在
已經離不開這東西了
不管是開發環境還是正式環境
有Docker在 環境問題基本上就不是問題了
「在我的電腦上可以跑」這句話也終於可以變成「在Docker裡可以跑」
之後如果有機會再來寫Docker在K8s上的應用吧
<!-- 反正我已經寫了一篇K8s MailServer了 -->
