如何配置Docker端口映射使用Nginx作为上游代理?

How to configure Docker port mapping to use Nginx as an upstream proxy?

更新二

现在是 2015 年 7 月 16 日,情况又发生了变化。我从 Jason Wilder 那里发现了这个自动容器:https://github.com/jwilder/nginx-proxy 并且它解决了这个问题,只要花费 docker 运行 容器。这是我现在用来解决这个问题的解决方案。

更新

现在是 2015 年 7 月,网络 Docker 容器方面发生了巨大变化。现在有许多不同的产品可以解决这个问题(以各种方式)。

您应该使用这个 post 来获得对 docker --link 服务发现方法的基本了解,它是最基本的,但效果很好,并且实际上比大多数其他解决方案需要更少的花式舞蹈。它的局限性在于,很难在任何给定集群中的不同主机上对容器进行联网,并且容器一旦联网就无法重新启动,但确实提供了一种快速且相对简单的方法来在同一主机上对容器进行联网。这是了解您可能用来解决此问题的软件实际上在做什么的好方法。

此外,您可能还想查看 Docker 的新生网络、Hashicorp 的 consul、Weaveworks weave、Jeff Lindsay 的 progrium/consul 和 gliderlabs/registrator,以及 Google 的 Kubernetes。

还有使用 etcd、fleet 和 flannel 的 CoreOS 产品。

如果你真的想举办派对,你可以将集群旋转到 运行 Mesosphere、Deis 或 Flynn。

如果您是网络新手(像我一样),那么您应该拿出老花镜,在 Wi-Hi-Fi 上播放“用星星画天空 — 恩雅之作”,然后喝杯啤酒— 需要一段时间才能真正准确地理解您正在尝试做什么。提示:您正在尝试在集群控制平面中实现服务发现层。这是度过周六晚上的好方法。

这很有趣,但我希望我能在开始之前花时间更好地了解一般网络知识。我最终从仁慈的数字海洋教程中找到了几个 posts gods: Introduction to Networking Terminology and Understanding ...网络。我建议在深入研究之前先阅读几次。

玩得开心!

原版Post

我似乎无法掌握 Docker 容器的端口映射。具体如何将请求从 Nginx 传递到另一个容器,监听另一个端口,在同一台服务器上。

我有一个 Nginx 容器的 Docker 文件,如下所示:

FROM ubuntu:14.04
MAINTAINER Me <me@myapp.com>

RUN apt-get update && apt-get install -y htop git nginx

ADD sites-enabled/api.myapp.com /etc/nginx/sites-enabled/api.myapp.com
ADD sites-enabled/app.myapp.com /etc/nginx/sites-enabled/app.myapp.com
ADD nginx.conf /etc/nginx/nginx.conf

RUN echo "daemon off;" >> /etc/nginx/nginx.conf

EXPOSE 80 443

CMD ["service", "nginx", "start"]



然后 api.myapp.com 配置文件如下所示:

upstream api_upstream{

    server 0.0.0.0:3333;

}


server {

    listen 80;
    server_name api.myapp.com;
    return 301 https://api.myapp.com/$request_uri;

}


server {

    listen 443;
    server_name api.mypp.com;
    
    location / {

        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
        proxy_pass http://api_upstream;
    
    }

}

然后 app.myapp.com 还有一个。

然后我运行:

sudo docker run -p 80:80 -p 443:443 -d --name Nginx myusername/nginx

一切都很好,但是请求没有传递给另一个 containers/ports。当我通过 ssh 进入 Nginx 容器并检查日志时,我没有看到任何错误。

有什么帮助吗?

使用docker links,可以link上游容器到nginx容器。一个附加功能是 docker 管理主机文件,这意味着您将能够使用名称而不是潜在的随机 ip 来引用 linked 容器。

是正确的,但我想我会详细说明,因为实际上我花了大约 20 个小时才最终实现了一个可行的解决方案。

如果您正在寻找 运行 Nginx 在其自己的容器中并将其用作反向代理以在同一服务器实例上负载平衡多个应用程序,那么您需要遵循的步骤如下:

Link 你的容器

当您 docker run 您的容器时,通常通过将 shell 脚本输入 User Data,您可以将 link 声明给任何其他 运行宁个集装箱。这意味着你需要按顺序启动你的容器,只有后面的容器可以 link 到前面的容器。像这样:

#!/bin/bash
sudo docker run -p 3000:3000 --name API mydockerhub/api
sudo docker run -p 3001:3001 --link API:API --name App mydockerhub/app
sudo docker run -p 80:80 -p 443:443 --link API:API --link App:App --name Nginx mydockerhub/nginx

