2 min read

Home Assistant stack

Step-by-step guide to running Home Assistant, Node-RED, PostgreSQL, and pgAdmin with Docker Compose, including backups and recorder integration.

This guide shows how to deploy a clean Home Assistant stack with Node-RED, PostgreSQL (for history storage), pgAdmin (web UI for DB management), and automated backups — all managed via Docker Compose.


📂 Directory Layout

/opt/docker/homeassistant/
├── config/        # Home Assistant config
├── node-red/      # Node-RED data
├── postgres/      # Postgres data
├── pgadmin/       # pgAdmin config
├── backups/       # Database dumps
├── docker-compose.yml
├── .env
└── backup.sh

1) .env

# General
TZ=Europe/Amsterdam
BASE_PATH=/opt/docker/homeassistant

# Postgres
POSTGRES_DB=homeassistant
POSTGRES_USER=hass
POSTGRES_PASSWORD=change-me-to-a-strong-password
POSTGRES_PORT=5432

# pgAdmin
PGADMIN_DEFAULT_EMAIL=admin@example.com
PGADMIN_DEFAULT_PASSWORD=change-me-too
PGADMIN_PORT=5050

# Home Assistant recorder connection string
RECORDER_DB_URL=postgresql://hass:change-me-to-a-strong-password@127.0.0.1:5432/homeassistant

# Backups
# keep the last N dumps (by newest first)
BACKUP_RETENTION=14

2) docker-compose.yml

version: "3.9"

services:
  homeassistant:
    container_name: homeassistant
    image: ghcr.io/home-assistant/home-assistant:stable
    volumes:
      - ${BASE_PATH}/config:/config
      - /etc/localtime:/etc/localtime:ro
      # Optional USB passthroughs:
      # - /dev/ttyUSB0:/dev/ttyUSB0
      # - /dev/ttyACM0:/dev/ttyACM0
    environment:
      - TZ=${TZ}
      - RECORDER_DB_URL=${RECORDER_DB_URL}
    restart: unless-stopped
    privileged: true
    network_mode: host

  node-red:
    container_name: node-red
    image: nodered/node-red:latest
    volumes:
      - ${BASE_PATH}/node-red:/data
    environment:
      - TZ=${TZ}
    restart: unless-stopped
    network_mode: host

  postgres:
    container_name: hass-postgres
    image: postgres:16
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      TZ: ${TZ}
    volumes:
      - ${BASE_PATH}/postgres:/var/lib/postgresql/data
      - ${BASE_PATH}/backups:/backups
    ports:
      - "${POSTGRES_PORT}:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    networks:
      - dbnet

  pgadmin:
    container_name: pgadmin
    image: dpage/pgadmin4:latest
    environment:
      PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
      PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
      TZ: ${TZ}
    volumes:
      - ${BASE_PATH}/pgadmin:/var/lib/pgadmin
    ports:
      - "${PGADMIN_PORT}:80"
    depends_on:
      - postgres
    restart: unless-stopped
    networks:
      - dbnet

networks:
  dbnet:
    driver: bridge

3) backup.sh

#!/usr/bin/env bash
# Simple Home Assistant Postgres backup script
# - Reads .env next to docker-compose.yml
# - Creates timestamped dumps in ${BASE_PATH}/backups
# - Keeps the last ${BACKUP_RETENTION} dumps

set -euo pipefail

# Move to compose directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

# Export vars from .env for this shell
if [[ -f .env ]]; then
  set -a
  # shellcheck disable=SC1091
  source .env
  set +a
else
  echo "ERROR: .env not found in $SCRIPT_DIR" >&2
  exit 1
fi

# Sanity checks
: "${BASE_PATH:?BASE_PATH missing}"
: "${POSTGRES_DB:?POSTGRES_DB missing}"
: "${POSTGRES_USER:?POSTGRES_USER missing}"
: "${BACKUP_RETENTION:=14}"

BACKUP_DIR="${BASE_PATH}/backups"
CONTAINER="hass-postgres"
STAMP="$(date +%F_%H%M%S)"
FILENAME="ha_${STAMP}.dump"
TARGET_PATH="${BACKUP_DIR}/${FILENAME}"

mkdir -p "$BACKUP_DIR"

# Verify container is running
if ! docker ps --format '{{.Names}}' | grep -qx "$CONTAINER"; then
  echo "ERROR: Container '$CONTAINER' not running." >&2
  exit 2
fi

echo "Creating backup: ${TARGET_PATH}"

# Create dump inside container, writing to mounted /backups
docker exec -t "$CONTAINER"   pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -F c -f "/backups/${FILENAME}"

# Quick integrity check: list dump TOC
docker exec -t "$CONTAINER" pg_restore -l "/backups/${FILENAME}" > /dev/null

# Prune older backups, keep newest BACKUP_RETENTION
echo "Pruning to last ${BACKUP_RETENTION} backups..."
ls -1t "${BACKUP_DIR}"/ha_*.dump 2>/dev/null | tail -n +$((BACKUP_RETENTION+1)) | xargs -r rm -f --

echo "Done."

Make it executable:

chmod +x ${BASE_PATH}/backup.sh

Optional cron (runs daily at 03:00):

0 3 * * * cd /opt/docker/homeassistant && ./backup.sh >> /opt/docker/homeassistant/backup.log 2>&1

4) Home Assistant Recorder Config

In ${BASE_PATH}/config/configuration.yaml:

recorder:
  db_url: !env_var RECORDER_DB_URL
  commit_interval: 5
  auto_purge: true
  purge_keep_days: 14
  # exclude:
  #   domains: [updater]
  #   entities: [sensor.date]

5) Restore from Backup

Pick a dump (e.g., ha_2025-09-13_030000.dump) from ${BASE_PATH}/backups:

# Drop & recreate DB
docker exec -i hass-postgres psql -U "$POSTGRES_USER" -d postgres <<'SQL'
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'homeassistant';
DROP DATABASE IF EXISTS homeassistant;
CREATE DATABASE homeassistant OWNER hass;
SQL

# Restore from dump
docker exec -i hass-postgres   pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists < ${BASE_PATH}/backups/ha_2025-09-13_030000.dump

✅ With this setup you get:

  • Home Assistant + Node-RED on host network
  • PostgreSQL with persistent storage
  • pgAdmin to inspect/manage your DB
  • Automated backup rotation
  • Easy relocation by editing one BASE_PATH variable in .env