容器化应用实战:Nginx 与 Tomcat

[!note] 核心摘要

  • 端口映射-p 宿主机端口:容器端口 可将服务暴露给局域网;不加 -p 只能本机/容器网络访问
  • 数据卷挂载-v 宿主机目录:容器目录 让网页目录、配置文件与宿主机共享,修改即时生效
  • 只读挂载:ro 表示容器内只读、宿主机可写,静态页面与配置文件生产环境推荐
  • Tomcat 差异:新版 tomcat 镜像默认不带 ROOT 首页,访问根路径返回 404,需自行挂载部署

一、Nginx 容器实战

1. 运行 Nginx 容器(不暴露端口)

不在 docker host 暴露端口,仅容器内网可访问:

[root@localhost ~]# docker run --name some-nginx -v /some/content:/usr/share/nginx/html:ro -d nginx
fd8c7de2e666ac60e4b098c4fc770c2c8bd7ef6aff3c7462a9de369d1deed4f7
[root@localhost ~]# docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS     NAMES
fd8c7de2e666   nginx     "/docker-entrypoint.…"   43 seconds ago   Up 41 seconds   80/tcp    some-nginx
[root@localhost ~]# docker inspect fd8c7de2e666 | grep IPAddress
        "SecondaryIPAddresses": null,
        "IPAddress": "172.17.0.2",
                "IPAddress": "172.17.0.2",

命令要点解读

  1. docker run --name some-nginx -v /some/content:/usr/share/nginx/html:ro -d nginx
    • --name some-nginx:给容器命名为 some-nginx
    • -v /some/content:/usr/share/nginx/html:ro:挂载卷,宿主机 /some/content 映射到容器 nginx 网页目录,ro = read-only 只读
    • 挂载卷不需要自己创建,Docker 会自动创建
    • -d:后台守护模式运行
    • 没有 -p 端口映射:不会把容器 80 端口暴露到宿主机,外部无法直接访问,只能在 Docker 内部网络访问。
  2. docker ps:PORTS 列只显示 80/tcp,代表容器内部开放 80 端口,但没有映射宿主机端口
  3. docker inspect | grep IPAddress:获取容器 docker 网桥(bridge)内网 IP 172.17.0.2。同一台宿主机上其他容器可以用这个 IP 访问 nginx,宿主机本机也可以访问该 IP,外部机器不能访问

[!warning] 访问范围
没有 -p 端口映射时,外部其他机器无法访问 172.17.0.2,只有本机宿主机、同 docker 网桥的其他容器可以访问。

验证挂载生效

[root@localhost ~]# curl http://172.17.0.2
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.29.0</center>
</body>
</html>
[root@localhost ~]# ls /some/
content
[root@localhost ~]# echo "nginx is start" > /some/content/index.html
[root@localhost ~]# curl http://172.17.0.2
nginx is start

过程解读:

  1. 通过 docker inspect 拿到容器内网 IP 172.17.0.2,宿主机可以直接访问这个容器网桥 IP。
  2. 第一次 curl 访问返回 403 Forbidden:宿主机 /some/content 目录下没有 index.html 首页文件,nginx 找不到默认主页,报 403。
  3. 在宿主机挂载目录写入 index.html 内容 nginx is start
  4. 再次 curl 访问容器 IP,直接读取到我们写入的页面内容,验证宿主机目录和容器目录挂载生效

2. 运行 Nginx 容器(暴露 80 端口)

在 docker host 暴露 80 端口:

[root@localhost ~]# docker run -d -p 80:80 --name some-nginx-port -v /some/content-port:/usr/share/nginx/html:ro nginx
[root@localhost ~]# docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS                               NAMES
fe78fd48861a   nginx     "/docker-entrypoint.…"   39 seconds ago   Up 38 seconds   0.0.0.0:80->80/tcp, :::80->80/tcp   some-nginx-port
[root@localhost ~]# ls /some/
content  content-port
[root@localhost ~]# echo "nginx is running" > /some/content-port/index.html

在宿主机上访问:浏览器地址 192.168.100.152,页面输出 nginx is running

命令要点解读

  1. -p 80:80端口映射,格式 宿主机端口:容器端口
    • 宿主机 80 端口映射到容器内部 80 端口
    • 0.0.0.0:80->80/tcp 代表监听本机所有网卡,外部局域网机器可以访问宿主机 IP 的 80 端口访问 nginx 服务。
  2. -v /some/content-port:/usr/share/nginx/html:ro:挂载新的宿主机目录 /some/content-port 为 nginx 网页目录,只读。
  3. 在宿主机挂载目录写入 index.html,浏览器访问宿主机 IP 192.168.100.152,即可读取页面内容。

