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

Docker 别只会 build/run:新手最容易踩的坑,都在这了(附源码)

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

Docker 使用避坑指南|附源码

Docker 很强,但“会用 Docker”和“用好 Docker”是两回事。
很多人第一次上手 Docker,通常只会这几步:

  • docker build
  • docker run
  • 出问题就 rm -rf
  • 再来一遍

结果就是:镜像越来越大、容器莫名退出、数据库数据丢失、端口映射混乱、线上和本地环境不一致……
这篇文章我会从实际踩坑场景出发,系统讲清楚 Docker 使用中最常见的坑,并附上一套可直接运行的示例源码,帮助你少走弯路。


一、先搞清楚 Docker 到底解决什么问题

Docker 不是虚拟机,它的核心价值是:

  • 环境一致:开发、测试、生产尽量使用同一套运行环境
  • 交付标准化:应用 + 依赖 + 启动方式都封装进镜像
  • 部署更快:几乎可以做到“拉镜像、起容器、就能跑”
  • 隔离更轻量:比传统虚拟机更省资源

但 Docker 不是银弹。
如果你把它当成“搬家工具”而不是“标准化交付工具”,就很容易踩坑。


二、Docker 最常见的坑,看看你中了几个


1. 不写 .dockerignore,构建上下文巨大

很多人构建镜像时直接:

docker build -t my-app .

看似没问题,实际上 Docker 会把当前目录所有文件都打包到构建上下文中。
如果你的项目里有:

  • node_modules
  • .git
  • dist
  • 日志文件
  • 本地测试数据
  • 大文件

那么构建速度会明显变慢,镜像层也可能变得很臃肿。

解决办法

一定要写 .dockerignore

.git
.gitignore
node_modules
dist
__pycache__
*.log
.env
.DS_Store

建议
构建上下文越小越好,这不只是提速,也能减少把敏感文件打进镜像的风险。


2. latest 标签看起来方便,实际很危险

很多人喜欢这样写:

docker pull nginx:latest

或者 Dockerfile 里:

FROM python:latest

问题在于:latest 不等于最新稳定版,也不等于你上次用的版本
它可能会变化,导致你今天构建成功,明天构建失败。

正确做法

尽量固定版本:

FROM python:3.11-slim

或者:

docker pull nginx:1.27.0

原则
线上环境尽量避免漂移,版本固定才能保证可复现。


3. 容器里跑多个进程,后期维护很痛苦

有些人喜欢把一个容器当成“迷你服务器”,在里面同时跑:

  • Web 服务
  • 定时任务
  • 日志收集
  • 数据库
  • Nginx
  • 监控脚本

这会带来很多问题:

  • 一个进程挂了,整个容器都可能异常
  • 日志难排查
  • 无法独立扩缩容
  • 更新和回滚复杂

建议

遵循一个容器一个职责
比如:

  • web 容器:只负责业务 API
  • db 容器:只负责数据库
  • redis 容器:只负责缓存

这也是 docker-compose 的典型用法。


4. 容器删除后,数据也没了

这是最经典的坑之一。

你以为数据库写进去了,结果:

docker rm -f mysql

再启动后发现数据全没了。
原因是:容器文件系统默认是临时的。容器删掉,里面的可写层也没了。

解决办法:使用 Volume

例如:

docker run -d \
  --name mysql \
  -v mysql_data:/var/lib/mysql \
  mysql:8.0

或者在 docker-compose.yml 中声明:

volumes:
  - mysql_data:/var/lib/mysql

记住一句话

容器负责运行,Volume 才负责持久化数据。


5. 把配置写死在镜像里,不利于环境切换

很多人会直接把数据库密码、API 地址、环境变量写进代码或 Dockerfile:

ENV DB_HOST=127.0.0.1
ENV DB_PASSWORD=123456

这在开发时似乎方便,但一旦进入测试、预发、生产,就会非常麻烦。
更糟糕的是,敏感信息可能直接被写进镜像层。

正确做法

使用环境变量注入:

docker run -e DB_HOST=mysql -e DB_PASSWORD=xxx my-app

或者使用 docker-compose.yml

environment:
  DB_HOST: mysql
  DB_PASSWORD: xxx

如果是敏感信息,建议使用:

  • .env 文件
  • Docker Secret
  • 配置中心

6. 在容器里写日志文件,不如直接输出到标准输出

很多应用习惯把日志写到本地文件,比如:

  • /var/log/app.log
  • logs/error.log

但在容器里,这种方式并不理想。
因为 Docker 更推荐把日志打到 stdout / stderr,然后由 Docker 或外部日志系统统一收集。

