0

I have a fairly standard ReactJS frontend (using port 3000) app which is served by a NodeJS backend server (using port 5000). Both apps are Dockerized and I have configured NGINX in order to proxy requests from the frontend to and from the server.

Dockerfile for front end (with NGINX "baked in"):

FROM node:lts-alpine as build

WORKDIR /app

COPY ./package.json ./
COPY ./package-lock.json ./

RUN npm install
COPY . .
RUN npm run build

FROM nginx

EXPOSE 3000
EXPOSE 443
EXPOSE 80

COPY ./cert/app.crt /etc/nginx/
COPY ./cert/app.key /etc/nginx/

ENV HTTPS=true
ENV SSL_CRT_FILE=/etc/nginx/app.crt
ENV SSL_KEY_FILE=/etc/nginx/app.key

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

COPY ./default.conf /etc/nginx/nginx.conf
COPY --from=build /app/build/ /usr/share/nginx/html

CMD ["nginx", "-g", "daemon off;"]

Dockerfile for server:

FROM node:lts-alpine as build

WORKDIR /app
EXPOSE 5000

ENV NODE_TLS_REJECT_UNAUTHORIZED=0
ENV DANGEROUSLY_DISABLE_HOST_CHECK=true
ENV NODE_CONFIG_DIR=./config/

COPY ./package.json ./
COPY ./package-lock.json ./

RUN npm install

COPY . .
CMD [ "npm", "start" ]

The docker-compose.yml for this setup is

version: '3.8'
services:
  client:
    container_name: client
    depends_on:
      - server
    stdin_open: true
    environment:
      - CHOKIDAR_USEPOLLING=true
      - HTTPS=true
      - SSL_CRT_FILE=/etc/nginx/app.crt
      - SSL_KEY_FILE=/etc/nginx/app.key
    build:
      dockerfile: Dockerfile
      context: ./client
    expose:
      - "8000"
      - "3000"
    ports:
      - "3000:443"
      - "8000:80"
    volumes:
      - ./client:/app
      - /app/node_modules
      - /etc/nginx
    networks:
      - internal-network

  server:
    container_name: server
    build:
      dockerfile: Dockerfile
      context: "./server"
    expose:
      - "5000"
    ports:
      - "5000:5000"
    volumes:
      - /app/node_modules
      - ./server:/app
    networks:
      - internal-network

networks:
  internal-network:
    driver: bridge
    

And crucially, the NGINX default.conf is

worker_processes auto;

events {
  worker_connections 1024;
}

pid /var/run/nginx.pid;

http {

    include mime.types;

    upstream loadbalancer {
        server server:5000 weight=3;
    }

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

        server_name _;

        port_in_redirect off;
        absolute_redirect off;

        return 301 https://$host$request_uri;
    }

    server {
        listen [::]:443 ssl;
        listen 443 ssl;

        server_name example.app* example.co* example.uksouth.azurecontainer.io* localhost*;
        error_page 497 https://$host:$server_port$request_uri;

        error_log /var/log/nginx/client-proxy-error.log;
        access_log /var/log/nginx/client-proxy-access.log;

        ssl_protocols              TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers                ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-ECDSA-RC4-SHA:AES128:AES256:RC4-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK;
        ssl_prefer_server_ciphers  on;
        ssl_session_cache          shared:SSL:10m;
        ssl_session_timeout        24h;

        keepalive_timeout 300;
        add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains';

        ssl_certificate     /etc/nginx/app.crt;
        ssl_certificate_key /etc/nginx/app.key;

        root /usr/share/nginx/html;
        index index.html index.htm index.nginx-debian.html;

         location / {
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_cache_bypass $http_upgrade;
            try_files $uri $uri/ /index.html;
        }
        
        location /tours {
            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_pass http://loadbalancer;
        }
    }
}

