上一篇 下一篇 分享链接 返回 返回顶部

Docker 实战踩坑总结:从镜像构建到生产部署的配置清单

发布人:慈云数据-客服中心 发布时间:18小时前 阅读量:4

Docker 使用避坑指南|附配置文件

Docker 作为当前最常用的容器化工具之一,已经广泛应用于本地开发、测试环境、CI/CD、微服务部署以及生产运维等场景。它能够帮助我们将应用及其依赖打包成统一的镜像,实现“一次构建,到处运行”。

但在实际使用 Docker 的过程中,很多问题并不会出现在入门教程里,而是在项目上线、多人协作、服务器迁移、持续集成、生产环境运行时逐渐暴露出来。例如:镜像越构建越大、容器数据丢失、端口冲突、权限异常、网络不通、日志撑爆磁盘、容器重启失败、配置写死、Dockerfile 缓存失效等。

本文将围绕 Docker 的常见使用误区进行系统梳理,并提供可直接参考的 Dockerfile、docker-compose.yml、.dockerignore、Nginx 配置等示例文件,帮助你在日常开发和生产部署中少踩坑。


一、不要把 Docker 当成虚拟机使用

很多初学者容易把 Docker 容器理解成一台“轻量级虚拟机”,然后在容器中安装各种工具、启动多个服务、手动修改文件、进入容器排查后直接改配置。这种使用方式并不推荐。

Docker 容器的核心思想是:

一个容器只做一件事,容器本身应当是可销毁、可重建、可替换的。

常见错误做法

docker exec -it app bash
apt update
apt install vim -y
vim /etc/nginx/nginx.conf
service nginx restart

这种方式虽然临时有效,但问题很明显:

  1. 修改不会体现在镜像中;
  2. 容器删除后改动全部丢失;
  3. 其他环境无法复现;
  4. 运维和开发无法追踪配置变更;
  5. 不利于自动化部署。

正确做法

应该将依赖安装、配置文件、启动命令全部写入 Dockerfile 或 docker-compose.yml 中,通过镜像和配置文件统一管理。

例如:

FROM nginx:1.25-alpine

COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./dist /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

这样容器即使被删除,也可以通过镜像快速重建,环境保持一致。


二、Dockerfile 编写避坑

Dockerfile 是构建镜像的核心文件。写得好,镜像小、构建快、安全性高;写得差,则会导致镜像臃肿、缓存失效、构建缓慢,甚至泄露敏感信息。


1. 不要使用 latest 标签

很多教程中会看到类似写法:

FROM node:latest

这在学习阶段问题不大,但在生产环境中非常危险。latest 并不代表稳定版本,它只是一个标签,可能随着官方镜像更新而指向新的版本。

如果某次构建时基础镜像发生变化,可能导致:

  1. Node、Java、Python 等运行时版本变化;
  2. 系统依赖变化;
  3. 构建结果不可预测;
  4. 本地正常,线上异常;
  5. 回滚困难。

推荐写法

FROM node:20.11.1-alpine

或:

FROM python:3.11.7-slim

固定版本能够保证构建过程更加稳定。


2. 合理利用 Docker 构建缓存

Dockerfile 每一行指令都会生成一层镜像缓存。如果上层发生变化,下层缓存也会失效。

以 Node.js 项目为例,很多人会这样写:

FROM node:20-alpine

WORKDIR /app

COPY . .

RUN npm install

CMD ["npm", "run", "start"]

这种写法的问题是:只要任意代码文件发生变化,COPY . . 就会导致后续 npm install 缓存失效,每次都重新安装依赖。

推荐写法

FROM node:20.11.1-alpine

WORKDIR /app

COPY package.json package-lock.json ./

RUN npm ci --registry=https://registry.npmmirror.com

COPY . .

EXPOSE 3000

CMD ["npm", "run", "start"]

这样只有 package.jsonpackage-lock.json 变化时,才会重新安装依赖,构建速度会明显提升。


3. 使用多阶段构建减少镜像体积

很多前端或后端项目在构建阶段需要大量依赖,但运行阶段并不需要。例如前端项目需要 Node.js 构建静态文件,但最终只需要 Nginx 提供静态资源服务。

前端项目 Dockerfile 示例

# 第一阶段:构建阶段
FROM node:20.11.1-alpine AS builder

WORKDIR /app

COPY package.json package-lock.json ./

RUN npm ci --registry=https://registry.npmmirror.com

COPY . .

RUN npm run build

# 第二阶段:运行阶段
FROM nginx:1.25-alpine