[!tip] 与上一例对比
不加 -p 只能本机/容器网络访问;加 -p 端口映射后,局域网其他机器也可以访问服务

3. 挂载配置文件运行

挂载配置文件,需要先创建 1 个 nginx 容器,把配置文件复制出来修改后使用。先查看容器运行的进程,nginx worker 进程数量默认与 CPU 核心数一致:

[root@localhost ~]# docker top some-nginx-port
UID        PID    PPID   C   STIME TTY TIME     CMD
root       2674   2654   0   09:55 ?   00:00:00 nginx: master process nginx -g daemon off;
101        2718   2674   0   09:55 ?   00:00:00 nginx: worker process
101        2719   2674   0   09:55 ?   00:00:00 nginx: worker process

创建 nginx 配置文件修改目录:

[root@localhost ~]# docker cp some-nginx-port:/etc/nginx/nginx.conf /opt/nginx_conf/
Successfully copied 2.56kB to /opt/nginx_conf/

修改配置文件,把进程数 auto 修改为 4:

[root@localhost ~]# ls /opt/nginx_conf/
nginx.conf
[root@localhost ~]# vim /opt/nginx_conf/nginx.conf
worker_processes  4;

重新创建容器,同时挂载网页目录与修改后的配置文件:

[root@localhost ~]# docker run -d \
> -p 82:80 --name nginx-server-conf \
> -v /opt/nginx-server-conf:/usr/share/nginx/html:ro \
> -v /opt/nginx_conf/nginx.conf:/etc/nginx/nginx.conf:ro \
> nginx
311116230fe29bf6a49dc007d07decf311b2e785889cf5d9951a5e61e190f662

# 后续验证 docker top nginx-server-conf 查看 worker 进程的数量

命令解读

  1. docker top some-nginx-port:查看容器内部进程,nginx 分为 master 主进程(root)+ worker 工作进程(uid=101),worker 进程默认跟随 CPU 核心数。
  2. docker cp:把正在运行容器内的 /etc/nginx/nginx.conf 配置文件拷贝到宿主机 /opt/nginx_conf/ 目录,拿到原始配置再修改。
  3. 修改 worker_processes 4;,指定 nginx 工作进程数为 4。
  4. 新建容器,同时挂载网页目录 + 挂载修改后的 nginx 主配置文件(ro 只读)
    • -p 82:80:宿主机 82 端口映射容器 80
    • -v 网页目录:网页静态资源挂载
    • -v 宿主机nginx.conf:/etc/nginx/nginx.conf:ro:把修改后的配置文件注入容器,只读模式防止容器内篡改配置。

[!warning] 配置文件热更新限制
直接挂载单个配置文件,宿主机修改后容器不会自动生效,需要重启容器;如果要热更新,需要挂载整个 conf 目录。

二、数据卷 ro 参数详解

1. 含义

ro = read-only,只读挂载

-v /opt/nginx-server-conf:/usr/share/nginx/html:ro

含义:宿主机 /opt/nginx-server-conf 挂载到容器内 /usr/share/nginx/html容器内部只能读,不能写这个目录

2. 对比两种权限

权限宿主机容器内部适用场景
:ro 只读✅ 可正常增删改文件❌ 不能新建、修改、删除任何文件nginx 静态页面、配置文件(推荐)
默认读写 rw✅ 可正常增删改文件✅ 拥有读写权限需要容器内写数据的场景

3. 为什么 nginx 网页目录要加 :ro

  1. 安全防护:如果 nginx 出现漏洞被入侵,黑客也无法通过 nginx 进程篡改页面文件,容器内没有写权限,页面只能由宿主机侧更新。
  2. nginx 业务本身只需要读取 html,完全不需要写这个目录。
  3. ro 限制的是容器内部,宿主机不受影响:在宿主机修改 index.html,容器立刻就能读到新内容。

[!warning] 容易踩坑点
ro 是容器视角只读,不是宿主机只读。

  • 宿主机:随便改文件
  • 容器内部:echo xxx > /usr/share/nginx/html/a.html 会报错 Read-only file system

4. 补充:对应参数 rw

:rw = read-write 读写,是 docker volume 挂载的默认值,可以省略不写

