Como configurar o mapeamento de porta do Docker para usar Nginx como um proxy upstream?

86

Atualização II

Agora é 16 de julho de 2015 e as coisas mudaram novamente. Eu descobri este contêiner automagical de Jason Wilder : https://github.com/jwilder/nginx-proxye ele resolve esse problema no tempo que leva para docker runo contêiner. Esta é agora a solução que estou usando para resolver este problema.

Atualizar

Agora é julho de 2015 e as coisas mudaram drasticamente com relação aos contêineres Docker de rede. Existem agora muitas ofertas diferentes que resolvem esse problema (de várias maneiras).

Você deve usar esta postagem para obter uma compreensão básica da docker --linkabordagem da descoberta de serviço, que é o mais básico possível, funciona muito bem e, na verdade, requer menos danças fantasiosas do que a maioria das outras soluções. Ele é limitado porque é bastante difícil conectar containers em hosts separados em qualquer cluster, e os containers não podem ser reiniciados depois de conectados em rede, mas oferece uma maneira rápida e relativamente fácil de conectar containers no mesmo host. É uma boa maneira de ter uma ideia do que o software que você provavelmente usará para resolver esse problema está realmente fazendo nos bastidores.

Além disso, você provavelmente vai querer verificar também o nascente do Docker network, o Hashicorp consul, o Weaveworks weave, o Jeff Lindsay's progrium/consul&gliderlabs/registrator e o Google Kubernetes.

Há também os CoreOS ofertas que utilizam etcd, fleete flannel.

E se você realmente deseja dar uma festa, pode montar um cluster para executar Mesosphere, ou Deis, ou Flynn.

Se você é novo em networking (como eu), deve pegar seus óculos de leitura, colocar "Paint The Sky With Stars - O Melhor da Enya" no Wi-Hi-Fi e abrir uma cerveja - vai ser um pouco antes de você realmente entender exatamente o que está tentando fazer. Dica: você está tentando implementar um Service Discovery Layerem seu Cluster Control Plane. É uma maneira muito legal de passar uma noite de sábado.

É muito divertido, mas gostaria de ter dedicado tempo para me educar melhor sobre redes em geral antes de mergulhar de cabeça. Acabei encontrando alguns posts dos deuses do Tutorial do Oceano Digital: Introduction to Networking Terminologye Understanding ... Networking. Eu sugiro que você os leia algumas vezes antes de mergulhar.

Diverta-se!



Postagem Original

Não consigo entender o mapeamento de portas para Dockercontêineres. Especificamente como passar solicitações do Nginx para outro contêiner, ouvindo em outra porta, no mesmo servidor.

Eu tenho um Dockerfile para um contêiner Nginx assim:

FROM ubuntu:14.04
MAINTAINER Me <[email protected]>

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"]



E então o api.myapp.comarquivo de configuração fica assim:

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;

    }

}

E depois outro app.myapp.comtambém.

E então eu corro:

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


E está tudo bem, mas as solicitações não estão sendo repassadas para os outros contêineres / portos. E quando eu ssh no contêiner Nginx e inspeciono os logs, não vejo erros.

Qualquer ajuda?

AJB
fonte
1
Por favor, coloque o material de resposta em sua resposta, não no corpo da pergunta.
jscs

Respostas:

56

A resposta de @T0xicCode está correta, mas pensei em expandir os detalhes, já que na verdade levei cerca de 20 horas para finalmente implementar uma solução funcional.

Se você deseja executar o Nginx em seu próprio contêiner e usá-lo como um proxy reverso para balancear a carga de vários aplicativos na mesma instância do servidor, as etapas que você precisa seguir são as seguintes:

Vincule seus contêineres

Ao usardocker run seus contêineres, normalmente inserindo um script de shell no User Data, você pode declarar links para qualquer outro contêiner em execução . Isso significa que você precisa iniciar seus contêineres em ordem e apenas os últimos contêineres podem se vincular aos anteriores. Igual a:

#!/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

Portanto, neste exemplo, o APIcontêiner não está vinculado a nenhum outro, mas o Appcontêiner está vinculado a APIe Nginxestá vinculado a APIe App.

O resultado disso são as alterações nos envvars e nos /etc/hostsarquivos que residem nos containers APIe App. Os resultados são assim:

/ etc / hosts

