Operating system

Operating system

Updated 2020-12-22: no longer requires beta firmware + use aarch64
Updated 2021-01-30: add auto-update Arch mirrorlist

In order for the Raspberry Pi to do something “useful” it requires1 an operating system (OS). The OS interfaces and manages all the hardware on the board, since the Raspberry Pi is popular there are quite a few options to choose from.

The goal is to have a headless, set-and-forget, (private) server. To reduce maintenance, receive bugfixes and new features it should auto-update as well.

Arch Linux ARM2 was chosen as OS because it is minimal, provides full control, is a rolling release3, light-weight and bleeding edge4. I have already experience with Arch and it checks all the boxes for me. While bleeding edge might be problematic for a long running stable system, in practice it rarely breaks in my experience (YMMV).
There are many other, officially supported, Linux flavors available as well. The official distribution, from the Raspberry Pi foundation, is Raspberry Pi OS (debian) which has the best support (followed by Ubuntu). As alternative to Linux one can also use Windows 10 ( IoT Core or if you get your hand dirty the full version).
However there are also downsides using Arch Linux ARM. It is not that popular and has less support5 (and resources) than the other official supported OS’s. Arch aims to keep packages vanilla6 if possible, to reduce maintenance, and thus greatly benefits when patches are upsteamed7 e.g. by the Raspberry Pi Foundation. Arch Linux is not aimed towards novices although their wiki is excellent.

Disclaimer: this guide might appear vague and incomplete if you aren’t sure what you’re doing. This is intentional. It is specifically not designed for users new to Linux/software.

If you use a HAT8, which require drivers/overlays to be installed, they are most likely specifically made for Raspberry Pi OS and might not work on Arch Linux ARM. YMMV.

This post is written for the Raspberry Pi 4B.


This section provides background on the hardware and can be skipped entirely.

The Raspberry Pi 4B comes with an ARMv8 Cortex-A72 (Armv8-A) CPU (Broadcom BCM2711 1.5GHz, official documentation, datasheet) and 1/2/4/8Gb LPDDR4-2400 SDRAM (Low Power Double Data Rate Random Access Memory, Synchronous Dynamic Random-Access Memory). The CPU architecture stems from 2015 (released 2016) and is an out of order9 quad core 64bit ARM CPU (also supports 32bit). The chip includes the GPU as well, a VideoCore VI 0.5 Ghz (32bit) which is also used in the boot process.

Comes with 4 USB ports (2x3.0 & 2x2.0) through a dedicated controller (VL805) connected to the CPU’s PCIe link. Fast enough for the max USB 3.0 speeds making SSD’s (properly) viable.

A dedicated full gigabit ethernet port using the BCM54213PE chip on a separate bus to the USB traffic.

Power management is handled by the MaxLinear mxl7704 chip which allows for dynamically reducing power to the SoC (CPU+GPU) when idle.

It has bluetooth 5.0 ( Cypress CYW43455 chip), although it is more 4.2 since none of the optional 5.0 Bluetooth features are supported. It also supports dual-band 2.4GHz and 5GHz IEEE 802.11.b/g/n/ac wireless networks.

40 GPIO pins are available which are overloaded and can be configured for different protocols. 6 UARTS of which 5 are fully featured PL011 and one is “mini” UART (no: break and framing errors detection, parity bit, receive timeout interrupts and DCD, DSR, DTR or RI signals) which is bound to the CPU clock (and will lock the CPU frequency if enabled). For more info see the official documentation.
There are 6x I²C and one SPI bus (2 chip selects), for more info see the official documentation.

For cooling, heat-sinks for the CPU, RAM and USB controller are advised, the Raspberry Pi 4B tends to produce more heat than its predecessors (also uses more power). The chance of damaging the device are neglectable since it will throttle the CPU (starting from 80-85°C); affecting performance. By default the CPU frequency is dynamic based on the load. When going for an active cooling solution you could opt to overclock the CPU max +0.647Ghz (43%), and GPU max +0.25Ghz (50%) which is linked to CPU performance; the GPU drives the L2 cache and memory bus. However these are the results for the best boards, yours might not get as high10 (24/7). Overclocking will also increase the power usage so make sure there is enough left for additional attached hardware, if any. The official power supply can deliver up to 3A; 0.6-1A used by a vanilla, bear, board. Official overclock guide and official documentation(contains important caveats); ≤50°C idle seems a good target. The voltage should remain above 4.8V at all times to retain a stable system. Good luck in the silicone lottery!

The Raspberry Pi is a general purpose budget computer that is designed around cost, while great in many regards it has compromises like shared clock lanes (all documented). Overall it is ok for most use-cases and has a superb value proposition.

Also have a look at the excellent official documentation and resources.

hardware overview

Raspberry Pi 4B

hardware overview

Source: electronics-lab.com

Operating system installation

I will be using an 2.5" SSD instead of micro SD card as NVM (non volatile memory), see this post for the why. In short, durability. To set-up your Raspberry Pi you stil might need an SD card if the firmware is not up-to-date though.


tl;dr use 64bit (aarch64)

Previously the official OS was only available in 32bit mode but since 28/05/2020 the beta for Raspberry Pi OS 64bit OS started and is currently out of beta and considered stable (since 06/07/2020). Before the beta the community already had 64bit support (kernel and user land) working as well, as early as the end of 2019, however those were only remained experimental and for hobbyists.

Why would you want to use 64bit? It has better performance, especially in networking and synthetic benchmarking. Another reason might be if you want a single process to take up more than 3.2Gb RAM; especially applicable for the 8Gb model. Also the virtual address space is much larger (although on ARM you already has Large Physical Address Extension ( LPAE), used by Raspberry Pi OS, which extends the physical address size from 32 bits to 40 bits on 32bit systems which makes this argument a bit mute).
Some software is only available for 64bit since it is much more popular than 32bit, especially for supercomputers ( HPC).

Switching from one to the other, without re-installing, is generally a pain and to be avoided, since the future is probably 64bit for Raspberry Pi i would strongly advice to already install 64bit now on your system if you want to avoid re-installing later on.

Make sure your SSD has the latest firmware installed. To check if your SSD has the latest firmware, and how to upgrade, contact your vendor. If it is a reputable brand you will most likely find this information on their website.

While you can upgrade your firmware at anytime, having no data on the disk may save you some stress (and/or time creating a back-up).

tl;dr it is a lottery, good luck!

There are a few things you want: USB 3.0, UASP and TRIM support. USB Attached SCSI Protocol ( UASP) is a dedicated protocol to connect HDD and SSD’s over USB; it provides significant performance improvements (↑throughput, ↓CPU) over plain USB so is highly recommended. Especially when using a SSD. The protocol also supports TRIM, which is important for SSD’s to efficiently handle garbage collection which enhances the life-time of the device (the OS notifies which blocks of data are no longer considered in use and can be wiped internally). The Raspberry Pi 4B only has USB 3.0 but 3.1 adapters will usually work as well (obviously they do not provide any performance benefit over 3.0 for the Raspberry Pi).

It should be as simple as picking an adapter which has these features and call it a day. Alas… usually they are not advertised. Even if they are you are never sure what you will get since adapter manufactures tend to change the chips they use, silently. Or vendors lie about the particular version number of the adapter they sell. So it is a lottery. Some cables advertise UASP support but it is so badly implemented the devices are blacklisted by the Linux kernel.
Therefore the community has created lists of known working adapters, Eluteng and StarTech seem to be the top brands. The desired chip is the ASMedia ASM1153E, alternatively the ASM1051E (needs a firmware update to support TRIM which is not officially supported for consumers, worst case you brick your adapter).

If you go for a well known brand be aware of look-alike (fake) adapters. That said some people buy an unbranded adapter and get the desired ASM1153E chip as well. On the other hand, people like me, buy the specific recommended adapter (StarTech/Eluteng (blue11))) but get scammed and send an old/fake version which supports neither UASP nor TRIM. Best of luck! The adapters also tend to be relatively pricey for what they are.
As for my personal recommendation, if you need a short cable go for the Eluteng (blue) otherwise the StarTech USB3S2SAT3CB.

Oh and to end, USB 3.0 is known to interfere with 2.4GHz wireless devices, a common frequency for bluetooth/wireless mice and keyboards. But that should not be a problem for a server. The solution? Move the SSD/dongle/keyboard/mouse away from each other!

Be careful your adapter supports the peak power consumption of your SSD, while the Raspberry Pi 4B can, in total, deliver up to 1200mA to its USB ports, the adapter might not. The

StarTech USB3S2SAT3CB adapter is only rated for max 900 mA which is not sufficient for all 2.5" SSD’s, e.g. the Samsung EVO 860 has a 1080 mA peak power consumption.