# 只读,容器不能写
-v /opt/nginx-server-conf:/usr/share/nginx/html:ro

# 等价读写,容器可以写(不推荐给静态网页目录)
-v /opt/nginx-server-conf:/usr/share/nginx/html:rw
# 和下面完全一样,rw 默认省略
-v /opt/nginx-server-conf:/usr/share/nginx/html

本套案例里,nginx 配置文件挂载也加了 :ro

-v /opt/nginx_conf/nginx.conf:/etc/nginx/nginx.conf:ro

同样目的:容器内部 nginx 进程不能修改配置文件,防止容器内篡改配置;宿主机修改文件后,重启容器生效。

[!tip] 记忆口诀
ro:容器只读,宿主机随便改;rw:容器读写,风险更高,业务不需要写就尽量用 ro。

三、Tomcat 容器实战

1. 不暴露端口运行

下载 Tomcat 镜像

[root@localhost ~]# docker pull tomcat
Using default tag: latest
latest: Pulling from library/tomcat
32f112e3802c: Pull complete
ea8f5ca39c1a: Pull complete
a4f9b8cc7730: Pull complete
4e45d9955da7: Pull complete
59425f3d7529: Pull complete
66c77c59c8e6: Pull complete
4f4fb700ef54: Pull complete
ccaa96572930: Pull complete
Digest: sha256:88c2a4ee830b56baec6cd6502935d96876a375889e51bdfa4c390d74df891d30
Status: Downloaded newer image for tomcat:latest
docker.io/library/tomcat:latest

如果下载慢,可以把备份的镜像文件 tomcat_latest.tar 导入:

[root@localhost ~]# docker load -i tomcat_latest.tar

运行容器

[root@localhost ~]# docker run -d --rm tomcat:latest
da3c37fd7e710ed3003f8de2ce3c5516aadd29bb2376906e166de3dbc9bf0884

查看容器

[root@localhost ~]# docker ps
CONTAINER ID   IMAGE         COMMAND             CREATED         STATUS         PORTS     NAMES
da3c37fd7e71   tomcat:latest "shturl.cc/d run"   6 minutes ago   Up 6 minutes   8080/tcp  objective_mirzakhani

命令解读

  1. docker pull tomcat:拉取 tomcat 镜像,不带 tag 默认拉取 tomcat:latest
  2. docker load -i tomcat_latest.tar:离线导入镜像,网络差的时候使用。
  3. docker run -d --rm tomcat:latest
    • -d:后台守护运行
    • --rm容器停止之后自动删除容器,测试环境临时用,生产不要用
    • 没有 -p 端口映射,不会把 8080 端口暴露到宿主机
  4. docker ps 输出:PORTS 列只显示 8080/tcp,表示容器内部监听 8080 端口,没有映射宿主机端口。

[!note] 容器命名
objective_mirzakhani:没有用 --name 指定容器名,docker 自动随机生成容器名字。
补充:tomcat 默认端口是 8080,和 nginx 的 80 端口区分开。

获取容器 IP 并验证

[root@localhost ~]# docker inspect da3 | grep "IPAddress"
        "SecondaryIPAddresses": null,
        "IPAddress": "172.17.0.2",
                "IPAddress": "172.17.0.2",

查看服务,运行的是 Tomcat 11.0 版本:

[root@localhost ~]# curl http://172.17.0.2:8080
<!doctype html><html lang="en"><head><title>HTTP Status 404 - Not Found</title><style type="text/css">body {font-family:Tahoma,Arial,sans-serif;} h1, h2, h3, b {color:white;background-color:#525D76;} h1 {font-size:22px;} h2 {font-size:16px;} h3 {font-size:14px;} p {font-size:12px;} a {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style>
</head><body><h1>HTTP Status 404 - Not Found</h1><hr class="line" /><p><b>Type</b> Status Report</p><p><b>Description</b> The origin server did not find a current representation for the target resource or is not willing to disclose that one exists.</p>
<hr class="line" /><h3>Apache Tomcat/11.0.9</h3></body></html>

解读

  1. docker inspect da3 | grep "IPAddress":截取容器内网 IP 为 172.17.0.2,仅宿主机本机可访问该 IP。
  2. curl http://172.17.0.2:8080:访问 tomcat 容器的 8080 端口。
  3. 返回 404-Not Found:新版 tomcat 镜像默认没有自带 ROOT 默认首页项目,服务本身已经正常启动,但是没有部署 web 应用,所以报 404。页面底部可以看到版本 Apache Tomcat/11.0.9

[!note] 与 nginx 对比
nginx 镜像自带默认页面;tomcat 新版本官方镜像不再内置示例网页,访问根路径直接 404。

  1. 没有做 -p 端口映射,其他局域网机器无法访问 172.17.0.2:8080

2. 暴露端口运行

[root@localhost ~]# docker run -d -p 8080:8080 tomcat:latest
dfc6850532ef43cdf5b4073b5eb008588a4f236a67d16c8608d91f7cb36313cc

查看容器:

[root@localhost ~]# docker ps
CONTAINER ID   IMAGE         COMMAND             CREATED         STATUS         PORTS                                     NAMES
dfc6850532ef   tomcat:latest "shturl.cc/H run"   25 seconds ago  Up 25 seconds  0.0.0.0:8080->8080/tcp, :::8080->8080/tcp musing_mahavira
da3c37fd7e71   tomcat:latest "shturl.cc/H run"   10 minutes ago Up 10 minutes  8080/tcp                                  objective_mirzakhani

命令解读

  1. -p 8080:8080:格式 宿主机端口:容器内部端口,把宿主机的 8080 端口映射到 tomcat 容器内部的 8080 端口。0.0.0.0:8080->8080/tcp:监听本机所有网卡,局域网其他机器可以访问 宿主机IP:8080 访问 tomcat 服务。
  2. docker ps 两个容器对比:
    • dfc6850532ef:做了端口映射,PORTS 显示 0.0.0.0:8080->8080/tcp,外部网络可访问。
    • da3c37fd7e71:没有 -p 映射,PORTS 只有 8080/tcp,仅宿主机本机可通过容器内网 IP 访问。
  3. 没有写 --name,docker 自动随机生成容器名字 musing_mahavira

[!warning] 新版 tomcat 镜像即便端口映射成功,直接访问根路径依旧是 404,需要部署 war 包/web 项目到 webapps 目录。

查看站点内容是否为空:

[root@localhost ~]# docker exec dfc6850532ef ls /usr/local/tomcat/webapps

3. 暴露端口并挂载网站目录

[root@localhost ~]# docker run -d -p 8081:8080 -v /opt/tomcat-server:/usr/local/tomcat/webapps/ROOT tomcat:latest
7cb2ab2b6d15154043f03eaf5523a6dee870fbb0740ee3b96840cf64f98ea63d
[root@localhost ~]# docker ps
CONTAINER ID   IMAGE         COMMAND             CREATED         STATUS         PORTS                                     NAMES
7cb2ab2b6d15   tomcat:latest "catalina.sh run"   47 seconds ago  Up 46 seconds  0.0.0.0:8081->8080/tcp, :::8081->8080/tcp sharp_cori

写入文件内容:

[root@localhost ~]# echo "tomcat running" > /opt/tomcat-server/index.html

浏览器访问 宿主机IP:8081,页面输出 tomcat running

命令解析

  1. docker exec dfc6850532ef ls /usr/local/tomcat/webapps:进入容器执行 ls 命令,查看 webapps 目录,新版 tomcat 镜像 webapps 下面是空的,所以访问根路径 404。
  2. -v /opt/tomcat-server:/usr/local/tomcat/webapps/ROOT
    • 宿主机目录 /opt/tomcat-server 挂载到 tomcat 容器的 ROOT 应用目录(tomcat 根访问路径 / 对应的项目目录)
    • -p 8081:8080:宿主机 8081 端口映射容器 8080 端口,外部访问 宿主机IP:8081
  3. echo "tomcat running" > /opt/tomcat-server/index.html:在宿主机挂载目录写入首页文件,容器内立刻可以读到,访问 IP:8081 就会输出 tomcat running

[!note] 挂载权限
这里挂载没有写 :ro,是默认 rw 读写模式。
ROOT 是 tomcat 特殊目录:访问 http://ip:8081/ 等价读取 webapps/ROOT 下面的资源。

四、命令速查

命令作用
docker run -d -p 80:80 --name xxx nginx运行 nginx 并映射端口
docker cp 容器:路径 宿主机路径拷贝容器内文件到宿主机
docker top 容器名查看容器内进程(宿主机视角)
docker pull tomcat / docker load -i tomcat.tar在线拉取 / 离线导入镜像
docker run -d --rm tomcat运行 tomcat,停止后自动删除容器
-v 宿主机目录:容器目录:ro只读挂载数据卷

更多推荐