所以在这个例子中,API 容器没有 linked 到任何其他容器,但是 App 容器被 link 编辑为 API 并且 Nginx 被 link 编辑为 APIApp

这样做的结果是 env 变量和驻留在 APIApp 容器中的 /etc/hosts 文件发生了变化。结果如下所示:

/etc/hosts

运行 cat /etc/hosts 在您的 Nginx 容器中将产生以下内容:

172.17.0.5  0fd9a40ab5ec
127.0.0.1   localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.3  App
172.17.0.2  API



ENV 变量

运行 env 在您的 Nginx 容器中将产生以下内容:

API_PORT=tcp://172.17.0.2:3000
API_PORT_3000_TCP_PROTO=tcp
API_PORT_3000_TCP_PORT=3000
API_PORT_3000_TCP_ADDR=172.17.0.2

APP_PORT=tcp://172.17.0.3:3001
APP_PORT_3001_TCP_PROTO=tcp
APP_PORT_3001_TCP_PORT=3001
APP_PORT_3001_TCP_ADDR=172.17.0.3

我已经运行列出了许多实际的变量,但以上是将流量代理到容器所需的关键值。

要在 运行ning 容器中获取 shell 到 运行 上述命令,请使用以下命令:

sudo docker exec -i -t Nginx bash

您可以看到您现在有 /etc/hosts 个文件条目和 env 个变量,其中包含 linked 的任何容器的本地 IP 地址。据我所知,这就是当你 运行 容器声明了 link 选项时发生的一切。但是您现在可以使用此信息在 Nginx 容器中配置 nginx



配置 Nginx

这里有点棘手,有几个选项。您可以选择将站点配置为指向 docker 创建的 /etc/hosts 文件中的条目,或者您可以使用 ENV 变量和 运行 字符串替换(我在你的 nginx.conf 和你的 /etc/nginx/sites-enabled 文件夹中的任何其他配置文件上使用 sed) 来插入 IP 值。



选项 A:使用环境变量配置 Nginx

This is the option that I went with because I couldn't get the /etc/hosts file option to work. I'll be trying Option B soon enough and update this post with any findings.

此选项与使用 /etc/hosts 文件选项的主要区别在于如何编写 Dockerfile 以使用 shell 脚本作为 CMD 参数,这依次处理字符串替换以将 IP 值从 ENV 复制到您的 conf 文件。

这是我最终得到的一组配置文件:

Dockerfile

FROM ubuntu:14.04
MAINTAINER Your Name <you@myapp.com>

RUN apt-get update && apt-get install -y nano htop git nginx

ADD nginx.conf /etc/nginx/nginx.conf
ADD api.myapp.conf /etc/nginx/sites-enabled/api.myapp.conf
ADD app.myapp.conf /etc/nginx/sites-enabled/app.myapp.conf
ADD Nginx-Startup.sh /etc/nginx/Nginx-Startup.sh

EXPOSE 80 443

CMD ["/bin/bash","/etc/nginx/Nginx-Startup.sh"]

nginx.conf

daemon off;
user www-data;
pid /var/run/nginx.pid;
worker_processes 1;


events {
    worker_connections 1024;
}