If you want to have multiple storage devices chances are they will draw too much power and you need to use powered adapters OR a dedicated (powered) data HAT. Also keep in mind the total throughput will always be bound to the maximum throughput of USB 3.0.

Check your adapter chipset & ID

For this test there does not need to be an SSD/HDD attached to the adapter. You can use any machine to perform this test, in this example an Arch Linux x86 machine was used.

Note: lsusb is part of the usbutils package

  1. lsusb
  2. Plugin in the adapter
  3. lsusb

The one that changed/added is your adapter, in my case the first line (not an adapter you want). You will also get its id (07ab:0621 in my case).

Bus 002 Device 002: ID 07ab:0621 Freecom Technologies Ext. HDD
Bus 002 device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 002: ID 2109:3431 VIA Labs, Inc. Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

Check for UASP support

For this test there does not need to be an SSD/HDD attached to the adapter. You can use any machine to perform this test, in this example an Arch Linux x86 machine was used.

Run lsusb -t, scroll to your adapter and you will see something like this:

Bus 02.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/6p, 5000M
  |__Port 2: Dev 5, If 0, Class=Mass Storage, Driver=uas, 5000M
  |__Port 4: Dev 3, If 0, Class=Mass Storage, Driver=usb-storage, 5000M

If you see a line with Driver=uas it supports UASP, otherwise it does not.

Alteratively run lsusb -v -d VPID | grep -i interface with VPID the id of your adapter (e.g. 07ab:0621 see previous section). You will get something like this:

Couldn't open device, some information will be missing
  bDeviceClass            0 (Defined at Interface level)
    bNumInterfaces          1
    Interface Descriptor:
      bInterfaceNumber        0
      bInterfaceClass         8 Mass Storage
      bInterfaceSubClass      6 SCSI
      bInterfaceProtocol     80 Bulk-Only
      iInterface              0
    Interface Descriptor:
      bInterfaceNumber        0
      bInterfaceClass         8 Mass Storage
      bInterfaceSubClass      6 SCSI
      bInterfaceProtocol     98
      iInterface              0

If you see an interface with bInterfaceProtocol 98 it supports UASP if you only see bInterfaceProtocol 80 is does NOT support UASP.
From the specification:

50h (80d): USB Mass Storage Class Bulk-Only (BBB) Transport
62h (98d): Allocated by USB-IF for UAS.

Check for TRIM support

Note: if your adapter does not support UASP it will not support TRIM

To check if your SSD supports TRIM run lsblk --discard, if the DISC-GRAN (discard granularity) and DISC-MAX (discard max bytes) columns have non-zero values it has TRIM support.

To verify whether or not your adapter supports TRIM you will have to enable it and test (or buy one with a chips that is known to be supporting TRIM).

If any of these checks fail i strongly recommend to return/refund the adapter and buy another one. Try to get one with the ASM1153E chip.


Below are some sections with background and steps to get everything running at the time of writing, December 2020.

Be careful when shutting down the Raspberry Pi, never just pull the plug (may cause damage) . Always use sudo halt or sudo shutdown -h now first before disconnecting the power plug.

tl;dr you can't without OS, just try it

Without operating system you will just have to prepare the SDD, see down below, and try to boot from it and see if it works. If it does not you will need to upgrade your firmware first.

At the time of writing the firmware supporting USB boot has been promoted to stable. It still has to be promoted to critical, loaded as default in the factory AND the old stock from your supplier has to be sold first before you will get the firmware supporting USB boot by default on your Raspberry Pi.
It is probably safe to say if you bought your Raspberry Pi before Q4 2020 chances are you will have to manually upgrade your firmware first, if your have not already done so.

If your Raspberry Pi already comes with firmware supporting USB boot this step is obsolete.

This requires micro SD card and another PC to prepare the micro SD card, in this case an Arch Linux x86 machine was used.

Set-up your micro SD card through the official Arch ARM Raspberry Pi installation guide (ssh is enabled by default). Remark: if you ever want to chroot into the SSD later on i advice to install the same architecture (32/64 bit) on the micro SD card as you are planning to install on the SSD (probably aarch64, hence 64bit). If it is just for upgrading firmware the architecture you pick does not matter.

Upgrade the firmware by doing a system update: pacman -Syu.

Once your are done SSH into the Raspberry Pi and do a system wide upgrade pacman -Syu (not strictly required). Install git, fakeroot, sudo (add the alarm user: gpasswd -a alarm wheel and through visudo uncomment %wheel ALL=(ALL) ALL (scroll with keyboard), use x/del to delete the # and :x to save). Optionally install nano. Reboot.

SSH again and install rpi-eeprom from AUR (the Raspberry Pi firmware upgrade daemon). Switch to the stable firmware releases: edit /etc/default/rpi-eeprom-update and change FIRMWARE_RELEASE_STATUS from critical to stable (e.g. nano /etc/default/rpi-eeprom-update). Upgrade you current firmware: rpi-eeprom-update -d -a. Reboot.

Check if it succeed by running /opt/vc/bin/vcgencmd bootloader_config which should give you:

Jun 15 2020 14:36:19 version c302dea096cc79f102cec12aeeb51abf392bd781 (release) timestamp 1592228179
$ vcgencmd bootloader_config

The boot order should say BOOT_ORDER=0xf41 which

indicates: look for USB mass storage first, otherwise look for the SD card and retry if not found.

That is it, you’re done and the micro SD card is now obsolete. You may still keep it around to install the operating system on the SSD or in case you break the your system and have to chroot into the SSD.

As of August 2020 Arch Linux ARM officially supports 64bit/aarch64 out of the box.

Note however: at the time of writing it provides an installation using the mainline kernel and U-Boot. Use this installation only if you have no dependencies on the closed source vendor libraries shipped with the Raspberry Pi Foundation official kernel (e.g. tool or libraries for example used by HAT driver). This installation has near full support for the device, including the VC4 graphics. There are however currently issues with detecting USB device and network connection.

There is a separate package which provides the official Raspberry Pi kernel which does not need U-Boot. It has to be installed after the regular installation.

Follow the

Arch ARM official installation guide. When you boot from the SSD for the first time it can take a few minutes, just give it some time. When finished get the IP of the Raspberry Pi, SSH from another machine into it (ssh alarm@xxx.xxx.xxx.xxx) and off you go!

If you prefer the official Raspberry Pi kernel instead one may switch after following the official guide. Install linux-raspberrypi4 and remove uboot-raspberrypi. This is what i went for.

This section is obsolete, the changes have been merged to the official Arch Linux ARM repositories.

This requires another machine or the Raspberry Pi with micro SD card to prepare the SSD. Both installations have to be either 32bit or 64bit linux.

First finish the

Arch ARM official installation guide for the aarch64 installation for aarch64. The last steps will require you to chroot into the SSD from another machine.

Install arch-install-scripts (apt install arch-install-scripts on debian based distributions or pacman -S arch-install-scripts on arch based systems). Mount the SSD: fdisk -l (to get the disk label, /dev/sda for me), mkdri /mnt, mount /dev/sda2 /mnt/root, mount /dev/sda1 /mnt/root/boot and finally arch-chroot /mnt/root.

To exit chroot use the exit command.

Boot from the other machine with the SSD connected and chroot into the SSD installation, follow the steps outlined

here (generously provided by graysky, an Arch Linux ARM maintainer) which installs the community kernel12. Alternatively compile the kernel yourself from the provided PKGBUILD files.

Before exitting the chroot environment update fstab with your boot partition: blkid to get the PARTUUID of SSD’s /boot (45a68f79-01 in this example) and /root (45a68f79-02 in this example) partitions, nano /etc/fstab and replace /dev/mmcblk0p1 by PARTUUID=45a68f79-01. Update nano /boot/cmdline.txt and replace root=... by root=PARTUUID=45a68f79-02.

Shut down the Raspberry Pi, remove the micro SD card if applicable (now permanently obsolete) connect the SSD, reboot and you’re done!

Once Arch Linux ARM officially supports the official Raspberry Pi kernel you could switch your installation to the official Arch Linux ARM kernel.

If you have boot problems see the ACT status led documentation for diagnosis. If you want more information attach a screen or capture (and enable) the serial output from the GPIO pins.

The ACT (green) led will blink with 3 short pulses in 2 second intervals and the journal will report failures. This is ok, even though it signals a general boot failure, it is confused there is no micro SD card present. I presume this will be resolved once the beta is done.

Work around is to insert a blank micro SD card in the Raspberry Pi OR turn of the ACT led.

Configure the operating system


These are all optional

Change the hostname: hostnamectl set-hostname NEW_HOSTNAME.

Change username: usermod -l NEW_USERNAME OLD_USERNAME, if there are running processes owned by the user you will have to kill them first pkill -9 -u OLD_USERNAME.

Install sudo

Install sudo, add the alarm user: gpasswd -a alarm wheel and through nano /etc/sudoers uncomment %wheel ALL=(ALL) ALL. Reboot is required to have effect.

Change default passwords

Change the default user and root passwords: passwd alarm and passwd root (you will be prompted to provide the new password).

This is highly advised with respect to securing and hardening your device; whether or not that is important depends on your use-case.

Configure Pacman

Install the pre requisites to install/build AUR packages: pacman -S --needed base-devel. Packages in AUR assume that the base-devel group is installed, i.e. they do not list the group’s members as build dependencies explicitly. Optionally adjust /etc/makepkg.conf, search for MAKEFLAGS and replace -j2 by -j$(nproc) so all cores are used when building a package. However i did not change the default to ensure the sensor data can still be processed while installing packages.

Install git.

To install a package from AUR use:

  • git clone https://aur.archlinux.org/<pkgName>.git
  • cd <pkgName>/
  • makepkg -sric (install all needed dependencies, remove make dependencies afterwards, install the package right away, clean leftover files)
  • rm -r <pkgName>/

Optional: in /etc/pacman.conf enable the color option.

Install yay (AUR), an extended version of pacman that will run pacman and automatically install updates for AUR packages as well. I would advice to keep AUR packages to a minimum if possible which lowers the change of something going wrong.

AUR packages are user produced content. Any use of the provided files is at your own risk.

To avoid the package cache filling up over time (it can become really large) periodically prune it using pacache (part of pacman-contrib package). To only keep the last version use paccache -rk1, to remove uninstalled packages paccache -ruk0. Run it automatically13, weekly, with a timer (enable paccache.timer) or invoke the command(s) manually, i will go for the latter in combination with an auto update script (see later section).


Configure Arch Linux mirror list

Pacman relies on mirrors to fetch its packages from, this list is not automatically updated and one may end up with subbar mirrors a few year down the line. This could result in slower downloads, fetching failures or outdated mirrors (returning not the most up-to-date packages). Arch linux provides a tool, reflector, which updates the mirrorlist according to set preferences.

To let it run automatically enable the service: reflector.service settings can be configured in /etc/xdg/reflector/reflector.conf. To run it periodically enable the timer reflector.timer (and edit if you want to change the interval). */}}