COPY ./nginx.conf /etc/nginx/nginx.conf

COPY --from=builder /app/dist /usr/share/nginx/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

这种方式的好处是:

  1. 最终镜像不包含 Node.js;
  2. 不包含源代码和构建依赖;
  3. 镜像体积更小;
  4. 安全性更高;
  5. 启动速度更快。

4. 不要在镜像中写入敏感信息

错误示例:

ENV MYSQL_PASSWORD=123456
ENV JWT_SECRET=my_secret_key
ENV ACCESS_KEY=xxxxxxxx

镜像一旦构建完成,环境变量、构建历史等信息可能被查看。如果镜像上传到公共仓库,风险更大。

可以通过以下命令查看镜像历史:

docker history image_name

推荐方式

敏感信息应通过以下方式传入:

  1. docker-compose 的 env_file;
  2. Kubernetes Secret;
  3. CI/CD Secret;
  4. 云厂商密钥管理服务;
  5. Docker Swarm Secret。

三、.dockerignore 文件不能忽略

很多人会忽略 .dockerignore 文件,导致构建上下文过大,镜像构建缓慢,甚至把本地敏感文件打进镜像。

比如项目目录中可能包含:

  1. node_modules;
  2. .git;
  3. dist;
  4. logs;
  5. .env;
  6. IDE 配置;
  7. 临时文件;
  8. 本地缓存。

如果没有 .dockerignore,执行:

docker build -t my-app .

Docker 会将整个目录发送给 Docker daemon,目录越大,构建越慢。

推荐 .dockerignore

.git
.gitignore

node_modules
npm-debug.log
yarn-error.log
pnpm-lock.yaml

dist
build
coverage

.env
.env.*
*.local

logs
*.log

.DS_Store
.idea
.vscode

docker-compose.override.yml
README.md

需要注意的是,如果你的构建依赖 dist.env.example,就不要盲目忽略,应根据项目实际情况调整。


四、容器数据不要只放在容器内部

Docker 容器默认是临时的。容器删除后,容器内部新增或修改的数据也会随之丢失。

错误示例:

docker run -d --name mysql mysql:8

此时 MySQL 数据存储在容器内部。如果容器被删除,数据也可能丢失。

推荐使用 volume

docker volume create mysql_data

docker run -d \
  --name mysql \
  -e MYSQL_ROOT_PASSWORD=123456 \
  -v mysql_data:/var/lib/mysql \
  -p 3306:3306 \
  mysql:8.0

docker-compose 示例

version: "3.9"

services:
  mysql:
    image: mysql:8.0.36
    container_name: mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: app_db
      TZ: Asia/Shanghai
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./mysql/conf.d:/etc/mysql/conf.d
      - ./mysql/init:/docker-entrypoint-initdb.d
    command:
      --default-authentication-plugin=mysql_native_password
      --character-set-server=utf8mb4
      --collation-server=utf8mb4_unicode_ci

volumes:
  mysql_data:

使用 volume 可以确保数据生命周期独立于容器生命周期。


五、端口映射要规划清楚

Docker 端口映射格式为:

宿主机端口:容器端口

例如:

docker run -p 8080:80 nginx

表示访问宿主机的 8080 端口,会转发到容器内的 80 端口。

常见坑点

1. 端口被占用

docker: Error response from daemon: driver failed programming external connectivity

可以使用以下命令查看端口占用:

lsof -i :8080

或:

netstat -tunlp | grep 8080

2. 宿主机端口和容器端口混淆

如果应用在容器内监听的是 3000,你写成:

ports:
  - "8080:80"

就会访问失败。应该写成:

ports:
  - "8080:3000"

3. 应用只监听 127.0.0.1

很多开发框架默认监听 localhost,在容器中会导致外部访问不到。

错误示例:

app.listen(3000, "127.0.0.1")

推荐:

app.listen(3000, "0.0.0.0")

容器内服务需要监听 0.0.0.0,才能被容器网络或端口映射访问。


六、容器日志一定要限制大小

Docker 默认日志驱动通常是 json-file。如果不做限制,容器日志可能持续增长,最终撑满磁盘。

可以通过以下命令查看容器日志位置:

docker inspect 容器ID | grep LogPath

单个服务配置日志限制

services:
  app:
    image: my-app:1.0.0
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "3"

这表示单个日志文件最大 100MB,最多保留 3 个文件。

Docker daemon 全局配置

创建或修改:

sudo vim /etc/docker/daemon.json

配置如下:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "3"
  }
}

重启 Docker:

sudo systemctl daemon-reload
sudo systemctl restart docker