http {

    # Basic Settings

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 33;
    types_hash_max_size 2048;

    server_tokens off;
    server_names_hash_bucket_size 64;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;


    # Logging Settings
    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;


    # Gzip Settings

gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 3;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_types text/plain text/xml text/css application/x-javascript application/json;
    gzip_disable "MSIE [1-6]\.(?!.*SV1)";

    # Virtual Host Configs  
    include /etc/nginx/sites-enabled/*;

    # Error Page Config
    #error_page 403 404 500 502 /srv/Splash;


}

NOTE: It's important to include daemon off; in your nginx.conf file to ensure that your container doesn't exit immediately after launching.

api.myapp.conf

upstream api_upstream{
    server APP_IP:3000;
}

server {
    listen 80;
    server_name api.myapp.com;
    return 301 https://api.myapp.com/$request_uri;
}

server {
    listen 443;
    server_name api.myapp.com;

    location / {
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
        proxy_pass http://api_upstream;
    }

}

Nginx-Startup.sh

#!/bin/bash
sed -i 's/APP_IP/'"$API_PORT_3000_TCP_ADDR"'/g' /etc/nginx/sites-enabled/api.myapp.com
sed -i 's/APP_IP/'"$APP_PORT_3001_TCP_ADDR"'/g' /etc/nginx/sites-enabled/app.myapp.com

service nginx start

关于 nginx.confapi.myapp.conf 的大部分内容,我会留给你做功课。

奇迹发生在 Nginx-Startup.sh 中,我们使用 sed 对我们写入 upstream 块的 APP_IP 占位符进行字符串替换 api.myapp.confapp.myapp.conf 个文件。

这个ask.ubuntu.com问题很好地解释了它: Find and replace text within a file using commands

GOTCHA On OSX, sed handles options differently, the -i flag specifically. On Ubuntu, the -i flag will handle the replacement 'in place'; it will open the file, change the text, and then 'save over' the same file. On OSX, the -i flag requires the file extension you'd like the resulting file to have. If you're working with a file that has no extension you must input '' as the value for the -i flag.

GOTCHA To use ENV vars within the regex that sed uses to find the string you want to replace you need to wrap the var within double-quotes. So the correct, albeit wonky-looking, syntax is as above.

所以 docker 启动了我们的容器并将 Nginx-Startup.sh 脚本触发到 运行,它使用 sed 将值 APP_IP 更改为我们在 sed 命令中提供的相应 ENV 变量。我们现在在我们的 /etc/nginx/sites-enabled 目录中有 conf 文件,这些文件具有 ENV vars 的 IP 地址,docker 在启动容器时设置。在您的 api.myapp.conf 文件中,您会看到 upstream 块已更改为:

upstream api_upstream{
    server 172.0.0.2:3000;
}

您看到的 IP 地址可能不同,但我注意到它通常是 172.0.0.x

您现在应该已正确路由所有内容。

GOTCHA You cannot restart/rerun any containers once you've run the initial instance launch. Docker provides each container with a new IP upon launch and does not seem to re-use any that its used before. So api.myapp.com will get 172.0.0.2 the first time, but then get 172.0.0.4 the next time. But Nginx will have already set the first IP into its conf files, or in its /etc/hosts file, so it won't be able to determine the new IP for api.myapp.com. The solution to this is likely to use CoreOS and its etcd service which, in my limited understanding, acts like a shared ENV for all machines registered into the same CoreOS cluster. This is the next toy I'm going to play with setting up.



选项 B:使用 /etc/hosts 文件条目

应该 是更快、更简单的方法,但我无法让它工作。表面上你只是将 /etc/hosts 条目的值输入到你的 api.myapp.confapp.myapp.conf 文件中,但我无法使用此方法。

UPDATE: See for instructions on how to make this method work.

这是我在 api.myapp.conf 中所做的尝试:

upstream api_upstream{
    server API:3000;
}

考虑到我的 /etc/hosts 文件中有一个条目,如下所示:172.0.0.2 API 我认为它只会提取值,但似乎不是。

我的 Elastic Load Balancer 从所有 AZ 采购时也有一些附属问题,所以当我尝试这条路线时,这可能是问题所在。相反,我必须学习如何处理 Linux 中的字符串替换,这很有趣。待会儿我会试一试,看看效果如何。

可以通过使用基础 Ubuntu 映像并自行设置 nginx 来工作。 (当我使用来自 Docker Hub 的 Nginx 图像时,它不起作用。)

这是我使用的 Docker 文件:

FROM ubuntu
RUN apt-get update && apt-get install -y nginx
RUN ln -sf /dev/stdout /var/log/nginx/access.log
RUN ln -sf /dev/stderr /var/log/nginx/error.log
RUN rm -rf /etc/nginx/sites-enabled/default
EXPOSE 80 443
COPY conf/mysite.com /etc/nginx/sites-enabled/mysite.com
CMD ["nginx", "-g", "daemon off;"]

我的 nginx 配置(又名:conf/mysite.com):

server {
    listen 80 default;
    server_name mysite.com;

    location / {
        proxy_pass http://website;
    }
}

upstream website {
    server website:3000;
}

最后,我是如何启动我的容器的:

$ docker run -dP --name website website
$ docker run -dP --name nginx --link website:website nginx

这让我兴奋起来 运行 所以我的 nginx 将上游指向第二个 docker 暴露端口 3000 的容器。

刚刚从 Anand Mani Sankar 那里找到了一篇 article,其中展示了将 nginx 上游代理与 docker composer 一起使用的简单方法。

基本上必须在 docker-compose 文件中配置实例链接和端口,并相应地在 nginx.conf 中更新上游。

我尝试使用流行的 Jason Wilder 反向代理,它的代码神奇地适用于所有人,但了解到它并不适用于所有人(即:我)。而且我是 NGINX 的新手,不喜欢我不了解我尝试使用的技术。

想补充我的 2 美分,因为上面关于 linking 容器在一起的讨论现在已经过时了,因为它是一个已弃用的功能。所以这里有一个关于如何使用 networks 来做的解释。这个答案是使用 Docker Compose 和 nginx 配置将 nginx 设置为静态分页网站的反向代理的完整示例。

TL;DR;

将需要相互通信的服务添加到预定义的网络上。对于 Docker 网络的逐步讨论,我在这里学到了一些东西: https://technologyconversations.com/2016/04/25/docker-networking-and-dns-the-good-the-bad-and-the-ugly/

定义网络

首先,我们需要一个网络,您的所有后端服务都可以在该网络上进行通信。我叫我的 web 但它可以是任何你想要的。

docker network create web

构建应用程序

我们只做一个简单的网站应用程序。该网站是一个简单的 index.html 页面,由 nginx 容器提供服务。内容是挂载到主机的文件夹 content

Docker文件:

FROM nginx
COPY default.conf /etc/nginx/conf.d/default.conf

default.conf

server {
    listen       80;
    server_name  localhost;

    location / {
        root   /var/www/html;
        index  index.html index.htm;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

docker-compose.yml

version: "2"

networks:
  mynetwork:
    external:
      name: web

services:
  nginx:
    container_name: sample-site
    build: .
    expose:
      - "80"
    volumes:
      - "./content/:/var/www/html/"
    networks:
      default: {}
      mynetwork:
        aliases:
          - sample-site

注意我们这里不再需要端口映射了。我们简单地暴露端口 80。这对于避免端口冲突很方便。

运行 应用程序

启动这个网站
docker-compose up -d

关于容器的 dns 映射的一些有趣的检查:

docker exec -it sample-site bash
ping sample-site

此 ping 应该在您的容器内工作。

构建代理

Nginx 反向代理:

Docker文件

FROM nginx

RUN rm /etc/nginx/conf.d/*

我们重置了所有虚拟主机配置,因为我们要对其进行自定义。

docker-compose.yml

version: "2"

networks:
  mynetwork:
    external:
      name: web


services:
  nginx:
    container_name: nginx-proxy
    build: .
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./conf.d/:/etc/nginx/conf.d/:ro
      - ./sites/:/var/www/
    networks:
      default: {}
      mynetwork:
        aliases:
          - nginx-proxy

运行 代理

使用我们的信任启动代理

docker-compose up -d

假设没有问题,那么您有两个容器 运行,它们可以使用它们的名称相互通信。让我们测试一下。

docker exec -it nginx-proxy bash
ping sample-site
ping nginx-proxy

设置虚拟主机

最后一个细节是设置虚拟主机文件,以便代理可以根据您想要设置匹配的方式引导流量:

示例-site.conf 用于我们的虚拟主机配置:

  server {
    listen 80;
    listen [::]:80;

    server_name my.domain.com;

    location / {
      proxy_pass http://sample-site;
    }

  }

根据代理的设置方式,您需要将此文件存储在本地 conf.d 文件夹下,我们通过 docker-compose 文件中的 volumes 声明安装了该文件夹。

最后但同样重要的是,告诉 nginx 重新加载它的配置。

docker exec nginx-proxy service nginx reload

这些步骤序列是我在与永远痛苦的 502 Bad Gateway 错误作斗争以及第一次学习 nginx 时头疼数小时的结果,因为我的大部分经验都是在 Apache 上使用的。

这个答案是为了演示如何消除因容器无法相互通信而导致的 502 Bad Gateway 错误。

我希望这个答案能为人们节省数小时的痛苦,因为出于某种原因,让容器相互通信真的很难弄清楚,尽管这是我预期的一个明显的用例。但话又说回来,我很笨。请告诉我如何改进这种方法。

@gdbj 的回答是一个很好的解释,也是最新的答案。然而,这里有一个更简单的方法。

因此,如果你想将所有流量从监听 80 的 nginx 重定向到另一个暴露 8080 的容器,最低配置可以低至:

nginx.conf:

server {
    listen 80;

    location / {
        proxy_pass http://client:8080; # this one here
        proxy_redirect off;
    }

}

docker-compose.yml

version: "2"
services:
  entrypoint:
    image: some-image-with-nginx
    ports:
      - "80:80"
    links:
      - client  # will use this one here

  client:
    image: some-image-with-api
    ports:
      - "8080:8080"

Docker docs