How-To: Raspberry Pi with Docker live Backup to NAS

Attached is a Bash script to live back up a Raspberry Pi running Docker to a QNAP NAS.

NAS configuration:

First, create a shared folder on your NAS and a user account with appropriate permissions.

Share name: NetBackup

NAS IP used in the example: 10.1.1.10.

Username in example: YOURNASSHAREUSER

Password in example: YOURSHAREUSERPASSWORD

So your NAS configuration is done.

Raspberry Pi Configuration:

# packages

sudo apt update
sudo apt install -y cifs-utils pigz

# mountpoint and access (edit your credentials)

sudo mkdir -p /mnt/qnap
echo -e "username=YOURNASSHAREUSER\npassword=YOURSHAREUSERPASSWORD\ndomain=WORKGROUP" | sudo tee /etc/cifs-creds-qnap >/dev/null
sudo chmod 600 /etc/cifs-creds-qnapCode-Sprache: JavaScript (javascript)

# persistent mount (edit your NAS IP)

echo "//10.1.1.10/NetBackup /mnt/qnap cifs _netdev,credentials=/etc/cifs-creds-qnap,iocharset=utf8,vers=3.0,serverino,nofail 0 0" | sudo tee -a /etc/fstabCode-Sprache: PHP (php)

# mount & check

sudo mount -a
ls -la /mnt/qnap

# folder create

sudo mkdir -p /mnt/qnap/rpi4/fullimages

# scriptfile create

sudo vi /usr/local/sbin/rpi_fullimage.sh

# copy script in scriptfile (edit Retention and device defaults)

sudo tee /usr/local/sbin/rpi_fullimage.sh >/dev/null <<'SH'
#!/usr/bin/env bash
set -euo pipefail

# ========= Einstellungen =========
TARGET_DIR="/mnt/qnap/rpi4/fullimages"   # Zielordner auf QNAP (NetBackup -> /mnt/qnap)
RETENTION_DAYS=180                       # Aufbewahrungsdauer in Tagen
BLOCK_SIZE="16M"                         # Lese-Blockgröße für dd
DEVICE_DEFAULT="/dev/mmcblk0"            # or /dev/sda if SDD over USB: df -h 

# ========= Hilfsfunktionen =========
abort() { echo "[FEHLER] $*" >&2; exit 1; }
info()  { echo "[*] $*"; }
ok()    { echo "[OK] $*"; }

# ========= Vorbedingungen =========
command -v pigz >/dev/null || abort "pigz fehlt. Installiere: sudo apt install -y pigz"
mountpoint -q /mnt/qnap || abort "QNAP nicht gemountet (/mnt/qnap). Prüfe /etc/fstab und 'sudo mount -a'."
mkdir -p "$TARGET_DIR"

DEV="$DEVICE_DEFAULT"
[ -b "$DEV" ] || abort "Blockgerät $DEV nicht gefunden. Bootest du evtl. von USB/NVMe? DEVICE_DEFAULT anpassen."

# Grobe Platzprüfung (mind. ~50% der Gerätegröße frei; gzip komprimiert stark)
FREE_KB=$(df -Pk "$TARGET_DIR" | awk 'NR==2{print $4}')
SECTORS=$(cat /sys/block/$(basename "$DEV")/size)
TOTAL_BYTES=$(( SECTORS * 512 ))
NEEDED_KB=$(( (TOTAL_BYTES / 2) / 1024 ))
[ "$FREE_KB" -ge "$NEEDED_KB" ] || abort "Zu wenig Platz auf QNAP. Frei: ${FREE_KB}KB, benötigt grob >= ${NEEDED_KB}KB."

DATE="$(date +%F_%H-%M-%S)"
IMG="${TARGET_DIR}/rpi4-${DATE}.img.gz"

# ========= Docker: laufende Container pausieren =========
DOCKER_AVAILABLE=true
command -v docker >/dev/null || DOCKER_AVAILABLE=false

RUNNING_IDS=""
if $DOCKER_AVAILABLE; then
  RUNNING_IDS="$(docker ps -q || true)"
  docker_restore() {
    if [ -z "$RUNNING_IDS" ]; then return 0; fi
    for id in $RUNNING_IDS; do
      st="$(docker inspect -f '{{.State.Status}}' "$id" 2>/dev/null || echo unknown)"
      [ "$st" = "paused" ] && docker unpause "$id" >/dev/null 2>&1 || true
      st="$(docker inspect -f '{{.State.Status}}' "$id" 2>/dev/null || echo unknown)"
      if [ "$st" = "exited" ] || [ "$st" = "created" ]; then docker start "$id" >/dev/null 2>&1 || true; fi
    done
  }
  trap 'docker_restore' EXIT
  [ -n "$RUNNING_IDS" ] && { info "Pausiere laufende Docker-Container…"; docker pause $RUNNING_IDS || true; }