To properly set-up the system local follow the Arch wiki: edit /etc/locale.gen e.g. uncomment en_US.UTF-8 (American English), run locale-gen. Set the system local: localectl set-locale LANG=en_US.UTF-8.

Log settings

To avoid logs piling up over time it is advised to automatically clean them up periodically. Edit the settings in /etc/systemd/journald.conf: set SystemMaxUse=500M. After you’re done restart the systemd-journald service.

Install rpi-eeprom ( AUR), to get the status use rpi-eeprom-update. After installation, edit /etc/default/rpi-eeprom-update and change FIRMWARE_RELEASE_STATUS to either: critical (the default), stable (required for USB boot at the time of writing) or beta.

For more info see the official documentation.

The current package in AUR does not include a service to automatically update the firmware, so you either have to do it manually for create your own service, e.g.:

Description=Check for Raspberry Pi EEPROM updates

ExecStart=/usr/bin/rpi-eeprom-update -a


However i have included it in the auto update script, see later section.

Before proceeding make sure you SSD supports TRIM (or you might lose data). To enable TRIM: systemctl enable fstrim.timer.

Note: you have continuous trim and periodical trim, the latter is preferred and will be used by default. See Arch Wiki for more information.

Arch Linux (ARM) provides a minimal installation which can be extended as one pleases. To get some useful tools install the raspberrypi-firmware package. This package is already included by default if you use Raspberry Pi OS.

Depending on your network set-up usually IP addresses are reassigned every time you connect to a network (through DHCP). For servers this is annoying since you will have to look-up its IP address over and over again and possibly adjust scripts (with the new address of the server). Therefor assign it a static IP address that will never change.

Since the systemd service is enabled by default we will use it to configure a static IP address (alternatively use Netctl)

  1. Get your network and default gateway: ip r | grep default (the IP after default)

  2. Get the current nameserver: nano /etc/resolv.conf, the IP after nameserver.

  3. Remove the existing default config: rm /etc/systemd/network/eth.network (or edit it instead of creating a new one in the next step)

  4. Create/edit the network profile: nano /etc/systemd/network/<INTERFACE>.network (with the desired interface, e.g. eth0)

  5. Add the following content:



Make sure <STATICIP> (you can pick any internal address) is outside of your routers dhcp range or you might get conflicts. Replace <ROUTERIP> by the ip from step 1 or the IP of your router. Replace <DNSIP> by the IP of the DNS server you want to use (from step 2 or another one).

If you would have dhcpcd running disable it: systemctl stop dhcpcd and systemctl disable dhcpcd.

Requires a reboot to have effect.

Even though the HDMI ports are not used, they are stil powered on in case you plug in a cable. To save some power, ~30 mW, you can disable them permanently.

To turn them off /opt/vc/bin/tvservice -o (will remain disabled after rebooting)

To turn them back on: /opt/vc/bin/tvservice -p

To check whether or not they are enabled: /opt/vc/bin/tvservice -s

Since the Raspberry Pi does not have a persistent hardware clock one must use the internet (network time protocol, NTP) to synchronize the SW clock periodically. Othewise it will drift over time and reset after a reboot.

Install chrony (or ntp), systemd-timesyncd.service (they conflict) and enable the service systemctl enable chronyd.service.

Edit the config file nano /etc/chrony.conf and adjust to your preference. e.g. enable pool 3.arch.pool.ntp.org iburst (pool is preferred over individual servers because it automatically selects the best servers), makestep 1.0 3 (required to reduce the boot time, if the time differs more than 1 second correct it immediately (ie. at boot). Otherwise gradually because some software cannot deal with large jumps forwards/backwards in time) and logchange 1.0 (log time difference larger than 1 second; something is wrong). Although the ethernet chip on the Raspberry Pi supports hardware timestamping (improving accuracy) running ethtool -T eth0 (install the ethtool package) indicates it was not supported so a special driver/some tinkering might be required to get it working on the Raspberry Pi; therefore i left it off.

Other systemd services and timers may depend on the time already being set correctly. Therefore we also must enable systemctl enable chrony-wait.service which will block time-sync.target until NTP has synchronized the clock. Unfortunately not all services which require the clock to be set wait on time-sync.target by default (hence are misconfigured). This can be corrected, manually per service, by specifying a drop-in file. These are retained even if the .service file is updated afterwards and are applied on top of the regular .service file settings.
For example to delay the Pi-hole service use: systemctl edit pihole-FTL and add the content:


This will however delay their startup time up to 7 seconds, the time it takes to synchronize the clock (you can check using systemd-analyze blame right after booting). One can always revert the unit file by using systemctl revert [service name].
No action is required for systemd timer when using OnCalendar, they will implicitly have After=time-sync.target to avoid being started before the system clock has been correctly set.

Requires a reboot to have effect.

When using Pi-hole and having /etc/resolv.conf set to just nameserver you MUST also add nameserver [gateway IP] otherwise chrony will not be able to resolve the NTP servers and chrony-wait will fail after 10 minutes (default timeout).
Alternatively one can use IP based servers in /etc/chrony.conf instead of hostnames (losing the advantage of being able to use a pool).

Services that have a drop-in file defined will have a warning when querying their status: “Warning: The unit file, source configuration file or drop-ins of [name].service changed on disk. Run ‘systemctl daemon-reload’ to reload units.” and be reported as dead (e.g. systemctl status -l pihole-FTL) as long as chrony-wait is not done yet.

These are harmless and can be ignored.

One can also use systemd-timesyncd but it only implements the simple NTP protocol (SNTP) which provides lower accuracy (>100ms) which might or might not be a problem for your use-case.

Alternatively you can buy a hardware real time clock (RTC) module for the Raspberry Pi which will make all these steps obsolete.

Note: you will need a secondary server(/NAS/cloud storage …) as destination for the backups.

If your backup destination is remote encryption is strongly advised to protect (obscure) your data from eavesdroppers. Backups typically contain sensitive information.

Ideally we would be able to take a full backup (1:1 disk image) while the system is running, alas this is too hard of a problem (lock files, temporary files, changes between starting and finishing the backup, …); so that will not be an option.

Therefore most applications will have to be configured individually to periodically back-up themselves.

Backup /etc

