If you usually develop with any of the main frontend frameworks/libraries, you probably want to deploy these apps.

If you are familiar with docker, you probably have met the advantages, and would like to serve your Web application using docker, so that you deployment is as simple as running a single command, however, you would like to use a server like nginx as opposed to node to deploy your app as it might give you more flexibility.

How to deploy an Angular, Vue or React app with an nginx container.

Some of the examples shown here will use a Vue App generated by vue-cli, given its simplicity, but since all of these frameworks have a similar static dist generation using npm scripts, they all work the same way.

To make our container as small as possibile we will also make use of docker multi-stage building

Folder Structure

|_/src
  |_/assets
  |_/components
  |_...
|_/public
  |_...
|_package.json
|_package-lock.json
|_Dockerfile
|_.dockerignore
|_nginx.conf
|_...

We will not enter into details, as each project might have its own folder, we are mainly interested in having the following files besides the app keeping the structure above, and those files are:

  • Dockerfile
  • package.json
  • package-lock.json
  • nginx.conf

Dockerfile

### Stage Build ###
FROM node:10-alpine AS builder
RUN apk --update add git
WORKDIR /home/node/app
COPY . .
RUN npm install
RUN npm ci
ENV NODE_ENV=production
RUN npm run build
### Stage Serve Prod ###
FROM andreilhicas/nginx-letsencrypt AS production
COPY --from=builder --chown=nginx:nginx /home/node/app/dist /usr/share/nginx/html
COPY --from=builder --chown=nginx:nginx /home/node/app/nginx.conf /
RUN ln -sf /dev/stdout /var/log/nginx/access.log && \
    ln -sf /dev/stderr /var/log/nginx/error.log
EXPOSE 8080
STOPSIGNAL SIGTERM

Let’s detail the Dockerfile above, namely the npm commands and the multi-stage building.

We start with a node image, where we will copy all the contents of our folder inside, followed by a typical npm install, that will make sure all the packages declared inside package.json, including dev dependencies, are install, this might be useful as sometimes the npm tasks you run, such as build might depend on specific tools declared as dev dependencies, such as vue-cli-service, ng or others.

npm ci is an important npm task for our purpose, as it will prepare our modules for building our distribution using package-lock.json ensuring we will only use our versioned declared modules and ensuring consistency across deployments.

Below is a snippet of the npm ci documentation.

As you can see from above it creates a clean install for deployment.

After running the npm ci command, we use the platform dependent task, named npm run build

The script ran from this task can vary from platform to platform, but the output should be the same for all, it should create a dist folder containing all of our statics under this folder, including our minified css, js and others.

So that’s why we now will use the multi-stage, by declaring a new Stage in our Dockerfile with the FROM andreilhicas/nginx-letsencrypt AS production directive. This is a specific nginx image, that you may see how it has been created in the previous post at nginx with letsencrypt in docker.

The image we are using will also allow us to serve our application with a valid ssl certificate issued by Let’s Encrypt Certificate Authority.

After the new FROM directive, we are copying our dist from our previous built image into the new one, giving us a smaller final image, as we won’t have node, nor its dependencies, just what we wanted to build with the previous image, and that what we are doing by using :

COPY --from=builder --chown=nginx:nginx /home/node/app/dist /usr/share/nginx/html

The other command for the copy could be issued directly from the host in the second stage, but given we’ve copied it before to the builder stage, we are copying from it.

So what is the contents for the nginx.conf file to allow us to serve the static dist we’ve created?

nginx.conf

server {
  listen 80;
  server_name serverName;
  
  location /.well-known/acme-challenge/ {
    root /var/www/certbot;
  }
  location / {
        return 301 https://$host$request_uri;
  }      
}
server {
    listen 443 ssl;
    server_name serverName;
    ssl_certificate /usr/share/nginx/certificates/fullchain.pem;
    ssl_certificate_key /usr/share/nginx/certificates/privkey.pem;
    include /etc/ssl-options/options-nginx-ssl.conf;
    ssl_dhparam /etc/ssl-options/ssl-dhparams.pem;

    sendfile on;
    default_type application/octet-stream;
    gzip on;
    gzip_http_version 1.1;
    gzip_disable "MSIE [1-6]";
    gzip_min_length 1100;
    gzip_vary on;
    gzip_proxied expired no-cache no-store private auth;
    gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
    gzip_comp_level 9;
    root /usr/share/nginx/html;
    location / {
        add_header Pragma "no-cache";
        add_header Cache-Control "no-store, no-cache, must-revalidate, post-check=0, pre-check=0";
        expires -1;
        index index.html index.html;
        try_files $uri $uri/ /index.html =404;
    }
}

So , disregard the optimization regarding the gzip declarations, the part that is actually serving our application, is the one below:

root /usr/share/nginx/html;
    location / {
        add_header Pragma "no-cache";
        add_header Cache-Control "no-store, no-cache, must-revalidate, post-check=0, pre-check=0";
        expires -1;
        index index.html index.html;
        try_files $uri $uri/ /index.html =404;
    }

Using the root location /usr/share/nginx/html, where we copied our dist to, and setting index.html as our entrypoint for all url’s starting at ‘/’

Using the configuration above we are serving our project under https only, as we will redirect all incoming traffic to https, and serve all our static files under https.

So if we build our image with the following command:

docker build -t my_awesome_app:1.0.0 --target production . 

We are using the stage/target production declared in our dockerfile with the AS production tag to build our container.

The following Dockerfile commands are only to ensure we are able to access nginx logs with docker logs command.

If we now want to run our container in production using Let’s Encrypt valid certificates to serve https, and our web application using nginx all we have to do is run the following command:

docker run -e DOMAIN=myawesomeapp.domain.example -e EMAIL=info@awesomeapp.example  -p 80:80 -p 443:443 my_awesome_app:1.0.0 

And that’s it, your Vue, Angular, React or any other frontend application is now running under nginx as opposed to using node, and being served under https only.

Hope this post helped you, and thank you for reading.

André Ilhicas dos Santos

Devops Padawan, curious about systems automation, learning new languages, paradigms tools each day.

ilhicas ilhicas


Published