注意:全局配置通常只对新创建的容器生效,已有容器需要重新创建。


七、restart 策略不要乱用

Docker 提供了多种重启策略:

restart: "no"
restart: always
restart: unless-stopped
restart: on-failure

常用说明

策略 说明
no 不自动重启
always 无论退出原因,都会自动重启
unless-stopped 除非手动停止,否则自动重启
on-failure 只有非 0 状态退出才重启

推荐用法

生产环境中通常推荐:

restart: unless-stopped

如果是任务型容器,如数据库迁移、一次性脚本,不应使用 always,否则任务执行完后可能不断重启。


八、容器时间及时区问题

很多镜像默认使用 UTC 时间,导致日志时间与本地时间相差 8 小时。常见解决方式有两种。

方式一:设置环境变量

environment:
  TZ: Asia/Shanghai

方式二:挂载宿主机时区文件

volumes:
  - /etc/localtime:/etc/localtime:ro

完整示例:

services:
  app:
    image: my-app:1.0.0
    environment:
      TZ: Asia/Shanghai
    volumes:
      - /etc/localtime:/etc/localtime:ro

一般情况下,设置 TZ 已经足够。如果镜像不支持,则考虑挂载时区文件。


九、docker-compose 使用避坑

docker-compose 非常适合管理多容器应用,如 Web 服务、数据库、Redis、Nginx、消息队列等。但如果配置不规范,后期维护成本会很高。


1. 不要把所有配置写死

错误示例:

environment:
  MYSQL_ROOT_PASSWORD: 123456
  REDIS_PASSWORD: abc123

推荐使用 .env 文件:

MYSQL_ROOT_PASSWORD=your_mysql_password
REDIS_PASSWORD=your_redis_password
APP_PORT=3000

docker-compose.yml:

environment:
  MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}

这样可以实现不同环境使用不同配置。


2. depends_on 不等于服务可用

很多人认为:

depends_on:
  - mysql

表示 MySQL 准备好了,应用才启动。实际上,depends_on 只保证容器启动顺序,不保证服务已经可连接。

应用如果启动时立即连接数据库,可能会报错。

解决方式

  1. 应用自身增加重试机制;
  2. 使用 healthcheck;
  3. 使用 wait-for-it、dockerize 等等待脚本。

healthcheck 示例

services:
  mysql:
    image: mysql:8.0.36
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p${MYSQL_ROOT_PASSWORD}"]
      interval: 10s
      timeout: 5s
      retries: 5

应用服务:

services:
  app:
    build: .
    depends_on:
      mysql:
        condition: service_healthy

注意:不同版本的 Compose 对 condition 支持情况可能不同,使用前应确认本地 Docker Compose 版本。


十、完整 docker-compose.yml 示例

下面是一个较完整的后端服务部署示例,包含应用、MySQL、Redis 和 Nginx。

version: "3.9"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    image: my-app:1.0.0
    container_name: my-app
    restart: unless-stopped
    env_file:
      - .env
    environment:
      TZ: Asia/Shanghai
      NODE_ENV: production
    ports:
      - "${APP_PORT}:3000"
    volumes:
      - ./logs:/app/logs
    depends_on:
      - mysql
      - redis
    networks:
      - app_net
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "3"

  mysql:
    image: mysql:8.0.36
    container_name: my-mysql
    restart: unless-stopped
    env_file:
      - .env
    environment:
      TZ: Asia/Shanghai
      MYSQL_DATABASE: app_db
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./mysql/conf.d:/etc/mysql/conf.d
      - ./mysql/init:/docker-entrypoint-initdb.d
    command:
      --default-authentication-plugin=mysql_native_password
      --character-set-server=utf8mb4
      --collation-server=utf8mb4_unicode_ci
    networks:
      - app_net
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "3"

  redis:
    image: redis:7.2-alpine
    container_name: my-redis
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes
    environment:
      TZ: Asia/Shanghai
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - app_net
    logging:
      driver: "json-file"
      options:
        max-size: "50m"
        max-file: "3"

  nginx:
    image: nginx:1.25-alpine
    container_name: my-nginx
    restart: unless-stopped
    ports:
      - "80:80"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
      - ./nginx/logs:/var/log/nginx
    depends_on:
      - app
    networks:
      - app_net
    logging:
      driver: "json-file"
      options:
        max-size: "50m"
        max-file: "3"

networks:
  app_net:
    driver: bridge

volumes:
  mysql_data:
  redis_data:

十一、Nginx 反向代理配置示例

./nginx/default.conf

