4. Docker Compose:多容器编排的艺术
说实话,刚开始用 Docker 的时候,我总觉得一个容器一个容器地启动也没什么。直到有一次,我在复现一个需要 MySQL + Redis + Nginx + 应用服务四个容器的漏洞环境……手动敲了二十多行 docker run 命令,还搞错了启动顺序,折腾了半小时。嗯,从那以后,Docker Compose 就成了我的标配。
4.1 为什么需要 Docker Compose?
你想想看,一个典型的漏洞环境,往往需要多个服务配合。比如一个 Web 漏洞靶场,可能需要:
- 一个 PHP 容器跑应用
- 一个 MySQL 容器存数据
- 一个 Redis 容器做缓存
- 一个 Nginx 容器做反向代理
手动一个个启动,不仅麻烦,还容易出错。Docker Compose 就是来解决这个问题的——用一个 YAML 文件,把所有的容器定义、网络配置、依赖关系一次性搞定。
核心思想:Docker Compose 通过 docker-compose.yml 文件,将多容器应用的定义、配置、启动、停止、网络连接等操作统一管理。说白了,就是「一键部署」。
4.2 docker-compose.yml 编写技巧
我个人习惯把 docker-compose.yml 分成三个核心部分来看:services(服务)、networks(网络)、volumes(数据卷)。下面我拿一个实际漏洞环境来举例。
4.2.1 基础结构
version: '3.8'
services:
web:
image: vulnweb:latest
ports:
- "8080:80"
depends_on:
- db
- redis
networks:
- vuln-net
db:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: vulndb
volumes:
- db-data:/var/lib/mysql
networks:
- vuln-net
redis:
image: redis:6-alpine
networks:
- vuln-net
volumes:
db-data:
networks:
vuln-net:
driver: bridge
这里有几个关键点,我踩过坑,得跟你说说:
- version 字段:建议用 3.8 或更高版本。我之前用 2.x 版本,结果有些新特性不支持,排查了半天。
- services 命名:尽量用有意义的名称,比如 web、db、cache,别用 s1、s2 这种。你想想看,三个月后你自己都看不懂。
- ports 映射:格式是 "宿主机端口:容器端口"。注意别跟本地其他服务冲突,我习惯用 8080、3307 这类非常用端口。
4.2.2 环境变量管理
在实际项目中,我从来不会把敏感信息直接写在 docker-compose.yml 里。为什么呢?因为代码可能会上传到 Git,密码就泄露了。
services:
db:
image: mysql:5.7
env_file:
- ./db.env
# 或者用环境变量文件
environment:
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
我的习惯:创建一个 .env 文件放在项目根目录,然后在 docker-compose.yml 里用 ${变量名} 引用。.env 文件记得加到 .gitignore 里。
4.3 服务依赖管理
这是最容易出问题的地方。我曾经在复现一个 CMS 漏洞时,Web 容器启动比 MySQL 快,结果应用连不上数据库,报了一堆连接错误。
4.3.1 depends_on 的正确用法
services:
web:
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
db:
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
注意了,depends_on 只控制启动顺序,不等待服务就绪。什么意思呢?就是 Docker 会先启动 db 容器,但不等 MySQL 服务完全启动好,就直接启动 web 容器了。所以需要配合 healthcheck 来真正等待服务就绪。
避坑指南:我曾经在写漏洞环境时,只用了 depends_on 没加 healthcheck,结果每次都要手动重启 web 容器才能连上数据库。后来加了 healthcheck,问题就解决了。
4.3.2 依赖关系的实际案例
来看一个更复杂的例子,一个包含初始化数据导入的场景:
services:
init-db:
image: mysql:5.7
volumes:
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
depends_on:
db:
condition: service_healthy
command: >
sh -c "echo 'Waiting for db...' && sleep 5 && exit 0"
db:
image: mysql:5.7
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 3s
retries: 10
volumes:
- db-data:/var/lib/mysql
web:
image: vulnweb:latest
ports:
- "8080:80"
depends_on:
init-db:
condition: service_completed_successfully
这个结构里,init-db 服务会在 db 健康后执行初始化 SQL,然后退出。web 服务要等 init-db 成功完成后才启动。这样就能保证数据库里有数据了,应用再启动。
4.4 网络配置技巧
我个人习惯为每个漏洞环境创建独立的网络,避免容器间端口冲突。
networks:
vuln-net:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
gateway: 172.20.0.1
为什么要指定子网?因为有时候多个环境同时运行,默认的 172.17.0.0/16 可能会冲突。指定一个不常用的网段,比如 172.20.0.0/16,就能避免这个问题。
4.5 常用命令速查
| 命令 | 用途 | 我的使用场景 |
|---|---|---|
| docker-compose up -d | 后台启动所有服务 | 日常启动漏洞环境 |
| docker-compose down | 停止并删除容器和网络 | 清理环境,释放资源 |
| docker-compose logs -f | 实时查看日志 | 调试启动失败的问题 |
| docker-compose exec web bash | 进入容器内部 | 手动执行漏洞利用代码 |
| docker-compose restart web | 重启单个服务 | 修改配置后快速生效 |
4.6 实战:一个完整的漏洞环境
最后,给你看一个我常用的漏洞环境模板。这是一个包含 SQL 注入靶场的完整配置:
version: '3.8'
services:
web:
build: ./web
ports:
- "8080:80"
volumes:
- ./web/src:/var/www/html
depends_on:
db:
condition: service_healthy
networks:
- sqli-lab
environment:
- DB_HOST=db
- DB_USER=root
- DB_PASS=root123
db:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: sqli_lab
volumes:
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
- db-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 5s
timeout: 3s
retries: 10
networks:
- sqli-lab
adminer:
image: adminer:latest
ports:
- "8081:8080"
networks:
- sqli-lab
depends_on:
- db
volumes:
db-data:
networks:
sqli-lab:
driver: bridge
这个配置里,我加了 adminer 这个数据库管理工具,方便在复现漏洞时查看数据库内容。你想想看,如果每次都要进容器查数据库,多麻烦?
小技巧:在开发调试阶段,可以加一个 adminer 或 phpMyAdmin 容器,方便查看数据。上线前再删掉就行。
嗯,Docker Compose 的内容差不多就这些了。记住一个原则:能用 Compose 解决的问题,就别手动敲命令。我在项目中见过太多因为手动操作导致的环境不一致问题,用 Compose 统一管理,省心又省力。