with this configuration I have two problems:

  1. By running docker-compose up -d, this setup builds and deploys two Docker containers locally. When I use https://localhost:3000/id this works and the data is retrieved and shown in browser correctly - when I type http://localhost:3000/id this gets redirected to http://localhost:443/id and this does not work. I have attempted to use NGINX commands port_in_redirect off; absolute_redirect off; but this has not helped. How can I make sure that the redirect does not edit the port number? (this is likely not going to be an issue in production where the port numbers are not used).

  2. The bigger problem: the deployment to Azure is done using a docker context and running docker-compose -f ./docker-compose-azure.yml up. This runs and creates two Docker containers and a side-car process. The docker-compose-azure.yml file is

    version: '3.8' services:

      client:
        image: dev.azurecr.io/example-client
        depends_on:
          - server
        stdin_open: true
        environment:
          - CHOKIDAR_USEPOLLING=true
          - HTTPS=true
          - SSL_CRT_FILE=/etc/nginx/app.crt
          - SSL_KEY_FILE=/etc/nginx/app.key
        restart: unless-stopped
        domainname: "example-dev"
        expose:
          - "3000"
        ports:
          - target: 3000
            #published: 3000
            protocol: tcp 
            mode: host
        networks:
          - internal-network
    
      server:
        image: dev.azurecr.io/example-server
        restart: unless-stopped
        ports:
          - "5000:5000"
        networks:
          - internal-network
    
    networks:
      internal-network:
        driver: bridge
    

If I don't use HTTPS and a simple reverse proxy - the two issues outline above go away. But with the configuration above, calls to the Azure FQDN/URL fail; HTTPS requests timing out "ERR_CONNECTION_TIMED_OUT", and for HTTP, the site could not be found. What am I doing wrong here?

Thanks for your time.

4
  • "Running on Azure" ? App Service, Kubernetes Service, Container Apps, Virtual Machine? Commented Apr 2, 2022 at 12:19
  • @CSharpRocks Sorry, I am deploying using Azure Container Instances - this is so I can setup using Docker network. Commented Apr 2, 2022 at 12:33
  • This looks like a infrastructure problem. You should to provide reproducible example how did you deploy your app. Why you don't use Azure LB service (e.g. Front Door, ...) for TLS offloading. Also define "this does not work" - what is not working? Network connectivity, DNS resolution, cert validation, ... there is many moving parts and it is not clear what is not working. Commented Apr 4, 2022 at 15:00
  • @JanGaraj I have updated the question - thanks for taking a look at this... Commented Apr 4, 2022 at 16:03

3 Answers 3

1
+500

I think Jan Garaj's answer has touched upon all the important bits. Here is my take, trying to give a targeted answer.

HTTP to HTTPS redirect

Currently the return 301 statement is using the $host variable that only holds the Hostname and not the port information. To capture both, you can use the $http_host variable instead. source

server {
    listen         [::]:80;

    #//307 to preserve POST data
    return 307 https://$http_host$request_uri; 
}

Problems with the Azure config

In the Azure config, you have this bit:

    ports:
      - target: 3000
        #published: 3000
        protocol: tcp 
        mode: host

which identifies 3000 as the internal client port which listens to the requests. But you have to remember that you have a NGINX proxy inside that only listens to ports 80 or 443 (the server blocks in Nginx config). So this is the reason you get the ERR_CONNECTION_TIMED_OUT error because the requests are sent to port 3000 where nothing is listening.

As you want to do a HTTPS deployment, you can set this to 443 and the Nginx will take care of the request.

enabling HTTP redirect on Azure

The final bit is to configure the Azure deployment such that when a HTTP request is made to your URL, it should get redirected to the HTTPS counterpart. We already have the NGINX redirect block for port 80.

BUT, it will not help. As we specify the target to be 443 inside the container, the HTTP request will try to hit 443 and get refused.This article also mentions the same towards the end

Use your browser to navigate to the public IP address of the container group. The IP address shown in this example is 52.157.22.76, so the URL is https://52.157.22.76. You must use HTTPS to see the running application, because of the Nginx server configuration. Attempts to connect over HTTP fail.

This could be solved if it were possible to add another port to Azure config, the port 80.

     ports:
      - port: 443
        protocol: TCP
      - port: 80
        protocol: TCP