推荐方式

Python、Node、Java 都应该尽量将日志输出到控制台。

例如 Python:

print("service started")

然后用:

docker logs -f container_name

查看日志。

这样做的好处

  • 更符合容器化理念
  • 日志收集统一
  • 容器销毁后不会丢失关键运行信息

7. 端口映射搞混:容器端口 ≠ 宿主机端口

这是新手最容易犯的错误之一。

比如:

docker run -p 8080:5000 my-app

这表示:

  • 宿主机端口:8080
  • 容器内部端口:5000

很多人会误以为容器里程序监听 8080 就行,结果访问失败。

正确理解

你的应用必须监听容器内实际端口。
比如 Flask 默认可能监听 5000,你就要把容器端口暴露出来,再映射到宿主机。

如果程序监听的是 8000:

docker run -p 8080:8000 my-app

访问时应该访问:

http://localhost:8080

8. 容器里访问 localhost,不是宿主机

这是另一个高频坑。

在容器内部,localhost 指的是容器自己,不是宿主机,也不是其他容器。
所以很多人会写:

db_host = "localhost"

然后在容器中连接数据库,结果当然失败。

解决办法

如果数据库也是容器,应该通过服务名访问。
例如在 docker-compose.yml 中:

services:
  web:
    depends_on:
      - db
  db:
    image: mysql:8.0

那么 web 容器里连接数据库的主机名应该是:

db

而不是 localhost


9. 权限问题:容器里默认 root,挂载目录后可能报错

有些场景下你会遇到:

  • 无法写入挂载目录
  • 文件权限错误
  • 容器内程序报 Permission denied

尤其是宿主机挂载卷到容器中时,更常见。

解决思路

  • 尽量不要长期以 root 用户运行应用
  • 容器启动前设置合理目录权限
  • 必要时使用 USER 指令切换用户

示例:

RUN useradd -m appuser
USER appuser

不过要注意:
切换用户后,应用要有权限访问所需目录,比如缓存目录、日志目录、临时目录等。


10. 镜像太大,启动慢、推送慢、部署慢

很多镜像一不小心就上 GB。
原因通常有:

  • 基础镜像太重
  • 安装了过多无关工具
  • 没有多阶段构建
  • 缓存层写法不合理

典型优化方式

方式 1:使用轻量基础镜像

FROM python:3.11-slim

而不是直接上超大镜像。

方式 2:多阶段构建

适用于前端、Go、Java 等构建型项目。
构建阶段和运行阶段分开,最终镜像只保留运行必需文件。

方式 3:合理排序 Dockerfile

比如先复制依赖文件,再安装依赖,最后再复制业务代码。
这样代码改动时,可以最大化复用缓存。


11. COPYADD 不要乱用

新手经常把 ADD 当成万能命令。
实际上,COPY 更适合大多数场景。

区别简述

  • COPY:只负责复制本地文件
  • ADD:除了复制,还支持自动解压 tar 包、远程 URL 等行为

建议

除非你明确需要 ADD 的特殊能力,否则优先使用:

COPY . /app

可读性更好,行为更明确。


12. 忽略时区问题,线上日志时间全乱

本地时间是北京时间,容器里却是 UTC。
结果日志看起来“晚了 8 小时”,排查问题非常烦。

解决方式

可以在容器内设置时区,或者挂载时区文件。
例如 Debian/Ubuntu 系镜像中:

RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

也可以通过环境变量和配置统一处理。
不过要注意:不要只在“显示层”修时间,更要在系统设计上理解时间戳和时区的关系。


三、Dockerfile 的正确写法思路

一个好的 Dockerfile,至少要满足下面几点:

  1. 基础镜像尽量固定版本
  2. 层级顺序合理,利用缓存
  3. 不把无关文件打进镜像
  4. 尽量减少镜像体积
  5. 容器启动命令清晰
  6. 运行用户尽量非 root

下面给你一个实战示例。


四、附源码:一个可直接运行的 Flask Docker 示例

这个示例包含:

  • 简单的 Flask Web 服务
  • Dockerfile
  • docker-compose.yml
  • .dockerignore
  • requirements.txt

你可以直接复制后运行。


1)项目结构

docker-demo/
├── app.py
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
└── .dockerignore

2)源码:app.py

from flask import Flask, jsonify
import os
import time

app = Flask(__name__)

@app.route("/")
def index():
    return jsonify({
        "message": "Hello Docker",
        "env": {
            "APP_NAME": os.getenv("APP_NAME", "docker-demo"),
            "APP_ENV": os.getenv("APP_ENV", "dev")
        },
        "time": time.strftime("%Y-%m-%d %H:%M:%S")
    })

@app.route("/health")
def health():
    return jsonify({"status": "ok"})

if __name__ == "__main__":
    host = os.getenv("HOST", "0.0.0.0")
    port = int(os.getenv("PORT", "5000"))
    app.run(host=host, port=port)

3)源码:requirements.txt

Flask==3.0.3

4)源码:Dockerfile

FROM python:3.11-slim

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "app.py"]

5)源码:.dockerignore

.git
.gitignore
__pycache__
*.pyc
*.log
.env
venv
.venv
dist
build

6)源码:docker-compose.yml

version: "3.9"

services:
  web:
    build: .
    container_name: docker-demo-web
    ports:
      - "8080:5000"
    environment:
      APP_NAME: "DockerDemo"
      APP_ENV: "production"
      PORT: 5000
      HOST: "0.0.0.0"
    restart: unless-stopped

7)如何运行

在项目目录下执行:

docker compose up -d --build

访问:

http://localhost:8080

查看日志:

docker logs -f docker-demo-web

停止服务:

docker compose down

8)如何验证健康检查

访问:

http://localhost:8080/health

如果返回:

{"status":"ok"}

说明服务正常。


五、再补几个实战避坑建议


1. 开发环境和生产环境不要混用同一套配置

很多问题不是 Docker 本身引起的,而是“开发配置”直接拿去跑生产。
建议至少区分:

  • docker-compose.yml:开发
  • docker-compose.prod.yml:生产
  • .env:环境变量

2. 不要把数据库直接裸奔暴露到公网

例如 MySQL、Redis、PostgreSQL。
很多人为了测试方便,直接把端口映射出来:

ports:
  - "3306:3306"

如果机器暴露在公网,这很危险。
建议:

  • 只在内网访问
  • 配置强密码
  • 限制防火墙
  • 必要时不要对外映射端口

3. 别忽略容器重启策略

服务偶尔崩掉很正常,但如果不设置重启策略,容器挂了就要人工处理。

常用策略:

  • no:默认,不重启
  • always:总是重启
  • unless-stopped:除非手动停止,否则一直重启
  • on-failure:失败时重启

一般服务推荐:

restart: unless-stopped

4. 定期清理无用镜像和缓存

Docker 用久了,磁盘会被各种历史镜像、缓存、停止容器占满。

常用清理命令:

docker system df
docker image prune
docker container prune
docker volume prune
docker system prune

注意:
清理前要确认是否会误删正在使用的数据卷。


5. 学会看 docker inspect

很多容器问题,靠猜没用,直接看容器详情更快:

docker inspect container_name

你可以查看:

  • 环境变量
  • 挂载卷
  • 网络信息
  • 启动命令
  • 容器状态

这比“重启一百次”更有效。


六、Docker 使用的黄金原则

如果只记住几条,建议记这几条:

  1. 镜像版本固定,不要乱用 latest
  2. 容器尽量单一职责
  3. 数据要放 Volume,不要只存在容器里
  4. 敏感配置不要写死到镜像中
  5. 日志输出到标准输出
  6. .dockerignore,减少构建上下文
  7. 尽量使用非 root 用户运行
  8. 容器内访问服务别乱写 localhost
  9. 开发、测试、生产配置要分离
  10. 先理解原理,再写命令

七、总结

Docker 的价值,不在于“让你多会一个命令”,而在于帮助你建立一套可复制、可迁移、可标准化的交付体系。
但如果使用不当,Docker 也会放大很多问题:镜像臃肿、配置混乱、数据丢失、端口错配、权限异常、日志难查。

你可以把这篇文章当成一份 Docker 使用清单:

  • 构建前先检查 .dockerignore
  • 镜像版本要固定
  • 数据必须持久化
  • 端口、网络、环境变量要分清
  • 日志和权限要提前考虑
  • 生产环境要更加克制

如果你是刚开始学 Docker,建议先把上面的示例跑通,再逐步扩展到数据库、Redis、Nginx、CI/CD。
只要你把这些坑提前避开,Docker 会成为你非常稳定高效的基础设施工具。


如果你愿意,我还可以继续帮你补一版:

  1. 更适合公众号发布的排版版式
  2. 加入 MySQL + Redis 的完整 docker-compose 实战版
  3. 加入 Java Spring Boot / Node.js / Python 三种 Dockerfile 模板
目录结构
全文