.. _docker_expose_publish_port:
======================================
Docker的expose和publish端口差异和关系
======================================
Dockerfile中的EXPOSE指令
=========================
在配置 :ref:`dockerfile` 时候,你会注意到Dockerfile中往往会有一行配置了容器 ``EXPOSE`` 端口的配置,例如:
.. literalinclude:: ../../linux/alpine_linux/alpine_dev/alpine-node
:language: dockerfile
:caption: alpine构建运行node的容器Dockerfile
:emphasize-lines: 10
这里我的案例是 :ref:`alpine_dev` 时一个运行 :ref:`nodejs` 的容器,采用Node.js官方指导文档 `How do I start with Node.js after I installed it? `_ 中的Hello World代码 ``app.js`` :
.. literalinclude:: ../../linux/alpine_linux/alpine_dev/app-localhost.js
:language: javascript
:caption: nodejs Hello World (绑定127.0.0.1)
:emphasize-lines: 4
你看,多么完美匹配: Dockerfile 中的 ``EXPOSE 3000`` 对应 js 中指令 ``const port = 3000;`` ,那么我们执行以下 ``docker run`` 命令,就能在网络中访问容器中这个服务了么::
docker build -t alpine-node .
docker run -itd alpine-node:latest
并不是如此,此时检查::
docker ps
可以看到::
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
588f22fe8980 alpine-node:latest "node app.js" About a minute ago Up About a minute 3000/tcp recursing_newton
这里端口显示 ``3000/tcp`` 是表示容器内部的端口 ``3000`` ,但是,为何docker没有在 ``host`` 主机上把这个容器内部的 ``3000`` 端口透出? 现在在物理主机上执行::
netstat -an | grep 3000
可以看到完全是空的,也就是在外部无法访问容器内部端口
多端口EXPOSE
----------------
在部署我的开发测试环境,我采用 :ref:`gentoo_on_gentoo` 方式运行 :ref:`gentoo_image` 容器,默认情况下,我希望我的开发测试服务器能够对外输出常用的端口。 ``Dockerfile`` 支持同时输出多个端口,案例采用 ``gentoo-dev`` 镜像:
.. literalinclude:: ../images/gentoo_image/dev/Dockerfile
:language: dockerfile
:caption: 在 ``gentoo-dev`` 上输出多个常用端口
:emphasize-lines: 67-69
EXPOSE和PUBLISH
=================
在Docker的概念中, ``EXPOSE`` 和 ``PUBLISH`` 是两个不同但是又有关联的概念:
- ``EXPOSE`` 端口只是在 :ref:`dockerfile` 的 ``metadata`` 中定义了暴露端口
- ``必须`` 在容器启动时候 ``PUBLISH`` (发布) 端口才能使得外部能够访问容器内部的服务端口
也就是说, ``exposing`` 端口不能立即生效,这个状态只是容器内部的应用服务器监听端口,并没有 ``binding`` (绑定) 到物理主机的网络接口上。
:ref:`dockerfile` 中列出 ``EXPOSE`` 端口可以帮助用户在启动容器时候配置正确的端口转发规则,特别是使用非标准端口。
- 使用以下命令可以查看运行容器定义的 ``EXPOSE`` 端口::
docker ps --format="table {{.ID}}\t{{.Image}}\t{{.Ports}}"
可以看到::
CONTAINER ID IMAGE PORTS
588f22fe8980 alpine-node:latest 3000/tcp
- 使用以下命令可以检查镜像中 ``EXPOSE`` 端口而无需启动容器::
docker inspect --format="{{json .Config.ExposedPorts}}" alpine-node:latest
输出显示::
{"3000/tcp":{}}
这样用户在启动容器时候就可以配置相应的 ``PUBLISH`` 端口了,见下文。
PUBLISH端口
=============
既然 ``EXPOSE`` 端口只是定义可暴露端口而并没有输出端口,我们就需要在 ``docker run`` 命令中 ``显式`` 地 ``PUBLISH`` (发布)端口::
docker run -itd -p 3000:3000 alpine-node:latest
然后检查 ``docker ps`` 可以看到明显不同的端口信息::
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f01c8a82dc61 alpine-node:latest "node app.js" 34 seconds ago Up 32 seconds 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp objective_blackwell
可以看到,此时物理主机所有接口上 ``3000`` 端口都被映射到容器内部 ``3000`` 端口::
0.0.0.0:3000->3000/tcp, :::3000->3000/tcp
此时在物理主机上执行 ``netstat -an | grep 3000`` 会看到物理主机上已经打开了 ``3000`` 端口监听::
tcp 0 0 0.0.0.0:3000 0.0.0.0:* LISTEN
tcp 0 0 :::3000 :::* LISTEN
PUBLISH 多个端口
==================
结合多个 ``-p`` 参数来执行 ``docker run`` 可以向外发布容器的多个服务端口,同样以 ``gentoo-dev`` 为例:
.. literalinclude:: ../images/gentoo_image/dev/run_gentoo-dev_container
:language: bash
:caption: 运行 ``gentoo-dev`` 容器 输出多个服务端口
PUBLISH ``所有`` 端口
=====================
``docker`` 运行时候还提供了一个 ``--publish-all`` 参数,也就是 ``-P`` (大写),会将Dockerfile中所有 ``EXPOSE`` 列出的端口全部 ``PUBLISH`` 出去::
docker run -itd -P alpine-node:latest
此时 ``docker ps`` 可以看到::
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0af8544b5cc0 alpine-node:latest "node app.js" 3 seconds ago Up 1 second 0.0.0.0:49153->3000/tcp, :::49153->3000/tcp elegant_yonath
虽然非常简单,但是有一个问题,就是在 ``host`` 主机上输出的端口是随机的,对于测试工作来说比较方便,但是如果是对外提供稳定服务则显然不受控制。
使用端口范围
=================
有时候容器内部需要 ``EXPOSE`` 一个端口范围::
EXPOSE 8000-8100
可以通过以下命令方式将 ``host`` 主机上的一段端口范围和容器内部对应起来::
docker run -p 6000-6100:8000-8100 ...
当然也可以使用 ``docker run --publish-all`` 把这100个端口直接输出,只不过物理主机上随机100个端口监听管理起来非常不方便。
什么时候不需要 ``PUBLISH`` 端口
===================================
如果你的容器只是在 ``host`` 物理主机内部做一个开发环境,并不对外通讯,你可以创建一个独立的内部网络,然后将这些容器连接到这个独立网络,而不会对外暴露(安全性)::
docker network create demo-network
docker run -d --network demo-network --name web web:latest
docker run -d --network demo-network --name database database:latest
且慢,真的能访问容器内部的 ``3000`` 端口服务了吗?
====================================================
如果你完全参考我上文的案例,并使用 ``EXPOSE 3000`` 结合 ``docker run -p 3000:3000 alpine-node:latest`` 来运行Nodes.js案例程序,你会惊讶地发现,依然无法访问应用界面的 ``Hellow World`` ,原因就是 ``app-localhost.js`` 使用了::
const hostname = '127.0.0.1';
这使得容器内部应用程序只监听回环地址;而 ``docker run`` 的 ``PUBLISH`` 指令是映射到容器内部的虚拟网卡上,所以无法访问。
.. figure:: ../../_static/docker/network/container_loopback.png
:scale: 50
解决方法是修改 ``app.js`` 监听 ``0.0.0.0`` ,如下:
.. literalinclude:: ../../linux/alpine_linux/alpine_dev/app.js
:language: javascript
:caption: nodejs Hello World (绑定0.0.0.0)
:emphasize-lines: 5
则此时容器内服务会监听在内部虚拟机接口 ``172.17.0.2`` 上,就能通过Docker ``PUBLISH`` 端口映射到外部进行通讯:
.. figure:: ../../_static/docker/network/container_all_interfaces.png
:scale: 50
参考
======
- `What’s the Difference Between Exposing and Publishing a Docker Port? `_
- `Connection refused? Docker networking and how it impacts your image `_
- `Understanding “EXPOSE” in Dockerfile `_
- `How can I expose more than 1 port with Docker? `_