I am not sure if Azure allows this, but if it does then thats the final solution.

Sign up to request clarification or add additional context in comments.

Comments

1

I think you need to check/update Nginx configuration file properly and also make sure SSL certificate files are available

# http block would be
server {
        listen 80 default_server;
        return 301 https://$server_name$request_uri;
}

and in https server block, you need to update location block

location /tours {
        proxy_pass http://server:5000;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
}
        
location / {
        try_files $uri $uri/ /index.html;
}

Updated

Your Nginx config file would be

worker_processes auto;

events {
  worker_connections 1024;
}

pid /var/run/nginx.pid;

http {

    include mime.types;

    server {
        listen [::]:443 ssl;
        listen 443 ssl;

        server_name my-redirected-domain.com my-azure-domain.io localhost;

        access_log /var/log/nginx/client-proxy.log;

        ssl_protocols              TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers                ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:ECDHE-RSA-RC4-SHA:ECDHE-ECDSA-RC4-SHA:AES128:AES256:RC4-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK;
        ssl_prefer_server_ciphers  on;
        ssl_session_cache          shared:SSL:10m;
        ssl_session_timeout        24h;

        keepalive_timeout 300;
        add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains';

        ssl_certificate     /etc/nginx/viewform.app.crt;
        ssl_certificate_key /etc/nginx/viewform.app.key;

        root /usr/share/nginx/html;
        index index.html index.htm index.nginx-debian.html;

        location / {
            try_files $uri $uri/ /index.html;
        }
        
        location /tours {
            proxy_pass http://server:5000;
            proxy_set_header Connection "";
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $remote_addr;
        }
    }

    server {
        listen 80 default_server;
        return 301 https://$server_name$request_uri;
    }
}

5 Comments

I will try this ASAP. Thanks for your time.
I don't understand this answer looking at it in detail. Can you provide more detail here please? Are you saying the ordering is not right?
The SSL crt and key are present where they are expected to be.
I have updated it, you should use this config, the important part is the reverse proxy to proxy_pass http://server:5000; instead proxy_pass http://localhost:3000;
Thanks for this - this has got me part of the way - but I am still having issues with HTTP redirection and the site just does not load when I deploy the containers to Azure - the containers run without issue.
1

Use port 443 everywhere to avoid any confusion with port remapping (that can be an advance setup):

1.) Define client container to be running on port 443:

version: '3.8'
services:
  client:
...
    ports:
      - port: 443
        protocol: TCP

2.) Define Nginx to be running on the port 443 with proper TLS setup as you have in your updated nginx.conf

Deploy and open https://<public IP> (you will very likely need to add sec. exception in the browser).

BTW: Azure has quite good article about Nginx with TLS (but more advance setup is used): https://learn.microsoft.com/en-us/azure/container-instances/container-instances-container-group-ssl

IMHO better redirect from http to https is:

server {
    listen         [::]:80;
    return 301 https://$host$request_uri;
}

4 Comments

Hi Jan, thank you very much for your response. I will try your suggestions - in the meantime I have a couple of questions; on 1. the docker-compose for the azure setup does not allow port mappings, so I will switch to use 443 alone. 2. I have seen the TLS Azure article - I would have said that this was not as complex as mine as it is doing revers proxy without redirect on the client. 3. Why do you say that you redirect is better than the one I have? Thanks for your time...
Ps. Locally when I use localhost:3000/9a75f7254a5244d29bf769835875ab7e this works - great. But localhost:3000/9a75f7254a5244d29bf769835875ab7e redirects to localhost/9a75f7254a5244d29bf769835875ab7e which does not work :'[ I think this is due to the mapping you have eluded to 3000 -> 443.
@MoonKnight I hope you know this is not and forum. Your question is already broad (Azure and Docker and TLS topics in one go) and you are generating additional questions. serverfault.com/questions/706438/…
I appreciate it is not a forum, but as you point out it is a complex issue and one I am trying myself daily to actively resolve, hence additional questions. To account for the complexity and moving target I have offered a generous bounty. What more can I say...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.