Executar cat /etc/hostsdentro de seu Nginxcontêiner produzirá o seguinte:

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 Vars

Executar envdentro de seu Nginxcontêiner produzirá o seguinte:

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

Trunquei muitos dos vars reais, mas os valores acima são os valores-chave de que você precisa para fazer proxy do tráfego para seus contêineres.

Para obter um shell para executar os comandos acima em um contêiner em execução, use o seguinte:

sudo docker exec -i -t Nginx bash

Você pode ver que agora você tem /etc/hostsentradas de arquivo e envvars que contêm o endereço IP local para qualquer um dos contêineres vinculados. Até onde posso dizer, isso é tudo o que acontece quando você executa contêineres com opções de link declaradas. Mas agora você pode usar essas informações para configurar nginxem seu Nginxcontêiner.



Configurando o Nginx

É aqui que fica um pouco complicado e há algumas opções. Você pode escolher configurar seus sites para apontar para uma entrada no /etc/hostsarquivo que dockercriou, ou você pode utilizar o ENVvars e executar uma substituição de string (eu usei sed) em seu nginx.confe em qualquer outro arquivo conf que possa estar em sua /etc/nginx/sites-enabledpasta para inserir o IP valores.



OPÇÃO A: Configurar Nginx usando ENV Vars

Esta é a opção que escolhi porque não consegui fazer a /etc/hostsopção de arquivo funcionar. Em breve tentarei a Opção B e atualizarei esta postagem com todas as descobertas.

A principal diferença entre esta opção e usar a /etc/hostsopção de arquivo é como você escreve seu Dockerfilepara usar um script de shell como o CMDargumento, que por sua vez lida com a substituição da string para copiar os valores IP de ENVpara seu (s) arquivo (s) conf.

Este é o conjunto de arquivos de configuração que terminei:

Dockerfile

FROM ubuntu:14.04
MAINTAINER Your Name <[email protected]>

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;


}

OBSERVAÇÃO: é importante incluí-lo daemon off;em seu nginx.confarquivo para garantir que o contêiner não saia imediatamente após a inicialização.

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

Vou deixar você fazer sua lição de casa sobre a maior parte do conteúdo de nginx.confe api.myapp.conf.

A mágica acontece Nginx-Startup.shquando usamos sedpara fazer a substituição da string no APP_IPplaceholder que escrevemos no upstreambloco de nossos arquivos api.myapp.confe app.myapp.conf.

Esta pergunta ask.ubuntu.com explica muito bem: Encontre e substitua texto dentro de um arquivo usando comandos

GOTCHA No OSX, sedtrata as opções de maneira diferente, o -isinalizador especificamente. No Ubuntu, a -ibandeira tratará da substituição 'no local'; ele irá abrir o arquivo, alterar o texto e então 'salvar' o mesmo arquivo. No OSX, o -isinalizador requer a extensão de arquivo que você gostaria que o arquivo resultante tivesse. Se estiver trabalhando com um arquivo sem extensão, você deve inserir '' como o valor do -isinalizador.

GOTCHA Para usar ENV vars dentro da regex que sedusa para encontrar a string que você deseja substituir, você precisa colocar a var entre aspas duplas. Portanto, a sintaxe correta, embora de aparência instável, é a acima.

Portanto, docker lançou nosso contêiner e acionou a Nginx-Startup.shexecução do script, que costumava sedalterar o valor APP_IPpara a ENVvariável correspondente que fornecemos no sedcomando. Agora temos arquivos conf em nosso /etc/nginx/sites-enableddiretório com os endereços IP dos ENVvars que o docker definiu ao iniciar o contêiner. Em seu api.myapp.confarquivo, você verá que o upstreambloco mudou para este:

upstream api_upstream{
    server 172.0.0.2:3000;
}

O endereço IP que você vê pode ser diferente, mas percebi que geralmente é 172.0.0.x.

Agora você deve ter todo o roteamento adequado.

GOTCHA Você não pode reiniciar / executar novamente nenhum contêiner depois de executar a inicialização da instância inicial. O Docker fornece a cada contêiner um novo IP na inicialização e não parece reutilizar nenhum que foi usado antes. Portanto api.myapp.com, obterá 172.0.0.2 na primeira vez, mas obterá 172.0.0.4 na próxima vez. Mas Nginxjá terá definido o primeiro IP em seus arquivos conf, ou em seu /etc/hostsarquivo, por isso não será capaz de determinar o novo IP para api.myapp.com. A solução para isso é usar CoreOSe seu etcdserviço que, no meu entendimento limitado, atua como um compartilhamento ENVpara todas as máquinas registradas no mesmo CoreOScluster. Este é o próximo brinquedo que vou brincar com a configuração.



