Docker@Home

·10 min read·Graham Mace
Docker@Home

With the portability advantages of Docker, setting up a new machine is simply a case of installing the software, running the corresponding docker-compose.yml file, and having the container downloaded and running within minutes.

As much as I admire Microsoft and their security and stability (😉), in my experience nothing beats a Linux-based OS for uptime and reliability. While I could use a Windows-based NUC, I’ve heard they can be noisier and run hot due to thermal management requirements. Given everything sits inside a 4RU Serveredge server cabinet, I didn’t want to worry too much about thermals.

I use both Windows and Linux-based operating systems day-to-day. Each has its advantages and disadvantages, but nothing beats a UNIX-based system for Docker given cgroups’ origins. Believe it or not, spec-for-spec, the price of a Mac Mini in Australia versus that of a NUC is incredibly close.

As such, I’ve been running a Mac Mini paired with a Synology NAS as my home server setup for quite a few years now. At first, I thought about running Docker containers directly on the NAS, but I decided against it – not only because it puts all my eggs in one basket, but because running Docker also demands more of the NAS’s available resources. I also wanted to keep the NAS as close to stock as possible, relying on as few DSM add-on packages as I could.

Containers

Over the years, I’ve run containers either for experimentation or as something longer lasting – a Minecraft server, Microsoft SQL Server, Firefly, LetsEncrypt, GitLab and Gitea among them.

However, following the adage of Run Less Software, I’ve started using the cloud more and outsourcing some of my services. Using GitHub for repositories and AWS for heavier workloads has allowed me to reduce the number of containers running on my home server.

Traefik

_/traefik

Traefik is a leading modern open source reverse proxy and ingress controller that makes deploying services and APIs easy. Traefik integrates with your existing infrastructure components and configures itself automatically and dynamically.

Networking with Docker can be a pain at the best of times, but Traefik makes it a lot easier to manage. When using Traefik, you can define routes and services in a simple and intuitive way, making it easier to manage and configure networking for your Docker containers. It also supports a wide range of features, such as load balancing, SSL termination, and more.

Given containers can be ephemeral, Traefik simplifies the management of Docker containers and makes it easier to configure and route traffic to services. There’s no need to keep exposing ports on the host machine – add labels to the container to define its networking configuration, set up the appropriate DNS records, and everything gets hooked up automatically.

Traefik also supports automated certificate generation via LetsEncrypt. Since the Mac Mini sits inside my home network, I use Route53 to provision certificates using the ACME protocol. Any container requiring a web interface can automatically have a certificate issued for the subdomain on which that interface is exposed – so container web interfaces live at subdomain.domain.com with certificates issued automatically.

It did take me quite a while to get Traefik up and running, though, due to the configuration incompatibilities between v1 and v2. The Ultimate Docker Home Server with Traefik guide, along with the accompanying Ultimate Docker-Traefik Repo by Anand, were invaluable references while building out the configuration.

