Blog.

Nginx 1.19 supports environment variables and templates in Docker

MF

Marco Franssen /

6 min read1105 words

Cover Image for Nginx 1.19 supports environment variables and templates in Docker

In this blog I want to show you a nice new feature in Nginx 1.19 Docker image. I requested it somewhere 2 years ago when I was trying to figure out how I could configure my static page applications more flexibly with various endpoints to backing microservices. Back then I used to have my static pages fetch a json file that contained the endpoints for the apis. This way I could simply mount this json file into my container with all kind of endpoints for this particular deployment. It was some sort of service discovery mechanism I applied back then by having my React application fetch this json file as the first step.

Now since the Release of Nginx 1.19 Docker image it is finally possible to do this using Environment variables which enables you to use Nginx images in a more immutable fashion. By doing so you can package your application in such way it actually also works with the React development server proxy. In a React project you would for example define this in following way in your package.json.

package.json
{
  "name": "my-react-app",
  "proxy": "http://localhost:5000"
}

This would make all calls to http://localhost:3000/api be proxied to http://localhost:5000 which is very convenient, because you can code all api calls as relative paths (e.g. /api/todos). How awesome would it be if I can simply deploy my app like this in an immutable Docker container where I only would have to specify the endpoint of my api.

Good news! As of May 2020 you can do this.

Setting the scene

We have a React application that fetches todo items from an api. During development we are using the following setup to reverse proxy these calls to our api backend service.

| Endpoint | Service | | ------------------------- | --------- | | http://localhost:3000 | React SPA | | http://localhost:5000 | TODO API |

In our package.json we configured this for our development flow as following.

package.json
{
  "name": "my-react-app",
  "proxy": "http://localhost:5000"
}

So now when I want to deploy this app to production, I would love to configure this TODO API endpoint based on the ip address or DNS entry it will get in for example my Kubernetes cluster. Let's have a look how we can achieve that by defining a Nginx template.

default.conf.template
upstream todoapi {
    server ${TODO_API};
}
 
server {
    listen            ${NGINX_PORT};
    listen       [::]:${NGINX_PORT};
    server_name  localhost;
 
    gzip on;
    gzip_disable "msie6";
 
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 8k;
    gzip_http_version 1.1;
    gzip_min_length 0;
    gzip_types text/plain application/javascript text/css text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype;
 
    root   /usr/share/nginx/html;
 
    location / {
        index  index.html index.htm;
        expires -1;
        try_files $uri $uri/ /index.html;
    }
 
    location /api {
        proxy_pass http://todoapi;
    }
 
    #error_page  404              /404.html;
 
    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
 
    location ~* \.(?:manifest|appcache|html?|xml|json)$ {
        expires -1;
        # access_log logs/static.log; # I don't usually include a static log
    }
 
    # Feed
    location ~* \.(?:rss|atom)$ {
        expires 1h;
        add_header Cache-Control "public";
    }
 
    # Media: images, icons, video, audio, HTC
    location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
        expires 1M;
        access_log off;
        add_header Cache-Control "public";
    }
 
    # CSS and Javascript
    location ~* \.(?:css|js)$ {
        expires 1y;
        access_log off;
        add_header Cache-Control "public";
    }
}

In above template I defined some caching rules for static assets and I configured gzip compression. I also defined an upstream where we reverse proxy the urls that start with /api and a variable to define the port on which Nginx runs. See the below abstract.

upstream todoapi {
    server ${TODO_API};
}
 
server {
    listen            ${NGINX_PORT};
    listen       [::]:${NGINX_PORT};
 
    location /api {
        proxy_pass http://todoapi;
    }
}

As you can see we can define variables using following syntax ${VAR}. These variables will be replaced when the Docker container starts with the value as you defined them in the environment variables.

Now we have the template, we can define our Dockerfile.

Dockerfile
FROM node:14-alpine as build
 
WORKDIR /app
COPY package*.json yarn.lock ./
RUN yarn install
COPY public public
COPY src src
RUN yarn build
 
FROM nginx:1.19-alpine
ENV NGINX_PORT 80
ENV TODO_API https://host.docker.internal:5000
COPY nginx/templates /etc/nginx/templates/
COPY --from=build /app/build /usr/share/nginx/html

