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 – runchmod 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"]