docker

这篇文档从 0 到 1 讲清楚 Docker 的日常使用方式,并用一个完整项目示例演示如何用 docker-compose 管理多服务。

示例架构:

  • nginx: 反向代理(统一入口)
  • web: 前端服务(Next.js)
  • api: 后端服务(Hono)
  • postgres: 数据库
  • redis: 缓存

1. Docker 核心概念(先建立直觉)

  • Image(镜像):应用和运行环境的打包结果,类似“模板”
  • Container(容器):镜像运行出来的实例,类似“进程”
  • Dockerfile:定义如何构建镜像
  • Volume:持久化数据(例如 PostgreSQL 数据目录)
  • Network:容器之间通信网络(compose 默认会创建)
  • docker-compose.yml:多容器编排文件

一句话理解:Dockerfile 负责造镜像,compose 负责把多个容器组织起来运行。


2. 安装与环境检查

安装 Docker Desktop(Windows/macOS)或 Linux Docker Engine 后,先验证:

1
2
3
docker --version
docker compose version
docker info

如果都正常输出版本信息,说明环境可用。


3. 从构建一个 Docker 开始

先看一个最小 Node 服务(可替换为你的 Hono API)。

3.1 准备项目结构

1
2
3
4
5
6
demo-api/
package.json
pnpm-lock.yaml
src/index.ts
Dockerfile
.dockerignore

3.2 示例 Dockerfile(生产可用基础版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FROM node:22-alpine AS base
WORKDIR /app
RUN corepack enable

FROM base AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

FROM deps AS build
COPY . .
RUN pnpm build

FROM base AS runtime
ENV NODE_ENV=production
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --prod --frozen-lockfile
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]

3.3 .dockerignore(非常重要)

1
2
3
4
5
6
node_modules
dist
.git
.next
*.log
.env

避免把无关内容打进镜像,加快构建、减小体积。

3.4 构建与运行

1
2
3
4
5
6
7
8
9
10
11
12
# 构建镜像
docker build -t demo-api:1.0.0 .

# 启动容器
docker run -d --name demo-api -p 3000:3000 demo-api:1.0.0

# 查看日志
docker logs -f demo-api

# 停止并删除容器
docker stop demo-api
docker rm demo-api

4. Docker 常见命令速查

4.1 镜像相关

1
2
3
4
docker images
docker build -t app:latest .
docker rmi app:latest
docker image prune -f

docker image prune -f 说明:

  • docker image prune:清理悬空镜像(dangling images),常见于多次构建后出现的 <none>:<none>
  • -f:跳过确认提示,直接执行
  • 默认不会删除你正常打了标签并在使用的镜像
  • 若使用 docker image prune -a -f,会删除所有“未被容器使用”的镜像,风险更高,需谨慎

4.2 容器相关

1
2
3
4
5
6
7
8
docker ps
docker ps -a
docker start <container>
docker stop <container>
docker restart <container>
docker rm <container>
docker logs -f <container>
docker exec -it <container> sh

4.3 资源排查

1
2
3
4
docker stats
docker inspect <container>
docker network ls
docker volume ls

4.4 清理(谨慎)

1
2
3
docker system df
docker system prune -f
docker system prune -a -f

5. 多服务项目示例(nginx + nextjs + hono + postgres + redis)

下面给一个可直接改造到真实项目的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
my-app/
docker-compose.yml
.env
nginx/
default.conf
web/
Dockerfile
package.json
...
api/
Dockerfile
package.json
...

5.1 docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
services:
nginx:
image: nginx:1.27-alpine
container_name: app-nginx
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- web
- api
restart: unless-stopped
networks:
- app-net

web:
build:
context: ./web
dockerfile: Dockerfile
container_name: app-web
environment:
- NODE_ENV=production
- NEXT_PUBLIC_API_BASE=/api
expose:
- "3000"
depends_on:
- api
restart: unless-stopped
networks:
- app-net

api:
build:
context: ./api
dockerfile: Dockerfile
container_name: app-api
environment:
- NODE_ENV=production
- PORT=3001
- DATABASE_URL=postgresql://app:app123@postgres:5432/appdb
- REDIS_URL=redis://redis:6379
expose:
- "3001"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped
networks:
- app-net

postgres:
image: postgres:16-alpine
container_name: app-postgres
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=app123
- POSTGRES_DB=appdb
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
networks:
- app-net

redis:
image: redis:7-alpine
container_name: app-redis
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis_data:/data
restart: unless-stopped
networks:
- app-net

volumes:
pg_data:
redis_data:

networks:
app-net:
driver: bridge

