Deploy a Full Stack Web Application with Docker and Traefik

docker python traefik Feb 06, 2019

Overview

Have you ever had to deploy a full stack web application? With different services, some mapping to different ports, some static html, maybe a couple databases thrown in there? Have you ever banged your head against your desk trying to figure out how to expose different ports on a remote server, or through a cloud platform?

If you have, you understand

I'm going to go over how I deploy such an application, with nice path names instead of annoying port numbers, using traefik and docker. I'm not going to go into the specific code so much, but I may go crazy and do that at a later time. This setup makes it much easier to deploy your application to cloud platforms that don't expose ports besides 80 by default, because you don't need any other ports!

Get the Code

Like I said, I'm not going to go so much into the code itself, this post is all about Traefik, but it's all here on github. 

The final website, deployed to AWS, is ​here.​​​

This is based off of a Udacity Project, code here. 

The Pain

Deploying full stack web applications has always been a painful experience for me. I used to do these in nginx. Each one of my web services would have its own container, and then a corresponding nginx service that existed solely for a proxy pass. This was verbose, and kind of aggravating to maintain. Invariably, I would screw this up and forget a forward slash somewhere or add a forward slash where there shouldn't have been one. (Ok, so much of the pain was me not having my act together.) This is not to say that I don't use nginx anymore, because of course I do, but for most of my network configuration I moved over to traefik, and I couldn't be happier.

Deploy all the things!

I've been working on the Udacity Machine Learning Engineer Nanodegree. I've really been enjoying it, but along with machine learning I would like to at least be functional in data visualization. I'm not sure if I want to be totally magical at it, but I should at least be able to throw up some bar charts and scatter plots from API calls. I wanted to write my machine learning code in python, and my visualization in javascript. Each of these is a different service that sits in its own docker container.

Traefik in a Nutshell

Ok, actually traefik does about a bazillion things. All of which I'm sure are awesome and amazing. I use it as a load balancer (more on this later), and as a way to map ports to paths without writing those aggravating proxy pass blocks in nginx configs.

PORT

PATH

PURPOSE

5000

/server

Serve up APIs from my python machine learning application (information about the model, data, grid search results, etc)

80

/

Serve up my html web pages created in Angular and D3js

Server Side

All of this is to say that I coopted the very nice example visualization code, along with the code to read in the data and create a model, and mapped those to some REST API end points in a python flask application. I like to have one directory per service, where each directory (and service for that matter) maps to a single docker container. You can see the python app here in github.

 

Client Side

I built the client side of this application in Angular and D3js. As a quick aside, I love Angular. I LOVE ANGULAR. I always found front end web dev to be a necessary evil that I just didn't enjoy. I've tried a bunch of different frameworks, and none of them have clicked with me the way Angular has. I don't do a ton of front end web development, but when I do I always use Angular. I very occasionally develop web interfaces, and I am able to get them out so much faster than I used to.

I use a two part process for deploying my angular apps. I have an angular6 (yes, I know angular is on version 7 now) docker container. I use that to build my application, and spit it out, and then copy it over to the document root of another docker image based off nginx. At the end of the day that index.html that is produced by angular in the dist directory gets served by nginx.

 

Redis Database

For good measure I have a redis database thrown in there. It can talk to the server side application, but nothing else. Since it doesn't need to talk to the outside world it doesn't need any traefik configurations.

Docker Compose Configuration

Our compose configuration has three main components. The server side python app, in this case udacity-finding-donors-server, the client side angular app, udacity-finding-donors-client, and the traefik manager and load balander, aptly named treafik-manager.

All Together Now

  version: '3'
   
  # Run as
  # docker-compose build; docker-compose up -d
  # Check with
  # docker ps
  # Then check the logs with
  # docker logs --tail 50 $container_id
  # docker-compose logs --tail 20 tf_counts
   
  services:
   
  traefik-manager:
  image: traefik:1.5-alpine
  restart: always
  command: [
  "traefik",
  "--api",
  "--docker",
  "--web",
  '--logLevel=info',
  '--docker.domain=localhost',
  '--docker.endpoint=unix:///var/run/docker.sock',
  '--docker.watch=true',
  '--docker.exposedbydefault=false'
  ]
  container_name: traefik
  labels:
  - traefik.frontend.entryPoints=http
  - traefik.frontend.rule=PathPrefixStrip:/traefik;Host:localhost
  - traefik.port=8080
  - traefik.enable=true
  networks:
  - proxy
  ports:
  - "80:80"
  - "443:443"
  - "8080:8080"
  volumes:
  - /var/run/docker.sock:/var/run/docker.sock
   
  redis:
  image: redis:alpine
  networks:
  - proxy
   
  udacity-finding-donors-server:
  build:
  context: finding_donors_flask_app
  dockerfile: Dockerfile
  environment:
  DONOR_DATA: /home/flask/finding_donors_flask_app/materials/census.csv
  ports:
  - "5000:5000"
  labels:
  - traefik.backend=udacity-finding-donors-server
  - traefik.frontend.entryPoints=http
  - traefik.frontend.rule=PathPrefixStrip:/server;Host:localhost
  - traefik.docker.network=proxy
  - traefik.frontend.headers.customresponseheaders.Access-Control-Allow-Origin = '*'
  - traefik.port=5000
  - traefik.enable=true
  depends_on:
  - redis
  networks:
  - proxy
   
  udacity-finding-donors-client:
  build:
  context: finding-donors-web-app
  dockerfile: nginx/Dockerfile
  labels:
  - traefik.backend=udacity-finding-donors-client
  - traefik.frontend.entryPoints=http
  - traefik.frontend.rule=Host:localhost
  - traefik.docker.network=proxy
  - traefik.frontend.headers.customresponseheaders.Access-Control-Allow-Origin = '*'
  - traefik.port=80
  - traefik.enable=true
  depends_on:
  - udacity-finding-donors-server
  networks:
  - proxy
   
  ## This image is only for building the angular interface. It is not used in the final AWS config
  angular6-build:
  build:
  context: finding-donors-web-app
  dockerfile: Dockerfile
  ports:
  - "4200:4200"
  volumes:
  - ./finding-donors-web-app:/app:rw
  networks:
  - proxy
   
  networks:
  proxy:
  driver: bridge

