Multiple Web Apps on One Server

DigitalOcean, Docker, Traefik, LetsEncrypt

Posted by Yasthil Bhagwandeen on February 23, 2020 · 6 mins read

Background

I wanted to move away from my previous web hosting company and wanted to try hosting my websites and web apps using a Digitial Ocean droplet instead. The tricky part was how do I use one virtual private server (VPS), to host the following:

  • Personal website
  • Blog
  • ownCloud/private cloud storage
  • Any future website/web app

Docker

  • These days, when one thinks of containerization the first thing that comes to mind is Docker - at least for me that is :)
  • We know we can use Docker to run multiple services on a single host - exactly what we need

So Docker checks the box with running multiple services, however we’ll need something to do the reverse proxying.

Traefik

  • “Traefik is an open-source Edge Router that makes publishing your services a fun and easy experience. It receives requests on behalf of your system and finds out which components are responsible for handling them.” - docs.traefik.io
  • Simply, Traefik knows how to map each entry point to it’s intended web app
  • It’s ideal to use with Docker
  • Supports SSL out the box - LetsEncrypt

Now we have the tools we need to get this done!

Prerequisites

  • Fully qualified domain name
  • Server needs to be accessible via port 80 for HTTP challenge
  • Docker installed
  • The 1GB droplet on DigitalOcean is ideal

Implementation

I have my main repo which has each app in it’s own directory - I used git submodules to achieve this. Using submodules ensures that each project is maintained separately.

The common factor between all the apps I’ve listed above is that they’ll need a web server. We could go with a traditional apache web server, however since we’re using Docker, we can use a tiny NGINX (~5MB). Having a ~5MB webserver is great news because each app will need a separate instance.

DockerFile

Within each folder (submodule), we’ll use a Docker file that looks like this:

FROM nginx:1.17.3-alpine
COPY . /usr/owncloud/nginx/html

All this does is:

  • Grabs the image from docker’s registry
  • Copies the contents of the entire directory to the nginx webserver location on the container

Now we’ll need something to tie all the service together. This is where docker-compose shines!

docker-compose.yml

  • An awesome tool that describes what containers you want to create and their specific properties
  • Here’s the sample of what this may look like
version: "3.7"
services:
  traefik:
    image: traefik:1.7.12
    restart: always      
    networks:
      - web  
    ports:
      - "80:80"      
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./traefik/traefik.toml:/traefik.toml
      - ./traefik/acme.json:/acme.json
  blog:
    build: ./blog
    restart: always
    networks:
      - web
    volumes:
      - ./blog/_site:/usr/owncloud/nginx/html
    labels:
      - "traefik.enable=true"      
      - "traefik.docker.network=web"
      - "traefik.frontend.protocol=http"
      - "traefik.frontend.rule=Host:blog.example.com,www.blog.example.com"
      - "traefik.frontend.redirect.regex=^https?://www.blog.example.com/(.*)"
      - "traefik.frontend.redirect.replacement=https://blog.example.com/$${1}"
      - "traefik.frontend.headers.frameDeny=false"
      - "traefik.frontend.headers.browserXSSFilter=true"
      - "traefik.frontend.headers.isDevelopment=false"
      - "traefik.frontend.headers.STSSeconds=31536000"
      - "traefik.frontend.headers.forceSTSHeader=false"
      - "traefik.frontend.headers.contentTypeNosniff=true"
      - "traefik.backend=blog-be"
    depends_on:
      - traefik
  personal-website:
    build: ./personal-website
    restart: always
    networks:
      - web
    volumes:
      - ./personal-website:/usr/owncloud/nginx/html
    labels:
      - "traefik.enable=true"      
      - "traefik.docker.network=web"
      - "traefik.frontend.protocol=http"
      - "traefik.frontend.rule=Host:example.com,www.example.com"
      - "traefik.frontend.redirect.regex=^https?://www.example.com/(.*)"
      - "traefik.frontend.redirect.replacement=https://example.com/$${1}"
      - "traefik.frontend.headers.frameDeny=false"
      - "traefik.frontend.headers.browserXSSFilter=true"
      - "traefik.frontend.headers.isDevelopment=false"
      - "traefik.frontend.headers.STSSeconds=31536000"
      - "traefik.frontend.headers.forceSTSHeader=false"
      - "traefik.frontend.headers.contentTypeNosniff=true"
      - "traefik.backend=personal-website-be"
    depends_on:
      - traefik   
owncloud:
    // THIS IS JUST FOR ILLUSTRATION, DON'T COPY PASTE THIS
    image: owncloud/server:latest
    restart: always
    networks:
      - web
      - default
    expose: 
      - "8080"
    labels:
      - "traefik.docker.network=web"
      - "traefik.enable=true"
      - "traefik.frontend.rule=Host:owncloud.example.com,www.owncloud.example.com"
      - "traefik.frontend.redirect.regex=^https?://www.owncloud.example.com/(.*)"
      - "traefik.frontend.redirect.replacement=https://owncloud.example.com/$${1}"
        // omitting config, see link below for complete docker-compose
    depends_on:
      - traefik
      - db
      - redis
    environment:
      - // omitting config, see link below for complete docker-compose
    healthcheck:
      - // omitting config, see link below for complete docker-compose
    volumes:
      - owncloudFiles:/mnt/data
  db:
    image: webhippie/mariadb:latest
    // omitting config, see link below for complete docker-compose

  redis:
    image: webhippie/redis:latest
    // omitting config, see link below for complete docker-compose

networks:
  web:
    external: true
    name: web
  • This includes:
    • All the traefik labels for required for the reverse proxying to occur
    • Security headers
    • Redirect rules
    • A defined Docker network (web) so we can specify which networks we want to expose to the internet
      • This isn’t much use in this example, since both the blog and website will be publically accessible, however if you have a private service that you don’t want to expose to the internet, you’ll need this
    • For complete ownCloud docker-compose details, please refer to the official ownCloud website

Now that we have the docker-compose.yml file, all we need to do is configure Traefik.

This is done within the traefike.toml file:

traefik.toml

  • This is where we define:
    • Redirect rules for port 80 to 443
    • Domain name and sub-domains
    • Details required by LetsEncrypt to issue SSL certificates
    • The file where the certs will be inserted into acme.json - ensure the user has read write permissions - run chmod 600
debug = false
logLevel = "ERROR"
defaultEntryPoints = ["https","http"]
[entryPoints]
  [entryPoints.http]
  address = ":80"
    [entryPoints.http.redirect]
    entryPoint = "https"
  [entryPoints.https]
  address = ":443"
  [entryPoints.https.tls]
[retry]

[docker]
endpoint = "unix:///var/run/docker.sock"
domain = "example.com"
watch = true
exposedByDefault = false

[acme]
email = "ENTER_EMAIL_HERE"
storage = "acme.json"
#caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
onHostRule = true
entryPoint = "https"
  [acme.httpChallenge]
  entryPoint = "http"
 [[acme.domains]]
    main = "example.com"
    sans = ["www.example.com", "owncloud.example.com", "www.owncloud.example.com", "blog.example.com", "www.blog.example.com"]