Instead of a simple git repository using etckeeper is preferred since it integrates with pacman and keeps the file permissions (git does not).

  1. Initialize git etckeeper init

  2. Do a first commit etckeeper commit "first commit"

  3. (optional) Enable the timer systemctl enable etckeeper.timer (it also has pacman pre- and post-install hooks)

  4. Configure the remote git repository git remote add origin gitea@[IP RPi]:[gitea user]/[repo]].git. Make sure to use ssh so no authentication is required every time pacman is ran.

  1. If you have not done already generate a keypair for alarm.

  2. Add the (public) ssh key to your account on gitea (web UI)

  3. Because etckeeper runs as root add /root/.ssh/config containing:

Host [RPi IP]
    HostName [RPi IP]
    User gitea
    IdentityFile /home/alarm/.ssh/id_rsa

To troubleshoot use: ssh -vvvT gitea@[RPi IP], you should get a message greating you with your gitea username. If this succeeds but etckeeper commit or /etc sudo git push does not (or asks for a password) the problem can be fixed with /root/.ssh/config.

Pushing your etckeeper repository to a publicly accessible remote repository can expose sensitive data such as password hashes or private keys. Proceed with caution.
  1. Configure etckeeper in /etc/etckeeper/etckeeper.conf set PUSH_REMOTE="origin"

Make sure the remote git repository is set-up to accept your ssh keys or store your credentials on your local git installation (on the Raspberry Pi).
Otherwise you will be prompted every time you run pacman.

Backup selected files

To sync files/backups to a remote location one can use rsync.

You can create a script and back-up individual file/folders and/or backups from databases & programs (e.g. Gitea, Pi-hole, …). To periodically run backups create a systemd service and timer.

Personally i use a script to weekly backup services and in the auto update script (see later sction) backup some specific files monthly. My update script (autoBackup.sh):

# Pi Server 1 auto backup script
# v0.1 -- initial version
# v0.2 -- minor improvements

# ------------------------------------
# Settings & set-up

rsyncOpions="-aqrm --delete -e ssh"


sudo mkdir -p ${nfsMount}
sudo chmod 777 ${nfsMount}
sudo mount ${nfsServer}:${nfsServerShare} ${nfsMount}

# -----------------------------------
# Gitea

giteaTmpFolder="${nfsMount}/gitea/tmp" # Avoid hitting the disk with the downside of a performance hit

# Gitea uses /tmp to store temporary files, which is in RAM hence limited on the Pi so we use another /tmp folder
giteaOutput=$(sudo -u gitea gitea dump -c /etc/gitea/app.ini --tempdir ${giteaTmpFolder} --file ${nfsMount}/gitea/${giteaBackupFilename})

# -----------------------------------
# Pi Hole

# All data is stored in /etc which is already backed-up.
# Alternative one can use "Teleporter" in the web UI but there is not CLI equivalent at the time of writing.

# -----------------------------------
# InfluxDB