Traefik Manager

The traefik manager is what makes the magic here. It maps our ports to paths, and also gives us a nice interface. Here, I only have one instance of each service, but if I had multiple instances it would act as a load balancer too. 

 

Traefik Manager Labels

The truly important thing to note here are the labels. The labels are what informs traefik of what goes where.

LABEL NAME

LABEL VALUE

DESCRIPTION

traefik.frontend.entryPoints

http

One of https or http. In my case its http

traefik.frontend.rule

PathPrefixStrip:/traefik;Host:localhost

Serve up the traefik service as my-awesome-web-page/traefik. (I'm honestly not sure about this Host:localhost business)

traefik.port

8080

The traefik web service runs internally, in the container, on port 8080, but we don't want to see that nonsense. We want a nice path!

traefik.enable

yes

Yes, Traefik, do your magic!

Server Side

This is the python flask application. It runs on port 5000, as all good gunicorn web apps do. The internal port doesn't matter much, we just have to keep track of it.

 

Server Side Traefik Labels

These are basically the same as before, but we add the traefik.backend tag to tell traefik what to call our service in its web UI, and some custom headers because flask and CORS didn't play nicely.

LABEL NAME

LABEL VALUE

DESCRIPTION

traefik.frontend.entryPoints

http

One of https or http. In my case its http

traefik.frontend.rule

PathPrefixStrip:/server;Host:localhost

Serve up the traefik service as my-awesome-web-page/server. A more common convention would be to see this as /api.

traefik.port

5000

The python app runs on port 5000, so tell traefik about it.

traefik.enable

yes

Yes, Traefik, do your magic!

traefik.backend

udacity-finding-donors-server

Your backend name maps to the name traefik will give your service in its web UI.  I like to keep these the same as my service name.

traefik.frontend.headers.customresponseheaders.Access-Control-Allow-Origin

'*'

Figuring out the CORS for flask and angular to talk to one another was PAINFUL. I kind of wound up throwing these headers all over the show, and it works. In a braver world I would figure out which I actually need, or whitelist particular IP addresses.

Client Side App

This is base nginx container, with my angular application copied into the document root. Nginx runs on port 80, which is one of those things that is not obvious until you know it. Apache runs on port 80 too, and I could have used that instead of nginx since my purpose was so simple.

 

Client Side Traefik Labels

This is the same as the previous configuration, with just a few simple changes.

The most notable change here is the lack of the PathPrefixStrip label value. This is because I want this application to be served up when I type in my-awesome-webpage in the urlbar. I don't want it served at any other location, so that is unnecessary.

LABEL NAME

LABEL VALUE

DESCRIPTION

traefik.frontend.entryPoints

http

One of https or http. In my case its http

traefik.frontend.rule

Host:localhost

I actually want this page to be my main 

traefik.port

80

Nginx runs on port 80, so tell traefik about it.

traefik.enable

yes

Yes, Traefik, do your magic!

traefik.backend

udacity-finding-donors-client

Your backend name maps to the name traefik will give your service in its web UI. 

traefik.frontend.headers.customresponseheaders.Access-Control-Allow-Origin

'*'

Again, CORS. I am baaaad at CORS.

A note on PathPrefixStrip:

This feature is pretty great. Any other time I've done proxy passes, if I had to prefix the path as /server or /api, I had to tell the actual application about it. I'm not sure how to do this with flask, but in most frameworks you pass it in as a command line or configuration argument. That is totally unnecessary here, because the /server gets stripped off the url before flask ever sees it. 

HOW COOL IS THAT?

Wrap Up

I hope I have provided an informative case study for using Traefik to deploy your full stack web applications. I am super stoked over this setup. I deployed this project to AWS, as a one click operation, without exposing any additional ports.

Thanks for reading!

Traefik Resources

Traefik website is here and the docs are here.

Bioinformatics Solutions on AWS Newsletter 

Get the first 3 chapters of my book, Bioinformatics Solutions on AWS, as well as weekly updates on the world of Bioinformatics and Cloud Computing, completely free, by filling out the form next to this text.

Bioinformatics Solutions on AWS

If you'd like to learn more about AWS and how it relates to the future of Bioinformatics, sign up here.

We won't send spam. Unsubscribe at any time.