Skip to content
HackHourHQ

HH-0011 · Rev A ·

Turn an old laptop into a 24/7 Ubuntu server: the full checklist

Kip CroninPublished ~11 min read

To run an old laptop as a 24/7 Ubuntu server, make six changes: ignore the lid switch, take suspend off the table, cap the battery charge, let your services run without anyone logged in, make timers catch up on runs missed while it was off, and set up remote access with automatic security updates. Our ThinkPad has been set up this way since 15 May 2026. Below is each change, the check that proves it, and what the machine's own logs show, including two things that didn't work the way we expected.

This isn't a fresh install staged for the article. We set up a ThinkPad X1 Carbon Gen 7 running Ubuntu 24.04 desktop as a server in May and gave it one daily job on a timer. Since then it has run four and a half days straight, sat switched off for most of the summer, run for about an hour in October and been off again for five and a half days. On the evening of 14 October we switched it off for two hours more to test a 15-minute timer. The journal kept all of it. Every output block below is pasted from two read-only runs on 14 October 2026; we show the host and user name as x1. The exact versions are in the Test conditions box.

The checklist

ProblemFixWhere
Closing the lid suspends itHandleLidSwitch=ignore (all three)/etc/systemd/logind.conf.d/ (hack)
Something else suspends itsystemctl mask the sleep targetsone command
Plugged in 24/7 at 100% wears the batterycharge_control_end_thresholda oneshot service (hack)
Your services only start when you log inloginctl enable-lingerone command (hack)
A daily job is skipped if the box was offPersistent=truethe .timer file (hack)
You need a keyboard to fix itopenssh-server, reached over Tailscaleapt
Nobody installs security updatesunattended-upgrades, on a network it doesn't think is metered/etc/apt/apt.conf.d/

Before you start: write down the machine

Versions decide which of these settings exist, so record them first:

lsb_release -d; uname -r; systemd --version | head -1; gnome-shell --version
cat /sys/class/dmi/id/sys_vendor /sys/class/dmi/id/product_version /sys/class/dmi/id/product_name \
    /sys/class/dmi/id/bios_version /sys/class/dmi/id/bios_date
cat /sys/power/mem_sleep
Description:	Ubuntu 24.04.4 LTS
6.17.0-23-generic
systemd 255 (255.4-1ubuntu8.15)
GNOME Shell 46.0
LENOVO
ThinkPad X1 Carbon 7th
20QES5JY25
N2HET83W (1.66 )
08/18/2025
[s2idle] deep

On Lenovo machines product_version holds the model name and product_name holds the machine type number. Other vendors use the fields differently.

1. Keep it running with the lid closed

By default, systemd-logind suspends the machine when the lid closes. Its man page gives the defaults: HandleLidSwitch=suspend, HandleLidSwitchDocked=ignore, and HandleLidSwitchExternalPower "is completely ignored by default", so on AC power the lid still suspends (logind.conf(5)). Ours is a drop-in file rather than an edit to logind.conf, as the man page recommends:

ls -l --time-style=full-iso /etc/systemd/logind.conf.d/; cat /etc/systemd/logind.conf.d/server-mode.conf
total 4
-rw-r--r-- 1 root root 114 2026-05-15 09:18:16.980177406 +0300 server-mode.conf
[Login]
HandleLidSwitch=ignore
HandleLidSwitchExternalPower=ignore
HandleLidSwitchDocked=ignore
IdleAction=ignore

IdleAction=ignore is already the default; it's there so nobody has to remember that. Ask logind over D-Bus what it's actually using:

for p in HandleLidSwitch HandleLidSwitchExternalPower HandleLidSwitchDocked IdleAction; do
  echo -n "$p "
  busctl get-property org.freedesktop.login1 /org/freedesktop/login1 org.freedesktop.login1.Manager $p
done
HandleLidSwitch s "ignore"
HandleLidSwitchExternalPower s "ignore"
HandleLidSwitchDocked s "ignore"
IdleAction s "ignore"

The proof is in logind's own log. Here is the lid closing on 11 February, before any of this was set: it suspends, wakes, and suspends again for as long as the lid stays shut.

