不讲历史不讲哲学,全是能跟着敲的命令、能跑起来的代码。从「容器是什么」到「多服务编排 + 生产优化」一条龙。
容器 = 把"应用 + 它依赖的所有东西"打成一个包,在任何能跑 Docker 的机器上一致运行。它不是虚拟机,没装一个完整的操作系统,更轻、更快。
| 概念 | 类比 | 解释 |
|---|---|---|
Image 镜像 | 蓝图 / class | 只读模板,由若干层(layer)叠加而成 |
Container 容器 | 实例 / object | 镜像的一次运行,可启停删,可有状态 |
Registry 仓库 | App Store | 存镜像的地方,如 Docker Hub、GHCR、阿里云 |
「我这能跑」从此作废。开发、测试、生产同一个镜像。
容器没装 OS,几百毫秒就能起一个新实例。
用 Linux namespace + cgroup 隔离,比 VM 轻一个数量级。
装 Docker Desktop。带图形界面,自带 Compose、Kubernetes。
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER # 免 sudo
newgrp docker
docker --version # Docker version 27.x.x
docker compose version # Docker Compose version v2.x.x
docker run hello-world # 拉镜像 → 跑容器 → 打印消息
docker-compose(带横线,独立程序),新的是 docker compose(空格,作为 Docker CLI 插件)。本教程全部使用新语法。
跑一个 nginx:
docker run -d -p 8080:80 --name my-nginx nginx:alpine
拆解这一行:
| 参数 | 含义 |
|---|---|
run | 创建并启动容器 |
-d | detach,后台运行 |
-p 8080:80 | 宿主机 8080 → 容器 80 |
--name my-nginx | 给容器起名(不写就随机) |
nginx:alpine | 镜像名:标签(alpine 是更小的版本) |
浏览器打开 http://localhost:8080 → 看到 Nginx 欢迎页 ✅
docker ps列出运行中的容器docker ps -a列出所有容器(含已停止)docker logs -f <name>查看日志,-f 跟随docker exec -it <name> sh进入容器 shelldocker stop <name>优雅停止(先 SIGTERM 后 SIGKILL)docker rm <name>删除已停止的容器docker rm -f <name>强制删除(运行中也删)docker stats实时 CPU/内存/IO 监控docker inspect <name>查看完整 JSON 配置docker images列出本地镜像docker pull nginx:1.27拉取镜像docker rmi <image>删除镜像docker build -t myapp:1.0 .从 Dockerfile 构建docker tag a:1 b:2打新标签docker push myapp:1.0推到仓库docker history <image>查看镜像分层历史docker system df # 看占用
docker system prune -a # 清除未使用的镜像/容器/网络
docker volume prune # 清除未使用的卷(小心数据)
Dockerfile 是一份"如何把代码做成镜像"的说明书。每一条指令产生一个层(layer),层会被缓存,改一行只重做那一层之后的内容。
| 指令 | 作用 | 示例 |
|---|---|---|
FROM | 基础镜像 | FROM python:3.12-slim |
WORKDIR | 工作目录(自动创建) | WORKDIR /app |
COPY | 从宿主复制到镜像 | COPY . . |
RUN | 构建时执行命令 | RUN pip install -r req.txt |
ENV | 设置环境变量 | ENV PORT=8000 |
EXPOSE | 声明端口(文档作用) | EXPOSE 8000 |
CMD | 默认启动命令 | CMD ["python","app.py"] |
ENTRYPOINT | 主入口(更"固定") | ENTRYPOINT ["python"] |
# app.py
from flask import Flask
app = Flask(__name__)
@app.get("/")
def hello():
return {"msg": "Hello from Docker!"}
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
# 关键:先 COPY requirements,单独一层 → 改代码不会重装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]
docker build -t flask-demo:1.0 .
docker run -d -p 8000:8000 --name web flask-demo:1.0
curl http://localhost:8000
COPY . .)放后面,变化少的(依赖安装)放前面。否则每次改代码都要重装依赖,构建慢 10 倍。
类似 .gitignore,避免把无用文件打进镜像,加快构建:
.git
.env
node_modules
__pycache__
*.log
dist
.DS_Store
容器删除后所有写入容器内的数据都会消失。要持久化必须用 volume。
| 类型 | 命令 | 场景 |
|---|---|---|
| Bind Mount | -v $(pwd):/app | 开发:宿主代码映射进容器,改了立刻生效 |
| Named Volume | -v mydata:/var/lib/mysql | 生产:数据库持久化,Docker 管理 |
| tmpfs | --tmpfs /tmp | 敏感临时数据,只存内存 |
# 不持久化(数据随容器消失)
docker run -d --name db -e MYSQL_ROOT_PASSWORD=secret mysql:8
# 持久化到 named volume(推荐)
docker run -d --name db \
-e MYSQL_ROOT_PASSWORD=secret \
-v mysql_data:/var/lib/mysql \
-p 3306:3306 \
mysql:8
# 查看 volume
docker volume ls
docker volume inspect mysql_data
--mount语义更明确,强烈推荐用于脚本和生产:
docker run -d \
--mount type=volume,source=mysql_data,target=/var/lib/mysql \
--mount type=bind,source=$(pwd)/conf,target=/etc/mysql/conf.d,readonly \
mysql:8
| 模式 | 特点 | 用法 |
|---|---|---|
bridge (默认) | 每个容器一个 IP,不能用容器名互相访问 | 不推荐 |
| 自定义 bridge ⭐ | 内置 DNS,容器名 = 主机名 | 必须用这个 |
host | 共享宿主网络栈,性能最好但无隔离 | 性能敏感场景 |
none | 无网络 | 纯计算/离线任务 |
# 1. 创建自定义网络
docker network create app-net
# 2. 容器挂到同一网络
docker run -d --name db --network app-net \
-e POSTGRES_PASSWORD=pw postgres:16
docker run -d --name web --network app-net -p 8000:8000 \
-e DATABASE_URL=postgres://postgres:pw@db:5432/postgres \
myapp:1.0
# 在 web 容器里直接用 "db" 作为主机名连接 ✅
# 登录
docker login # Docker Hub
docker login ghcr.io # GitHub Container Registry
docker login registry.gitlab.com # GitLab Registry
# 推送(镜像名前缀 = 仓库地址)
docker tag myapp:1.0 ghcr.io/myorg/myapp:1.0
docker push ghcr.io/myorg/myapp:1.0
# 拉取私有镜像
docker pull ghcr.io/myorg/myapp:1.0
[registry/][namespace/]name[:tag]
例:
nginx # Docker Hub library/nginx:latest
python:3.12-slim # 指定 tag
ghcr.io/myorg/myapp:v1.2.3 # GHCR + 版本
registry.cn-hangzhou.aliyuncs.com/myns/myapp:latest
:latest。它不是最新版,它是"上次有人 push 时打了 latest 的版本"。生产环境用具体 tag 或 SHA digest。
一个能跑的镜像 vs. 一个好的镜像,差别在 3 个维度:体积、构建速度、安全表面。
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
问题:基础镜像太大、装了 dev 依赖、源码+构建工具都进了最终镜像。
# 阶段 1: builder
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 阶段 2: runner(只要产物)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
EXPOSE 3000
USER node
CMD ["node", "dist/server.js"]
体积 -85%,无 dev 依赖,非 root 用户。
alpine ~5MB,但 musl 偶有兼容问题-slim ~80MB,glibc 兼容好(推荐)distroless 无 shell,最安全scratch 空镜像,仅 Go 静态二进制RUN apt-get update \
&& apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
关键:apt list、pip cache、npm cache 都要在同一个 RUN 层里删,否则层还在。
现代 Docker 默认开启。可用 缓存挂载、secret 挂载:
# 缓存挂载:npm/pip cache 跨构建复用
RUN --mount=type=cache,target=/root/.npm \
npm ci
# secret 挂载:构建期需要 token 但不留进镜像
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm install
USER node 或 USER 1000--read-only + tmpfs 临时目录--memory=512m --cpus=1 防失控docker scout cves myapp:1.0 或 trivy# 生产启动模板
docker run -d \
--name web \
--read-only \
--tmpfs /tmp \
--memory=512m --cpus=1 \
--security-opt no-new-privileges \
--cap-drop ALL \
--user 1000:1000 \
-p 8000:8000 \
myapp@sha256:abc123... # 用 digest 锁死版本
# 1. 看日志
docker logs my-container
docker logs --tail 100 -f my-container
# 2. 查退出原因
docker inspect my-container --format='{{.State.ExitCode}} {{.State.Error}}'
# 3. 改启动命令进去看
docker run -it --entrypoint sh myapp:1.0
# 进运行中的容器
docker exec -it web sh # alpine
docker exec -it web bash # debian/ubuntu
# 容器没装调试工具?用临时 sidecar
docker run -it --rm --pid=container:web \
--net=container:web nicolaka/netshoot
docker stats实时 CPU/内存/网络docker top <name>容器内进程列表docker port <name>端口映射docker network inspect <net>网络详情 + 谁连着docker events实时事件流(创建/启停/OOM)单条 docker run 适合一个容器。但真实应用是 web + db + redis + worker + …,手敲命令既慢又错。Compose 用一个 YAML 文件描述整个应用栈,一条命令起停所有服务。
# compose.yaml
services:
web:
image: nginx:alpine
ports:
- "8080:80"
docker compose up -d # 后台启动
docker compose ps # 查看
docker compose logs -f # 看日志
docker compose down # 停止 + 删容器(保留 volume)
docker compose down -v # 连 volume 一起删
compose.yaml(Compose Spec),老名字 docker-compose.yml 仍兼容。两者行为一致,新项目用前者。
下面是一份能直接跑的完整栈,覆盖几乎所有常用字段。
# compose.yaml
services:
# --- 反向代理 ---
nginx:
image: nginx:1.27-alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
api:
condition: service_healthy
restart: unless-stopped
# --- 应用 API(用 Dockerfile 构建)---
api:
build:
context: ./api
dockerfile: Dockerfile
args:
NODE_ENV: production
environment:
DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/app
REDIS_URL: redis://redis:6379
NODE_ENV: production
env_file: .env
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:8000/health"]
interval: 10s
timeout: 3s
retries: 5
start_period: 20s
deploy:
resources:
limits: { cpus: '1.0', memory: 512M }
restart: unless-stopped
# --- 数据库 ---
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
retries: 10
restart: unless-stopped
# --- 缓存 ---
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
restart: unless-stopped
volumes:
db_data:
redis_data:
.env 文件# .env (加进 .gitignore)
DB_PASSWORD=super-secret-pw-please-rotate
NODE_ENV=production
| 字段 | 说明 |
|---|---|
build | 本地 Dockerfile 构建(可以和 image 共存:build 后用该 image tag) |
environment / env_file | 注入环境变量;${VAR} 从宿主 / .env 读 |
depends_on | 启动顺序 + 健康检查依赖 |
healthcheck | 判断服务"真活着",影响 depends_on |
volumes | 顶层声明卷 + 服务里挂载,:ro 只读 |
restart | no / always / on-failure / unless-stopped ⭐ |
deploy.resources | CPU/内存上限 |
docker compose up -d启动全部,后台docker compose up -d --build代码改了 → 强制重建镜像docker compose ps所有服务状态docker compose logs -f api看某个服务的日志docker compose exec api sh进入某服务容器docker compose restart api重启某服务docker compose up -d --scale api=3横向扩容到 3 实例docker compose config解析并展示最终配置(debug 利器)docker compose down -v销毁全部 + 卷Compose 默认会合并两个文件:compose.yaml + compose.override.yaml。后者覆盖前者。这是区分开发/生产的官方方式:
# compose.yaml(生产基准)
services:
api:
image: ghcr.io/myorg/api:1.2.0
restart: unless-stopped
# compose.override.yaml(开发本地,自动加载)
services:
api:
build: ./api
volumes:
- ./api:/app # 代码热更新
environment:
DEBUG: "true"
ports:
- "8000:8000"
docker compose up -d # 开发
docker compose -f compose.yaml up -d # 只用 base(生产)
docker compose -f compose.yaml -f compose.prod.yaml up -d # 显式合并
services:
api: { image: myapi }
db: { image: postgres }
# 调试工具,平时不起
adminer:
image: adminer
profiles: ["debug"]
# 性能测试
k6:
image: grafana/k6
profiles: ["perf"]
docker compose up -d # 只起 api + db
docker compose --profile debug up -d # 加 adminer
docker compose --profile perf up -d k6 # 临时跑压测
x-common-env: &common-env
TZ: Asia/Shanghai
LOG_LEVEL: info
services:
api:
image: myapi
environment:
<<: *common-env
ROLE: api
worker:
image: myapi
environment:
<<: *common-env
ROLE: worker
command: ["python", "worker.py"]
services:
nginx:
image: nginx
networks: [frontend]
api:
image: myapi
networks: [frontend, backend] # 双网卡
db:
image: postgres
networks: [backend] # 数据库只在内网
networks:
frontend:
backend:
internal: true # 完全无外网
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_pw
secrets:
- db_pw
secrets:
db_pw:
file: ./secrets/db_password.txt
Compose 2.22+ 支持 watch,本地代码变化自动同步/重建:
services:
api:
build: ./api
develop:
watch:
- action: sync # 同步文件,无需重启
path: ./api/src
target: /app/src
- action: rebuild # package.json 变了 → 重建镜像
path: ./api/package.json
docker compose up --watch
Compose 在单机生产是完全够用的(不少团队用它跑业务好几年)。要做好这几件事:
restart: unless-stoppedhealthcheckdeploy.resources.limits 防 OOMlogging: { driver: json-file, options: { max-size: 10m, max-file: 3 } }kompose 工具能初步转换 yaml)#!/usr/bin/env bash
set -euo pipefail
cd /opt/myapp
# 拉最新代码
git pull
# 用 .env.prod 启动
docker compose --env-file .env.prod \
-f compose.yaml -f compose.prod.yaml \
pull
docker compose --env-file .env.prod \
-f compose.yaml -f compose.prod.yaml \
up -d --remove-orphans
# 清理旧镜像
docker image prune -f