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 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: truePlex
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-authPortainer
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: trueSmokePing
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: truetraefik-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: bridgeWatchtower
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: bridgeHomer
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: trueGit 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.bakWrapping 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
2024-03-06
2024-01-07