# InfluxDB uses the current date and timestamp as file names, hence  old file are not overwritten so we delete them first (assuming the destination has snapshots)
rm -rf ${nfsMount}/influxdb/* 

influxdbOutput=$(influx backup --bucket "sensordata" -o "${influxdDBOrg}" -t ${influxDBToken} ${nfsMount}/influxdb)
if [[ ${influxdbStatus} -ne 0 ]]; then
influxdbOutput+=$(influx backup --bucket "solaredge" -o "${influxdDBOrg}" -t ${influxDBToken} ${nfsMount}/influxdb)

# -----------------------------------
# Telegraf

# All data is stored in /etc which is already backed-up.

# -----------------------------------
# Grafana


grafanaOutput=$(sudo -u grafana rsync ${rsyncOpions} --exclude '.ssh' ${grafanaDir} ${rsyncUser}@${rsyncServer}:${rsyncDestination}/grafana)

# -----------------------------------
# Handle the status

sudo umount ${nfsMount}

if [[ $giteaStatus -eq 0 ]] && [[ $influxdbstatus -eq 0 ]] && [[ $grafanaStatus -eq 0 ]] && [[ $umountStatus -eq 0 ]]; then
 exit 0

# -----------------------------------
# Compose the email


giteareportstatus="successful" && [[ ${giteaStatus} -ne 0 ]] && giteareportstatus="failed"
influxdbreportstatus="successful" && [[ ${influxdbStatus} -ne 0 ]] && influxdbreportstatus="failed"
grafanareportstatus="successful" && [[ ${grafanaStatus} -ne 0 ]] && grafanareportstatus="failed"
umountreportstatus="successful" && [[ ${umountStatus} -ne 0 ]] && umountreportstatus="failed"
hostname=$(${nonroot} hostnamectl --static)

DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null 2>&1 && pwd)"

# Create the email
email="From: ${senderemail}\r\n"
email+="To: ${recipientemail}\r\n"
email+="Date: $(date --rfc-email)\r\n"
email+='Message-ID: <dcd7cb36-11db-487a-9f3a-e652a9458efd@rfcpedant.example.org>\r\n'
email+='Content-Type: text/html; charset="utf8"\r\n'
email+='Content-Transfer-Encoding: quoted-printable\r\n'
email+='Mime-version: 1.0\r\n'
email+="Subject: [${hostname}] Automatic backup failed\r\n"
email+='\r\n' # Empty line to separate body and header
email+='<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">'
email+='<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>'
email+='<style type="text/css">'
email+="$(sh ${DIR}/../ansi2html.sh  --css-only --quiet --palette=twilightdark --minify-css)"
email+='<body class="f9 b9">' # body class for colored HTML
email+="<h1>Automatic backup report for '${hostname}'</h1>"
email+="<h3>Gitea (${giteareportstatus})</h3>"
email+='<p>'                # for the colored HTML
email+="$(printf '%s' "${giteaOutput}" | sh ${DIR}/../ansi2html.sh --body-only --quiet)"
email+="<h3>InfluxDB (${influxdbreportstatus})</h3>"
email+='<p>'                # for the colored HTML
email+="$(printf '%s' "${influxdbOutput}" | sh ${DIR}/../ansi2html.sh --body-only --quiet)"
email+="<h3>Grafana (${grafanareportstatus})</h3>"
email+='<p>'                # for the colored HTML
email+="$(printf '%s' "${grafanaOutput}" | sh ${DIR}/../ansi2html.sh --body-only --quiet)"
email+="<h4>Umount (${umountreportstatus})</h4>"

# Send the report
curl -S -s -o /dev/null --url "SMTPS://${smtp}:${port}" --ssl-reqd --mail-from "${senderemail}" --mail-rcpt "${recipientemail}" --user "${senderemail}:${senderemailpw}" -T <(echo -e "${email}")

if [[ ${sendmailstatus} -ne 0 ]]; then
    echo 'Sending email failed.' | systemd-cat -t 'AutoBackup' -p emerg
    exit ${sendmailstatus}

exit 0

You will need to enable rsync on your remote destination and pair the ssh keys so you do not need to provide a password every time you run rsync.

The service which runs the script, as sudo (autoBackup.service):

Description=Automatic backup

ExecStart=sudo /home/alarm/PiServer1Scripts/AutoBackup/autoBackup.sh

The timer to trigger the service (autoBackup.timer):

Description=Every Tuesday

OnCalendar=Tue, 03:00


For the ansi2html.sh script and more information on how to set up the backup script, service and timer see the “Automatic system updates” section.

To use these steps it is assumed you have nano installed from the community packages (or use vim).

When you speficy rsync’s destination pointing to the NAS make sure to include the volume label as well, e.g. [user]@[IP NAS]:/volume1/homes/[user]/backup

Set-up ssh keys

On the NAS:

  1. Make sure SSH is enabled, see

    official documentation.

  2. Set-up a user account (new/existing) and add it to the administrator group (required by Synology, stay tuned we will come back to this).

  3. SSH into the NAS using your admin account ssh [admin]@[IP NAS]

  4. Use sudo -i and edit nano /etc/ssh/sshd_config, uncomment: PubkeyAuthentication yes and AuthorizedKeysFile .ssh/authorized_keys.

  5. Restart the ssh service, either sudo synoservicectl --restart sshd or toggle the SSH service in the DSM UI.

On the Raspberry Pi:

  1. If you have no ssh key pair yet, generate a new one: ssh-keygen -t rsa (leave the password blank)

  2. Copy your key to the NAS: ssh-copy-id [user]@[IP NAS] (or copy it manually)

On the NAS:

  1. With [user] ssh into the NAS: ssh [user]@[IP NAS]

  2. Set the proper file permission (and if the folders do not exist yet create them first):

    • chmod 0711 ~
    • chmod 0711 ~/.ssh
    • chmod 0600 ~/.ssh/authorized_keys
  3. In the DSM UI enable rsync, see the

    official documentation. Do not check “enabling rsync accounts”, it is meant for outside users which do not a have a dedicated user account on your NAS, however we have already created a dedicated user on the NAS for rsync.

  4. (optional) DSM will create a NetBackup folder which you cannot remove but you can hide it when browsing shared folders: in DSM UI, Shared folders > NetBackUP > edit and select “Hide this shared folder in “My Network Places””.

  5. Make sure the [user] got read/write permission to the back destination of your choice (if in the home folder of the user you need to enable home service and make sure the user has read/write permission).

Big thanks to @foaly for his guide which this is a reformulation of.

System hardening

Remember we had to add the user to the administrator group? That is not desired. This is due to a restriction set by Synology whereby only users in the local administrator group are allowed to use ssh. Unfortunately they do not explain why.

If you only want to use rsync with [user] (not SSH) you can, after the steps above, remove the user from the administrators group and be done. If you want to use ssh as well keep reading.

Most likely this is a security measure since all user content is stored with 777 file permissions (everyone can read, write and execute) so once you have a local shell you can access all the data. Therefore limiting ssh to local admin accounts can protect one self against a false feeling of security. So out of the box inside attacks are a risk when enabling SSH. Fortunately you can limit what a user can do even though they are local admin.
It is not confirmed by Synology whether not this is their actual reason for requiring users to be in the administrator group to be able to use SSH.

To reduce the risk one can use the least privilege principle to reduce damage in case of a breach.

  1. Remove all application permissions (even DSM UI) from the account except for rsync (depending on your use case) and read/write access to the desired volumes.

  2. Create a new file (file name is not important) in /etc/sudoers.d, e.g. /etc/sudoers.d/01_[user]] with the following content: [user] ALL=(ALL) !ALL.
    This sudoers rule overrides the general rule for group administrators in /etc/sudoers (%administrators ALL=(ALL) ALL) granting sudo rights to all members of the group administrators. The result: by default, all administrator users have full sudo privileges. However, our special user [user] is not allowed to execute any command with sudo. However it can still login though SSH and has access to the volumes you have granted access to in the DSM UI.

  3. (optionally) configure your firewall to only allow SSH traffic from LAN (e.g. specify the local LAN id and subnet mask in the firewall rules).

Thanks to @sgrubsmyon and @zopper for sharing their findings on the synology community forum. This guide is a reformulation of theirs.

NFS share

To avoid duplicating the backup data on the Raspberry Pi before sending it to the backup destination (through rsync) one can mount the Synology NAS as destination so the data is copied straight to the NAS instead of hitting the Raspberry Pi persistent storage first. This could improve the life span of your SSD.

I will use the

NFS (network file system) protocol to mount the Synology NAS on the Raspberry Pi. To enable it on the NAS follow

Synology’s official guide.

To be able to mount NFS shares on Arch Linux one needs to install: nfs-utils

Pacman assumes it is operated by a human and occasionally requires you to resolve problems, its output may contain important information.
Automatic updates are therefore generally not advised.

AUR packages are user produced content. Any use of the provided files is at your own risk[^aurpackage].

Automatically updating them poses a significant security risk.

The goal is a hands-off system which is not possible without automatic updates (or you miss out on bug fixes and new features). I also accept the risks of automatically updating AUR packages and aim to have as few as possible, preferably none.

The auto update script will update the system and send a report (by email) with general system health information. If the update fails, for any reason, it will be reported and manual intervention will be required (this is unavoidable due to the philosophy of Arch Linux).

Yay will refuse to upgrade AUR packages when running as root. The auto update script will run as root, thus if there are AUR updates autoupdating will fail unfortuantely.

The update script:

# Arch linux auto updated and report script
# v0.1 -- initial version
# v0.2 -- minor improvements

# ------------------------------------
# Take a back-up/snapshot here if you want to


rsyncOpions="-aqrm --delete -e ssh"


sudo mkdir -p ${bootMount}
sudo chmod 777 ${bootMount}
sudo mount ${bootDir} ${bootMount}
sudo -u alarm rsync ${rsyncOpions} ${bootMount}/config.txt ${rsyncUser}@${rsyncServer}:${rsyncDestination}/boot
sudo -u alarm rsync ${rsyncOpions} ${bootMount}/cmdline.txt ${rsyncUser}@${rsyncServer}:${rsyncDestination}/boot
sudo umount ${bootMount}

pacman -Qqet > /tmp/pkglist.txt # List of all installed packages
sudo -u alarm rsync ${rsyncOpions} /tmp/pkglist.txt ${rsyncUser}@${rsyncServer}:${rsyncDestination}/pacman

# -----------------------------------
# Gather the system info & update

# Uptime
uptime=$(uptime -p)

# Firmware update
firmwareupdate=$(rpi-eeprom-update -a)

# System update
systemupdate=$(yay -Syu --noconfirm --noprogressbar --answerclean None --answerdiff None --answeredit None --answerupgrade Repo --removemake --nodevel --color always 2>&1)

if [[ ${systemupdatestatus} -eq 0 ]]; then
 systemupdate+=$(paccache -rk2 2>&1)
 systemupdate+=$(paccache -ruk0 2>&1)

# Grafana update
grafanapluginupdate=$(grafana-cli plugins update-all)

# Package overview
packagereport=$(yay -Ps --color always)

# Failed services
failedservices=$(SYSTEMD_COLORS=1 systemctl --failed)

# Audit
audit=$(arch-audit --color always)

# -----------------------------------
# Compose the email


reportstatus="successful" && [[ ${systemupdatestatus} -ne 0 ]] && reportstatus="failed"
firmwarestatus="successful" && [[ ${firmwareupdatestatus} -ne 0 ]] && firmwarestatus="failed"
grafanapluginstatus="successful" && [[ ${grafanapluginupdatestatus} -ne 0 ]] && grafanapluginstatus="failed"
hostname=$(hostnamectl --static)

DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )" > /dev/null 2>&1 && pwd)"

# Create the email
email="From: ${senderemail}\r\n"
email+="To: ${recipientemail}\r\n"
email+="Date: $(date --rfc-email)\r\n"
email+='Message-ID: <dcd7cb36-11db-487a-9f3a-e652a9458efd@rfcpedant.example.org>\r\n'
email+='Content-Type: text/html; charset="utf8"\r\n'
email+='Content-Transfer-Encoding: quoted-printable\r\n'
email+='Mime-version: 1.0\r\n'
email+="Subject: [${hostname}] Update report - ${reportstatus}\r\n"
email+='\r\n' # Empty line to separate body and header
email+='<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">'
email+='<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>'
email+='<style type="text/css">'
email+="$(sh ${DIR}/../ansi2html.sh  --css-only --quiet --palette=twilightdark --minify-css)"
email+='<body class="f9 b9">' # body class for colored HTML
email+="<h1>Update report for '${hostname}'</h1>"
email+="<p>System ${uptime}<br/>"
email+="Status: ${reportstatus}</p>"
email+="<h3>System update</h3>"
email+='<p>'                # for the colored HTML
email+="$(printf '%s' "${systemupdate}" | sh ${DIR}/../ansi2html.sh --body-only --quiet)"
email+="<h3>Package report</h3>"
email+='<p>'                # for the colored HTML
email+="$(printf '%s' "${packagereport}" | bash ${DIR}/../ansi2html.sh --body-only --quiet)"
email+="<h3>Firmware update (${firmwarestatus})</h3>"
email+='<p>'                # for the colored HTML
email+="$(printf '%s' "${firmwareupdate}" | bash ${DIR}/../ansi2html.sh --body-only --quiet)"
email+="<h3>Grafana plugin update (${grafanapluginstatus})</h3>"
email+='<p>'                # for the colored HTML
email+="$(printf '%s' "${grafanapluginupdate}" | bash ${DIR}/../ansi2html.sh --body-only --quiet)"
email+="<h3>Failed services</h3>"
email+='<p>'                # for the colored HTML
email+="$(printf '%s' "${failedservices}" | bash ${DIR}/../ansi2html.sh --body-only --quiet)"
email+='<p>'                # for the colored HTML
email+="$(printf '%s' "${audit}" | bash ${DIR}/../ansi2html.sh --body-only --quiet)"
if [[ $systemupdatestatus -eq 0 ]]; then
email+="<p>The system will reboot now.</p>"
email+="<p>The system will <bold>not</bold> reboot.</p>"

# Send the report
curl -S -s -o /dev/null --url "SMTPS://${smtp}:${port}" --ssl-reqd --mail-from "${senderemail}" --mail-rcpt "${recipientemail}" --user "${senderemail}:${senderemailpw}" -T <(echo -e "${email}")

if [[ ${sendmailstatus} -ne 0 ]]; then
    echo 'Sending email failed.' | systemd-cat -t 'AutoUpdate' -p emerg
    exit ${sendmailstatus}

# -----------------------------------
# Handle the status

if [[ $systemupdatestatus -eq 0 ]]; then

exit "${systemupdatestatus}"

Make sure it has the proper rights: chmod +x /path/to/script/autoUpdate.sh

Because some of the used commands require admin privileges and we do not want to hard-code the password in the script add it to sudoers instead: nano /etc/sudoers, add alarm ALL=(ALL) NOPASSWD : /path/to/script.sh (it must go behind all other rules, last one takes precedence).

This is a significant security risk, everything in the script will be executed as root. To harden your system you could make the root owner so other users cannot use the script for privilege escalation. e.g

chown root autoUpdate.sh
chmod 0755 autoUpdate.sh
chgrp sudo autoUpdate.sh

Run the script manually for the first time to add the SSH key of the destination, alternatively you can add it manually to your known hosts.

In the report the output of the commands is shown, so you know what is updated, to have a pretty report and properly convert the console output to HTML the following script is used (ansi2html.sh):


# Convert ANSI (terminal) colours and attributes to HTML

# Licence: LGPLv2
# Author:
#    http://www.pixelbeat.org/docs/terminal_colours/
# Examples:
#    ls -l --color=always | ansi2html.sh > ls.html
#    git show --color | ansi2html.sh > last_change.html
#    Generally one can use the `script` util to capture full terminal output.
# Changes:
#    V0.1, 24 Apr 2008, Initial release
#    V0.2, 01 Jan 2009, Phil Harnish <philharnish@gmail.com>
#                         Support `git diff --color` output by
#                         matching ANSI codes that specify only
#                         bold or background colour.
#                       P@draigBrady.com
#                         Support `ls --color` output by stripping
#                         redundant leading 0s from ANSI codes.
#                         Support `grep --color=always` by stripping
#                         unhandled ANSI codes (specifically ^[[K).
#    V0.3, 20 Mar 2009, http://eexpress.blog.ubuntu.org.cn/
#                         Remove cat -v usage which mangled non ascii input.
#                         Cleanup regular expressions used.
#                         Support other attributes like reverse, ...
#                       P@draigBrady.com
#                         Correctly nest <span> tags (even across lines).
#                         Add a command line option to use a dark background.
#                         Strip more terminal control codes.
#    V0.4, 17 Sep 2009, P@draigBrady.com
#                         Handle codes with combined attributes and color.
#                         Handle isolated <bold> attributes with css.
#                         Strip more terminal control codes.
#    V0.26, 16 Nov 2019
#      http://github.com/pixelb/scripts/commits/master/scripts/ansi2html.sh
#    V0.27, July 2020,	Nelis Oostens
#			                   Add quiet mode, support minify CSS, 
#                        add twilightdark color palette.

gawk --version >/dev/null || exit 1

if [ "$1" = "--version" ]; then
    printf '0.27\n' && exit

printf '%s\n' \
'This utility converts ANSI codes in data passed to stdin
It has 4 optional parameters:
--bg=dark --palette=linux|solarized|tango|xterm --css-only|--body-only
E.g.: ls -l --color=always | ansi2html.sh --bg=dark > ls.html' >&2

if [ "$1" = "--help" ]; then

    [ "$1" = "--bg=dark" ] && { dark_bg=yes; return; }
    [ "$1" = "--css-only" ] && { css_only=yes; return; }
    [ "$1" = "--body-only" ] && { body_only=yes; return; }
    [ "$1" = "--quiet" ] && { quiet=yes; return; }
    [ "$1" = "--minify-css" ] && { minify_css=yes; return; }
    if [ "$1" = "--palette=solarized" ]; then
       # See http://ethanschoonover.com/solarized
       P0=073642;  P1=D30102;  P2=859900;  P3=B58900;
       P4=268BD2;  P5=D33682;  P6=2AA198;  P7=EEE8D5;
       P8=002B36;  P9=CB4B16; P10=586E75; P11=657B83;
      P12=839496; P13=6C71C4; P14=93A1A1; P15=FDF6E3;
    elif [ "$1" = "--palette=solarized-xterm" ]; then
       # Above mapped onto the xterm 256 color palette
       P0=262626;  P1=AF0000;  P2=5F8700;  P3=AF8700;
       P4=0087FF;  P5=AF005F;  P6=00AFAF;  P7=E4E4E4;
       P8=1C1C1C;  P9=D75F00; P10=585858; P11=626262;
      P12=808080; P13=5F5FAF; P14=8A8A8A; P15=FFFFD7;
    elif [ "$1" = "--palette=tango" ]; then
       # Gnome default
       P0=000000;  P1=CC0000;  P2=4E9A06;  P3=C4A000;
       P4=3465A4;  P5=75507B;  P6=06989A;  P7=D3D7CF;
       P8=555753;  P9=EF2929; P10=8AE234; P11=FCE94F;
      P12=729FCF; P13=AD7FA8; P14=34E2E2; P15=EEEEEC;
    elif [ "$1" = "--palette=xterm" ]; then
       P0=000000;  P1=CD0000;  P2=00CD00;  P3=CDCD00;
       P4=0000EE;  P5=CD00CD;  P6=00CDCD;  P7=E5E5E5;
       P8=7F7F7F;  P9=FF0000; P10=00FF00; P11=FFFF00;
      P12=5C5CFF; P13=FF00FF; P14=00FFFF; P15=FFFFFF;
    elif [ "$1" = "--palette=twilightdark" ]; then
       P0=1e1e1e;  P1=cf6a4c;  P2=8f9d6a;  P3=f9ee98;
       P4=7587a6;  P5=9b859d;  P6=afc4db;  P7=a7a7a7;
       P8=5f5a60;  P9=cf6a4c; P10=8f9d6a; P11=f9ee98;
      P12=7587a6; P13=9b859d; P14=afc4db; P15=FFFFFF;
     else # bash colors
       P0=000000;  P1=AA0000;  P2=00AA00;  P3=AA5500;
       P4=0000AA;  P5=AA00AA;  P6=00AAAA;  P7=AAAAAA;
       P8=555555;  P9=FF5555; P10=55FF55; P11=FFFF55;
      P12=5555FF; P13=FF55FF; P14=55FFFF; P15=FFFFFF;
      [ "$1" = "--palette=linux" ] && return;

processArg #defaults
for var in "$@"; do processArg $var; done
[ "$css_only" ] && [ "$body_only" ] && usage

# Mac OSX's GNU sed is installed as gsed
# use e.g. homebrew 'gnu-sed' to get it
if ! sed --version >/dev/null 2>&1; then
  if gsed --version >/dev/null 2>&1; then
    alias sed=gsed
    echo "Error, can't find an acceptable GNU sed." >&2
    exit 1

css_sed='' && [ "$minify_css" ] && css_sed=':a;N;$!ba;s/[\n \t]//g'

[ "$css_only" ] || [ "$body_only" ] || printf '%s' "<html>
<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>
<style type=\"text/css\">
pre { white-space: pre-wrap; }
[ "$body_only" ] || printf ".ef0, .f0 { color: #$P0; } .eb0, .b0 { background-color: #$P0; }
.ef1,.f1 { color: #$P1; } .eb1,.b1 { background-color: #$P1; }
.ef2,.f2 { color: #$P2; } .eb2,.b2 { background-color: #$P2; }
.ef3,.f3 { color: #$P3; } .eb3,.b3 { background-color: #$P3; }
.ef4,.f4 { color: #$P4; } .eb4,.b4 { background-color: #$P4; }
.ef5,.f5 { color: #$P5; } .eb5,.b5 { background-color: #$P5; }
.ef6,.f6 { color: #$P6; } .eb6,.b6 { background-color: #$P6; }
.ef7,.f7 { color: #$P7; } .eb7,.b7 { background-color: #$P7; }
.ef8, .f0 > .bold,.bold > .f0 { color: #$P8; font-weight: normal; }
.ef9, .f1 > .bold,.bold > .f1 { color: #$P9; font-weight: normal; }
.ef10,.f2 > .bold,.bold > .f2 { color: #$P10; font-weight: normal; }
.ef11,.f3 > .bold,.bold > .f3 { color: #$P11; font-weight: normal; }
.ef12,.f4 > .bold,.bold > .f4 { color: #$P12; font-weight: normal; }
.ef13,.f5 > .bold,.bold > .f5 { color: #$P13; font-weight: normal; }
.ef14,.f6 > .bold,.bold > .f6 { color: #$P14; font-weight: normal; }
.ef15,.f7 > .bold,.bold > .f7 { color: #$P15; font-weight: normal; }
.eb8  { background-color: #$P8; }
.eb9  { background-color: #$P9; }
.eb10 { background-color: #$P10; }
.eb11 { background-color: #$P11; }
.eb12 { background-color: #$P12; }
.eb13 { background-color: #$P13; }
.eb14 { background-color: #$P14; }
.eb15 { background-color: #$P15; }
" | sed "${css_sed}"
# The default xterm 256 colour palette
for red in 0 1 2 3 4 5 ; do
  for green in 0 1 2 3 4 5 ; do
    for blue in 0 1 2 3 4 5 ; do
      c=$((16 + ($red * 36) + ($green * 6) + $blue))
      r=$((($red * 40 + 55) * ($red > 0)))
      g=$((($green * 40 + 55) * ($green > 0)))
      b=$((($blue * 40 + 55) * ($blue > 0)))
      [ "$body_only" ] || printf ".ef%d { color: #%2.2x%2.2x%2.2x; } " $c $r $g $b | sed "${css_sed}"
      [ "$body_only" ] || printf ".eb%d { background-color: #%2.2x%2.2x%2.2x; }\n" $c $r $g $b | sed "${css_sed}"
for gray in $(seq 0 23); do
  l=$(($gray*10 + 8))
  [ "$body_only" ] || printf ".ef%d { color: #%2.2x%2.2x%2.2x; } " $c $l $l $l | sed "${css_sed}"
  [ "$body_only" ] || printf ".eb%d { background-color: #%2.2x%2.2x%2.2x; }\n" $c $l $l $l | sed "${css_sed}"

[ "$body_only" ] || printf '%s' '.f9 { color: '`[ "$dark_bg" ] && printf "#$P7;" || printf "#$P0;"`' }
.b9 { background-color: #'`[ "$dark_bg" ] && printf $P0 || printf $P15`'; }
.f9 > .bold,.bold > .f9, body.f9 > pre > .bold {
  color: '`[ "$dark_bg" ] && printf "#$P15;" || printf "#$P0;"`'
  font-weight: '`[ "$dark_bg" ] && printf 'normal;' || printf 'bold;'`'
.reverse {  '"color: #$P0; background-color: #$P7;"'}
.underline { text-decoration: underline; }
.line-through { text-decoration: line-through; }
.blink { text-decoration: blink; }
' | sed "${css_sed}"
# F9 > .bold: Bold is heavy black on white, or bright white
#             depending on the default background.
#  reverse: CSS does not support swapping fg and bg colours unfortunately,
#           so just hardcode something that will look OK on all backgrounds.
# Avoid pixels between adjacent span elements.
#   Note this only works for lines less than 80 chars
#   where we close span elements on the same line.
# span { display: inline-block; }
([ "$quiet" != yes ] && ([ "$body_only" ] || [ "$css_only" ])) && printf '%s\n' \
'To use the css generated from --css-only, do: '\
'<head><link rel="stylesheet" type="text/css" href="style.css"></head>' >&2
[ "$css_only" ] && exit
[ "$body_only" ] || printf '%s' '</style>
<body class="f9 b9">
([ "$quiet" != yes ] && [ "$body_only" ]) && printf '%s\n' 'Be sure to use <body class="f9 b9"> and <pre>' >&2

p='\x1b\['        #shortcut to match escape codes

# Handle various xterm control sequences.
# See /usr/share/doc/xterm-*/ctlseqs.txt
sed "
# escape ampersand and quote
s#&#\&amp;#g; s#\"#\&quot;#g;
s#\x1b[^\x1b]*\x1b\\\##g  # strip anything between \e and ST
s#\x1b][0-9]*;[^\a]*\a##g # strip any OSC (xterm title etc.)
s#\r\$## # strip trailing \r
# strip other non SGR escape sequences
# Mark cursor positioning codes \"Jr;c;
# Mark clear as \"Cn where n=1 is screen and n=0 is to end-of-line
# Mark Cursor move columns as \"Mn where n is +ve for right, -ve for left
" |

