Introduction

Are you fed up with using localhost and a range of different ports when developing locally, having difficulty with testing content security policies, having to configure TLS for a range of different server applications, and using different URLs when running applications on your laptop natively vs in docker?

Using a reverse proxy can solve all of the above.

You can clone the code I’ve written for this article via this GitHub repository.

Outcome

We will configure Nginx server to run on ports 80/443 via docker, proxying traffic to your web application running natively or in docker.

TLS will be configured in Nginx meaning you will only need to do certificate-related config in Nginx.

flowchart LR a[Browser] -->|https://my-website.com| b[Nginx] b -->|http://localhost:1337| c[Application]

Code

Nginx configuration

Firstly we’ll configure Nginx to forward requests to your local site(s), this will need to run on port 80/443 to make the developer experience better as browsers will allow you to omit the port. i.e. you can use https://my-website.com instead of https://my-website.com:port.

The following server code block will tell Nginx to listen to requests to my-website.com on ports 80 and 443.

Additional to proxying HTTP requests it has some config to support larger requests, the reason I’ve included this is because ideally, we don’t want Nginx to block requests that your original server would have served.

proxy_pass http://host.docker.internal:1337/; is where we tell Nginx where to proxy the request to. It should use host.docker.internal for the address not localhost as localhost when running the proxy in a container will not communicate with the host or another container.

As you add more servers into the proxy I’d recommend adding the shared config into another .conf file and including it rather than repeating it in every server and/or location block.

 1server {
 2    listen 80;
 3    listen 443 ssl;
 4
 5    # Support Large Requests
 6    proxy_buffer_size   128k;
 7    proxy_buffers   4 256k;
 8    proxy_busy_buffers_size   256k;
 9    large_client_header_buffers 4 16k;
10    
11    server_name my-website.com;
12
13    # Load Certificates
14    ssl_certificate /etc/ssl/certs/my-cert.crt;
15    ssl_certificate_key /etc/ssl/certs/my-cert.key;
16
17    location / {
18        # Site we want to proxy to
19        proxy_pass http://host.docker.internal:1337/;
20        proxy_pass_request_headers on;
21
22        # Support Large Requests
23        fastcgi_buffers 16 16k;
24        fastcgi_buffer_size 32k;
25
26        # Headers supporting proxying
27        proxy_set_header Host $host;
28        proxy_set_header X-Real-IP $remote_addr;
29        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
30        proxy_set_header X-Forwarded-Server $host;
31        proxy_set_header X-Forwarded-Host $host:$server_port;
32        proxy_set_header X-Forwarded-Proto https;
33        
34        # Headers to proxy websockets
35        proxy_set_header Upgrade $http_upgrade;
36        proxy_set_header Connection "Upgrade";        
37    }
38}

Trusted root certificate

Next, we’ll do the necessary configuration to set up TLS for the Nginx reverse proxy. The reason we need to do this is so that the browser doesn’t complain when we navigate to our site using HTTPS, we need our machine to trust the certificates being provided by Nginx.

The following assumes you have git for windows installed (which comes with OpenSSL bundled as part of it). If you’re not using git for windows then you’ll need to install OpenSSL.

The following Powershell will create an x509 certificate and import it into your trusted certificate store.

1New-Item -ItemType Directory -Force -Path ./certs
2
3$env:Path += ";C:\Program Files\Git\usr\bin"; #Use this when you have git for windows installed
4
5openssl genrsa -des3 -out ./certs/root-ca.key 2048
6openssl req -x509 -new -nodes -key ./certs/root-ca.key -sha256 -days 1825 -out ./certs/root-ca.pem -subj "/C=GB/ST=Somewhere/L=Somewhere/O=MyOrg/OU=IT/CN=my-website.com"
7
8Import-Certificate -FilePath ./certs/root-ca.pem -CertStoreLocation Cert:\LocalMachine\Root

Certificates for your site(s)

Next, we’ll create the relevant certificates for the domains that you want to use for your sites locally in this example we’ll generate a wildcard cert, and a cert for my-website.com this will allow you to run sites like admin.my-website.com and my-website.com etc.

Create a file called certificate.ext and add the following content, this is used when generating the certificates.

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = *.my-website.com
DNS.2 = my-website.com
 1New-Item -ItemType Directory -Force -Path ./docker/certs
 2
 3$domains = @("*.my-website.com", "my-website.com");
 4
 5# Generate certs for the above domains, signing them with our own root cert
 6
 7$certExtFile = "./certs/certificate.ext"
 8$dockerCertPath = "./docker/certs"
 9$rootCaPath = "./certs";
10
11$env:Path += ";C:\Program Files\Git\usr\bin"; #Use this when you have git for windows installed
12
13foreach($domain in $domains)
14{
15    $domainFile = $domain.Replace("*", "");
16
17    openssl genrsa -out "$dockerCertPath/$domainFile.key" 2048;
18    openssl req -new -key "$dockerCertPath/$domainFile.key" -out "$dockerCertPath/$domainFile.csr" -subj "/C=GB/ST=Somewhere/L=Somewhere/O=MyOrg/OU=IT/CN=$domain";
19    openssl x509 -req -in "$dockerCertPath/$domainFile.csr" -CA "$rootCaPath/root-ca.pem" -CAkey "$rootCaPath/root-ca.key" -CAcreateserial -out "$dockerCertPath/$domainFile.crt" -days 825 -sha256 -extfile $certExtFile;
20}

Dockerfile & Docker compose

Here we’re going to create the related config necessary for running the above Nginx reverse proxy via Docker.

The following Dockerfile will run a stable Nginx server inside of a Linux alpine container, and the docker-compose.yml file makes it easy for you to run it and provide it with certificates via a volume.

1FROM nginx:stable-alpine as run
2
3RUN rm -rf /usr/share/nginx/html/*
4
5COPY *.conf /etc/nginx/conf.d/

The following docker-compose file will run the reverse proxy, and two example sites to demonstrate the reverse proxy

 1name: reverse-proxy-example
 2
 3networks:
 4  dev-network:
 5    name: dev-network
 6    driver: bridge
 7
 8services:    
 9  nginx-reverse-proxy:
10    restart: always
11    deploy:
12      resources:
13        limits:
14          memory: 200M
15    container_name: dev-reverse-proxy
16    build:
17      context: nginx
18      dockerfile: Dockerfile
19    ports:
20      - 80:80
21      - 443:443
22    networks:
23      - dev-network
24    volumes:
25      - ./certs/:/etc/ssl/certs
26
27  my-website:
28    restart: always
29    deploy:
30      resources:
31        limits:
32          memory: 200M
33    container_name: my-website
34    build:
35      context: sites
36      dockerfile: my-website.Dockerfile
37    ports:
38      - 1338:80
39    networks:
40      - dev-network
41
42  admin.my-website:
43    restart: always
44    deploy:
45      resources:
46        limits:
47          memory: 200M
48    container_name: admin.my-website
49    build:
50      context: sites
51      dockerfile: admin-my-website.Dockerfile
52    ports:
53      - 1337:80
54    networks:
55      - dev-network

Outro

Congratulations! Your local dev experience should now be more representative of your staging and production environments, and you shouldn’t need to use localhost in your browser anymore when building websites locally.

This also provides a good foundation for more advanced development environment improvements like the ability to proxy to a cloud-hosted application.