fi

sync

# ========= Image erstellen (fsync am Zieldatei-dd) =========
info "Erzeuge Image von ${DEV} -> ${IMG}"
dd if="$DEV" bs="$BLOCK_SIZE" status=progress iflag=fullblock \
| pigz -c \
| dd of="$IMG" bs=4M status=progress conv=fsync

# ========= Prüfsumme =========
info "Erzeuge SHA256-Prüfsumme…"
sha256sum "$IMG" > "${IMG}.sha256"

# ========= Docker-Vorzustand wiederherstellen =========
if $DOCKER_AVAILABLE; then
  docker_restore
  trap - EXIT
fi

# ========= Aufräumen =========
info "Entferne Backups älter als ${RETENTION_DAYS} Tage…"
find "$TARGET_DIR" -type f -name "rpi4-*.img.gz" -mtime +$RETENTION_DAYS -delete
find "$TARGET_DIR" -type f -name "rpi4-*.img.gz.sha256" -mtime +$RETENTION_DAYS -delete

ok "Voll-Image abgeschlossen: $IMG"
SHCode-Sprache: PHP (php)

# make script executable

sudo chmod +x /usr/local/sbin/rpi_fullimage.sh

# run script (docker will suspend)

sudo /usr/local/sbin/rpi_fullimage.sh

After reboot if auto mount not work:

# mount & check

sudo mount -a
ls -la /mnt/qnap

# run script (docker will suspend)

sudo /usr/local/sbin/rpi_fullimage.sh

Backup restore (test first on an other drive):

# Linux

sha256sum -c rpi4-*.img.gz.sha256
lsblk
gunzip -c rpi4-*.img.gz | sudo dd of=/dev/sdX bs=16M status=progress conv=fsync
syncCode-Sprache: JavaScript (javascript)

# Mac

diskutil list
diskutil unmountDisk /dev/disk3
gunzip -c rpi4-*.img.gz | sudo dd of=/dev/rdisk3 bs=16m status=progress
diskutil eject /dev/disk3Code-Sprache: JavaScript (javascript)

# Windows / Mac / Linux

Raspberry Pi Imager oder balenaEtcher

How-To: Update Portainer on a Raspberry Pi (Docker)

This procedure updates Portainer to the latest version. It pulls portainer/portainer-ce:latest, stops/removes the old container, and recreates it with the same /data volume—your settings are preserved.

First things first: make a backup of your Portainer data.

Back up Docker volume (“portainer_data”)

mkdir -p backups && docker run --rm -v portainer_data:/data:ro -v "$PWD/backups:/backup" alpine sh -c "tar -czf /backup/portainer-data-$(date +%F_%H-%M-%S).tgz -C / data"
Code-Sprache: PHP (php)

Back up a bind mount

mkdir -p backups && tar -czf "backups/portainer-data-$(date +%F_%H-%M-%S).tgz" -C /srv/portainer data
Code-Sprache: JavaScript (javascript)

Quick integrity check

tar -tzf backups/portainer-data-*.tgz | head

Now let’s start…

Check your Portainer container name.
By default it’s portainer; if yours is different, adjust the commands accordingly.

docker volume ls

Stop and remove the container:

docker stop portainer
docker rm portainer

Pull latest container:

docker pull portainer/portainer-ce

Run the latest container:

docker run -d -p 8000:8000 -p 9000:9000 -p 9443:9443 \
   --name=portainer \
   -v /var/run/docker.sock:/var/run/docker.sock \
   -v portainer_data:/data \
   --restart=always \
   portainer/portainer-ceCode-Sprache: JavaScript (javascript)

done

Aquarium: PV-Optimized Lighting & Heating (Day) – Load Reduction After Sunset

This automation optimizes your aquarium’s light and heater to make the most of PV surplus during the day and reduce power consumption after sunset. It’s designed for clear, reliable scheduling with sensible fallbacks—so your tank stays stable while your energy bill stays low.

What it does

  • Uses daytime PV to run lighting and maintain temperature efficiently
  • Switches to a night-time eco mode to cut load after sunset
  • Keeps logic simple and transparent (easy to tweak in YAML)

Important: Temperature first

