Deploy a Full Stack Web Application with Docker and Traefik
Feb 06, 2019Overview
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!