services:
  traefik:
    image: traefik:latest
    container_name: traefik
    hostname: traefik
    domainname: ${DOMAINNAME}
    restart: unless-stopped
    environment:
      - PUID
      - PGID
      - TZ
      - AWS_ACCESS_KEY_ID
      - AWS_SECRET_ACCESS_KEY
      - AWS_REGION
      - AWS_HOSTED_ZONE_ID
      - AWS_TTL
    networks:
      - traefik_proxy
    ports:
      - target: 80
        published: 80
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: tcp
        mode: host
      - target: 8080
        published: 8080
        protocol: tcp
        mode: host
    labels:
      - "com.centurylinklabs.watchtower.enable=false"
      - "traefik.enable=true"

      # Global redirect to HTTPS
      - "traefik.http.routers.http-catchall.entrypoints=websecure"
      - "traefik.http.routers.http-catchall.rule=HostRegexp(`{host:.+}`)"
      - "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"

      # Global wildcard certificates
      - "traefik.http.routers.traefik-rtr.tls.certresolver=dns-route53" # Comment out this line after first run of traefik to force the use of wildcard certs
      - "traefik.http.routers.traefik-rtr.tls.domains[0].main=$DOMAINNAME"
      - "traefik.http.routers.traefik-rtr.tls.domains[0].sans=*.$DOMAINNAME"

      # Dashboard
      - "traefik.http.routers.traefik-rtr.rule=Host(`traefik.$DOMAINNAME`)"
      - "traefik.http.routers.traefik-rtr.tls=true"
      - "traefik.http.routers.traefik-rtr.entrypoints=websecure"
      - "traefik.http.routers.traefik-rtr.service=api@internal"
      - "traefik.http.routers.traefik-rtr.middlewares=chain-basic-auth@file"
      
      # HTTP headers
      - "traefik.http.middlewares.traefik-headers.headers.accesscontrolallowmethods=GET, OPTIONS, PUT"
      - "traefik.http.middlewares.traefik-headers.headers.accesscontrolalloworiginlist=https://$DOMAINNAME"
      - "traefik.http.middlewares.traefik-headers.headers.accesscontrolmaxage=100"
      - "traefik.http.middlewares.traefik-headers.headers.addvaryheader=true" 
      - "traefik.http.middlewares.traefik-headers.headers.allowedhosts=traefik.$DOMAINNAME" 
      - "traefik.http.middlewares.traefik-headers.headers.hostsproxyheaders=X-Forwarded-Host"
      - "traefik.http.middlewares.traefik-headers.headers.sslredirect=true"
      - "traefik.http.middlewares.traefik-headers.headers.sslhost=traefik.$DOMAINNAME" 
      - "traefik.http.middlewares.traefik-headers.headers.sslforcehost=true"
      - "traefik.http.middlewares.traefik-headers.headers.sslproxyheaders.X-Forwarded-Proto=https"
      - "traefik.http.middlewares.traefik-headers.headers.stsseconds=63072000"
      - "traefik.http.middlewares.traefik-headers.headers.stsincludesubdomains=true"
      - "traefik.http.middlewares.traefik-headers.headers.stspreload=true"
      - "traefik.http.middlewares.traefik-headers.headers.forcestsheader=true"
      - "traefik.http.middlewares.traefik-headers.headers.framedeny=true"
#      - "traefik.http.middlewares.traefik-headers.headers.customframeoptionsvalue=SAMEORIGIN" # This option overrides FrameDeny
      - "traefik.http.middlewares.traefik-headers.headers.contenttypenosniff=true"
      - "traefik.http.middlewares.traefik-headers.headers.browserxssfilter=true"
#      - "traefik.http.middlewares.traefik-headers.headers.contentsecuritypolicy=frame-ancestors 'none'; object-src 'none'; base-uri 'none';"
      - "traefik.http.middlewares.traefik-headers.headers.referrerpolicy=same-origin"
      - "traefik.http.middlewares.traefik-headers.headers.featurepolicy=camera 'none'; geolocation 'none'; microphone 'none'; payment 'none'; usb 'none'; vr 'none';"
      - "traefik.http.middlewares.traefik-headers.headers.customresponseheaders.X-Robots-Tag=none,noarchive,nosnippet,notranslate,noimageindex,"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ${VOLUMES_TRAEFIK}/traefik.yml:/traefik.yml:ro
      - ${VOLUMES_TRAEFIK}/rules:/rules
      - ${VOLUMES_TRAEFIK}/acme/acme.json:/acme.json
      - ${VOLUMES_TRAEFIK}/traefik.log:/traefik.log
    security_opt:
      - no-new-privileges:true

networks:
  traefik_proxy:
    name: traefik_proxy
    external: true

Plex

While I could run Plex in a container, I’ve found performance suffers badly – particularly when transcoding content – so Plex runs on the host machine instead.

To expose the web UI, I use a rules/app-plex.yml file in Traefik to provide a more secure path to the Plex web UI. Traefik then proxies to Plex on the host on port 32400, and clients can reach the Plex web UI through Traefik via a secure subdomain:

http:
  routers:
    plex-rtr:
      rule: "Host(`plex.subdomain.com`)"
      entryPoints:
        - websecure
      middlewares:
        - chain-no-auth
      service: plex-svc
      tls:
        certResolver: dns-route53
  middlewares:
    plex-add-admin:
      addPrefix:
        prefix: "/admin"
  services:
    plex-svc:
      loadBalancer:
        servers:
          - url: "http://10.0.0.3:32400"

As Plex has its own built-in authentication mechanism, then a middleware-chains.yml file just allows access to this subdomain without authentication via the Traefik middleware:

http:
  middlewares:
    chain-no-auth:
      chain:
        middlewares:
          - middlewares-rate-limit
          - middlewares-secure-headers

    chain-basic-auth:
      chain:
        middlewares:
          - middlewares-rate-limit
          - middlewares-secure-headers
          - middlewares-basic-auth

Portainer

portainer/portainer-ce