journalctl -u systemd-logind -o short-iso --grep 'Lid (closed|opened)|Suspending|will suspend|Operation .suspend. finished'
2026-02-11T11:00:41+02:00 x1 systemd-logind[978]: Lid closed.
2026-02-11T11:00:41+02:00 x1 systemd-logind[978]: Suspending...
2026-02-11T11:01:27+02:00 x1 systemd-logind[978]: Operation 'suspend' finished.
2026-02-11T11:01:52+02:00 x1 systemd-logind[978]: Suspending...
2026-02-11T11:02:36+02:00 x1 systemd-logind[978]: Operation 'suspend' finished.

And here is everything after the drop-in went in on 15 May. The lid was shut from Friday evening to Monday evening, and there isn't a single Suspending... line:

-- Boot 85dad5b508e74b92932c4750640dcb2a --
2026-05-15T19:00:19+03:00 x1 systemd-logind[1012]: Lid closed.
2026-05-18T18:35:08+03:00 x1 systemd-logind[1012]: Lid opened.
2026-05-18T18:35:09+03:00 x1 systemd-logind[1012]: Lid closed.

Two more things back that up. The daily job's timer started its service at 09:00:06 on 16, 17 and 18 May, all with the lid closed. And the kernel's monotonic clock pauses during suspend (systemd.timer(5) says so). That boot's journal stops at 387,862 seconds on it, which is 4 days, 11 hours, 44 minutes. The wall clock from 09:26 on 15 May to 21:10 on 19 May is the same. The log doesn't record whether it was plugged in during those three days.

The details, including the inhibitor lock that can override all of this, are in Keep a Linux laptop running with the lid closed.

2. Take suspend off the table

The lid setting only covers the lid. GNOME has its own idle timers, and anything with the right permission can ask for suspend. We masked the sleep targets, which links them to /dev/null, "making it impossible to start them" (systemctl(1)):

sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
ls -l --time-style=full-iso /etc/systemd/system/sleep.target /etc/systemd/system/suspend.target /etc/systemd/system/hibernate.target /etc/systemd/system/hybrid-sleep.target
systemctl is-enabled sleep.target suspend.target hibernate.target hybrid-sleep.target suspend-then-hibernate.target
lrwxrwxrwx 1 root root 9 2026-05-15 09:18:17.094224391 +0300 /etc/systemd/system/hibernate.target -> /dev/null
lrwxrwxrwx 1 root root 9 2026-05-15 09:18:17.094237888 +0300 /etc/systemd/system/hybrid-sleep.target -> /dev/null
lrwxrwxrwx 1 root root 9 2026-05-15 09:18:17.092823510 +0300 /etc/systemd/system/sleep.target -> /dev/null
lrwxrwxrwx 1 root root 9 2026-05-15 09:18:17.092823510 +0300 /etc/systemd/system/suspend.target -> /dev/null
masked
masked
masked
masked
static

We didn't mask suspend-then-hibernate.target, but logind refuses it too:

for m in CanSuspend CanHibernate CanHybridSleep CanSuspendThenHibernate; do
  echo -n "$m "
  busctl call org.freedesktop.login1 /org/freedesktop/login1 org.freedesktop.login1.Manager $m
done
CanSuspend s "no"
CanHibernate s "no"
CanHybridSleep s "no"
CanSuspendThenHibernate s "no"

"no" means the operation exists but the caller isn't allowed to run it (org.freedesktop.login1(5)).

The other common way is AllowSuspend=no and AllowHibernation=no in a /etc/systemd/sleep.conf.d/ drop-in (systemd-sleep.conf(5)), which turns the modes off in systemd's sleep configuration instead of blocking the units. We haven't run that one on this machine, so we can't show you its output. Either way, also check GNOME's idle settings. These are ours:

sleep-inactive-ac-type 'nothing'
sleep-inactive-ac-timeout 3600
sleep-inactive-battery-type 'nothing'
sleep-inactive-battery-timeout 900

3. Cap the battery at 80%

A laptop that's always plugged in sits at 100%. On a ThinkPad, the thinkpad_acpi driver exposes charge_control_end_threshold, "a battery percentage level, above which charging will stop" (kernel docs). We set only that one, from a oneshot service that runs at every boot:

# /etc/systemd/system/battery-charge-cap.service
[Unit]
Description=Cap BAT0 charge at 80%
After=multi-user.target

[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo 80 > /sys/class/power_supply/BAT0/charge_control_end_threshold'

[Install]
WantedBy=multi-user.target

UPower's history shows it working. On 8 October, about a minute after the laptop booted, the battery was at 4%. It charged and stopped at exactly 80:

2026-10-08T22:26:23 78.000 charging
2026-10-08T22:26:53 79.000 charging
2026-10-08T22:27:19 80.000 pending-charge

The start threshold is still 0, and we haven't seen whether the end value survives a cold boot without the service. The service runs within the first minute of every boot, and nothing logs the value before it does. Both points are in Cap a laptop battery at 80% on Linux.

4. Run your services without logging in

systemctl --user services belong to your user manager, which normally starts when you log in. loginctl enable-linger starts it at boot instead (loginctl(1)). Ours has been on since February:

sudo loginctl enable-linger "$USER"
loginctl show-user "$USER" --property=Linger
Linger=yes

On the evening boot of 14 October we left the laptop at the login screen. The user manager came up at 22:50:05, a test timer ran its job in the same second, and the only session before that was gdm's login screen. The first session for our user was our own SSH connection, three minutes later:

2026-10-14T22:50:04+03:00 x1 systemd-logind[1027]: New session c1 of user gdm.
2026-10-14T22:50:05+03:00 x1 systemd[1650]: Started daily-job.timer - Run daily-job every 15 minutes.
2026-10-14T22:50:05+03:00 x1 systemd[1650]: Starting daily-job.service - Daily job...
2026-10-14T22:53:12+03:00 x1 systemd-logind[1027]: New session 4 of user x1.

More in Run user services without logging in, including the network catch that bit a job at an earlier boot.

5. Make timers catch up after time off

A .timer with OnCalendar= skips any run that fell due while the machine was off, unless it has Persistent=true (systemd.timer(5)). Our test timer runs every 15 minutes:

[Timer]
OnCalendar=*:0/15
Persistent=true

Its last run before we switched the laptop off was at 20:30 on 14 October. The laptop shut down at 20:41 and booted again at 22:49, so nine slots, 20:45 to 22:45, fell due while it was off. At boot the timer fired once, straight away:

2026-10-14T20:41:27+03:00 x1 systemd[1651]: Stopped daily-job.timer - Run daily-job every 15 minutes.
-- Boot 55bb0af46fa54ec881f2777779a77f6c --
2026-10-14T22:50:05+03:00 x1 systemd[1650]: Started daily-job.timer - Run daily-job every 15 minutes.
2026-10-14T22:50:05+03:00 x1 systemd[1650]: Starting daily-job.service - Daily job...
2026-10-14T22:50:05+03:00 x1 systemd[1650]: Finished daily-job.service - Daily job.

Catching up isn't the same as succeeding. The catch-up runs before the Wi-Fi is up (at 22:50:08 on that boot). Earlier that day a daily job that calls a web API made its catch-up run after a five-day power-off and failed, because it couldn't resolve the API's hostname. A user service can't wait for the system's network-online.target; the user manager doesn't have that unit. The other surprise was a wrong clock at boot. Both are in Make a systemd timer catch up on runs it missed.

6. Remote access and security updates

You'll want to reach it without opening the lid. Ours runs Ubuntu's OpenSSH server, installed with one package (Ubuntu Server: OpenSSH). On 24.04 it's socket-activated, so ssh.socket is the unit that's enabled:

sudo apt install openssh-server
systemctl is-enabled ssh.socket ssh.service; systemctl is-active ssh.socket
enabled
disabled
active

We don't forward any ports. The laptop is on our Tailscale network, and we connect with plain ssh to its Tailscale address. Tailscale also has its own SSH server, Tailscale SSH, which handles authentication through Tailscale instead of SSH keys (Tailscale docs). It's off on this machine, and we haven't tested it:

	"RunSSH": false,

Ubuntu installs unattended-upgrades by default. It runs daily when both lines in /etc/apt/apt.conf.d/20auto-upgrades are "1" (Ubuntu Server: Automatic updates). Ours are. Now check the log, not just the config:

cat /etc/apt/apt.conf.d/20auto-upgrades
cat /var/log/unattended-upgrades/unattended-upgrades.log
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
2026-10-08 22:09:17,867 WARNING System is on metered connection, stopping
2026-10-08 22:38:00,194 WARNING System is on metered connection, stopping
2026-10-14 15:10:19,988 WARNING System is on metered connection, stopping

Every run logged since 8 October stopped there. The last run in May finished normally, with "No packages found that can be upgraded unattended". NetworkManager decides whether the current connection is metered, and for this Wi-Fi it guessed yes:

nmcli -f GENERAL.TYPE,GENERAL.METERED device show wlp0s20f3
GENERAL.TYPE:                           wifi
GENERAL.METERED:                        yes (guessed)

Skipping updates on metered connections is the default. Ubuntu's /etc/apt/apt.conf.d/50unattended-upgrades shows it as a commented-out line, Unattended-Upgrade::Skip-Updates-On-Metered-Connections "true";. There are two ways out. Mark the connection as not metered with nmcli connection modify <name> connection.metered no, if it really isn't metered (nm-settings-nmcli(5)). Or set that option to "false" in a file of your own under /etc/apt/apt.conf.d/. We haven't made either change yet, so at the time of writing this laptop isn't installing its security updates. That's exactly why you read the log.

Hardware notes for the X1 Carbon Gen 7

  • It has no full-size Ethernet jack. The Ethernet controller is there, but it needs Lenovo's Ethernet extension adapter, so ours runs on Wi-Fi:
wlp0s20f3:wifi:connected
tailscale0:tun:connected (externally)
lo:loopback:connected (externally)
p2p-dev-wlp0s20f3:wifi-p2p:disconnected
enp0s31f6:ethernet:unavailable
  • /sys/power/mem_sleep offers [s2idle] deep. With suspend masked, that no longer matters here.
  • The battery threshold file comes from the thinkpad_acpi driver. Other brands may have a different driver or no threshold at all.
  • The clock was wrong at one boot (see the timer hack). If your old laptop's clock resets, expect odd timestamps until it syncs.

Undo everything

Every change is a file you can delete or a command with an opposite:

sudo rm /etc/systemd/logind.conf.d/server-mode.conf
sudo systemctl unmask sleep.target suspend.target hibernate.target hybrid-sleep.target
sudo systemctl disable battery-charge-cap.service
echo 100 | sudo tee /sys/class/power_supply/BAT0/charge_control_end_threshold
sudo loginctl disable-linger "$USER"

Then reboot. 100 is the top of the range the kernel docs give for the end threshold. We haven't recorded what this laptop shipped with.

HackHour Weekly, every Wednesday

One email a week: the week's hack and what changed. Unsubscribe anytime.

What's in an issue

Sources

Platform facts in this guide link to these pages. Last checked .

  1. logind.conf(5), Ubuntu Manpages (noble)
  2. systemctl(1), Ubuntu Manpages (noble)
  3. org.freedesktop.login1(5), Ubuntu Manpages (noble)
  4. systemd-sleep.conf(5), Ubuntu Manpages (noble)
  5. thinkpad_acpi: Battery charge control, Linux kernel documentation
  6. loginctl(1), Ubuntu Manpages (noble)
  7. systemd.timer(5), Ubuntu Manpages (noble)
  8. OpenSSH server, Ubuntu Server documentation
  9. Tailscale SSH, Tailscale documentation
  10. Automatic updates, Ubuntu Server documentation
  11. nm-settings-nmcli(5), Ubuntu Manpages (noble)

Revision history

RevDateChange
AFirst published.

Errata

No corrections so far. Spotted one? Tell us through the site chat.