# Normalize the input before transformation
sed "
# escape HTML (ampersand and quote done above)
s#>#\&gt;#g; s#<#\&lt;#g;
# handle truecolor
<span style=\"color:rgb(\1\,\2\,\3\)\">#g
<span style=\"background-color:rgb(\1\,\2\,\3\)\">#g
# normalize SGR codes a little
# split 256 colors out and mark so that they're not
# recognised by the following 'split combined' line
s#${p}\([0-9;]\{1,\}\);\([34]8;5;[0-9]\{1,3\}\)m#${p}\1m${p}¬\2m#g; t e
s#${p}\([0-9]\{1,\}\);\([0-9;]\{1,\}\)m#${p}\1m${p}\2m#g; t c   # split combined
s#${p}0\([0-7]\)#${p}\1#g                                 #strip leading 0
s#${p}1m\(\(${p}[4579]m\)*\)#\1${p}1m#g                   #bold last (with clr)
s#${p}m#${p}0m#g                                          #add leading 0 to norm
# undo any 256 color marking
# map 16 color codes to color + bold
# change 'reset' code to \"R
" |

# Convert SGR sequences to HTML
sed "
# common combinations to minimise html (optional)
s#${p}3[0-7]m${p}3\([0-7]\)m#${p}3\1m#g; t f
s#${p}4[0-7]m${p}4\([0-7]\)m#${p}4\1m#g; t b
s#${p}3\([0-7]\)m${p}4\([0-7]\)m#<span class=\"f\1 b\2\">#g
s#${p}4\([0-7]\)m${p}3\([0-7]\)m#<span class=\"f\2 b\1\">#g
s#${p}1m#<span class=\"bold\">#g
s#${p}4m#<span class=\"underline\">#g
s#${p}5m#<span class=\"blink\">#g
s#${p}7m#<span class=\"reverse\">#g
s#${p}9m#<span class=\"line-through\">#g
s#${p}3\([0-9]\)m#<span class=\"f\1\">#g
s#${p}4\([0-9]\)m#<span class=\"b\1\">#g
s#${p}38;5;\([0-9]\{1,3\}\)m#<span class=\"ef\1\">#g
s#${p}48;5;\([0-9]\{1,3\}\)m#<span class=\"eb\1\">#g
s#${p}[0-9;]*m##g # strip unhandled codes
" |

