Responsible negligence for self-hosted services

How I keep my playtime from turning into a full-time sysadmin job

I’ve got a serious self-hosting habit. I’ve got a ridiculously large server in my home, a VPS at Vultr, and a big dedicated server over at Hetzner. My servers run Debian stable or Ubuntu LTS, depending on the mood I was in when I installed them. I run almost all of my services in Docker containers:

❯ ssh colossus docker ps | wc -l
32
❯ ssh citadel docker ps | wc -l
34
❯ ssh wolfgang docker ps | wc -l
8

I also work for a living (at least, in theory) and have a family that I like to spend time with. How do I responsibly run all of these things and still have time for my family and career?

Automatic upgrades!

Yep, that’s right, I just set everything to upgrade and restart automatically!

Automatic restarts!? What about downtime?
This is how I handle my personal stuff, which me and maybe a few other people use. We're not going for five nines here.
What if an upgrade breaks something?
Then I'll notice and fix it the next time I try to use that thing. For my personal stuff, I'd rather something be down and/or broken than up and potentially unpatched.

This approach is all about making sure the stuff that I do for fun doesn’t inadvertently become not fun:

FunNot fun
Deploying new software!Worrying about software
Using software!Getting hacked

I thought this information might be particularly relevant today, what with today’s (still impending at the time of this writing) OpenSSL circus. With that being said, let’s get down to how to set it up.

Automatically upgrading packages with unattended-upgrades

unattended-upgrades is a lovely little package that automatically downloads and installs upgrades for you. It comes from Debian and is also usable on Ubuntu. It’s been around for a long time and is available in the default repositories for both distributions; I’d guess that it’s also available in Debian and Ubuntu derivatives like siduction and Pop!_OS but I don’t have any experience with those personally. No matter which version you’re running, you should be able to get it installed like so:

$ apt-get update && apt-get install unattended-upgrades

unattended-upgrades is configured in the same way as apt; that is to say, by a bunch of configuration fragments in a weird Perl-like syntax that live in /etc/apt/conf.d. This directory is probably one of my least-favorite bits of Debian and its derivatives. The configuration files in this directory are processed in order by their filename. On my systems, the unattended-upgrades package installed a configuration file called 50unattended-upgrades, but configuration directives that affect unattended-upgrades can appear in any file in this directory. Fortunately, all of them begin with a common prefix, so if changing the configuration doesn’t seem to be working for you, try searching for lines containing the string Unattended-Upgrade:: in other files in /etc/apt/conf.d.

The default configuration is pretty conservative. Out of the box, it won’t upgrade packages from 3rd party package repositories, and it won’t automatically reboot your machine.

To get unattended-upgrades to install packages from 3rd-party sources, you’ll need to adjust Unattended-Upgrade::Allowed-Origins. On my Ubuntu machines, it looks something like this by default:

Unattended-Upgrade::Allowed-Origins {
	"${distro_id}:${distro_codename}";
	"${distro_id}:${distro_codename}-security";
	// Extended Security Maintenance; doesn't necessarily exist for
	// every release and this system may not have it installed, but if
	// available, the policy for updates is such that unattended-upgrades
	// should also install from here by default.
	"${distro_id}ESMApps:${distro_codename}-apps-security";
	"${distro_id}ESM:${distro_codename}-infra-security";
//	"${distro_id}:${distro_codename}-updates";
//	"${distro_id}:${distro_codename}-proposed";
//	"${distro_id}:${distro_codename}-backports";
};