Portainer is a single, fully integrated product for managing container infrastructure, Docker, Swarm, or Kubernetes, on-premises, in the cloud, or at the edge. It is built to remove skills and experience as a prerequisite for adoption: the platform absorbs the complexity and keeps you on the safe path, so running production containers no longer depends on hiring people who already know how.

Before Docker Desktop introduced its dashboard, executing commands over SSH was the only realistic way to see what was happening on a host – and connecting via SSH or remote desktop just to check on Docker can be a pain.

Portainer simplifies this with a comprehensive web dashboard showing details of all running containers, along with the ability to start, stop, and restart them. While a fully-fledged commercial version exists, I’ve found the Community Edition more than enough for my needs.

services:
  portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    hostname: portainer
    domainname: ${DOMAINNAME}
    restart: unless-stopped
    environment:
      - PUID
      - PGID
      - TZ
    networks:
      - traefik_proxy
    command: -H unix:///var/run/docker.sock
    # ports:
    #   - "9000:9000"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.portainer-rtr.entrypoints=websecure"
      - "traefik.http.routers.portainer-rtr.rule=Host(`portainer.$DOMAINNAME`)"
      - "traefik.http.routers.portainer-rtr.tls=true"
      - "traefik.http.routers.portainer-rtr.middlewares=chain-no-auth@file"
      - "traefik.http.routers.portainer-rtr.service=portainer-svc"
      - "traefik.http.services.portainer-svc.loadbalancer.server.port=9000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ${VOLUMES_DATA}:/data
    security_opt:
      - no-new-privileges:true

networks:
  traefik_proxy:
    name: traefik_proxy
    external: true

SmokePing

linuxserver/smokeping

SmokePing is a latency logging and graphing and alerting system. It consists of a daemon process which organizes the latency measurements and a CGI which presents the graphs.

At one point in the distant past, I had an issue with network latency on a certain ISP here in Australia. To prove the issue existed, I set up SmokePing to monitor latency between my home network and several external locations.

I’ve continued running it ever since. It’s a great tool for diagnosing network issues and confirming your network performs as expected.

services:
  smokeping:
    image: linuxserver/smokeping
    container_name: smokeping
    hostname: smokeping
    domainname: ${DOMAINNAME}
    restart: unless-stopped
    networks:
      - traefik_proxy
    environment:
      - PUID
      - PGID
      - TZ
    # ports:
    #   - "127.0.0.1:84:80"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.smokeping-rtr.entrypoints=websecure"
      - "traefik.http.routers.smokeping-rtr.rule=Host(`smokeping.$DOMAINNAME`)" 
      - "traefik.http.routers.smokeping-rtr.tls=true"
      - "traefik.http.routers.smokeping-rtr.middlewares=chain-no-auth@file" 
      - "traefik.http.routers.smokeping-rtr.service=smokeping-svc"
      - "traefik.http.services.smokeping-svc.loadbalancer.server.port=80"
    volumes:
      - ${VOLUMES_CONFIG}:/config
      - ${VOLUMES_DATA}:/data

networks:
  traefik_proxy:
    name: traefik_proxy
    external: true

traefik-certs-dumper

ldez/traefik-certs-dumper

Dump ACME data from Traefik to certificates

After setting up Traefik for issuing certificates, I wanted to encrypt traffic to the web interfaces of my NAS, router, and Pi-hole as well. The traefik-certs-dumper container regularly extracts the ACME data from Traefik into certificates, which can then be uploaded to each device to serve its web interface over HTTPS.

I’ve tried to automate the certificate updates with a script, but that’s a topic for another post 🙂

services:
  traefik-certs-dumper:
    image: ldez/traefik-certs-dumper:v2.8.1
    container_name: traefik-certs-dumper
    hostname: traefik-certs-dumper
    domainname: maceage.com
    restart: unless-stopped
    environment:
      - TZ
    entrypoint: sh -c '
      apk add jq
      ; while ! [ -e /acme/acme.json ]
      || ! [ `jq ".[] | .Certificates | length" /acme/acme.json` != 0 ]; do
      sleep 1
      ; done
      && traefik-certs-dumper file --version v2 --watch
        --source /acme/acme.json --dest /data
        --domain-subdir
        --crt-ext=.pem --key-ext=.pem
        --crt-name=fullchain --key-name=privkey'
    labels:
      traefik.enable: false
    volumes:
      - ${VOLUMES_DATA}:/data
      - ${VOLUMES_ACME}:/acme

networks:
  default:
    name: traefik-certs-dumper
    driver: bridge

Watchtower

containrrr/watchtower