# Convert alternative character set and handle cursor movement codes
# Note we convert here, as if we do at start we have to worry about avoiding
# conversion of SGR codes etc., whereas doing here we only have to
# avoid conversions of stuff between &...; or <...>
# Note we could use sed to do this based around:
#   sed 'y/abcdefghijklmnopqrstuvwxyz{}`~/▒␉␌␍␊°±␤␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π£◆·/'
# However that would be very awkward as we need to only conv some input.
# The basic scheme that we do in the awk script below is:
#  1. enable transliterate once "T1; is seen
#  2. disable once "T0; is seen (may be on diff line)
#  3. never transliterate between &; or <> chars
#  4. track x,y movements and active display mode at each position
#  5. buffer line/screen and dump when required
sed "
# change 'smacs' and 'rmacs' to \"T1 and \"T0 to simplify matching.
" |
gawk '
function dump_line(l,del,c,blanks,ret) {
  for(c=1;c<maxX;c++) {
    if ((c SUBSEP l) in attr || alength(cur)) {
      ret = ret blanks fixas(cur,attr[c,l])
      if(del) delete attr[c,l]
    if ((c SUBSEP l) in dump) {
      ret=ret blanks dump[c,l]
      if(del) delete dump[c,l]
    } else blanks=blanks " "
  if(alength(cur)) ret=ret blanks
  return ret
function dump_screen(l,ret) {
    ret=ret dump_line(l,0) "\n"
  return ret fixas(cur, "")
function atos(a,i,ret) {
  for(i=1;i<=alength(a);i++) if(i in a) ret=ret a[i]
  return ret
function alength(a, i, k) {
    k = 0
    for(i in a) k++
    return k
function fixas(a,s,spc,i,attr,rm,ret) {
  for(i=1;i<=spc;i++) {
    if(rm) {
      ret=ret "</span>"
      delete a[i];
  for(i=1;i<l;i++) {
    if(a[i]!=attr[i]) {
      ret = ret attr[i]
  return ret
function encode(string,start,end,i,ret,pos,sc,buf) {
   if(!end) end=length(string);
   if(!start) start=1;
   for(i=1;i<=length(string);i++) {
     if(state==2) {
       sc=sc c
       if(c==";") {
       } else continue
     } else {
       if(c=="\r") { x=1; continue }
       if(c=="<") {
         # Change attributes - store current active
         # attributes in span array
         span[++spc]=cord[1] ">"
       else if(c=="&") {
         # All goes to single position till we see a semicolon
       else if(c=="\b") {
          # backspace move insertion point back 1
          if(spc) attr[x,y]=atos(span)
       else if(c=="\"") {
          if(cc=="T") {
              # Transliterate on/off
              if(cord[1]==1&&state==3) last_mode=state=4
              if(cord[1]==0&&state==4) last_mode=state=3
          else if(cc=="C") {
              # Clear
              if(cord[1]+0) {
                # Screen - if Recording dump screen
                if(dumpStatus==dsActive) ret=ret dump_screen()
                delete dump
                delete attr
              } else {
                # To end of line
                for(pos=x;pos<maxX;pos++) {
                  dump[pos,y]=" "
                  if (!spc) delete attr[pos,y]
                  else attr[pos,y]=atos(span)
          else if(cc=="J") {
              # Jump to x,y
              # If line is higher - dump previous screen
              if(dumpStatus==dsActive&&cord[1]<y) {
                ret=ret dump_screen();
              if(length(cord[1]) && y!=cord[1]){
                if(y>maxY) maxY=y
                # Change y - start recording
          else if(cc=="M") {
              # Move left/right on current line
          else if(cc=="X") {
              # delete on right
              for(pos=x;pos<=maxX;pos++) {
                if(nx<maxX) {
                  if((nx SUBSEP y) in attr) attr[pos,y] = attr[nx,y]
                  else delete attr[pos,y]
                  if((nx SUBSEP y) in dump) dump[pos,y] = dump[nx,y]
                  else delete dump[pos,y]
                } else if(spc) {
                  dump[pos,y]=" "
          else if(cc=="R") {
              # Reset attributes
              while(spc) delete span[spc--]
       else if(state==4&&i>=start&&i<=end&&c in Trans) c=Trans[c]
     if(dumpStatus==dsReset) {
       delete dump
       delete attr
     if(dumpStatus==dsNew) {
       # After moving/clearing we are now ready to write
       # somthing to the screen so start recording now
     if(dumpStatus==dsActive||dumpStatus==dsOff) {
       dump[x,y] = c
       if(!spc) delete attr[x,y]
       else attr[x,y] = atos(span)
       if(++x>maxX) maxX=x;
    # End of line if dumping increment y and set x back to first col
    if(!dumpStatus) return ret dump_line(y,1);
    else if(++y>maxY) maxY=y;
    return ret
  # dump screen status
  dsOff=0    # Not dumping screen contents just write output direct
  dsNew=1    # Just after move/clear waiting for activity to start recording
  dsReset=2  # Screen cleared build new empty buffer and record
  dsActive=3 # Currently recording
  delete cur;
{ $0=encode($0) }
  if(dumpStatus) {
    print dump_screen();

[ "$body_only" ] || printf '</pre>

It will be called automatically by the autoUpdate script.

Since Arch Linux ARM uses systemd we will create a service to periodically install new updates. Create a new service (autoUpdate.service):

Description=Automatic update the system

ExecStart=sudo /home/alarm/PiServer1Scripts/AutoUpdate/autoUpdate.sh

And a timer which will trigger the service the first Monday of the month (autoUpdate.timer):

Description=First monday of the month

OnCalendar=Mon *-*-01..07 03:00:00


Make sure the .service and .timer files are stored at /usr/lib/systemd/system/ (user) or /etc/systemd/system/ (has precedence).

Enable the timer systemctl enable --now autoUpdate.timer. To list all the active timers use: systemctl list-timers --all

Usefull commands:

  • If you want to see the status of the service: systemctl status autoUpdate
  • If you updated the service and do not want to reboot, refresh it after an update: systemctl daemon-reload (and restart the service)

Configure Config.txt

Using disable-bt effectively turns a BT Pi into a non-BT Pi, at which point clock-fixing is disabled.


This is the /boot/config.txt file i’m using, changes require a reboot to have effect:

  GNU nano 5.6                                           /boot/config.txt                                                      
# See /boot/overlays/README for all available options

# Uncomment if monitors/rendering is required
initramfs initramfs-linux.img followkernel

# Boot
## disable rainbow splash

# Video
## disable detecting the Raspbery Pi LCD/touchscreen

# Light
## turn off mainboard LEDs

## disable ACT LED

# disable PWR LED

## turn off ethernet port LEDs

# Hardware
## turn off bluetooth

## turn off wifi

## turn off onboad audio

## enable I2C

If you are interested in improving your boot times see this post by furkan tokac.

An interesting Raspberry Pi Forum post with info about power saving options for the Raspberry Pi 4.

Configure Cmdline.txt

The Cortex-A72 is a speculative execution9 CPU and vulnerable to speculative execution attacks (Spectre, there are multiple variants of this attack). There are software mitigation’s in the Linux kernel which unfortunately affect the performance notably. When first made public in 2018 ( Spectre & Meltdown) those type of attacks dropped like a bomb in the industry and have cause many performance degrading software mitigation’s since. Almost all CPU vendors are affected in one way or another but Intel seems to have suffered the most.

You are (probably) an adult. You can and should wisely decide just how much risk you are willing to take. Do or don’t try this at home. You do not want to try this at work.


You can turn the mitigation’s off by specifying mitigations=off (default auto) as boot parameter. Available from 5.2 and was backported to some earlier kernels.

To get the PARTUUID run blkid.

This is the /boot/cmdline.txt file i’m using, changes require a reboot to have effect:

root=PARTUUID=45a68f79-02 rw rootwait quiet console=serial0,115200 console=tty1 rootfstype=ext4 selinux=0 plymouth.enable=0 smsc95xx.turbo_mode=N dwc_otg.lpm_enable=0 kgdboc=ttyAMA0,115200 fsck.repair=yes mitigations=off

Reminder: you might have spend quite a bit of time on getting everything properly configured make sure to back-up all the configuration files!

  1. one either has to write his own bare metal program or use an existing OS which range from very light weight (embedded) to regular (Linux). ↩︎

  2. Arch Linux ARM is a separate project from Arch Linux and has its own support forums and resources (less). Generally speaking it mirrors the Arch Linux packages and recompiles them for ARM. If changes are required they host their own, modified, PKGBUILD. ↩︎

  3. Rolling release, in software development, is the concept of frequently delivering updates to applications. This is in contrast to a standard or point release development model which uses software versions that must be reinstalled over the previous version. Source Wikipedia↩︎

  4. Bleeding edge technology is a category of technologies so new that they could have a high risk of being unreliable and lead adopters to incur greater expense in order to make use of them. Source Wikipedia↩︎

  5. Arch ARM has less support as main x64 Arch Linux project. It is less bleeding edge, e.g. at the time of writing Arch ARM only provides a 32bit version for the Raspberry Pi 4B while the community got 64bit working end 2019 already. Also other distributions provide 64bit support already and Raspberry OS has a beta for 64bit. A bit of a bummer Arch ARM seems behind on the curve. There is no communication about the plans, most likely it will be supported once Raspberry Pi OS finishes their beta. ↩︎

  6. Computer software, and sometimes also other computing-related systems like computer hardware or algorithms, are called vanilla when not customized from their original form. Source Wikipedia↩︎

  7. To support new devices you might have to apply changes on top of the Linux source code to make it work, as long as those changes are not included in the general Linux kernel others cannot benefit from them. The process of offering the changes to the original project is called upstreaming↩︎

  8. Hardware Attached on Top, a hardware board on top of the Raspberry Pi connected through the GPIO pins. ↩︎

  9. Speculative execution is an optimization technique where a computer system performs some task that may not be needed. Work is done before it is known whether it is actually needed, so as to prevent a delay that would have to be incurred by doing the work after it is known that it is needed. Source Wikipedia↩︎ ↩︎

  10. Not all chips perform the same and to reliably offer the same performance (1.5 Ghz for the BCM2711) some chips might be able to perform better while others are close to their limit. So if you are lucky you might be able to overclock yours a lot. Source Wikipedia↩︎

  11. There is also a black version, which is the previous model that does not support TRIM out of the box but requires an unofficial firmware update. So get the blue one. ↩︎

  12. Running software from user repositories always carries a risk, especially for the kernel. The repository may be compromised or the user might be malicious. Needless to say whether or not this risk is acceptable may depend on your use-case. ↩︎

  13. You can also use the pacman-cleanup-hook package which will automatically clean the packages after a pacman command is ran. ↩︎

Noticed an error in this post? Corrections are appreciated.

© Nelis Oostens