Multiple Web Apps on One Server

DigitalOcean droplet server rack

Background

I wanted to move away from my previous web hosting company and try hosting my websites and web apps using a DigitalOcean 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 for 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 its intended web app.
  • It’s ideal to use with Docker.
  • Supports SSL out of the box via Let’s Encrypt.

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 the HTTP challenge
  • Docker installed
  • The 1GB droplet on DigitalOcean is ideal

Implementation

I have a main repo with each app in its 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 listed above is that they’ll each need a web server. We could go with a traditional Apache web server, but since we’re using Docker, we can use a tiny NGINX (~5MB) instead. Having a ~5MB webserver is great news, because each app will need a separate instance.

DockerFile

Within each folder (submodule), we use a Dockerfile 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 need something to tie all the services 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 a 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 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 publicly accessible, but if you have a private service 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 traefik.toml file.

traefik.toml

This is where we define:

  • Redirect rules for port 80 to 443
  • Domain name and sub-domains
  • Details required by Let’s Encrypt to issue SSL certificates
  • The file where the certs will be inserted, acme.json — ensure the user has read/write permissions by running 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"]