TL;DR

  • systemd timers are a serious upgrade from cron. Accept no substitutes.
  • ~/.config/systemd/user lets you run user-scoped timers and services,
    • in a place where you can version control them with Git easily,
      • and even keep a handy backup with chezmoi!

Story

We had the great honor of having our friend Tri Phung (LinkedIn, GitHub) over for dinner yesterday. As is often the case the conversation turned to work. I’ve been on a kick of using Github Actions remotely and shell scripts + systemd services/timers locally to sketch out some ideas for new websites in Hugo.

There’s just one problem: systemd default directory at /etc/systemd/system contains timers, services, and who knows what else for the whole system.

  • What if i just want to manage my own timers and services?
  • What if I need to have access to my own user-facing SSH keys, e.g. for private Github repository access, and not whatever the ‘default’ systemd user has?
  • What if i want to keep them in a Git repo of their own?
  • What if I want to keep them in this Git repo and have them run as my user and have restoring them after any future reinstalls of Ubuntu to be relatively painless? (My shell-bling-ubuntu Repo suggests that, despite my DevOps background I’m not keen on maintaining a hosts: localhost Ansible file for the task!)

Luckily, there’s a ~/.config for that. The wizards at the Arch wiki prophesy

~/.config/systemd/user/ where the user puts their own units.

All the user units will be placed in ~/.config/systemd/user/. If you want to start units on first login, execute systemctl --user enable unit for any unit you want to be autostarted.

Great! Let’s try it!

Experiment 1: A user-scoped git pull to a public Git repo every minute

We start small: A service+timer to git pull the latest scrape at https://github.com/hiAndrewQuinn/selkouutiset-scrape (a repo for recording the Simple Finnish language daily newsfeed, using Simon Willison’s Github Actions git scraping technique).

Here’s what it looks like:

1
2
3
4
5
6
7
.config/systemd/user 
❯ tree
.
├── git-pull-selkouutiset-scrape.service
├── git-pull-selkouutiset-scrape.timer
└── timers.target.wants
    └── git-pull-selkouutiset-scrape.timer -> /home/andrew/.config/systemd/user/git-pull-selkouutiset-scrape.timer

(timers.target.wants gets made after running systemctl --user enable git-pull-selkouutiset-scrape.timer).

And the files in question:

1
2
3
4
5
6
7
# selkouutiset-scrape.service
[Unit]
Description=Git Pull Service for updating the selkouutiset-scrape repository

[Service]
Type=oneshot
ExecStart=git -C /home/andrew/Code/selkouutiset-scrape/ pull
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# selkouutiset-scrape.timer
[Unit]
Description=Timer for Git Pull Service

[Timer]
OnCalendar=*:*
Unit=git-pull-selkouutiset-scrape.service

[Install]
WantedBy=timers.target

A quick check with journalctl --user -u selkouutiset-scrape.service shows all is well!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Nov 21 08:29:22 andrew-XPS-13-9310 systemd[1687]: Finished git-pull-selkouutiset-scrape.service - Git Pull Service for updating the selkouutiset-scrape repository.
Nov 21 08:30:20 andrew-XPS-13-9310 systemd[1687]: Starting git-pull-selkouutiset-scrape.service - Git Pull Service for updating the selkouutiset-scrape repository...
Nov 21 08:30:22 andrew-XPS-13-9310 git[22225]: From github.com:hiAndrewQuinn/selkouutiset-scrape
Nov 21 08:30:22 andrew-XPS-13-9310 git[22225]:    b398474..ca29b90  main       -> origin/main
Nov 21 08:30:22 andrew-XPS-13-9310 git[22235]: Updating b398474..ca29b90
Nov 21 08:30:22 andrew-XPS-13-9310 git[22235]: Fast-forward
Nov 21 08:30:22 andrew-XPS-13-9310 git[22235]:  .github/workflows/scrape.yml            | 2 +-
Nov 21 08:30:22 andrew-XPS-13-9310 git[22235]:  2023/11/21/selkouutiset_2023_11_21.html | 2 +-
Nov 21 08:30:22 andrew-XPS-13-9310 git[22235]:  2 files changed, 2 insertions(+), 2 deletions(-)
Nov 21 08:30:22 andrew-XPS-13-9310 systemd[1687]: Finished git-pull-selkouutiset-scrape.service - Git Pull Service for updating the selkouutiset-scrape repository.

Experiment 2: A user-scoped git push to a private repo every minute

selkouutiset-scrape is the raw material I work with, so as long as that little curl happens every 6 hours I can always recover my “real” project (a public archive of the site in YYYY/MM/DD/ format) very quickly.

The real reason I’m interested in this is I don’t want to waste GH Actions minutes every time downloading and installing pandoc. Now let’s see if we can run my Fish script, which involves committing to a private repo called selkouutiset-scrape-cleaned.

And… success! We now know that we can push to private repos as well.

Experiment 3: Keeping them controlled with chezmoi

I use chezmoi for all my ‘make dotfiles / dotflags / dot-directories portable and sane’ needs. Almost everything in my ~/.config is VC’d by chezmoi on a private Github repo, which – as we’ve just seen – we can push to! On a timer!