In the Dockerfile I am using a multi-stage build to have a Docker image which is as small as possible.

  1. First we install our package dependencies
  2. Next we build the React SPA

Now with the output of this intermediate Docker image we will create a new container from the nginx:1.19-alpine image.

  1. We set sane defaults for the 2 environment variables we defined in our default.conf.template.

    host.docker.internal points to our Docker host (your Mac, or Windows). Using this default I can run the image and connect it by default to the service running on my localhost which allows for easy debugging of the API in a development flow.

  2. Then we copy our nginx/templates into the templates folder (you could use more templates and use includes for example)

  3. Last we copy the output of yarn build from our intermediate build image into the server root as defined in our template.

With all of this in place we can now build our Docker image.

docker build -t todo-app .

How does this work

By default the nginx entrypoint replaces all variables found in /etc/nginx/templates/*.template with their respective values using envsubst. The results of this substitution are written to /etc/nginx/conf.d.

So make sure you copy the file in the templates folder and ensure its extension template.

Running the image

Now we can run the image as following using the defaults which still connect to our api running on our localhost via host.docker.internal.

docker run --rm -p 3000:80 todo-app

If you want to run all in docker-compose you can run as following. In the following docker-compose we route the api requests via the internal docker network using the containers default dns record.

docker-compose.yml
version: "2.7"
 
services:
  web:
    image: todo-app
    build: .
    environment:
      TODO_API: http://api:5000
    ports:
      - "3000:80"
 
  api:
    image: todo-api
    build: .

When doing a Kubernetes deployment you simply point the TODO_API variable to the Service deployed on top of your pods.

Now go ahead and try this for yourself. Feel free to comment below if you require help.

Thanks for reading this blog. Please do share it with your friends and colleagues.

Who doesn't want to have a immutable, configurable static web application?

You have disabled cookies. To leave me a comment please allow cookies at functionality level.

More Stories

Cover Image for Hello Next.js, goodbye Hexo

Hello Next.js, goodbye Hexo

MF

Marco Franssen /

For the folks reading my blog for a long time, you might have noticed I'm using my current theme and blogging engine for a long time. About 5 years ago I migrated from Wordpress to Hexo. Wordpress at that point in time was costing me serious money to get a decent performing webpage according to modern standards. So back then I decided to move into a statically generated blog, where I could write my blogs offline using markdown. Hexo has served me very well the last couple of years. It is a stat…

Cover Image for Remove files from Git history using git-filter-repo

Remove files from Git history using git-filter-repo

MF

Marco Franssen /

Many of you have probably been in a situation where you committed a file in your repository which you shouldn't have done in the first place. For example a file with credentials or a crazy big file that made your repository clones very slow. Now there are a lot of blogs and guides already available on how to get these files completely removed. It involves git filter-branch or bfg sourcery. In this blog I'm going to show you the new recommended way of doing this using git-filter-repo, which simpl…

Cover Image for Building a Elasticsearch cluster using Docker-Compose and Traefik

Building a Elasticsearch cluster using Docker-Compose and Traefik

MF

Marco Franssen /

In a previous blog I have written on setting up Elasticsearch in docker-compose.yml already. I have also shown you before how to setup Traefik 1.7 in docker-compose.yml. Today I want to show you how we can use Traefik to expose a loadbalanced endpoint on top of a Elasticsearch cluster. Simplify networking complexity while designing, deploying, and running applications. We will setup our cluster using docker-compose so we can easily run and cleanup this cluster from our laptop. Create a Elasti…

Cover Image for Use the ACME DNS-Challenge to get a TLS certificate

Use the ACME DNS-Challenge to get a TLS certificate

MF

Marco Franssen /

In my previous 2 blogs I have shown you how to build a HTTP/2 webserver. In these blogs we have covered self signed TLS certificates as well retrieving a Certificate via Letsencrypt. I mentioned there you will have to expose your server publicly on the internet. However I now figured out there is another way. So please continue reading. Let's Encrypt is a free, automated, and open certificate authority brought to you by the nonprofit Internet Security Research Group (ISRG). Letsencrypt impleme…