From d5c4df30fc6960dd3519d8fff6b5424a732b7187 Mon Sep 17 00:00:00 2001 From: Adrien Poupa Date: Mon, 15 Apr 2024 00:00:12 -0400 Subject: [PATCH] feat(homeassistant): add HomeAssistant backup --- .env.example | 2 +- .gitignore | 1 - README.md | 17 +----- docker-compose.yml | 35 ------------ homeassistant/.gitignore | 4 ++ homeassistant/README.md | 92 ++++++++++++++++++++++++++++++++ homeassistant/backup.env.example | 6 +++ homeassistant/docker-compose.yml | 50 +++++++++++++++++ joplin/docker-compose.yml | 16 +++--- tandoor/docker-compose.yml | 20 +++---- update-config.sh | 32 +++++------ 11 files changed, 188 insertions(+), 87 deletions(-) create mode 100644 homeassistant/.gitignore create mode 100644 homeassistant/README.md create mode 100644 homeassistant/backup.env.example create mode 100644 homeassistant/docker-compose.yml diff --git a/.env.example b/.env.example index d3c2c8b..9835303 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ COMPOSE_PROFILES= COMPOSE_PATH_SEPARATOR=: -COMPOSE_FILE=docker-compose.yml:adguardhome/docker-compose.yml:tandoor/docker-compose.yml:joplin/docker-compose.yml +COMPOSE_FILE=docker-compose.yml:adguardhome/docker-compose.yml:tandoor/docker-compose.yml:joplin/docker-compose.yml:homeassistant/docker-compose.yml USER_ID=1000 GROUP_ID=1000 TIMEZONE="America/New_York" diff --git a/.gitignore b/.gitignore index 2b4254d..a596d82 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,3 @@ docker-compose.override.yml /adguardhome/conf /adguardhome/work /sabnzbd -/homeassistant diff --git a/README.md b/README.md index c784915..810d259 100644 --- a/README.md +++ b/README.md @@ -387,22 +387,7 @@ See [here](./joplin/README.md). ### Home Assistant -Enable Home Assistant by setting `COMPOSE_PROFILES=homeassistant`. - -Set the `HOMEASSISTANT_HOSTNAME`, since it does not support -[running in a subfolder](https://github.com/home-assistant/architecture/issues/156). -Add the necessary DNS records in your domain. - -You will need to allow Traefik to access Home Assistant by adding the following in `homeassistant/configuration.yaml`: - -```yaml -http: - use_x_forwarded_for: true - trusted_proxies: - - 172.0.0.0/8 # You can put a more precise range instead -``` - -Set the `HOMEASSISTANT_ACCESS_TOKEN` for homepage support. +See [here](./homeassistant/README.md). ## Customization diff --git a/docker-compose.yml b/docker-compose.yml index 535ca46..dadf543 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -433,41 +433,6 @@ services: - homepage.widget.type=jellyfin - homepage.widget.url=http://jellyfin:8096/jellyfin - homepage.widget.key=${JELLYFIN_API_KEY} - homeassistant: - image: ghcr.io/home-assistant/home-assistant:stable - container_name: homeassistant - network_mode: host - environment: - - PUID=${USER_ID} - - PGID=${GROUP_ID} - - TZ=${TIMEZONE} - volumes: - - ${CONFIG_ROOT:-.}/homeassistant:/config - - /etc/localtime:/etc/localtime:ro - - /run/dbus:/run/dbus:ro - restart: always - healthcheck: - test: [ "CMD", "curl", "--fail", "http://127.0.0.1:8123" ] - interval: 30s - retries: 10 - privileged: true - labels: - - traefik.enable=true - - traefik.http.routers.homeassistant.rule=(Host(`${HOMEASSISTANT_HOSTNAME}`)) - - traefik.http.routers.homeassistant.tls=true - - traefik.http.routers.homeassistant.tls.certresolver=myresolver - - traefik.http.services.homeassistant.loadbalancer.server.port=8123 - - homepage.group=Apps - - homepage.name=Home Assistant - - homepage.icon=home-assistant.png - - homepage.href=https://${HOMEASSISTANT_HOSTNAME} - - homepage.description=Open source home automation that puts local control and privacy first - - homepage.weight=3 - - homepage.widget.type=homeassistant - - homepage.widget.url=https://${HOMEASSISTANT_HOSTNAME} - - homepage.widget.key=${HOMEASSISTANT_ACCESS_TOKEN} - profiles: - - homeassistant homepage: image: ghcr.io/gethomepage/homepage:latest container_name: homepage diff --git a/homeassistant/.gitignore b/homeassistant/.gitignore new file mode 100644 index 0000000..3c2994b --- /dev/null +++ b/homeassistant/.gitignore @@ -0,0 +1,4 @@ +* +!README.md +!docker-compose.yml +!backup.env.example diff --git a/homeassistant/README.md b/homeassistant/README.md new file mode 100644 index 0000000..e5d1aa5 --- /dev/null +++ b/homeassistant/README.md @@ -0,0 +1,92 @@ +# Home Assistant + +Open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts + +## Installation + +Enable Home Assistant by setting `COMPOSE_PROFILES=homeassistant`. + +Set the `HOMEASSISTANT_HOSTNAME`, since it does not support +[running in a subfolder](https://github.com/home-assistant/architecture/issues/156). +Add the necessary DNS records in your domain. + +You will need to allow Traefik to access Home Assistant by adding the following in `homeassistant/configuration.yaml`: + +```yaml +http: + use_x_forwarded_for: true + trusted_proxies: + - 172.0.0.0/8 # You can put a more precise range instead +``` + +Set the `HOMEASSISTANT_ACCESS_TOKEN` for homepage support. + +## Backup + +### Enable Backups in HomeAssistant + +We will create an automation that will create backups nightly and clear old ones. + +Add a `command_line` inclusion in your `configuration.yaml`: `command_line: !include command_lines.yaml` + +The `command_lines.yaml` defines a switch that removes backups older than 7 days: + +```yaml +- switch: + name: Purge old backups + unique_id: switch.purge_backups + icon: mdi:trash-can + command_on: 'cd /config/backups/ && find . -maxdepth 1 -type f -mtime +7 -print | xargs rm -f' +``` + +Then, create an automation that will trigger backups nightly and call the purge old backups switch: + +```yaml +alias: Backup Home Assistant every night at 3 AM +description: Backup Home Assistant every night at 3 AM +trigger: + - platform: time + at: "03:00:00" +action: + - service: backup.create + data: {} + - service: switch.turn_on + data: {} + target: + entity_id: switch.purge_old_backups + - service: switch.turn_off + data: {} + target: + entity_id: switch.purge_old_backups +mode: single +``` + +### Save Backups Remotely + +Home Assistant can be backed up in the cloud storage product of your choice with [Rclone](https://rclone.org/). + +Before a backup can be made, `rclone config` must be run to generate the configuration file: + +```shell +docker compose run --rm -it homeassistant-backup rclone config +``` + +It will generate a `rclone.conf` configuration file in ./homeassistant/rclone/rclone.conf. + +Copy the backup environment file to `backup.env` and fill it as needed: +`cp backup.env.exmple backup.env` + +| Variable | Description | Default | +|----------------------|---------------------------------------------------------------------|---------------------------| +| `RCLONE_REMOTE_NAME` | Name of the remote you chose during rclone config | | +| `RCLONE_REMOTE_DIR` | Name of the rclone remote dir, eg: S3 bucket name, folder name, etc | | +| `CRON` | How often to run the backup | `@daily` backup every day | +| `TIMEZONE` | Timezone, used for cron times | `America/New_York` | +| `ZIP_PASSWORD` | Password to protect the backup archive with | `123456` | +| `BACKUP_KEEP_DAYS` | How long to keep the backup in the destination | `31` days | + +You can test your backup manually with: + +```shell +docker compose run --rm -it homeassistant-backup backup +``` diff --git a/homeassistant/backup.env.example b/homeassistant/backup.env.example new file mode 100644 index 0000000..2013cc3 --- /dev/null +++ b/homeassistant/backup.env.example @@ -0,0 +1,6 @@ +RCLONE_REMOTE_NAME= +RCLONE_REMOTE_DIR= +CRON=@daily +TIMEZONE=America/New_York +ZIP_PASSWORD=123456 +BACKUP_KEEP_DAYS=1 diff --git a/homeassistant/docker-compose.yml b/homeassistant/docker-compose.yml new file mode 100644 index 0000000..ce82a42 --- /dev/null +++ b/homeassistant/docker-compose.yml @@ -0,0 +1,50 @@ +services: + homeassistant: + image: ghcr.io/home-assistant/home-assistant:stable + container_name: homeassistant + network_mode: host + environment: + - PUID=${USER_ID} + - PGID=${GROUP_ID} + - TZ=${TIMEZONE} + volumes: + - ${CONFIG_ROOT:-.}/homeassistant:/config + - /etc/localtime:/etc/localtime:ro + - /run/dbus:/run/dbus:ro + restart: always + healthcheck: + test: [ "CMD", "curl", "--fail", "http://127.0.0.1:8123" ] + interval: 30s + retries: 10 + privileged: true + labels: + - traefik.enable=true + - traefik.http.routers.homeassistant.rule=(Host(`${HOMEASSISTANT_HOSTNAME}`)) + - traefik.http.routers.homeassistant.tls=true + - traefik.http.routers.homeassistant.tls.certresolver=myresolver + - traefik.http.services.homeassistant.loadbalancer.server.port=8123 + - homepage.group=Apps + - homepage.name=Home Assistant + - homepage.icon=home-assistant.png + - homepage.href=https://${HOMEASSISTANT_HOSTNAME} + - homepage.description=Open source home automation that puts local control and privacy first + - homepage.weight=3 + - homepage.widget.type=homeassistant + - homepage.widget.url=https://${HOMEASSISTANT_HOSTNAME} + - homepage.widget.key=${HOMEASSISTANT_ACCESS_TOKEN} + profiles: + - homeassistant + homeassistant-backup: + image: adrienpoupa/rclone-backup:latest + container_name: homeassistant-backup + restart: always + env_file: + - ${CONFIG_ROOT:-.}/homeassistant/backup.env + environment: + - BACKUP_FOLDER_NAME=backups + - BACKUP_FOLDER_PATH=/backups + volumes: + - ${CONFIG_ROOT:-.}/homeassistant/backups:/backups + - ${CONFIG_ROOT:-.}/homeassistant/backup:/config + profiles: + - homeassistant \ No newline at end of file diff --git a/joplin/docker-compose.yml b/joplin/docker-compose.yml index 9d3a1f9..42f3525 100644 --- a/joplin/docker-compose.yml +++ b/joplin/docker-compose.yml @@ -5,7 +5,7 @@ services: container_name: joplin restart: always env_file: - - ./joplin/.env + - ${CONFIG_ROOT:-.}/joplin/.env environment: - APP_PORT=22300 - APP_BASE_URL=https://${HOSTNAME}/joplin @@ -14,9 +14,9 @@ services: - SQLITE_DATABASE=/database/joplin.db - STORAGE_DRIVER=Type=Filesystem; Path=/storage volumes: - - ./joplin/database:/database - - ./joplin/storage:/storage - - ./joplin/healthcheck:/healthcheck + - ${CONFIG_ROOT:-.}/joplin/database:/database + - ${CONFIG_ROOT:-.}/joplin/storage:/storage + - ${CONFIG_ROOT:-.}/joplin/healthcheck:/healthcheck healthcheck: test: ["CMD", "node", "/healthcheck/healthcheck.js"] interval: 30s @@ -43,15 +43,15 @@ services: container_name: joplin-backup restart: always env_file: - - ./joplin/backup.env + - ${CONFIG_ROOT:-.}/joplin/backup.env environment: - BACKUP_FOLDER_NAME=storage - BACKUP_FOLDER_PATH=/storage - DB_TYPE=sqlite - SQLITE_DATABASE=/database/joplin.db volumes: - - ./joplin/database:/database - - ./joplin/storage:/storage - - ./joplin/backup:/config + - ${CONFIG_ROOT:-.}/joplin/database:/database + - ${CONFIG_ROOT:-.}/joplin/storage:/storage + - ${CONFIG_ROOT:-.}/joplin/backup:/config profiles: - joplin \ No newline at end of file diff --git a/tandoor/docker-compose.yml b/tandoor/docker-compose.yml index 76f6e17..d6c3fda 100644 --- a/tandoor/docker-compose.yml +++ b/tandoor/docker-compose.yml @@ -4,10 +4,10 @@ services: container_name: tandoor restart: always env_file: - - ./tandoor/.env + - ${CONFIG_ROOT:-.}/tandoor/.env volumes: - - ./tandoor/database:/opt/recipes/database - - ./tandoor/mediafiles:/opt/recipes/mediafiles + - ${CONFIG_ROOT:-.}/tandoor/database:/opt/recipes/database + - ${CONFIG_ROOT:-.}/tandoor/mediafiles:/opt/recipes/mediafiles - tandoor-staticfiles:/opt/recipes/staticfiles healthcheck: test: ["CMD", "wget", "http://127.0.0.1:8080/recipes", "-qO", "/dev/null"] @@ -21,10 +21,10 @@ services: container_name: tandoor-nginx restart: always env_file: - - ./tandoor/.env + - ${CONFIG_ROOT:-.}/tandoor/.env volumes: - - ./tandoor/nginx:/etc/nginx/conf.d:ro - - ./tandoor/mediafiles:/media:ro + - ${CONFIG_ROOT:-.}/tandoor/nginx:/etc/nginx/conf.d:ro + - ${CONFIG_ROOT:-.}/tandoor/mediafiles:/media:ro - tandoor-staticfiles:/static:ro healthcheck: test: ["CMD", "wget", "http://127.0.0.1/recipes", "-qO", "/dev/null"] @@ -52,16 +52,16 @@ services: container_name: tandoor-backup restart: always env_file: - - ./tandoor/backup.env + - ${CONFIG_ROOT:-.}/tandoor/backup.env environment: - BACKUP_FOLDER_NAME=mediafiles - BACKUP_FOLDER_PATH=/data/mediafiles - DB_TYPE=sqlite - SQLITE_DATABASE=/database/recipes.db volumes: - - ./tandoor/database:/database - - ./tandoor/mediafiles:/data/mediafiles - - ./tandoor/backup:/config + - ${CONFIG_ROOT:-.}/tandoor/database:/database + - ${CONFIG_ROOT:-.}/tandoor/mediafiles:/data/mediafiles + - ${CONFIG_ROOT:-.}/tandoor/backup:/config profiles: - tandoor diff --git a/update-config.sh b/update-config.sh index 1c19b5e..652341f 100755 --- a/update-config.sh +++ b/update-config.sh @@ -4,9 +4,9 @@ function update_arr_config { echo "Updating ${container^} configuration..." - until [ -f ${CONFIG_ROOT:-.}/"$container"/config.xml ]; do sleep 1; done - sed -i.bak "s/<\/UrlBase>/\/$1<\/UrlBase>/" ${CONFIG_ROOT:-.}/"$container"/config.xml && rm ${CONFIG_ROOT:-.}/"$container"/config.xml.bak - sed -i.bak 's/^'"${container^^}"'_API_KEY=.*/'"${1^^}"'_API_KEY='"$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' ${CONFIG_ROOT:-.}/"$container"/config.xml)"'/' .env && rm .env.bak + until [ -f "${CONFIG_ROOT:-.}"/"$container"/config.xml ]; do sleep 1; done + sed -i.bak "s/<\/UrlBase>/\/$1<\/UrlBase>/" "${CONFIG_ROOT:-.}"/"$container"/config.xml && rm "${CONFIG_ROOT:-.}"/"$container"/config.xml.bak + sed -i.bak 's/^'"${container^^}"'_API_KEY=.*/'"${1^^}"'_API_KEY='"$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/"$container"/config.xml)"'/' .env && rm .env.bak echo "Update of ${container^} configuration complete." echo "Restarting ${container^}..." docker compose restart "$container" @@ -14,8 +14,8 @@ function update_arr_config { function update_jellyfin_config { echo "Updating ${container^} configuration..." - until [ -f ${CONFIG_ROOT:-.}/"$container"/network.xml ]; do sleep 1; done - sed -i.bak "s//\/$container<\/BaseUrl>/" ${CONFIG_ROOT:-.}/"$container"/network.xml && rm ${CONFIG_ROOT:-.}/"$container"/network.xml.bak + until [ -f "${CONFIG_ROOT:-.}"/"$container"/network.xml ]; do sleep 1; done + sed -i.bak "s//\/$container<\/BaseUrl>/" "${CONFIG_ROOT:-.}"/"$container"/network.xml && rm "${CONFIG_ROOT:-.}"/"$container"/network.xml.bak echo "Update of ${container^} configuration complete." echo "Restarting ${container^}..." docker compose restart "$container" @@ -23,17 +23,17 @@ function update_jellyfin_config { function update_bazarr_config { echo "Updating ${container^} configuration..." - until [ -f ${CONFIG_ROOT:-.}/"$container"/config/config/config.yaml ]; do sleep 1; done - sed -i.bak "s/base_url: ''/base_url: '\/$container'/" ${CONFIG_ROOT:-.}/"$container"/config/config/config.yaml && rm ${CONFIG_ROOT:-.}/"$container"/config/config/config.yaml.bak - sed -i.bak "s/use_radarr: false/use_radarr: true/" ${CONFIG_ROOT:-.}/"$container"/config/config/config.yaml && rm ${CONFIG_ROOT:-.}/"$container"/config/config/config.yaml.bak - sed -i.bak "s/use_sonarr: false/use_sonarr: true/" ${CONFIG_ROOT:-.}/"$container"/config/config/config.yaml && rm ${CONFIG_ROOT:-.}/"$container"/config/config/config.yaml.bak - until [ -f ${CONFIG_ROOT:-.}/sonarr/config.xml ]; do sleep 1; done - SONARR_API_KEY=$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' ${CONFIG_ROOT:-.}/sonarr/config.xml) - sed -i.bak "/sonarr:/,/^radarr:/ { s/apikey: .*/apikey: $SONARR_API_KEY/; s/base_url: .*/base_url: \/sonarr/; s/ip: .*/ip: sonarr/ }" ${CONFIG_ROOT:-.}/"$container"/config/config/config.yaml && rm ${CONFIG_ROOT:-.}/"$container"/config/config/config.yaml.bak - until [ -f ${CONFIG_ROOT:-.}/radarr/config.xml ]; do sleep 1; done - RADARR_API_KEY=$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' ${CONFIG_ROOT:-.}/radarr/config.xml) - sed -i.bak "/radarr:/,/^sonarr:/ { s/apikey: .*/apikey: $RADARR_API_KEY/; s/base_url: .*/base_url: \/radarr/; s/ip: .*/ip: radarr/ }" ${CONFIG_ROOT:-.}/"$container"/config/config/config.yaml && rm ${CONFIG_ROOT:-.}/"$container"/config/config/config.yaml.bak - sed -i.bak 's/^'"${container^^}"'_API_KEY=.*/'"${container^^}"'_API_KEY='"$(sed -n 's/.*apikey: \(.*\)*/\1/p' ${CONFIG_ROOT:-.}/"$container"/config/config/config.yaml | head -n 1)"'/' .env && rm .env.bak + until [ -f "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml ]; do sleep 1; done + sed -i.bak "s/base_url: ''/base_url: '\/$container'/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak + sed -i.bak "s/use_radarr: false/use_radarr: true/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak + sed -i.bak "s/use_sonarr: false/use_sonarr: true/" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak + until [ -f "${CONFIG_ROOT:-.}"/sonarr/config.xml ]; do sleep 1; done + SONARR_API_KEY=$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/sonarr/config.xml) + sed -i.bak "/sonarr:/,/^radarr:/ { s/apikey: .*/apikey: $SONARR_API_KEY/; s/base_url: .*/base_url: \/sonarr/; s/ip: .*/ip: sonarr/ }" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak + until [ -f "${CONFIG_ROOT:-.}"/radarr/config.xml ]; do sleep 1; done + RADARR_API_KEY=$(sed -n 's/.*\(.*\)<\/ApiKey>.*/\1/p' "${CONFIG_ROOT:-.}"/radarr/config.xml) + sed -i.bak "/radarr:/,/^sonarr:/ { s/apikey: .*/apikey: $RADARR_API_KEY/; s/base_url: .*/base_url: \/radarr/; s/ip: .*/ip: radarr/ }" "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml && rm "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml.bak + sed -i.bak 's/^'"${container^^}"'_API_KEY=.*/'"${container^^}"'_API_KEY='"$(sed -n 's/.*apikey: \(.*\)*/\1/p' "${CONFIG_ROOT:-.}"/"$container"/config/config/config.yaml | head -n 1)"'/' .env && rm .env.bak echo "Update of ${container^} configuration complete." echo "Restarting ${container^}..." docker compose restart "$container"