Docker的expose和publish端口差异和关系¶
Dockerfile中的EXPOSE指令¶
在配置 从Dockerfile构建Docker镜像 时候,你会注意到Dockerfile中往往会有一行配置了容器 EXPOSE
端口的配置,例如:
FROM alpine:latest
RUN apk update && apk upgrade
RUN apk add --no-cache nodejs npm
RUN addgroup -S node && adduser -S node -G node
USER node
RUN mkdir /home/node/code
WORKDIR /home/node/code
COPY --chown=node:node app.js ./
USER root
EXPOSE 3000
CMD ["node", "app.js"]
这里我的案例是 Alpine Linux软件开发环境构建 时一个运行 Node.js Atlas 的容器,采用Node.js官方指导文档 How do I start with Node.js after I installed it? 中的Hello World代码 app.js
:
const http = require('http'); const hostname = '127.0.0.1'; const port = 3000; const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello World'); }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
你看,多么完美匹配: 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¶
在部署我的开发测试环境,我采用 在Gentoo上运行Gentoo(容器) 方式运行 Gentoo镜像 容器,默认情况下,我希望我的开发测试服务器能够对外输出常用的端口。 Dockerfile
支持同时输出多个端口,案例采用 gentoo-dev
镜像:
# name the portage image
FROM gentoo/portage:latest as portage
# based on stage3 image
FROM gentoo/stage3:latest
# copy the entire portage volume in
COPY --from=portage /var/db/repos/gentoo /var/db/repos/gentoo
# config make.conf: use chinese mirror
RUN echo 'GENTOO_MIRRORS="http://mirrors.aliyun.com/gentoo http://distfiles.gentoo.org http://www.ibiblio.org/pub/Linux/distributions/gentoo"' >> /etc/portage/make.conf
RUN sed -i 's/\-O2 \-pipe/\-march=native \-O2 \-pipe/g' /etc/portage/make.conf
# config gentoo.conf: use chinese repos
RUN mkdir /etc/portage/repos.conf
RUN cp /usr/share/portage/config/repos.conf /etc/portage/repos.conf/gentoo.conf
RUN sed -i 's/rsync.gentoo.org/rsync.cn.gentoo.org/g' /etc/portage/repos.conf/gentoo.conf
# timezone
RUN echo "Asia/Shanghai" > /etc/timezone
RUN emerge --config sys-libs/timezone-data
# sync
RUN emaint -a sync
# USE for cpu
RUN emerge -qv app-portage/cpuid2cpuflags
RUN echo "*/* $(cpuid2cpuflags)" > /etc/portage/package.use/00cpu-flags
# upgrade: emerge quiet (-q)
RUN emerge -qvuDN @world
# continue with image build ...
RUN emerge -qv sys-apps/openrc
RUN emerge -qv sys-apps/mlocate
RUN emerge -qv net-dns/bind-tools
RUN emerge -qv net-analyzer/netcat
RUN emerge -qv app-editors/neovim
RUN emerge -qv app-admin/sudo
RUN emerge -qv app-misc/tmux
# sshd
RUN rc-update add sshd default
# add account "admin" and give sudo privilege
RUN groupadd -g 1001 admin
RUN useradd -g 1001 -u 1001 -d /home/admin -m admin
RUN usermod -aG wheel admin
RUN echo "%wheel ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
# Add ssh public key for login
RUN mkdir -p /home/admin/.ssh
COPY authorized_keys /home/admin/.ssh/authorized_keys
RUN chown -R admin:admin /home/admin/.ssh
RUN chmod 600 /home/admin/.ssh/authorized_keys
RUN chmod 700 /home/admin/.ssh
# 墙内RVM安装需要梯子,在Dockerfile中注入代理配置
#ENV HTTP_PROXY "http://192.168.6.200:3128"
#ENV HTTPS_PROXY "http://192.168.6.200:3128"
#ENV NO_PROXY "*.baidu.com,.taobao.com"
# Ruby Rails (master)
RUN gpg2 --keyserver keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
RUN curl -sSL https://get.rvm.io | bash -s master --rails
# expose ssh/http/https AND some dev ports
EXPOSE 22
EXPOSE 80
EXPOSE 443
#EXPOSE 3000
#EXPOSE 8000
CMD ["/sbin/init"]
EXPOSE和PUBLISH¶
在Docker的概念中, EXPOSE
和 PUBLISH
是两个不同但是又有关联的概念:
EXPOSE
端口只是在 从Dockerfile构建Docker镜像 的metadata
中定义了暴露端口必须
在容器启动时候PUBLISH
(发布) 端口才能使得外部能够访问容器内部的服务端口
也就是说, exposing
端口不能立即生效,这个状态只是容器内部的应用服务器监听端口,并没有 binding
(绑定) 到物理主机的网络接口上。
从Dockerfile构建Docker镜像 中列出 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
为例:
docker run -dt -p 1122:22 -p 1180:80 -p 11443:443 \
--name gentoo-dev --hostname gentoo-dev 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
指令是映射到容器内部的虚拟网卡上,所以无法访问。
解决方法是修改 app.js
监听 0.0.0.0
,如下:
const http = require('http'); //const hostname = '127.0.0.1'; //容器化运行,node需要监听在所有端口才能publish port const hostname = '0.0.0.0'; const port = 3000; const server = http.createServer((req, res) => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Hello World'); }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
则此时容器内服务会监听在内部虚拟机接口 172.17.0.2
上,就能通过Docker PUBLISH
端口映射到外部进行通讯: