Docker 核心进阶指南:卷、网络、端口与日志,一篇讲透
很多 Docker 初学者会问:容器删了,数据去哪了?两个容器怎么通信?怎么让外部访问容器里的服务?日志把磁盘撑爆了怎么办?
本文将从零开始,系统讲解 Docker 的卷(Volume)、网络(Network)、端口映射(Port)和日志(Logging),配合大量实战命令和场景示例,帮你彻底搞懂这些核心概念。
一、为什么需要卷?—— 容器是“一次性”的
默认情况下,容器里创建的文件(比如数据库的数据文件、应用生成的日志)都存储在容器的可写层。一旦容器被删除,这些数据就会永久消失。
bash
# 示例:运行一个临时容器,创建一个文件 docker run --name temp alpine touch /data/hello.txt # 删除容器 docker rm temp # 文件已经丢失,无法找回
为了解决“数据持久化”和“容器间共享数据”的问题,Docker 提供了三种存储方式:
| 类型 | 特点 | 适用场景 |
|---|---|---|
| Volume | 由 Docker 管理,存放在/var/lib/docker/volumes/,支持多种驱动 | 生产环境持久化数据,最推荐 |
| Bind mount | 将宿主机任意路径挂载到容器,权限依赖宿主机目录结构 | 开发环境热加载代码、配置文件 |
| tmpfs | 仅存于宿主机内存中,容器停止后数据消失 | 存放敏感或临时数据,如密钥 |
1.1 Volume — 最推荐的持久化方式
基本操作
bash
# 创建卷 docker volume create mydata # 列出所有卷 docker volume ls # 查看卷详情 docker volume inspect mydata # 使用卷运行容器(挂载到容器的 /var/lib/mysql) docker run -d --name mysql-db -v mydata:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0 # 删除卷(需要先停止并删除使用该卷的容器) docker volume rm mydata
实战:MySQL 数据持久化
bash
# 1. 创建数据卷 docker volume create mysql-data # 2. 运行 MySQL 容器,挂载卷 docker run -d \ --name mysql-prod \ -v mysql-data:/var/lib/mysql \ -e MYSQL_ROOT_PASSWORD=root123 \ -p 3306:3306 \ mysql:8.0 # 3. 连接 MySQL 并创建测试数据库 docker exec -it mysql-prod mysql -uroot -proot123 -e "CREATE DATABASE testdb;" # 4. 停止并删除容器(卷仍然存在) docker stop mysql-prod && docker rm mysql-prod # 5. 新建一个容器,使用同一个卷,数据依然存在 docker run -d --name mysql-new -v mysql-data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=root123 mysql:8.0 docker exec -it mysql-new mysql -uroot -proot123 -e "SHOW DATABASES;" # testdb 还在
1.2 Bind Mount — 开发热加载利器
直接将宿主机的文件夹映射到容器,修改宿主机代码,容器内立即生效(常用于开发环境)。
bash
# 将当前目录的 ./app 挂载到容器的 /app docker run -d \ --name dev-node \ -v "$(pwd)"/app:/app \ -w /app \ node:18 npm run dev
注意:Bind mount 依赖宿主机绝对路径,权限问题较多(如容器内用户 uid=1000,而宿主机文件属于 root)。
1.3 tmpfs — 内存级临时存储
bash
# 挂载 tmpfs 到容器的 /tmp,数据不落地磁盘 docker run -d --name temp-cache --tmpfs /tmp:rw,noexec,nosuid,size=2g alpine tail -f /dev/null
二、Docker 网络 — 让容器互相通信
Docker 默认提供三种网络驱动:bridge(默认)、host、none。此外还有overlay(Swarm/K8s)、macvlan等。
2.1 默认 bridge 网络
所有未指定网络的容器都会自动连接到名为bridge的默认网络。但默认 bridge 不支持容器名自动 DNS 解析,只能通过 IP 互相访问,且容器间需要--link才能通信(已过时)。
bash
# 查看默认 bridge 网络 docker network inspect bridge # 运行两个容器 docker run -d --name c1 alpine sleep 3600 docker run -d --name c2 alpine sleep 3600 # 获取 c1 的 IP docker inspect c1 | grep IPAddress # 在 c2 中 ping c1 的 IP(可以,但无法用 ping c1 域名) docker exec c2 ping -c 2 172.17.0.2
2.2 用户自定义 bridge — 推荐方式
自定义 bridge 网络具备自动 DNS 解析功能,容器之间可以通过容器名直接通信。
bash
# 创建自定义网络 docker network create mynet # 运行两个容器并连接到 mynet docker run -d --name web --network mynet nginx docker run -d --name app --network mynet alpine sleep 3600 # 在 app 容器中 ping web(通过容器名) docker exec app ping web # 成功解析
实战:一个 WordPress + MySQL 的例子
bash
# 创建公共网络 docker network create wp-net # MySQL 容器 docker run -d --name mysql-db \ --network wp-net \ -e MYSQL_ROOT_PASSWORD=root \ -e MYSQL_DATABASE=wordpress \ mysql:8.0 # WordPress 容器 docker run -d --name wordpress \ --network wp-net \ -e WORDPRESS_DB_HOST=mysql-db \ -e WORDPRESS_DB_USER=root \ -e WORDPRESS_DB_PASSWORD=root \ -p 8080:80 \ wordpress:latest
现在通过http://localhost:8080即可访问 WordPress,两个容器通过mysql-db这个容器名自动通信。
2.3 host 网络 — 与宿主机共享网络栈
使用--network host后,容器不再拥有独立 IP,而是直接使用宿主机的网络栈(端口也直接暴露在宿主机上)。
bash
# 运行 nginx 使用 host 网络,访问 http://localhost 即可看到 nginx 首页 docker run -d --name nginx-host --network host nginx
适用场景:追求极致网络性能、不需要端口映射(如某些监控 agent)。
2.4 none 网络 — 完全隔离
容器只有 loopback 接口,无法访问外部网络。
bash
docker run -d --name isolated --network none alpine sleep 3600
三、端口映射 — 让外部访问容器服务
容器默认运行在隔离网络内,外部无法直接访问。通过-p或-P将宿主机端口映射到容器端口。
3.1 基本格式
bash
# 宿主机端口:容器端口 -p 8080:80 # 指定 IP(默认 0.0.0.0) -p 127.0.0.1:8080:80 # 只绑定容器端口(随机映射宿主机端口) -p 80 # UDP 端口 -p 53:53/udp # 多个端口映射 -p 8080:80 -p 8443:443
3.2 实战:运行一个 Flask 应用
python
# app.py from flask import Flask app = Flask(__name__) @app.route('/') def hello(): return "Hello from Docker!" if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)bash
# 构建镜像并运行,映射宿主机 5001 -> 容器 5000 docker build -t flask-app . docker run -d --name myflask -p 5001:5000 flask-app
访问http://localhost:5001即可看到输出。
3.3 查看端口映射
bash
docker port myflask # 输出 5000/tcp -> 0.0.0.0:5001 docker ps # PORTS 列显示映射关系
3.4 随机端口分配-P
-P会将 Dockerfile 中EXPOSE声明的端口自动映射到宿主机随机高位端口。
bash
docker run -d --name random-nginx -P nginx docker port random-nginx # 显示 80/tcp -> 0.0.0.0:32768
四、Docker 日志 — 控制、查看与管理
容器产生的标准输出(stdout/stderr)会被 Docker 捕获并交给日志驱动处理。默认驱动是json-file,将日志以 JSON 格式保存在宿主机/var/lib/docker/containers/<container_id>/<container_id>-json.log。
4.1 查看日志 —docker logs
bash
# 查看全部日志 docker logs <container> # 实时跟踪(类似 tail -f) docker logs -f <container> # 查看最后 100 行 docker logs --tail 100 <container> # 带时间戳 docker logs -t <container> # 查看最近 5 分钟的日志 docker logs --since 5m <container>
4.2 限制日志大小 — 防止磁盘爆满
如果不加限制,长期运行的容器(如 MySQL、Nginx)会产生 GB 级别的日志,占满磁盘。通过--log-opt限制:
bash
# 单个日志文件最大 10M,最多保留 3 个文件 docker run -d --name app \ --log-opt max-size=10m \ --log-opt max-file=3 \ nginx
也可以在/etc/docker/daemon.json中全局配置:
json
{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } }然后重启 Docker:systemctl restart docker(会重启所有容器,谨慎操作)。
4.3 切换日志驱动
Docker 支持多种日志驱动,常用的有:
| 驱动 | 说明 | 适用场景 |
|---|---|---|
json-file | 默认,写入本地 JSON 文件 | 单机开发、测试 |
journald | 发送到 systemd journal | 使用 systemd 的 Linux 发行版 |
syslog | 发送到 syslog 服务 | 传统日志集中化 |
fluentd | 发送到 Fluentd 日志收集器 | 容器化微服务日志聚合 |
gelf | 发送到 Graylog 或 ELK | 生产日志分析 |
none | 禁用日志(不建议) | - |
示例:使用 syslog 驱动
bash
docker run -d --name syslog-app --log-driver syslog --log-opt syslog-address=tcp://192.168.1.100:514 alpine sh -c "while true; do echo hello; sleep 1; done"
4.4 生产环境日志最佳实践
始终限制日志大小(
max-size/max-file)不要在容器内写日志文件,而是输出到 stdout/stderr(Docker 自动收集)
使用集中式日志系统(如 ELK、Loki、Graylog),配合
fluentd或gelf驱动定期清理旧日志:
docker system prune -a --volumes或手动删除/var/lib/docker/containers/*/*.log
五、综合实战:部署一个完整的 Web 应用
我们将部署一个Node.js + Redis的计数器应用,包含卷(Redis 数据持久化)、自定义网络、端口映射、日志限制。
5.1 目录结构
text
myapp/ ├── Dockerfile ├── index.js └── package.json
index.js
javascript
const express = require('express'); const redis = require('redis'); const app = express(); const client = redis.createClient({ host: 'redis-server', port: 6379 }); client.set('visits', 0); app.get('/', async (req, res) => { const visits = await client.get('visits'); res.send(`Number of visits: ${visits}`); await client.set('visits', parseInt(visits) + 1); }); app.listen(3000, () => console.log('App listening on port 3000'));package.json
json
{ "name": "myapp", "scripts": { "start": "node index.js" }, "dependencies": { "express": "^4.18.2", "redis": "^4.6.5" } }Dockerfile
dockerfile
FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . CMD ["npm", "start"]
5.2 构建并运行
bash
# 1. 创建自定义网络 docker network create app-net # 2. 创建 Redis 数据卷 docker volume create redis-data # 3. 运行 Redis 容器(挂载卷,限制日志) docker run -d --name redis-server \ --network app-net \ -v redis-data:/data \ --log-opt max-size=5m --log-opt max-file=2 \ redis:7-alpine # 4. 构建 Node 镜像 docker build -t myapp . # 5. 运行 Node 应用(端口映射,日志限制) docker run -d --name myapp \ --network app-net \ -p 3000:3000 \ --log-opt max-size=5m --log-opt max-file=2 \ myapp
访问http://localhost:3000,每次刷新计数器加 1,Redis 数据持久化在卷中,即使删除 redis-server 容器再重新创建(使用同一卷),计数依然保留。
六、常见问题与 FAQ
Q1:为什么我的 bind mount 在容器里看不到文件?
A:检查宿主机路径是否存在,以及容器内挂载点是否覆盖了原有目录内容(bind mount 会使目标目录原有内容隐藏)。
Q2:容器之间用--link和自定义网络有什么区别?
A:--link是旧版解决方案,只允许单向通信且已过时;自定义网络提供自动 DNS,容器名即可通信,推荐使用。
Q3:docker logs显示日志,但为什么我的日志驱动是journald还能看?
A:docker logs实际上会从日志驱动读取,如果驱动不支持(如syslog),docker logs会报错。json-file和journald都支持docker logs。
Q4:如何清理所有未使用的卷?
bash
docker volume prune
Q5:容器端口映射后,宿主机端口被占用怎么办?
A:换一个宿主机端口,或停止占用端口的进程,或使用随机端口-P让 Docker 自动分配。
总结
| 概念 | 核心要点 |
|---|---|
| 卷 | 数据持久化首选 Volume;开发热加载用 bind mount;临时敏感用 tmpfs |
| 网络 | 自定义 bridge 网络实现容器名自动解析;host 高性能;none 隔离 |
| 端口 | -p 宿主机:容器对外暴露服务;-P随机映射 EXPOSE 端口 |
| 日志 | 默认 json-file 需限制 max-size;生产环境建议集中式日志收集 |
掌握这些内容,你已经可以熟练使用 Docker 部署有状态应用、搭建微服务通信、并合理管理日志了。如果你觉得本文对你有帮助,欢迎点赞、收藏、转发~