Fish and plants are sensitive to rapid temperature swings. Even small daily spikes or drops can stress the ecosystem. Please:

  • Monitor water temperature with a reliable sensor; set alerts for out-of-range values.
  • Aim for gradual changes (avoid frequent on/off cycles).
  • Improve insulation/cover (tight lid, reduced drafts, stable room temp) to keep heat in.
  • Size the heater appropriately; overspec’d heaters can cause overshoot.

Safety note: Always test changes at a low-risk time and watch the tank closely for a few days. If temperature deviates, prioritize stability over savings by relaxing the eco schedule.

Script:
Go to Settings → Automations & Scenes → Scripts → + Add Script.
ScriptName: script.aquarium_verify_after_30_minutes

Use your own entity IDs. In this example and below:

  • Water temperature: sensor.sonoff_1002215fd9_temperature
  • Aquarium heater (switch): switch.aquarium_heizung_none
  • Aquarium heater power sensor: sensor.aquarium_heizung_leistung
  • Aquarium light 1 (switch): switch.aquarium_licht_none
  • Aquarium light 1 power sensor: sensor.aquarium_licht_leistung
  • Aquarium light 2 (switch): switch.nanoaqua_licht_none
  • Aquarium light 2 power sensor: sensor.nanoaqua_licht_leistung
  • NotifyGroup mobiles: notify.iphones
alias: "Aquarium: Verify after 30 minutes"
description: "Prüft 30 Min. nach dem Schalten den gewünschten Zustand und korrigiert ggf. + Push"
mode: parallel
sequence:
  # 1) 30 Minuten warten
  - delay: "00:30:00"

  # 2) Eingaben absichern + Ist-Zustand lesen
  - variables:
      ent: "{{ entity | default('') }}"
      desired: "{{ desired_state | default('') }}"
      reas: "{{ reason | default('verification') }}"
      current: "{{ states(ent) if ent else 'unknown' }}"

  # 3) Nur agieren, wenn ent & desired sinnvoll sind
  - choose:
      - conditions:
          - condition: template
            value_template: >
              {{ ent != '' and desired in ['on','off'] and current != desired }}
        sequence:
          # 3a) Korrektur durchführen
          - choose:
              - conditions: "{{ desired == 'on' }}"
                sequence:
                  - service: switch.turn_on
                    target:
                      entity_id: "{{ ent }}"
              - conditions: "{{ desired == 'off' }}"
                sequence:
                  - service: switch.turn_off
                    target:
                      entity_id: "{{ ent }}"
          # 3b) Push mit Status + Leistungen
          - service: notify.iphones
            data:
              title: "Aquarium: Korrektur ({{ reas }})"
              message: >-
                {{ ent }} war '{{ current }}', sollte '{{ desired }}' sein – korrigiert.
                Temp: {{ states('sensor.sonoff_1002215fd9_temperature') }}°C |
                Heizung: {{ states('switch.aquarium_heizung_none') }} ({{ states('sensor.aquarium_heizung_leistung') }} W) |
                Licht1: {{ states('switch.aquarium_licht_none') }} ({{ states('sensor.aquarium_licht_leistung') }} W) |
                Licht2: {{ states('switch.nanoaqua_licht_none') }} ({{ states('sensor.nanoaqua_licht_leistung') }} W)
Code-Sprache: PHP (php)

Automatiion – light off

alias: "[AQUARIUM] Licht AUS (-60m vor Sonnenuntergang)"
description: ""
triggers:
  - event: sunset
    offset: "-01:00:00"
    trigger: sun
conditions: []
actions:
  - target:
      entity_id:
        - switch.aquarium_licht_none
        - switch.nanoaqua_licht_none
    action: switch.turn_off
  - data:
      entity: switch.aquarium_licht_none
      desired_state: "off"
      reason: lights_off
    action: script.aquarium_verify_after_30_minutes
  - data:
      entity: switch.nanoaqua_licht_none
      desired_state: "off"
      reason: lights_off
    action: script.aquarium_verify_after_30_minutes
mode: singleCode-Sprache: CSS (css)

Heating on

alias: "[AQUARIUM] Heizung EIN (+60m nach Sonnenaufgang)"
description: ""
triggers:
  - event: sunrise
    offset: "01:00:00"
    trigger: sun
conditions: []
actions:
  - target:
      entity_id: switch.aquarium_heizung_none
    action: switch.turn_on
  - data:
      entity: switch.aquarium_heizung_none
      desired_state: "on"
      reason: heater_on
    action: script.aquarium_verify_after_30_minutes
mode: singleCode-Sprache: CSS (css)

Heating off

alias: "[AQUARIUM] Heizung AUS (-30m vor Sonnenuntergang)"
description: ""
triggers:
  - event: sunset
    offset: "-00:30:00"
    trigger: sun
conditions: []
actions:
  - target:
      entity_id: switch.aquarium_heizung_none
    action: switch.turn_off
  - data:
      entity: switch.aquarium_heizung_none
      desired_state: "off"
      reason: heater_off
    action: script.aquarium_verify_after_30_minutes
mode: singleCode-Sprache: CSS (css)

Reconcile

alias: "[AQUA] Reconcile: Zustände an Zeitplan anpassen"
description: "Stellt nach Neustart/Offline-Phase die Sollzustände wieder her (robust)."
mode: restart
trigger:
  - platform: homeassistant
    event: start
  - platform: time_pattern
    minutes: "/15"
condition: []
variables:
  now_local: "{{ now() }}"
  # Sun-Times sicher in lokale Datetimes umwandeln
  next_rising: "{{ as_local(as_datetime(state_attr('sun.sun','next_rising'))) }}"
  next_setting: "{{ as_local(as_datetime(state_attr('sun.sun','next_setting'))) }}"
  # 'heutiges' Sunrise/Sunset ableiten:
  sunrise_today: >-
    {% set nr = next_rising %}
    {% if nr.date() == now().date() %}
      {{ nr }}
    {% else %}
      {{ nr - timedelta(days=1) }}
    {% endif %}
  sunset_today: >-
    {% set ns = next_setting %}
    {% if ns.date() == now().date() %}
      {{ ns }}
    {% else %}
      {{ ns - timedelta(days=1) }}
    {% endif %}
  # Zeitfenster laut Anforderung:
  heater_on_from: "{{ sunrise_today + timedelta(hours=1) }}"
  heater_off_at:  "{{ sunset_today - timedelta(minutes=30) }}"
  lights_on_from: "{{ today_at('09:30:00') }}"
  lights_off_at:  "{{ sunset_today - timedelta(hours=1) }}"
  # Sollzustände jetzt:
  want_heater_on: "{{ now_local >= heater_on_from and now_local < heater_off_at }}"
  want_lights_on: "{{ now_local >= lights_on_from and now_local < lights_off_at }}"
action:
  - choose:
      - conditions: "{{ want_heater_on and is_state('switch.aquarium_heizung_none','off') }}"
        sequence:
          - service: switch.turn_on
            target:
              entity_id: switch.aquarium_heizung_none
      - conditions: "{{ not want_heater_on and is_state('switch.aquarium_heizung_none','on') }}"
        sequence:
          - service: switch.turn_off
            target:
              entity_id: switch.aquarium_heizung_none
  - choose:
      - conditions: "{{ want_lights_on and (is_state('switch.aquarium_licht_none','off') or is_state('switch.nanoaqua_licht_none','off')) }}"
        sequence:
          - service: switch.turn_on
            target:
              entity_id:
                - switch.aquarium_licht_none
                - switch.nanoaqua_licht_none
      - conditions: "{{ not want_lights_on and (is_state('switch.aquarium_licht_none','on') or is_state('switch.nanoaqua_licht_none','on')) }}"
        sequence:
          - service: switch.turn_off
            target:
              entity_id:
                - switch.aquarium_licht_none
                - switch.nanoaqua_licht_none
Code-Sprache: PHP (php)

Lights on

alias: "[AQUARIUM]  Licht EIN (09:30)"
description: ""
triggers:
  - at: "09:30:00"
    trigger: time
conditions: []
actions:
  - target:
      entity_id:
        - switch.aquarium_licht_none
        - switch.nanoaqua_licht_none
    action: switch.turn_on
  - data:
      entity: switch.aquarium_licht_none
      desired_state: "on"
      reason: lights_on
    action: script.aquarium_verify_after_30_minutes
  - data:
      entity: switch.nanoaqua_licht_none
      desired_state: "on"
      reason: lights_on
    action: script.aquarium_verify_after_30_minutes
mode: single
Code-Sprache: CSS (css)

7. Alarm if water is too cold (this example 22 degrees)

alias: "[AQUARIUM]  Alarm: Temperatur unter 22°C"
description: ""
triggers:
  - entity_id: sensor.sonoff_1002215fd9_temperature
    below: 22
    trigger: numeric_state
conditions: []
actions:
  - data:
      title: "Aquarium: Temperatur-Alarm (<22°C)"
      message: >-
        Temp: {{ states('sensor.sonoff_1002215fd9_temperature') }}°C. Heizung:
        {{ states('switch.aquarium_heizung_none') }} ({{
        states('sensor.aquarium_heizung_leistung') }} W). Licht1: {{
        states('switch.aquarium_licht_none') }} ({{
        states('sensor.aquarium_licht_leistung') }} W). Licht2: {{
        states('switch.nanoaqua_licht_none') }} ({{
        states('sensor.nanoaqua_licht_leistung') }} W).
    action: notify.iphones
mode: single
Code-Sprache: JavaScript (javascript)

Alert when leaving home and light is on or door/window open

This automation sends a push alert to our iPhones when all household members have left home but something was left on or open—for example, any light is still on or a door/window/garage is open.

How it works (overview):

  1. Tracks presence for all people via the Home Assistant Companion app (installed on each device with Location permission granted).
  2. When everyone is away, it checks a group of entities (lights, doors/windows, garage).
  3. If any of these are on/open, it sends a mobile notification to the iPhones so we can act quickly.

Why this is useful:

  1. It’s an easy safety and energy-saving catch-all—no more second-guessing whether a light is burning or a door is ajar after you’ve already left.

Prerequisites:

  1. Each person uses the Home Assistant Companion app on iOS, logged into the same HA instance.
  2. Location permissions enabled for presence tracking.
  3. Your lights and contact sensors (doors/windows/garage) are available in HA (ideally grouped for simple checks).
  4. Home Assistant must be reachable from the internet so iPhones can receive notifications and open deep-links reliably—e.g., via Home Assistant Cloud (Nabu Casa) or a secure reverse proxy with valid HTTPS. Ensure your HA host has a stable uplink.

The example below uses two people, but you can extend it to any number of household members by adding them to the presence group.

device_tracker.iphone1
device_tracker.iphone2Code-Sprache: CSS (css)

Mobile notification group:

notify.iphonesCode-Sprache: CSS (css)

You can optionally define an exclude_lights list to ignore specific lights in the “left on” check—useful for outdoor fixtures, always-on night lights, holiday decorations, or any lamp you don’t want to trigger alerts.

Configuration note:

Add the entity IDs you want to exclude to exclude_lights (e.g., light.porch, light.garden_spots, light.nightlight_bedroom). The automation will still check all other lights and any open doors/windows/garage.

/dashboard-leavehome/0 is a self build dashboard which shows all open things. You can link your own dashboard there

<code>alias: "[ALERT] Alle weg + etwas offen/AN → Hinweis (mit Ausschlussliste)"
triggers:
  - trigger: template
    value_template: |-
      {% set d1 = is_state('device_tracker.iphone1', 'not_home')
                 and distance('device_tracker.iphone1','zone.home') > 0.1 %}
      {% set d2 = is_state('device_tracker.iphone2', 'not_home')
                 and distance('device_tracker.iphone2','zone.home') > 0.1 %}
      {{ d1 and d2 }}
    for: "00:00:30"
conditions:
  - condition: template
    value_template: |-
      {% set lights_on = states.light
         | rejectattr('entity_id','in',exclude_lights)
         | selectattr('state','eq','on')
         | list | length %}
      {% set contacts_open = states.binary_sensor
         | selectattr('attributes.device_class','in',['door','window','opening'])
         | rejectattr('entity_id','in',exclude_contacts)
         | selectattr('state','eq','on')
         | list | length %}
      {{ lights_on > 0 or contacts_open > 0 }}
actions:
  - variables:
      open_items: >-
        {%- set lights = states.light
             | rejectattr('entity_id','in',exclude_lights)
             | selectattr('state','eq','on')
             | map(attribute='name') | list -%}
        {%- set contacts = states.binary_sensor
             | selectattr('attributes.device_class','in',['door','window','opening'])
             | rejectattr('entity_id','in',exclude_contacts)
             | selectattr('state','eq','on')
             | map(attribute='name') | list -%}
        {%- set items = (lights + contacts) -%} {%- if items | length == 0 -%}
        nichts {%- else -%} {{ items | sort | join(', ') }} {%- endif -%}
  - action: notify.iphones
    data:
      title: "🏠 Achtung: Niemand zu Hause"
      message: "Offen/AN: {{ open_items }}."
      data:
        url: /dashboard-leavehome/0
        push:
          interruption-level: time-sensitive
        apns_headers:
          apns-collapse-id: away-open
        actions:
          - action: OPEN_DASH
            title: 🔎 Jetzt prüfen
            uri: /dashboard-leavehome/0
mode: single
variables:
  exclude_lights:
    - light.smart_garage
    - light.example2
    - light.outdoor
  exclude_contacts: []</code>Code-Sprache: PHP (php)