OPÇÃO B: usar /etc/hostsentradas de arquivo

Esta deveria ser a maneira mais rápida e fácil de fazer isso, mas não consegui fazer funcionar. Aparentemente, você acabou de inserir o valor da /etc/hostsentrada em seus arquivos api.myapp.confe app.myapp.conf, mas não consegui fazer esse método funcionar.

ATUALIZAÇÃO: Veja a resposta de @Wes Tod para obter instruções sobre como fazer esse método funcionar.

Aqui está a tentativa que fiz em api.myapp.conf:

upstream api_upstream{
    server API:3000;
}

Considerando que há uma entrada em meu /etc/hostsarquivo assim: 172.0.0.2 APIAchei que apenas puxaria o valor, mas não parece ser.

Eu também tive alguns problemas auxiliares com meu Elastic Load Balancersourcing de todos os AZ, então esse pode ter sido o problema quando tentei essa rota. Em vez disso, tive que aprender como lidar com a substituição de strings no Linux, o que foi divertido. Vou tentar isso daqui a pouco e ver no que dá.

AJB
fonte
2
Outra pegadinha no uso de links é que se você reiniciar o contêiner da API, ele provavelmente obterá um novo ip. Isso não se reflete no arquivo nginx containers / etc / hosts, que continuará usando o ip antigo e, portanto, também deve ser reiniciado.
judô de
13

Tentei usar o proxy reverso popular Jason Wilder, que funciona como um passe de mágica para todos e aprendi que não funciona para todos (ou seja, eu). E sou totalmente novo no NGINX e não gostei de não entender as tecnologias que estava tentando usar.

Queria adicionar meus 2 centavos, porque a discussão acima sobre linkingcontêineres juntos agora está desatualizada, pois é um recurso obsoleto. Então aqui está uma explicação de como fazer isso usando networks. Esta resposta é um exemplo completo de configuração do nginx como proxy reverso para um site paginado estaticamente usando uma Docker Composeconfiguração do nginx.

TL; DR;

Adicione os serviços que precisam se comunicar entre si em uma rede predefinida. Para uma discussão passo a passo sobre redes Docker, aprendi algumas coisas aqui: https://technologyconversations.com/2016/04/25/docker-networking-and-dns-the-good-the-bad-and- o feio/

Defina a rede

Em primeiro lugar, precisamos de uma rede na qual todos os seus serviços de back-end possam se comunicar. Liguei para o meu, webmas pode ser o que você quiser.

docker network create web

Crie o aplicativo

Faremos apenas um aplicativo de site simples. O site é uma página index.html simples servida por um contêiner nginx. O conteúdo é um volume montado para o host em uma pastacontent

DockerFile:

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

Observe que não precisamos mais de mapeamento de porta aqui. Simplesmente expomos a porta 80. Isso é útil para evitar colisões de portas.

Execute o aplicativo

Abra este site com

docker-compose up -d

Algumas verificações divertidas sobre os mapeamentos de dns para seu contêiner:

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

Este ping deve funcionar, dentro do seu container.

Crie o proxy

Proxy reverso Nginx:

Dockerfile

FROM nginx

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

Reinicializamos toda a configuração do host virtual, já que iremos customizá-la.

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

Execute o proxy

Abra o proxy usando nosso confiável

docker-compose up -d

Presumindo que não haja problemas, você terá dois contêineres em execução que podem se comunicar usando seus nomes. Vamos testar.

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

Configurar Host Virtual

O último detalhe é configurar o arquivo de hospedagem virtual para que o proxy possa direcionar o tráfego com base em como você deseja configurar sua correspondência:

sample-site.conf para nossa configuração de hospedagem virtual:

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

    server_name my.domain.com;

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

  }

Com base em como o proxy foi configurado, você precisará desse arquivo armazenado em sua conf.dpasta local, que montamos por meio da volumesdeclaração no docker-composearquivo.

Por último, mas não menos importante, diga ao nginx para recarregar sua configuração.