5.2 nginx 配置(nginx/default.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
server {
listen 80;
server_name _;

# 前端
location / {
proxy_pass http://web:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}

# 后端 API
location /api/ {
proxy_pass http://api:3001/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

5.3 启动整套服务

1
2
3
4
5
6
7
8
9
# 首次启动(后台)
docker compose up -d --build

# 查看状态
docker compose ps

# 查看某个服务日志
docker compose logs -f api
docker compose logs -f web

6. 日常开发与更新流程(重点)

6.1 代码更新后重建并发布

1
2
3
4
5
6
# 只重建并更新 web / api
docker compose build web api
docker compose up -d web api

# 或者一步到位
docker compose up -d --build web api

Nginx、PostgreSQL、Redis 不会被重建,数据卷也会保留。

6.2 同步数据库(迁移)

推荐把迁移命令放在 api 容器里执行,保证环境一致:

1
2
3
4
5
# 进入 api 容器执行迁移
docker compose exec api pnpm db:migrate

# 如果服务未启动,也可一次性 run
docker compose run --rm api pnpm db:migrate

如果是 Prisma/Drizzle/Knex,只要把命令替换成你的实际脚本即可。

6.3 服务重启与回滚思路

1
2
3
4
5
6
7
# 重启单个服务
docker compose restart api

# 拉取新基础镜像再重建
docker compose pull
docker compose build --no-cache web api
docker compose up -d web api

回滚常见做法:

  1. 镜像使用明确版本标签(如 api:2026.02.28-1
  2. compose 改回旧标签
  3. docker compose up -d

6.4 配置变更(环境变量)

改了 .env 或 compose 中环境变量后:

1
docker compose up -d

若应用未自动读取新配置,重建相关服务:

1
docker compose up -d --build api web

7. 数据持久化与备份建议

  • PostgreSQL 数据放在 pg_data volume,不随容器销毁而丢失
  • Redis 如果开启 AOF(示例已开启),可提高数据恢复能力
  • 定期备份数据库(建议至少每日)

示例备份命令:

1
docker compose exec postgres pg_dump -U app appdb > backup.sql

恢复示例:

1
docker compose exec -T postgres psql -U app -d appdb < backup.sql

8. 常见问题排查

8.1 端口冲突

报错 bind: address already in use 时,修改宿主机端口映射,例如:

1
2
ports:
- "8080:80"

8.2 服务已启动但不可访问

按顺序检查:

  1. docker compose ps 看服务是否 Up
  2. docker compose logs -f nginx 看代理错误
  3. docker compose logs -f web/api 看应用报错
  4. docker compose exec nginx shwget/curl 内网地址测试连通

8.3 数据库连接失败

  • 检查 DATABASE_URL 中主机是否写 postgres(服务名),而不是 localhost
  • 查看 postgres healthcheck 是否通过
  • 确认迁移是否执行、用户权限是否正确

9. 生产环境建议(简版)

  • 镜像固定版本,不要长期用 latest
  • 前后端镜像分离构建,缓存依赖层提高 CI 速度
  • 数据库和 Redis 配置合理的备份/监控/告警
  • 使用反向代理统一做 HTTPS(可加 certbot 或外部网关)
  • 在 CI 中执行:测试 -> 构建镜像 -> 推镜像 -> 服务器 docker compose pull && up -d

10. 生产级配置模板(可直接改造)

下面给一套偏生产实践的模板:webapinginx 与部署脚本。
你可以先按原样跑通,再替换为自己的镜像名、域名、迁移命令。

10.1 web(Next.js)生产 Dockerfile

建议开启 Next.js standalone 输出(next.config.js 里设置 output: "standalone")。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
FROM node:22-alpine AS base
WORKDIR /app
RUN apk add --no-cache libc6-compat && corepack enable

FROM base AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

FROM deps AS builder
COPY . .
RUN pnpm build

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# 非 root 用户运行
RUN addgroup -S nextjs && adduser -S nextjs -G nextjs

# standalone 产物(Next.js 推荐)
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

10.2 api(Hono)生产 Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FROM node:22-alpine AS base
WORKDIR /app
RUN corepack enable

FROM base AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

FROM deps AS builder
COPY . .
RUN pnpm build

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -S app && adduser -S app -G app && corepack enable

COPY package.json pnpm-lock.yaml ./
RUN pnpm install --prod --frozen-lockfile
COPY --from=builder /app/dist ./dist

USER app
EXPOSE 3001
CMD ["node", "dist/index.js"]

10.3 生产级 Nginx 配置(nginx/default.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
upstream web_upstream {
server web:3000;
keepalive 64;
}

upstream api_upstream {
server api:3001;
keepalive 64;
}

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

server {
listen 80;
server_name _;

client_max_body_size 20m;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;

gzip on;
gzip_types text/plain text/css application/json application/javascript application/xml+rss image/svg+xml;
gzip_min_length 1024;

add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
add_header Referrer-Policy strict-origin-when-cross-origin always;

# Next.js 静态资源长期缓存
location /_next/static/ {
proxy_pass http://web_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
expires 30d;
add_header Cache-Control "public, immutable";
}

# 前端
location / {
proxy_pass http://web_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 60s;
}

# 后端 API
location /api/ {
proxy_pass http://api_upstream/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 60s;
}

# 健康检查
location /healthz {
return 200 "ok\n";
add_header Content-Type text/plain;
}
}

10.4 推荐生产编排(docker-compose.prod.yml

生产环境建议使用已构建并推送到仓库的镜像,不在服务器上 build

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
services:
nginx:
image: nginx:1.27-alpine
container_name: app-nginx
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
web:
condition: service_started
api:
condition: service_started
restart: always
healthcheck:
test: ["CMD-SHELL", "wget -q -O - http://localhost/healthz || exit 1"]
interval: 10s
timeout: 3s
retries: 10
networks:
- app-net

web:
image: ghcr.io/your-org/your-web:${APP_TAG}
container_name: app-web
env_file:
- .env.production
expose:
- "3000"
restart: always
healthcheck:
test: ["CMD-SHELL", "wget -q -O - http://localhost:3000 || exit 1"]
interval: 15s
timeout: 3s
retries: 10
networks:
- app-net

api:
image: ghcr.io/your-org/your-api:${APP_TAG}
container_name: app-api
env_file:
- .env.production
environment:
- DATABASE_URL=postgresql://app:${POSTGRES_PASSWORD}@postgres:5432/appdb
- REDIS_URL=redis://redis:6379
expose:
- "3001"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
restart: always
healthcheck:
test: ["CMD-SHELL", "wget -q -O - http://localhost:3001/healthz || exit 1"]
interval: 10s
timeout: 3s
retries: 10
networks:
- app-net

postgres:
image: postgres:16-alpine
container_name: app-postgres
environment:
- POSTGRES_USER=app
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=appdb
volumes:
- pg_data:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d appdb"]
interval: 5s
timeout: 3s
retries: 20
networks:
- app-net

redis:
image: redis:7-alpine
container_name: app-redis
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis_data:/data
restart: always
networks:
- app-net

volumes:
pg_data:
redis_data:

networks:
app-net:
driver: bridge

11. deploy.sh 发布脚本(生产可用基础版)

这个脚本做了几件事:

  1. 校验环境与参数
  2. 拉取最新镜像
  3. 确保数据库与缓存先起来
  4. 执行数据库迁移
  5. 更新 web/api/nginx
  6. 做健康检查,不通过则退出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#!/usr/bin/env bash
set -Eeuo pipefail

APP_DIR="/opt/my-app"
COMPOSE_FILE="docker-compose.prod.yml"
ENV_FILE=".env.production"
TAG="${1:-}"

if [[ -z "${TAG}" ]]; then
echo "Usage: ./deploy.sh <APP_TAG>"
echo "Example: ./deploy.sh 2026.02.28-1"
exit 1
fi

cd "${APP_DIR}"

if ! command -v docker >/dev/null 2>&1; then
echo "docker not found"
exit 1
fi

if ! docker compose version >/dev/null 2>&1; then
echo "docker compose not available"
exit 1
fi

if [[ ! -f "${COMPOSE_FILE}" ]]; then
echo "compose file not found: ${COMPOSE_FILE}"
exit 1
fi

if [[ ! -f "${ENV_FILE}" ]]; then
echo "env file not found: ${ENV_FILE}"
exit 1
fi

export APP_TAG="${TAG}"

echo "[1/6] Pull latest images"
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" pull web api nginx

echo "[2/6] Ensure postgres/redis are up"
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" up -d postgres redis

echo "[3/6] Run DB migration"
# 按你的项目实际迁移命令修改
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" run --rm api pnpm db:migrate

echo "[4/6] Update app services"
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" up -d web api nginx

echo "[5/6] Wait for health checks"
for i in {1..30}; do
if curl -fsS "http://127.0.0.1/healthz" >/dev/null && \
curl -fsS "http://127.0.0.1/api/healthz" >/dev/null; then
echo "Health check passed"
break
fi
if [[ "${i}" == "30" ]]; then
echo "Health check failed after retries"
exit 1
fi
sleep 2
done

echo "[6/6] Done"
docker compose --env-file "${ENV_FILE}" -f "${COMPOSE_FILE}" ps

建议把脚本权限加上:

1
chmod +x deploy.sh

执行方式:

1
./deploy.sh 2026.02.28-1