server {
    listen 80;
    server_name example.com;

    client_max_body_size 50m;

    location / {
        proxy_pass http://app: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;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

在 Docker Compose 的同一个网络中,Nginx 可以直接通过服务名 app 访问后端容器,而不需要写容器 IP。容器 IP 可能变化,服务名更加稳定。


十二、生产环境安全建议

Docker 并不是天然安全的,生产环境使用时需要注意以下几点。

1. 尽量不要使用 root 用户运行应用

很多官方镜像默认使用 root 用户。应用如果被攻击,攻击者可能获得容器内 root 权限,进一步增加逃逸风险。

Node.js 示例:

FROM node:20.11.1-alpine

WORKDIR /app

COPY package.json package-lock.json ./

RUN npm ci --only=production

COPY . .

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

EXPOSE 3000

CMD ["node", "server.js"]

2. 最小化镜像

优先选择:

alpine
slim
distroless

但也要注意 Alpine 使用 musl libc,某些依赖可能存在兼容问题。如果遇到奇怪的编译或运行错误,可以尝试 slim 版本。

3. 不暴露不必要端口

数据库和 Redis 如果只给内部应用使用,可以不映射到宿主机端口:

redis:
  image: redis:7.2-alpine
  expose:
    - "6379"

expose 只在 Docker 网络内部暴露,不会映射到宿主机。

4. 定期清理无用资源

Docker 长期运行后会积累大量无用镜像、容器、网络、构建缓存。

查看磁盘占用:

docker system df

清理无用资源:

docker system prune

清理更彻底:

docker system prune -a

注意:-a 会删除所有未被容器使用的镜像,执行前务必确认。


十三、常用排查命令

查看容器状态

docker ps
docker ps -a

查看容器日志

docker logs -f --tail=200 容器名

进入容器

docker exec -it 容器名 sh

如果镜像中有 bash:

docker exec -it 容器名 bash

查看容器详细信息

docker inspect 容器名

查看网络

docker network ls
docker network inspect 网络名

查看资源占用

docker stats

查看镜像

docker images

删除停止的容器

docker container prune

十四、推荐项目目录结构

一个较规范的 Docker 项目目录可以参考如下:

project
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
├── .env
├── package.json
├── package-lock.json
├── src
│   └── ...
├── logs
├── nginx
│   └── default.conf
├── mysql
│   ├── conf.d
│   │   └── my.cnf
│   └── init
│       └── init.sql
└── README.md

MySQL 配置示例 mysql/conf.d/my.cnf

[mysqld]
default-time-zone = '+08:00'
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
max_connections = 500

[client]
default-character-set = utf8mb4

十五、上线前检查清单

在将 Docker 服务部署到生产环境前,建议按照以下清单检查:

  • [ ] 基础镜像是否固定版本,避免使用 latest
  • [ ] 是否编写 .dockerignore
  • [ ] 是否使用多阶段构建;
  • [ ] 是否避免将密钥写入镜像;
  • [ ] 是否配置日志大小限制;
  • [ ] 数据库、Redis 是否使用 volume 持久化;
  • [ ] 是否设置合适的 restart 策略;
  • [ ] 应用是否监听 0.0.0.0
  • [ ] 容器时区是否正确;
  • [ ] 不必要的端口是否未暴露到公网;
  • [ ] 是否配置健康检查;
  • [ ] 是否设置资源限制;
  • [ ] 是否准备备份和恢复方案;
  • [ ] 是否验证过容器重启、宿主机重启后的恢复情况。

总结

Docker 能够显著提升应用交付效率,但它并不是简单地把应用“塞进容器”就万事大吉。真正稳定的 Docker 使用方式,需要关注镜像构建、配置管理、数据持久化、网络通信、日志控制、安全权限、服务编排和故障排查等多个方面。

对于个人项目,Docker 可以让环境搭建更简单;对于团队项目,Docker 可以让开发、测试、生产环境更加一致;对于生产系统,Docker 则需要配合规范的配置文件、监控、日志、备份和安全策略一起使用。

记住几个核心原则:

  1. 容器是可销毁的,数据必须持久化;
  2. 镜像应当可重复构建,版本必须可追踪;
  3. 配置不要写死,敏感信息不要进镜像;
  4. 一个容器尽量只做一件事;
  5. 日志、端口、权限、网络都要提前规划;
  6. 生产环境不要依赖手工进入容器修改配置。

只要遵循这些原则,Docker 不仅能提升部署效率,也能让系统更加稳定、可维护、易迁移。

目录结构
全文