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
1
2
3
4
{
"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.

EndpointService
http://localhost:3000React SPA
http://localhost:5000TODO API

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

package.json
1
2
3
4
{
"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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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.

1
2
3
4
5
6
7
8
9
10
11
12
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.

1
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.

1
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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?

Share