docker exec nginx-proxy service nginx reload

Essa sequência de etapas é o culminar de horas de fortes dores de cabeça enquanto eu lutava com o sempre doloroso erro 502 Bad Gateway e aprendia nginx pela primeira vez, já que a maior parte da minha experiência foi com o Apache.

Esta resposta é para demonstrar como eliminar o erro 502 Bad Gateway que resulta da impossibilidade de comunicação entre os contêineres.

Espero que essa resposta poupe horas de dor para alguém, já que fazer os containers falarem entre si era realmente difícil de descobrir por algum motivo, apesar de ser o que eu esperava ser um caso de uso óbvio. Mas, novamente, meu idiota. E, por favor, deixe-me saber como posso melhorar essa abordagem.

gdbj
fonte
Ah! O velho 502 Gateway Error, um clássico infame nos dias de hoje. Obrigado @gdbj por gastar tempo para avançar a conversa e fornecer uma solução tão detalhada.
AJB
Só queria agradecer por dedicar seu tempo a isso, isso me poupou um pouco de aborrecimento. Obrigado.
Entidade Única
10

Usando links do docker , você pode vincular o contêiner upstream ao contêiner nginx. Um recurso adicionado é que o docker gerencia o arquivo host, o que significa que você poderá se referir ao contêiner vinculado usando um nome em vez do ip potencialmente aleatório.

T0xicCode
fonte
7

"Opção B" de AJB pode ser feita para funcionar usando a imagem base do Ubuntu e configurando o nginx por conta própria. (Não funcionou quando usei a imagem Nginx do Docker Hub.)

Aqui está o arquivo Docker que usei:

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;"]

Minha configuração nginx (também conhecida como: conf / mysite.com):

server {
    listen 80 default;
    server_name mysite.com;

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

upstream website {
    server website:3000;
}

E, finalmente, como inicio meus contêineres:

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

Isso me colocou em funcionamento, então meu nginx apontou o rio acima para o segundo contêiner docker que expôs a porta 3000.

Wes Todd
fonte
Obrigado pela ajuda Wes! Vou tentar quando voltar das férias.
AJB
Tive alguns problemas como esse com as imagens oficiais. Achei muito melhor basear na caixa do ubuntu e, em seguida, extrair as linhas dos arquivos do docker diretamente. Não que isso seja necessário, mas, infelizmente ...
Wes Todd
1
Isso é ótimo, mas o que eu não entendo é como a configuração do nginx apenas sabe o valor de 'website'. Magia Nginx ou magia docker ou outra coisa?
Kyle Chadha
1
A linha upstream website {define o valor do site para nginx. Isso é o que você usa no seu proxy_pass. A parte docker disso usa apenas o mesmo nome para consistência, mas não tem nada a ver com a configuração do nginx. Para deixar um pouco mais claro, a senha do proxy deve ser:upstream website { server localhost:3000; }
Wes Todd
2
Para sua informação, estou usando a imagem nginx oficial mais recente (1.9.2) e parece estar funcionando para mim. Então, talvez eles tenham corrigido o problema.
Pejvan
6

A resposta de @gdbj é uma ótima explicação e a resposta mais atualizada. No entanto, aqui está uma abordagem mais simples.

Portanto, se você deseja redirecionar todo o tráfego do nginx que escuta para 80a exposição de outro contêiner 8080, a configuração mínima pode ser tão pequena quanto:

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"

Docs Docker

Diolor
fonte
A princípio pensei que você teria problemas com colisão de portas.
gdbj
@gdbj Meu problema era a resolução de url / ip entre os contêineres. Acho que tivemos o mesmo. No seu caso você usa redes, o que também está funcionando bem, no meu caso eu simplesmente link os contêineres
Diolor
Ótimo, era tudo que eu precisava! Obrigado por essa resposta.
Floran Gmehlin
2

Acabei de encontrar um artigo de Anand Mani Sankar que mostra uma maneira simples de usar o proxy upstream nginx com o docker composer.

Basicamente, deve-se configurar a vinculação da instância e as portas no arquivo docker-compose e atualizar o upstream em nginx.conf de acordo.

Isborg
fonte
1
O artigo usa linksque estão obsoletos. Use redes agora: docs.docker.com/engine/userguide/networking
gdbj
A substituição mágica do nginx conf funcionará neste caso também?
Leirão