|
| 1 | +#!/bin/sh |
| 2 | + |
| 3 | +# SPDX-FileCopyrightText: Copyright 2025-2026, macmpi |
| 4 | +# SPDX-License-Identifier: MIT |
| 5 | + |
| 6 | +## Install minimal sys-based Alpine with Home-assistant docker image (with tailscale and mosquitto) |
| 7 | +## e.g., on 512MB PiZeroW (armhf), uses ~290MB RAM while running; leaves ~170MB RAM available |
| 8 | +## PiZero2W should stick to 32bit (armv7) releases, as 512MB RAM is too tight for 64bit containers |
| 9 | +## Home-Assistant 32bit last release is 2025.11.3 |
| 10 | + |
| 11 | +# HOW TO USE (Customize MY_xxxx values to your needs. Defaults are ok for Pi) |
| 12 | +# - prepare install media (Alpine 3.23 and later) as per Alpine wiki for your target hardware |
| 13 | +# - add headless.apkovl.tar.gz, this file (as unattended.sh) and wpa_supplicant.conf (if wifi) onto media |
| 14 | +# - boot machine, and let unattended install proceed & reboot (may be observed via root ssh login) |
| 15 | +# - after reboot, log-in as admin user via ssh (WARNING change default password in MY_PASS) |
| 16 | +# - execute: doas ./update_container.sh |
| 17 | +# - be patient as initial home-assistant image pull may take (very) long time, ~1h15 on PiZero |
| 18 | +# - then setup home-assistant from remote machine via Web interface (avoid logs to preserve SD) |
| 19 | +# - associate tailscale to your account info if needed for remote access (enabled by default) |
| 20 | +# - finetune mosquitto if needed (enabled by default, WARNING unsecured anonymous allowed on port 1883) |
| 21 | + |
| 22 | +## CUSTOMIZE values below to your needs |
| 23 | +MY_USER="alpine" # admin account user name |
| 24 | +MY_PASS="enipla" # password for that user |
| 25 | +MY_IFACE="wlan0" # network interface to be used; may be eth0, etc...(DHCP by default) |
| 26 | +MY_DISK="mmcblk0" # WARNING: this disk dev will be erased for good -- double-check!! |
| 27 | +MY_BOOT="${MY_DISK}p1" # dev partition for bootfs on related disk, usually 1st partition |
| 28 | +MY_ROOT="${MY_DISK}p2" # dev partition for rootfs related disk, may be 3rd if swap is present |
| 29 | +MY_ROOT_SIZE="$((6*2*1024))" # rootfs partition size in MB (allow twice the minimum size for containers updates) |
| 30 | +# set to false if willing to use none, or sibling containers (check availability for arch) |
| 31 | +NATIVE_TAILSCALE=true |
| 32 | +NATIVE_MQTT=true |
| 33 | + |
| 34 | +# Uncomment to redirect stdout and errors to logfile as service won't show messages |
| 35 | +# exec 1>>/tmp/alhb 2>&1 |
| 36 | + |
| 37 | +# shellcheck disable=SC2142 # known special case |
| 38 | +alias _logger='logger -st "${0##*/}"' |
| 39 | + |
| 40 | +# Last Home Assistant docker image for 32-bit (armhf,armv7,x86) is tagged '2025.11.3' not 'stable' |
| 41 | +# https://www.home-assistant.io/blog/2025/06/11/release-20256/#deprecating-installation-methods-and-32-bit-architectures |
| 42 | +case "$(cat /etc/apk/arch)" in |
| 43 | + armhf|armv7|x86) TAG="2025.11.3";; |
| 44 | + aarch64|x86_64) TAG="stable";; |
| 45 | + *) _logger "Unavailable container image! Exiting..."; exit 1;; |
| 46 | +esac |
| 47 | + |
| 48 | +# grab used ovl filename from dmesg |
| 49 | +ovl="$( dmesg | grep -o 'Loading user settings from .*:' | awk '{print $5}' | sed 's/:.*$//' )" |
| 50 | +if [ -f "${ovl}" ]; then |
| 51 | + ovlpath="$( dirname "$ovl" )" |
| 52 | +else |
| 53 | + # search path again as mountpoint have been changed later in the boot process... |
| 54 | + ovl="$( basename "${ovl}" )" |
| 55 | + ovlpath=$( find /media -maxdepth 2 -type d -path '*/.*' -prune -o -type f -name "${ovl}" -exec dirname {} \; | head -1 ) |
| 56 | + ovl="${ovlpath}/${ovl}" |
| 57 | +fi |
| 58 | + |
| 59 | +# Setup wifi if available |
| 60 | +if [ -e "$ovlpath/wpa_supplicant.conf" ]; then |
| 61 | + apk add wpa_supplicant |
| 62 | + cp "$ovlpath/wpa_supplicant.conf" /etc/wpa_supplicant/wpa_supplicant.conf |
| 63 | + rc-update add wpa_supplicant boot |
| 64 | + _logger "Wifi configured" |
| 65 | +fi |
| 66 | + |
| 67 | +_logger "Starting base sys disk installation" |
| 68 | +cat <<-EOF > /tmp/ANSWERFILE |
| 69 | + KEYMAPOPTS=none |
| 70 | + HOSTNAMEOPTS=home-assistant |
| 71 | + DEVDOPTS=mdev |
| 72 | + INTERFACESOPTS="auto lo |
| 73 | + iface lo inet loopback |
| 74 | +
|
| 75 | + auto $MY_IFACE |
| 76 | + iface $MY_IFACE inet dhcp |
| 77 | + " |
| 78 | + DNSOPTS="" |
| 79 | + TIMEZONEOPTS=UTC |
| 80 | + PROXYOPTS=none |
| 81 | + APKREPOSOPTS="-1 -c" |
| 82 | + USEROPTS="-a -u $MY_USER" |
| 83 | + SSHDOPTS=openssh |
| 84 | + NTPOPTS=chrony |
| 85 | +
|
| 86 | + export ERASE_DISKS=/dev/$MY_DISK |
| 87 | + export ROOT_SIZE=$MY_ROOT_SIZE |
| 88 | + DISKOPTS="-m sys /dev/$MY_DISK" |
| 89 | + EOF |
| 90 | + |
| 91 | +SSH_CONNECTION="FAKE" setup-alpine -ef /tmp/ANSWERFILE |
| 92 | + |
| 93 | +# Prep install script for destination sys-based system |
| 94 | +_logger "Prepare sys-setup script" |
| 95 | +cat <<-EOF1 >/tmp/setup_homeassistant.sh |
| 96 | + #!/bin/sh |
| 97 | +
|
| 98 | + echo "$MY_USER:$MY_PASS" | chpasswd |
| 99 | + passwd -l root |
| 100 | +
|
| 101 | + apk update |
| 102 | + apk upgrade --available |
| 103 | +
|
| 104 | + # On Pi, reclaim ~48MB more RAM for CPU (GPU minimal) |
| 105 | + if grep -q "Raspberry Pi" /proc/device-tree/model 2>/dev/null; then |
| 106 | + apk add raspberrypi-bootloader-cutdown |
| 107 | + echo "gpu_mem=16" >> /boot/config.txt |
| 108 | + fi |
| 109 | +
|
| 110 | + apk add bluez |
| 111 | + rc-update add bluetooth default |
| 112 | +
|
| 113 | + apk add docker docker-compose |
| 114 | + # TBD rootless config: https://wiki.alpinelinux.org/wiki/Docker#Docker_rootless |
| 115 | +# adduser -G docker $MY_USER |
| 116 | +# apk add docker-rootless-extras shadow-subids |
| 117 | +# echo "# Sets config for Docker rootless >> /etc/rc.conf |
| 118 | +# echo "rc_cgroup_mode=\"unified\"" >> /etc/rc.conf |
| 119 | +# rc-update add cgroups default |
| 120 | +# echo "$MY_USER:231072:65536" >> /etc/subuid |
| 121 | +# echo "$MY_USER:231072:65536" >> /etc/subgid |
| 122 | +
|
| 123 | + rc-update add docker default |
| 124 | +
|
| 125 | + # admin account has home-assistant docker-related setup and config files |
| 126 | + mkdir -p /home/$MY_USER/homeassistant # home-assistant config directory |
| 127 | + chown $MY_USER /home/$MY_USER/homeassistant |
| 128 | + cat <<-EOF2 >/home/$MY_USER/compose.yaml |
| 129 | + services: |
| 130 | + homeassistant: |
| 131 | + image: ghcr.io/home-assistant/home-assistant:$TAG |
| 132 | + container_name: homeassistant |
| 133 | + privileged: true |
| 134 | + restart: unless-stopped |
| 135 | + network_mode: host |
| 136 | + environment: |
| 137 | + - TZ=Europe/Paris |
| 138 | + - PUID=$(id -u $MY_USER) |
| 139 | + - PGID=$(id -u $MY_USER) |
| 140 | + - UMASK=007 |
| 141 | + volumes: |
| 142 | + - /run/dbus:/run/dbus:ro |
| 143 | + - /home/$MY_USER/homeassistant:/config |
| 144 | + EOF2 |
| 145 | + chown $MY_USER /home/$MY_USER/compose.yaml |
| 146 | +
|
| 147 | + cat <<-EOF2 >/home/$MY_USER/update_container.sh |
| 148 | + #!/bin/sh |
| 149 | +
|
| 150 | + ! [ "$(id -u)" -eq 0 ] && { echo "Please run with administrator privileges." >&2; exit 1; } |
| 151 | +
|
| 152 | + echo "This may be (very) long...grab a coffee (or more)!" |
| 153 | + docker-compose pull && \ |
| 154 | + docker-compose up -d && \ |
| 155 | + docker image prune -af |
| 156 | + EOF2 |
| 157 | + chmod +x /home/$MY_USER/update_container.sh |
| 158 | + chown $MY_USER /home/$MY_USER/update_container.sh |
| 159 | +
|
| 160 | + # Optional native add-on components (set install option accordingly) |
| 161 | + if [ "$NATIVE_TAILSCALE" = "true" ]; then |
| 162 | + apk add tailscale |
| 163 | + # do not run as root |
| 164 | + sed -i 's/^#command_user=.*/command_user=\"tailscale:tailscale\"/' /etc/conf.d/tailscale |
| 165 | + rc-update add tailscale default |
| 166 | + fi |
| 167 | + if [ "$NATIVE_MQTT" = "true" ]; then |
| 168 | + apk add mosquitto |
| 169 | + # WARNING unsecured anonymous: add password file & disable anonymous |
| 170 | + cat <<-EOF2 >>/etc/mosquitto/mosquitto.conf |
| 171 | + allow_anonymous true |
| 172 | + listener 1883 |
| 173 | + EOF2 |
| 174 | + rc-update add mosquitto default |
| 175 | + fi |
| 176 | +
|
| 177 | + EOF1 |
| 178 | +chmod +x /tmp/setup_homeassistant.sh |
| 179 | + |
| 180 | +_logger "Mounting new system for post-installation" |
| 181 | +mkdir -p /mnt/boot /mnt/tmp /mnt/dev /mnt/proc /mnt/sys |
| 182 | +mount /dev/$MY_ROOT /mnt |
| 183 | +mount /dev/$MY_BOOT /mnt/boot |
| 184 | +mount --bind /tmp /mnt/tmp |
| 185 | +mount --bind /dev /mnt/dev |
| 186 | +mount --bind /proc /mnt/proc |
| 187 | +mount --bind /sys /mnt/sys |
| 188 | + |
| 189 | +_logger "Running sys-setup script on disk-based system" |
| 190 | +chroot /mnt /tmp/setup_homeassistant.sh |
| 191 | +sync |
| 192 | + |
| 193 | +_logger "Cleaning up mounts" |
| 194 | +umount /mnt/sys |
| 195 | +umount /mnt/proc |
| 196 | +umount /mnt/dev |
| 197 | +umount /mnt/tmp |
| 198 | +umount /mnt/boot |
| 199 | +umount /mnt |
| 200 | + |
| 201 | +_logger "Finished unattended script - rebooting system" |
| 202 | +reboot |
| 203 | + |
0 commit comments