Docker 别只会 build/run:新手最容易踩的坑,都在这了(附源码)
Docker 使用避坑指南|附源码
Docker 很强,但“会用 Docker”和“用好 Docker”是两回事。
很多人第一次上手 Docker,通常只会这几步:
docker builddocker run- 出问题就
rm -rf - 再来一遍
结果就是:镜像越来越大、容器莫名退出、数据库数据丢失、端口映射混乱、线上和本地环境不一致……
这篇文章我会从实际踩坑场景出发,系统讲清楚 Docker 使用中最常见的坑,并附上一套可直接运行的示例源码,帮助你少走弯路。
一、先搞清楚 Docker 到底解决什么问题
Docker 不是虚拟机,它的核心价值是:
- 环境一致:开发、测试、生产尽量使用同一套运行环境
- 交付标准化:应用 + 依赖 + 启动方式都封装进镜像
- 部署更快:几乎可以做到“拉镜像、起容器、就能跑”
- 隔离更轻量:比传统虚拟机更省资源
但 Docker 不是银弹。
如果你把它当成“搬家工具”而不是“标准化交付工具”,就很容易踩坑。
二、Docker 最常见的坑,看看你中了几个
1. 不写 .dockerignore,构建上下文巨大
很多人构建镜像时直接:
docker build -t my-app .
看似没问题,实际上 Docker 会把当前目录所有文件都打包到构建上下文中。
如果你的项目里有:
node_modules.gitdist- 日志文件
- 本地测试数据
- 大文件
那么构建速度会明显变慢,镜像层也可能变得很臃肿。
解决办法
一定要写 .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容器:只负责业务 APIdb容器:只负责数据库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.loglogs/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. COPY 和 ADD 不要乱用
新手经常把 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,至少要满足下面几点:
- 基础镜像尽量固定版本
- 层级顺序合理,利用缓存
- 不把无关文件打进镜像
- 尽量减少镜像体积
- 容器启动命令清晰
- 运行用户尽量非 root
下面给你一个实战示例。
四、附源码:一个可直接运行的 Flask Docker 示例
这个示例包含:
- 简单的 Flask Web 服务
Dockerfiledocker-compose.yml.dockerignorerequirements.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 使用的黄金原则
如果只记住几条,建议记这几条:
- 镜像版本固定,不要乱用
latest - 容器尽量单一职责
- 数据要放 Volume,不要只存在容器里
- 敏感配置不要写死到镜像中
- 日志输出到标准输出
- 配
.dockerignore,减少构建上下文 - 尽量使用非 root 用户运行
- 容器内访问服务别乱写
localhost - 开发、测试、生产配置要分离
- 先理解原理,再写命令
七、总结
Docker 的价值,不在于“让你多会一个命令”,而在于帮助你建立一套可复制、可迁移、可标准化的交付体系。
但如果使用不当,Docker 也会放大很多问题:镜像臃肿、配置混乱、数据丢失、端口错配、权限异常、日志难查。
你可以把这篇文章当成一份 Docker 使用清单:
- 构建前先检查
.dockerignore - 镜像版本要固定
- 数据必须持久化
- 端口、网络、环境变量要分清
- 日志和权限要提前考虑
- 生产环境要更加克制
如果你是刚开始学 Docker,建议先把上面的示例跑通,再逐步扩展到数据库、Redis、Nginx、CI/CD。
只要你把这些坑提前避开,Docker 会成为你非常稳定高效的基础设施工具。
如果你愿意,我还可以继续帮你补一版:
- 更适合公众号发布的排版版式
- 加入 MySQL + Redis 的完整
docker-compose实战版 - 加入 Java Spring Boot / Node.js / Python 三种 Dockerfile 模板