很多 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 部署有状态应用、搭建微服务通信、并合理管理日志了。如果你觉得本文对你有帮助,欢迎点赞、收藏、转发~

