在正在运行的Docker容器中暴露一个端口

498

我正在尝试创建一个类似完整虚拟机的Docker容器。我知道可以在Dockerfile中使用EXPOSE指令来暴露端口,并可以使用docker run命令中的-p标志来分配端口,但一旦容器实际运行起来,是否有命令可以动态开放/映射其他端口?

例如,假设我有一个正在运行sshd的Docker容器。另一个人使用容器ssh进入并安装httpd。是否有一种方法可以将容器上的80端口映射到主机上的8080端口,以便人们可以访问在容器中运行的Web服务器,而无需重新启动它?


1
您可以为容器分配可路由的IP地址(https://dev59.com/tKDia4cB1Zd3GeqPHKhy#43244221),因此无需进行端口映射。 - Matt
16个回答

399
您无法通过Docker完成此操作,但可以从主机访问容器的未公开端口。
如果您有一个在8000端口上运行某些内容的容器,您可以运行以下命令:
wget http://container_ip:8000

要获取容器的IP地址,请运行以下2个命令:

docker ps
docker inspect container_name | grep IPAddress

在内部,当您运行一个镜像时,Docker会调用iptables来进行操作,因此也许可以对此进行一些变化以使其工作。

要将容器的端口8000暴露到本地主机的端口8001:

iptables -t nat -A  DOCKER -p tcp --dport 8001 -j DNAT --to-destination 172.17.0.19:8000

一种解决方法是设置另一个容器并映射所需的端口,然后比较iptables-save命令的输出(但我必须删除一些其他选项,以便流量不通过docker代理)。 注意:这是规避docker的做法,因此应该意识到它可能会导致问题。 或者,您可以查看(新的?0.6.6之后的)-P选项,该选项将使用随机主机端口,然后将其连接起来。
在0.6.5中,您可以使用LINKs功能启动一个新容器,该容器与现有容器通信,并进行一些额外的中继以连接该容器的-p标志? (我还没有使用过LINKs。)
在Docker 0.11?您可以使用docker run --net host ..将容器直接附加到主机的网络接口(即,net未命名空间化),从而公开容器中打开的所有端口。

8
至少在 Docker 1.3.0 版本中似乎无法使用。当使用 -p 运行 Docker 时,会创建 DOCKER DNAT 规则,但手动添加规则似乎无法允许连接。奇怪的是,即使在容器运行时删除规则,它似乎也不会停止工作... - Silas Davis
4
谢谢。我曾被一种安全感所迷惑,认为未暴露的端口是安全的。 - seanmcl
自动化事务并使用 jq + sed,可能会很有用:CONTAINER_IP=$(docker inspect container_name | jq .[0].NetworkSettings.IPAddress | sed -r 's/\"([^\"]+)\"/\1/''])'); iptables -t nat -A DOCKER -p tcp --dport 8001 -j DNAT --to-destination ${CONTAINER_IP}:8000 - ericson.cepeda
7
你可以使用docker inspect-f选项,而不是使用jqsed命令来获取容器IP地址: CONTAINER_IP=$(docker inspect -f '{{ .NetworkSettings.IPAddress }}' container_name)。请注意,这不会改变原来的意思,只是使其更加通俗易懂。 - pwes
我收到了这个错误信息:“iptables”不被识别为命令的名称。 - Blue Clouds
显示剩余2条评论

157

以下是我的建议:

  • 提交现有容器。
  • 使用新镜像再次运行容器,并打开端口(我建议挂载共享卷并打开ssh端口)。
sudo docker ps 
sudo docker commit <containerid> <foo/live>
sudo docker run -i -p 22 -p 8000:80 -m /data:/data -t <foo/live> /bin/bash

52
我问题的关键部分是需要在不重启容器的情况下进行此操作...切换到新容器可能会保留文件,但实际上会终止任何正在运行的进程,并类似于物理机器的重启。我需要在没有这种情况发生的情况下进行操作。谢谢! - reberhardt
好的。不过我更倾向于将其比作启动一个并行实例。因为现在旧的和新的都在运行,一旦必要的迁移完成,代理到新容器可能会更容易。 - bosky101
是的,并行实例和反向代理是我喜欢Docker的一些主要原因。然而,在这种情况下,我需要保留容器中通过SSH启动的所有运行进程。尽管在提交镜像并启动并行实例时可保存可执行文件,但是可执行文件本身不会被启动,RAM中的任何内容都将丢失。 - reberhardt
7
为什么要运行 sudo docker 而不是仅仅运行 docker - Thiago Figueiro
1
这个技巧对我帮助很大,因为我在一个慢速网络中安装了大量的软件包。通过运行 docker commit 命令,我可以立即测试应用程序,而不必花费数小时重新安装所有软件包。 - gustavohenke
我知道这个问题已经很老了,但是提问者提到“进程...通过SSH启动”。如果您能够SSH连接到容器,则可以使用SSH端口转发打开任意数量的端口(而无需重新启动容器) - 只需注销SSH然后重新登录,或者在最坏的情况下第二次登录到容器。 - Ralph Bolton

92

虽然您无法公开暴露现有容器的新端口,但可以在相同的Docker网络中启动一个新容器并将流量转发到原始容器。

# docker run \
  --rm \
  -p $PORT:1234 \
  verb/socat \
    TCP-LISTEN:1234,fork \
    TCP-CONNECT:$TARGET_CONTAINER_IP:$TARGET_CONTAINER_PORT

操作范例

启动一个监听端口为80的Web服务,但不要暴露其内部端口80(糟糕!):

# docker run -ti mkodockx/docker-pastebin   # Forgot to expose PORT 80!

找到它的 Docker 网络 IP:

# docker inspect 63256f72142a | grep IPAddress
                    "IPAddress": "172.17.0.2",

使用verb/socat启动,并且暴露端口8080,将TCP流量转发到该IP的端口80:

# docker run --rm -p 8080:1234 verb/socat TCP-LISTEN:1234,fork TCP-CONNECT:172.17.0.2:80

现在你可以访问 http://localhost:8080/ 上的 pastebin,而你的请求将会被发送到 socat:1234 并被转发到 pastebin:80,响应也将沿着同样的路径返回。


9
相当聪明!不过,最好使用verb/socat:alpine,因为它的镜像占用空间只有5%(除非你遇到libc或DNS不兼容的情况)。 - jpaugh
8
也有 alpine/socat - Jamby
3
优秀的答案。简洁明了,只需一行代码即可完成,无需采用任何不正当手段。 - Bruno Brant
6
谢谢!这对我来说完美地解决了问题。由于我在Docker组合中启动了未公开的容器并创建了网络,所以我必须在我的 verb/socat 启动命令中添加 --net myfoldername_default。请注意不要更改原意。 - emazzotta
1
非常适合我的PostgreSQL。我甚至不需要触碰服务器,就可以随时打开/关闭外部访问。 - Magno C

42

在 Docker 1.4.1 上,IPtables 的黑客技巧不起作用。

最好的方法是使用 socat 运行另一个容器来公开端口并进行中继。这就是我用 SQLPlus (暂时) 连接数据库所做的。

docker run -d --name sqlplus --link db:db -p 1521:1521 sqlplus

Dockerfile:

FROM debian:7

RUN apt-get update && \
    apt-get -y install socat && \
    apt-get clean

USER nobody

CMD socat -dddd TCP-LISTEN:1521,reuseaddr,fork TCP:db:1521

2
iptables的黑科技应该从主机运行,而不是docker容器。它本质上是将对主机上某些端口的请求转发到相应的docker容器端口。它根本不是Docker特定的,您可以在完全不同的主机上进行操作。您能在Docker文件中添加代码格式吗?它看起来非常有用。 - AusIV
1
2016年2月:运行Docker 1.9.1,这是唯一成功的解决方案。没有任何IPTables解决方案起作用。值得建议使用与您的DB容器相同的FROM基础镜像,以有效利用资源。 - Excalibur
5
你可以尝试使用apline/socat,它已预安装socat并将socat选项作为命令接受,因此你根本不需要编写Dockerfile。 - trkoch

37

这里有另一个想法。使用SSH进行端口转发;这样可以在Docker主机是虚拟机时,也能在OS X(可能也适用于Windows)上工作。

docker exec -it <containterid> ssh -R5432:localhost:5432 <user>@<hostip>

6
在我的情况下,正在运行的Docker镜像没有SSH二进制文件。 - Radu Toader
我需要在我的机器上创建一个SSH密钥并将其放入Docker容器中吗? - bsky
@octavian 在容器内使用 ssh-keygen -t rsa 生成密钥。将公钥添加到主机上的 authorized_keys 文件中。 - Luke W

11
为了补充已被接受的答案 iptables解决方案,我还需要在主机上运行另外两个命令来将其开放到外部世界。
HOST> iptables -t nat -A DOCKER -p tcp --dport 443 -j DNAT --to-destination 172.17.0.2:443
HOST> iptables -t nat -A POSTROUTING -j MASQUERADE -p tcp --source 172.17.0.2 --destination 172.17.0.2 --dport https
HOST> iptables -A DOCKER -j ACCEPT -p tcp --destination 172.17.0.2 --dport https

注意:我正在打开https端口(443),我的docker内部IP是172.17.0.2

注意2:这些规则是临时的,只会持续到容器重新启动为止


这对我有用...但后来出现了一些问题。根据注释2,如果容器重新启动,它可能会停止,并且可能在不同的IP上。但更重要的是,Docker不知道这些iptables条目,因此当您稍后完全重新启动服务并让Docker正确执行时,它们将不会被删除。结果是多个完全相同的iptables条目,导致它失败而几乎没有错误或指示原因。一旦排除了额外的规则,问题就消失了。换句话说,在任何更改后,请非常仔细地检查您的IP表。 - anthony

8
我曾处理过同样的问题,而且能够在不停止任何正在运行的容器的情况下解决它。这是一个截至2016年2月最新的解决方案,使用Docker 1.9.1。无论如何,本答案是对@ricardo-branco答案的详细版本,但对于新用户更加深入。

在我的场景中,我想临时连接到在容器中运行的MySQL,由于其他应用程序容器都与其链接,停止、重新配置和重新运行数据库容器是不可行的。

由于我想从 Sequel Pro通过SSH隧道访问MySQL数据库,所以我要在主机上使用端口33306。(不要使用3306,以防外部MySQL实例运行。)

大约花了一小时的时间进行iptables调整,但都没有效果:

步骤如下:

mkdir db-expose-33306
cd db-expose-33306
vim Dockerfile

编辑dockerfile,将其放置在其中:

# Exposes port 3306 on linked "db" container, to be accessible at host:33306
FROM ubuntu:latest # (Recommended to use the same base as the DB container)

RUN apt-get update && \
    apt-get -y install socat && \
    apt-get clean

USER nobody
EXPOSE 33306

CMD socat -dddd TCP-LISTEN:33306,reuseaddr,fork TCP:db:3306

然后构建镜像:

docker build -t your-namespace/db-expose-33306 .

然后运行它,链接到正在运行的容器。(在此情况下,使用-d而不是-rm将其保留在后台,直到明确停止和删除。我只想在这种情况下暂时运行它。)

docker run -it --rm --name=db-33306 --link the_live_db_container:db -p 33306:33306  your-namespace/db-expose-33306

您可以直接使用动词/ socat或alpine / socat镜像。请参见verb / socat。 - pjotr_dolphin

7
你可以使用SSH来创建一个隧道,并将你的容器暴露在你的主机中。你可以从容器到主机和从主机到容器两种方式进行。但是,你需要在两个环境中都安装一个SSH工具,比如OpenSSH(客户端和服务器各自需要安装)。例如,在容器中,你可以这样做:
$ yum install -y openssh openssh-server.x86_64
service sshd restart
Stopping sshd:                                             [FAILED]
Generating SSH2 RSA host key:                              [  OK  ]
Generating SSH1 RSA host key:                              [  OK  ]
Generating SSH2 DSA host key:                              [  OK  ]
Starting sshd:                                             [  OK  ]
$ passwd # You need to set a root password..

你可以从容器内的此行中找到容器的IP地址:
$ ifconfig eth0 | grep "inet addr" | sed 's/^[^:]*:\([^ ]*\).*/\1/g'
172.17.0.2

然后在主机上,你只需要这样做:

sudo ssh -NfL 80:0.0.0.0:80 root@172.17.0.2

这对我有用,只需要做一个小修正...这个命令可以工作:sudo ssh -NfL 25252:172.17.0.50:22 $USER@localhost - Friendly Genius

6

基于Robm的答案,我创建了一个名为portcat的Docker镜像和Bash脚本。

使用portcat,您可以轻松地将多个端口映射到现有的Docker容器。以下是使用(可选)Bash脚本的示例:

curl -sL https://raw.githubusercontent.com/archan937/portcat/master/script/install | sudo bash
portcat my-awesome-container 3456 4444:8080

好的!Portcat正在进行映射:

  • 将端口3456映射到my-awesome-container:3456
  • 将端口4444映射到my-awesome-container:8080

请注意,Bash脚本是可选的,以下命令:

ipAddress=$(docker inspect my-awesome-container | grep IPAddress | grep -o '[0-9]\{1,3\}\(\.[0-9]\{1,3\}\)\{3\}' | head -n 1)
docker run -p 3456:3456 -p 4444:4444 --name=alpine-portcat -it pmelegend/portcat:latest $ipAddress 3456 4444:8080

希望 portcat 能对你们有用。干杯!


3

如果没有任何答案对某人有用 - 检查您的目标容器是否已在Docker网络中运行:

CONTAINER=my-target-container
docker inspect $CONTAINER | grep NetworkMode
        "NetworkMode": "my-network-name",

将其保存在变量$NET_NAME中以备后用:

NET_NAME=$(docker inspect --format '{{.HostConfig.NetworkMode}}' $CONTAINER)

如果是这样,您应该在同一网络中运行代理容器。接下来查找容器的别名:
docker inspect $CONTAINER | grep -A2 Aliases
                "Aliases": [
                    "my-alias",
                    "23ea4ea42e34a"

请将其存储到变量$ALIAS中以备后用:

ALIAS=$(docker inspect --format '{{index .NetworkSettings.Networks "'$NET_NAME'" "Aliases" 0}}' $CONTAINER)

现在在网络$NET_NAME中的容器中运行socat,以便桥接到$ALIAS容器的暴露端口(但未发布):

docker run \
    --detach --name my-new-proxy \
    --net $NET_NAME \
    --publish 8080:1234 \
    alpine/socat TCP-LISTEN:1234,fork TCP-CONNECT:$ALIAS:80

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接