This is a list of package repositories that unattended-upgrades will install updates from. Each item is in the form of an “origin” and an “archive” separated by a colon (ORIGIN:ARCHIVE). Each item in the list is enclosed in double quotation marks (") and terminated with a semicolon (;). Lines beginning with // are comments. In this default configuration, upgrades are installed from the base package repository and from the security repositories.

As mentioned in the README for unattended-upgrades, you can find the “origin” and “archive” for each repository configured on your system by running apt-cache policy. On my home server running Ubuntu, that looks something like this:

# apt-cache policy
Package files:
 100 /var/lib/dpkg/status
     release a=now
 500 https://download.docker.com/linux/ubuntu jammy/stable amd64 Packages
     release o=Docker,a=jammy,l=Docker CE,c=stable,b=amd64
     origin download.docker.com
 500 http://security.ubuntu.com/ubuntu jammy-security/multiverse amd64 Packages
     release v=22.04,o=Ubuntu,a=jammy-security,n=jammy,l=Ubuntu,c=multiverse,b=amd64
     origin security.ubuntu.com
 500 http://security.ubuntu.com/ubuntu jammy-security/universe amd64 Packages
     release v=22.04,o=Ubuntu,a=jammy-security,n=jammy,l=Ubuntu,c=universe,b=amd64
     origin security.ubuntu.com
 500 http://security.ubuntu.com/ubuntu jammy-security/restricted amd64 Packages
     release v=22.04,o=Ubuntu,a=jammy-security,n=jammy,l=Ubuntu,c=restricted,b=amd64
     origin security.ubuntu.com
 500 http://security.ubuntu.com/ubuntu jammy-security/main amd64 Packages
     release v=22.04,o=Ubuntu,a=jammy-security,n=jammy,l=Ubuntu,c=main,b=amd64
     origin security.ubuntu.com
 100 http://archive.ubuntu.com/ubuntu jammy-backports/universe amd64 Packages
     release v=22.04,o=Ubuntu,a=jammy-backports,n=jammy,l=Ubuntu,c=universe,b=amd64
     origin archive.ubuntu.com
 100 http://archive.ubuntu.com/ubuntu jammy-backports/main amd64 Packages
     release v=22.04,o=Ubuntu,a=jammy-backports,n=jammy,l=Ubuntu,c=main,b=amd64
     origin archive.ubuntu.com
 500 http://archive.ubuntu.com/ubuntu jammy-updates/multiverse amd64 Packages
     release v=22.04,o=Ubuntu,a=jammy-updates,n=jammy,l=Ubuntu,c=multiverse,b=amd64
     origin archive.ubuntu.com
 500 http://archive.ubuntu.com/ubuntu jammy-updates/universe amd64 Packages
     release v=22.04,o=Ubuntu,a=jammy-updates,n=jammy,l=Ubuntu,c=universe,b=amd64
     origin archive.ubuntu.com
 500 http://archive.ubuntu.com/ubuntu jammy-updates/restricted amd64 Packages
     release v=22.04,o=Ubuntu,a=jammy-updates,n=jammy,l=Ubuntu,c=restricted,b=amd64
     origin archive.ubuntu.com
 500 http://archive.ubuntu.com/ubuntu jammy-updates/main amd64 Packages
     release v=22.04,o=Ubuntu,a=jammy-updates,n=jammy,l=Ubuntu,c=main,b=amd64
     origin archive.ubuntu.com
 500 http://archive.ubuntu.com/ubuntu jammy/multiverse amd64 Packages
     release v=22.04,o=Ubuntu,a=jammy,n=jammy,l=Ubuntu,c=multiverse,b=amd64
     origin archive.ubuntu.com
 500 http://archive.ubuntu.com/ubuntu jammy/universe amd64 Packages
     release v=22.04,o=Ubuntu,a=jammy,n=jammy,l=Ubuntu,c=universe,b=amd64
     origin archive.ubuntu.com
 500 http://archive.ubuntu.com/ubuntu jammy/restricted amd64 Packages
     release v=22.04,o=Ubuntu,a=jammy,n=jammy,l=Ubuntu,c=restricted,b=amd64
     origin archive.ubuntu.com
 500 http://archive.ubuntu.com/ubuntu jammy/main amd64 Packages
     release v=22.04,o=Ubuntu,a=jammy,n=jammy,l=Ubuntu,c=main,b=amd64
     origin archive.ubuntu.com

For each repository, you can find the origin by looking for value after o=, and the archive by looking for the value after a=. Confusingly, you are NOT looking for the lines that begin with origin.

I want to automatically install updates from the backports and updates repositories as well. I’d also like to automatically install updates from Docker. To that end, I’ve reconfigured my Unattended-Upgrade::Allowed-Origins like so:

Unattended-Upgrade::Allowed-Origins {
	"${distro_id}:${distro_codename}";
	"${distro_id}:${distro_codename}-security";
	// Extended Security Maintenance; doesn't necessarily exist for
	// every release and this system may not have it installed, but if
	// available, the policy for updates is such that unattended-upgrades
	// should also install from here by default.
	"${distro_id}ESMApps:${distro_codename}-apps-security";
	"${distro_id}ESM:${distro_codename}-infra-security";
	"${distro_id}:${distro_codename}-updates";
//	"${distro_id}:${distro_codename}-proposed";
	"${distro_id}:${distro_codename}-backports";
	Docker:${distro_codename}";
};

I’d also like unattended-upgrades to automatically reboot my machines when necessary. This is controlled by Unattended-Upgrade::Automatic-Reboot. By default, my machines were configured with:

//Unattended-Upgrade::Automatic-Reboot "false";

I have changed this to:

Unattended-Upgrade::Automatic-Reboot "true";

I tend to leave SSH sessions hanging around (bad habit, I know) so I want the reboot to proceed even if I’m logged in, so I’ve also set:

Unattended-Upgrade::Automatic-Reboot-WithUsers "true";

But, I want some warning so that if I am actively using the session, I can quickly wrap up a thing or cancel the reboot if necessary, so I’ve also set:

Unattended-Upgrade::Automatic-Reboot-Time "+10";

The example configuration only shows setting a specific time for this, but the argument is passed directly to shutdown -r, so you can use any time format that shutdown accepts. In this case, +10 means “in 10 minutes,” which gives me a reasonable window of time to react to the automatic reboot if I need to.

Automatically restarting services with needrestart

unattended-upgrades can automatically reboot your machine when necessary, but sometimes updates only need a few services to be restarted. This usually happens when a shared library that is used by several programs is updated; today’s OpenSSL update is a good example. This can be handled by a different bit of software called needrestart. You can install needrestart like so:

$ apt-get update && apt-get install needrestart

By default, needrestart will run in interactive mode when you run a manual upgrade, and ask which services you’d like to restart. Busy dads don’t have time for questions from computers with obvious answers, though, so I’ve reconfigured needrestart to just automatically restart anything it thinks needs restarting.

needrestart installs a configuration file at /etc/needrestart/needrestart.conf. Unlike apt’s configuration files, this actually appears to be Perl, instead of merely a Perl-like syntax. The mode that needrestart runs in is controlled by $nrconf{restart}; the default configuration has it commented out:

# Restart mode: (l)ist only, (i)nteractive or (a)utomatically.
#
# ATTENTION: If needrestart is configured to run in interactive mode but is run
# non-interactive (i.e. unattended-upgrades) it will fallback to list only mode.
#
#$nrconf{restart} = 'i';

I’ve changed it to automatic mode:

# Restart mode: (l)ist only, (i)nteractive or (a)utomatically.
#
# ATTENTION: If needrestart is configured to run in interactive mode but is run
# non-interactive (i.e. unattended-upgrades) it will fallback to list only mode.
#
$nrconf{restart} = 'a';

Automatically updating containers with Watchtower

unattended-upgrades and needrestart have you covered for your base operating system, but if you’re anything like me, you also run a lot of containers, and doing package upgrades inside of containers feels gross for a number of reasons:

  • Unless you’ve set up some sort of persistence for the container’s root filesystem, you’re going to have to reinstall the upgrades every time you restart the container
  • Your containers might be running a different distribution than your host, possibly with an entirely different package manager, and you’re going to have to learn them all
  • Your containers might even be “distroless” and not even include a package manager to do updates inside of the container

Instead, when you need to update the software inside of a container, the better way to do it is to build a new container image containing the updated software and replace your running container.

Watchtower is a very helpful bit of software that can automatically update and restart containers when new images are available. Like most of my self-hosted services, I run Watchtower with Docker Compose. Here’s my Compose file for Watchtower:

services:
  watchtower:
    image: containrrr/watchtower:latest
    container_name: watchtower
    restart: unless-stopped
    network_mode: host
    environment:
      - TZ=America/Chicago
      - WATCHTOWER_LABEL_ENABLE=true
    env_file: ./watchtower.env
    volumes:
      - /root/.docker/config.json:/config.json
      - /var/run/docker.sock:/var/run/docker.sock
    labels:
      - com.centurylinklabs.watchtower.enable=true

Watchtower is configured via environment variables. I like to keep everything in Chicago time, because brain no good at timezones, so I set TZ to America/Chicago. By default, Watchtower will update all of your running containers, but I tend to have a lot of one-offs and temporary stuff and the occasional hand-built thing that doesn’t have automatic updates, so I set WATCHTOWER_LABEL_ENABLE to true; this tells Watchtower to only act on containers that have a label of com.centurylinklabs.watchtower.enable=true. I gave Watchtower itself this label, so that it will keep itself up-to-date. Everything else that I want Watchtower to maintain for me also gets the com.centurylinklabs.watchtower.enable=true label.

Watchtower needs access to the Docker socket at /var/run/docker.sock to work, so that is bound into the container. I have also bound root’s Docker CLI configuration; Docker Hub put in some pretty strict rate-limiting for anonymous users a while back, so this lets Watchtower log into Docker Hub using the credentials I gave to the root user.

I’ve got more configuration for Watchtower in watchtower.env:

WATCHTOWER_NOTIFICATIONS=slack
WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL=https://discord.com/api/webhooks/EXAMPLE/fakeurl/slack
WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER=watchtower
WATCHTOWER_NOTIFICATION_SLACK_CHANNEL=#alerts
WATCHTOWER_NO_STARTUP_MESSAGE=false
WATCHTOWER_POLL_INTERVAL=86400

Most of these are about getting Watchtower to send notifications to Discord every time it does something. I very much prefer Matrix to Discord, and even run my own Matrix homeserver, but I don’t think it’s a good idea to self-host the infrastructure that notifies me if one of my self-hosted things is broken, so all of my “chatops” types of alerts go to Discord 😄 — for the same reason, my iCloud inbox is mostly full of mail from Cron Daemon. I’m using Discord’s webhook endpoint in “Slack emulation mode” which you can do by adding /slack to the end of any Discord webhook URL. I’m doing this because at the time that I set this up, Watchtower knew how to talk to Slack but didn’t natively know how to talk to Discord; I don’t think it’s necessary these days but I also haven’t found any reason to update my configuration. I set Watchtower to poll only once per day, again to avoid running afoul of Docker Hub’s rate limiting.

It’s important to note that using Watchtower to automatically update your containers only helps if your containers actually get updated regularly. If you are building something from a Dockerfile, you need to automatically build that Dockerfile periodically and push the result to a registry where Watchtower can find it. If you are using images from Docker Hub or other public registries, choose carefully; make sure the image is being published by someone you feel that you can trust and that it is receiving regular updates. If you’re not sure where to look, LinuxServer.io is a great collection of well-maintained container images for almost everything you might want to self-host.

Sit back, relax, and have fun

Don’t let your self-hosting habit turn you into an involuntary sysadmin. Quit worrying so much about uptime for stuff that only you really use and let the computers take care of themselves.

A: You need to patch your servers! B: Haha automatic updates go brrrrrr