With watchtower you can update the running version of your containerized app simply by pushing a new image to the Docker Hub or your own image registry. Watchtower will pull down your new image, gracefully shut down your existing container and restart it with the same options that were used when it was deployed initially

Watchtower lets you update the running version of your containerised app simply by pushing a new image to Docker Hub or your own registry. Watchtower pulls down the new image, gracefully shuts down the existing container, and restarts it with the same options it was originally deployed with.

Constantly checking for and applying container updates manually is a pain. I used to maintain Bash scripts that would stop a container, pull a new image, and restart it – but with Watchtower, I no longer have to think about it. Container images are checked automatically, and updates are pulled and applied gracefully. This saves a lot of time and effort, and ensures containers always run the latest version. It’s disabled selectively for containers I don’t want auto-updating (such as Traefik itself – see the watchtower label in its compose file above).

services:
    watchtower:
        image: containrrr/watchtower:latest
        container_name: watchtower
        hostname: watchtower
        domainname: ${DOMAINNAME}
        restart: unless-stopped
        environment:
            - PUID
            - PGID
            - TZ
            - WATCHTOWER_SCHEDULE
            - WATCHTOWER_TIMEOUT
            - WATCHTOWER_CLEANUP
            - WATCHTOWER_NOTIFICATION_URL
            - DOCKER_API_VERSION=1.44
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock

networks:
    default:
        name: watchtower
        driver: bridge

Homer

b4bz/homer

A dead simple static HOMepage for your servER to keep your services on hand, from a simple yaml configuration

With so many services running locally, I got tired of typing the same addresses into the browser each time – especially since I switch between phone, tablet, laptop, and desktop machines fairly frequently.

Bookmarks worked initially, but didn’t travel well between devices. After a bit of searching, I found Homer, which was perfect for my use case. It’s hosted on a single internal subdomain and provides a landing page linking to every service on my home network – so I only need one bookmark in the browser bar.

I previously used Organizr for this, but found it too clunky since it serves content through iframes. I found that I prefer separate tabs for each service anyway.

services:
  homer:
    image: b4bz/homer
    container_name: homer
    hostname: homer
    domainname: ${DOMAINNAME}
    restart: unless-stopped
    environment:
      - PUID
      - PGID
      - TZ
      - INIT_ASSETS
    networks:
      - traefik_proxy
    # ports:
    #   - "8080:8080"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.homer-rtr.entrypoints=websecure"
      - "traefik.http.routers.homer-rtr.rule=Host(`homer.$DOMAINNAME`)"
      - "traefik.http.routers.homer-rtr.tls=true"
      - "traefik.http.routers.homer-rtr.middlewares=chain-no-auth@file"
      - "traefik.http.routers.homer-rtr.service=homer-svc"
      - "traefik.http.services.homer-svc.loadbalancer.server.port=8080"
    volumes:
      - ${VOLUMES_ASSETS}:/www/assets
    security_opt:
      - no-new-privileges:true

networks:
  traefik_proxy:
    name: traefik_proxy
    external: true

Git repositories

To keep a versioned history of the containers that are running, I use two private Git repositories – not only in case I need to roll back in time, but also as a backup of the configuration across containers.

Docker Compose files repository

This repository stores only the docker-compose.yml files and deliberately contains none of the .env files, for security purposes. Losing the Mac Mini would be a minor inconvenience rather than a disaster – which is precisely the point.

Docker Config files repository

This repository stores configuration for and files generated by Docker containers – sometimes files I’ve created to configure a service, other times files automatically generated by the container at runtime.

Because of this mixed content, the .gitignore file contains a significant number of exclusions to ensure only the necessary files end up in the repository:

.DS_Store

portainer/backups/common
portainer/portainer.db
portainer/portainer.key
portainer/portainer.pub

shared/.htpasswd

smokeping/config/log
smokeping/config/keys
smokeping/data/

traefik/acme/*
traefik/traefik.log
traefik/acme/acme.json

portainer/docker_config/config.json
portainer/backups/portainer.db.bak

Wrapping Up

That’s the full tour of my current home server setup. Between Traefik handling routing and certificates, Watchtower keeping everything patched, and the two Git repositories acting as both documentation and backup, the whole thing more or less looks after itself – exactly what you want from infrastructure humming away in a cabinet under the stairs.

It’s also worth noting how much lighter this list has become compared to a few years ago. Moving source control to GitHub and heavy workloads to AWS has meant fewer things to maintain, patch, and worry about at home, with the remaining containers each earning their place.

Related Posts