Multiple Web Apps on One Server

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 runningchmod 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"]