July 14, 2020

Converting an old Macbook into a home Linux server

I’m a fan of home automation and have run Home Assistant for some time to streamline automating lights and AV controls. Until recently I ran it on a small Raspberry Pi 3, but as I’ve continued to add more integrations and automations I found I needed more horsepower. This is especially true as I’ve added additional services around HA like Prometheus and Grafana to better monitor the servers and sensors around the house.

I prefer using hardware I already own rather than running out and buying a new NUC to do the job. Luckily, I have an old 2013 Macbook Pro which is a bit slow and heavy for a laptop these days but works perfectly for a small home server. After installing Debian and configuring a static IP I configured some Ansible playbooks to make the server manage itself. The examples I provide are from my Ansible code, but even if you don’t use Ansible yourself it is easy to do these steps manually or to convert it to your favorite automation tool.

Making my laptop a headless server

By default, the laptop screen will stay on and closing the lid will put the machine to sleep. I really want to keep this machine closed on a small shelf in my basement without wasting power keeping the display on. Thankfully, disabling these behaviors is straightforward by configuring GRUB and the logind unit file for systemd. We configure the display to turn off after 30 seconds of being idle so you can still use it if necessary, but otherwise it will automatically turn itself off. We also disable the default lid closure handler.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
---
- hosts: macbookserver
  become: true

  handlers:
    - name: update-grub
      shell: "/sbin/update-grub"

  tasks:
    # On the GRUB_CMDLINE_LINUX line, set the value consoleblank=30
    - name: Setup screen blanking
      lineinfile:
        path: /etc/default/grub
        regexp: '^GRUB_CMDLINE_LINUX='
        line: 'GRUB_CMDLINE_LINUX="consoleblank=30"'
      notify:
        - update-grub # If we modifed the cmdline, run the update-grub handler

    # On the HandleLidSwitch line, replace it with a line that disables the handler
    - name: Disable laptop lid
      lineinfile:
        path: /etc/systemd/logind.conf
        regexp: '^HandleLidSwitch='
        line: 'HandleLidSwitch=ignore'

Automatic Updates

unattended-upgrades is one of my favorite features of Debian and Debian-derived distributions. Configuring and enabling unattended-upgrades makes it simple to keep my machines up to date with security patches without doing it myself. I use the excellent jnv.unattended-upgrades Ansible role that allows me to install and configure unattended-upgrades with just a few variables. Since this is not a mission critical server for me, I’m allowing the system to automatically reboot itself when necessary. (Kernel updates are the most typical reason.)

I configured unattended-upgrades to look at the Debian-Security and Debian labels so we get security and stable updates.

I also needed to manually configure unattended-upgrades to allow upgrading when on battery power. For whatever reason, my server often thinks it is on battery power even while it is plugged in. I do this by overriding a few different sets of configurations.

First, I added an additional configuration file to the /etc/apt/apt.conf.d/ directory which I choose to call 51unattended-upgrades-acpower to override the Unattended-Upgrade::OnlyOnACPower property.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- name: debian unattended upgrades
  hosts: debian
  become: true
  vars:
    unattended_automatic_reboot: true
    unattended_origins_patterns:
      - 'origin=Debian,codename=${distro_codename},label=Debian-Security' # security updates
      - 'o=Debian,codename=${distro_codename},label=Debian' # updates including non-security updates

  roles:
    - jnv.unattended-upgrades

  tasks:
    - name: Allow updates when on AC Power
      blockinfile:
        path: '/etc/apt/apt.conf.d/51unattended-upgrades-acpower'
        create: yes
        owner: root
        group: root
        mode: 0644
        marker: "// {mark} ANSIBLE MANAGED BLOCK"
        block: |
          Unattended-Upgrade::OnlyOnACPower "false";

Second, I overrode the ConditionACPower property for the apt-daily.service and apt-daily-upgrade.service unit files. This allowed systemd to kick off these scheduled tasks despite thinking the laptop was on AC power. I created a systemd override file that looks like the following.

1
2
[Unit]
ConditionACPower=

I then deploy it with the following ansible code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  tasks:
    - name: Create apt unit drop-in directories
      file:
        path: "/etc/systemd/system/{{item}}"
        state: directory
        owner: root
        group: root
        mode: '0755'
      with_items:
        - 'apt-daily.service.d'
        - 'apt-daily-upgrade.service.d'
      notify:
        - reload systemd

    - name: Override apt unit files
      copy:
        src: 'apt/systemd/acpoweroverride.conf'
        dest: "/etc/systemd/system/{{item}}.service.d/acpoweroverride.conf"
        mode: 0644
      with_items:
        - 'apt-daily'
        - 'apt-daily-upgrade'
      notify:
        - reload systemd

  handlers:
    - name: reload systemd
      systemd:
        daemon_reload: yes

Wrapup

I’m mostly writing this down to remind myself of what I did in the future. Tracking down these specific configurations was painful the first time around, so hopefully this can help some other folks save some effort. If you found this useful or interesting, please let me know!

© Mike Keesey 2020