So, you have a fresh and clean VPS server instance, wipe it and install a clean slate Ubuntu (LTS preferred) or a modern Debian.

Now ssh into it and start shelling:

$ sudo apt update

$ service apache2 stop - just in case you didn’t clean slate ;-)

Install Docker

$ curl -fsSL https://get.docker.com -o get-docker.sh

$ sh get-docker.sh

$ rm get-docker.sh

Install a Firewall, Python Pip for Docker Compose, Fail2Ban for ssh security

$ sudo apt install ufw python-pip fail2ban

$ ufw default deny incoming

$ ufw default allow outgoing

$ ufw allow 22/tcp this usually is ssh (make sure you allow your chosen ssh key)

$ ufw allow 80/tcp for http requests

$ ufw allow 443/tcp for https requests

$ ufw status check your settings before enabling

$ ufw enable

Read more about UncomplicatedFirewall here

Fail2Ban

Protect, watch, track, lock suspicious logins using Fail2Ban

Read a bit and setup, as much as you need.

SSH Keys

Add your local ssh key as an authorized key, usually here (use an editor of your choice, if like vim):

$ vim .ssh/authorized_keys

Taken from the Traefik Documentation (2019-03-31)

I advise you read it yourself ;-) Traefik - Let’s Encrypt & Docker

$ docker network create web

$ mkdir -p /opt/traefik

$ touch /opt/traefik/docker-compose.yml $ touch /opt/traefik/acme.json && chmod 600 /opt/traefik/acme.json $ touch /opt/traefik/traefik.toml

# /opt/traefik/docker-compose.yml
version: '2'

services:
  traefik:
    image: traefik:1.5.4
    restart: always
    ports:
      - 80:80
      - 443:443
    networks:
      - web
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /opt/traefik/traefik.toml:/traefik.toml
      - /opt/traefik/acme.json:/acme.json
    container_name: traefik

networks:
  web:
    external: true
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 = "my-awesome-app.org"
watch = true
exposedByDefault = false

[acme]
email = "your-email-here@my-awesome-app.org"
storage = "acme.json"
entryPoint = "https"
onHostRule = true
[acme.httpChallenge]
entryPoint = "http"

Example Docker Compose

version: "2.1"

services:
  app:
    image: my-docker-registry.com/my-awesome-app/app:latest
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: always
    networks:
      - web
      - default
    expose:
      - "9000"
    labels:
      - "traefik.docker.network=web"
      - "traefik.enable=true"
      - "traefik.basic.frontend.rule=Host:app.my-awesome-app.org"
      - "traefik.basic.port=9000"
      - "traefik.basic.protocol=http"
      - "traefik.admin.frontend.rule=Host:admin-app.my-awesome-app.org"
      - "traefik.admin.protocol=https"
      - "traefik.admin.port=9443"

  db:
    image: my-docker-registry.com/back-end/5.7
    restart: always

  redis:
    image: my-docker-registry.com/back-end/redis:4-alpine
    restart: always

  events:
    image: my-docker-registry.com/my-awesome-app/events:latest
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: always
    networks:
      - web
      - default
    expose:
      - "3000"
    labels:
      - "traefik.backend=my-awesome-app-events"
      - "traefik.docker.network=web"
      - "traefik.frontend.rule=Host:events.my-awesome-app.org"
      - "traefik.enable=true"
      - "traefik.port=3000"

networks:
  web:
    external: true

Most Important Tip in the Docs

Always specify the correct port where the container expects HTTP traffic using traefik.port label. If a container exposes multiple ports, Traefik may forward traffic to the wrong port. Even if a container only exposes one port, you should always write configuration defensively and explicitly.