Installation · Home Assistant

Installing Home Assistant on Ubuntu with Docker.

Reading time
~12 min · 2,596 words
Section
All Home Assistant pages

Docker-based Home Assistant on Ubuntu is the collective's recommended installation pattern for agricultural monitoring. Home Assistant runs as a container on top of Ubuntu Server, sharing the machine with other useful services: Mosquitto for MQTT, MariaDB for the history database, ESPHome for custom device management, Zigbee2MQTT if the operation uses Zigbee, Frigate if cameras with object detection are deployed, and others as needed. All of this is managed through Docker Compose, a tool that describes the full stack in a single configuration file. This is the graybox pattern: one capable machine running the full monitoring and control stack, under the grower's control. The installation runs from a fresh Ubuntu Server to a working Home Assistant with Mosquitto and MariaDB alongside it, ready for its first integrations.

Before starting.

Prerequisites:

Ubuntu Server installed and updated. Per Installing Ubuntu Server. The server should be reachable via SSH from the administration computer, with the grower's user account able to run sudo.

Static IP or DHCP reservation configured. The Home Assistant host needs a consistent network address. This was covered in the Ubuntu install page.

Hardware appropriate for the graybox. Per Choosing Your Hardware, a repurposed business desktop, repurposed laptop, or mini PC with at least 16 GB RAM and 256 GB SSD. Tier-3 hardware (Raspberry Pi, Home Assistant Green) is not appropriate for the full graybox pattern. Those deployments should use Installing Home Assistant OS instead.

Time. Allow 60 to 90 minutes for a first-time install. Subsequent graybox deployments go faster as the pattern becomes familiar.

Why Docker and Docker Compose.

Docker packages software into isolated containers. Each container includes everything the software needs to run (the application, its libraries, its configuration) without conflicting with other software on the same host. A grower running Home Assistant in a Docker container can run five other services in other containers on the same host, and updates to any one of them do not affect the others.

Docker Compose describes multi-container applications in a single YAML file (docker-compose.yml). Instead of running individual docker run commands for each service, the grower writes one file that defines the full stack and starts it all with docker compose up -d. The same file stops the stack, updates containers, shows status, and captures the full configuration for version control and backup.

For the graybox pattern specifically, Compose is the right tool because the services (Home Assistant, Mosquitto, MariaDB, and others) need to communicate with each other, share data volumes, and start in a coordinated way. Compose handles all of that with minimal ceremony.

The alternative (Home Assistant Supervised) provides a similar outcome with a different approach. Supervised is Home Assistant with its full Supervisor running on a bare Debian install, giving the grower the same add-on management experience as Home Assistant OS while keeping a general-purpose Linux underneath. The tradeoff: Supervised is more opinionated (Debian only, Ubuntu is not a supported host, plus strict host configuration requirements) and runs its own container management rather than integrating cleanly with Docker Compose. For growers who want the Supervisor experience, Installing Home Assistant Supervised is the appropriate path. For growers who prefer direct Docker Compose control, this page is the path.

Installing Docker.

Docker Engine for Ubuntu is installed from Docker's official repository, not from Ubuntu's default packages (the version in Ubuntu's repository is usually older).

After installation, verify Docker works:

docker --version
docker compose version
sudo docker run hello-world

The hello-world container prints a success message if Docker is working.

Add the current user to the docker group. Running Docker commands with sudo every time is tedious. Adding the user to the docker group allows the user to run Docker commands directly:

sudo usermod -aG docker $USER

Log out and back in for the group change to take effect. After logging back in:

docker ps

Should run without sudo and show an empty container list (no containers running yet).

Security note: Membership in the docker group is equivalent to root access on the host, because Docker containers can access the host's kernel and file system. The grower's user with Docker group membership is a privileged account. Keep the password strong, use SSH key authentication, and treat SSH access to this machine as administrative access.

Directory structure for the graybox.

The collective's recommended directory structure keeps the Docker Compose configuration and the service data organized under one path.

/opt/graybox/
├── docker-compose.yml
├── .env
├── homeassistant/
│   ├── config/
│   └── ...
├── mosquitto/
│   ├── config/
│   ├── data/
│   └── log/
├── mariadb/
│   └── data/
├── esphome/
│   └── config/
├── backups/
└── ...

The docker-compose.yml at the root defines the full stack. Each service has its own subdirectory for persistent data. .env stores environment variables and secrets (passwords, API keys) that should not be hardcoded in docker-compose.yml.

Create the directory structure:

sudo mkdir -p /opt/graybox
sudo chown -R $USER:$USER /opt/graybox
cd /opt/graybox
mkdir -p homeassistant/config
mkdir -p mosquitto/config mosquitto/data mosquitto/log
mkdir -p mariadb/data
mkdir -p esphome/config
mkdir -p backups

The grower owns the directory, which simplifies file management without needing sudo for every operation.

Some growers prefer a different location (/srv/graybox, /home/grower/graybox, or similar). The specific path is a matter of preference; the structure is what matters.

The base docker-compose.yml.

A minimal starting docker-compose.yml for the graybox includes Home Assistant, Mosquitto, and MariaDB.

Key elements your compose file should include:

Home Assistant service. Image homeassistant/home-assistant:stable (or specific version). Network mode host: required for some Home Assistant integrations (mDNS discovery, BLE, and others). Volume mount from ./homeassistant/config to /config inside the container. Privileged mode if the host will use USB devices (Zigbee sticks, Bluetooth adapters). Restart policy unless-stopped. Environment variables for timezone (TZ=America/Chicago or similar).

Mosquitto service. Image eclipse-mosquitto:2 (or current version). Port mappings for 1883 (MQTT) and 9001 (MQTT over WebSockets, optional). Volume mounts for ./mosquitto/config, ./mosquitto/data, ./mosquitto/log. Restart policy unless-stopped. Requires a mosquitto.conf file in ./mosquitto/config/. The Compose file references it.

MariaDB service. Image mariadb:lts (or mariadb:11 for current version). Environment variables for root password and Home Assistant database user (pulled from .env). Volume mount for ./mariadb/data. Restart policy unless-stopped. Port 3306 published on 127.0.0.1 only, not to the LAN, so Home Assistant (which runs with network_mode: host, not on the Compose network) can still reach it at localhost:3306.

services:
  homeassistant:
    container_name: homeassistant
    image: homeassistant/home-assistant:stable
    volumes:
      - ./homeassistant/config:/config
      - /etc/localtime:/etc/localtime:ro
    environment:
      - TZ=${TZ}
    restart: unless-stopped
    network_mode: host
    # privileged: true   # uncomment if using a USB Zigbee/BLE adapter or other passthrough device

  mosquitto:
    container_name: mosquitto
    image: eclipse-mosquitto:2
    volumes:
      - ./mosquitto/config:/mosquitto/config
      - ./mosquitto/data:/mosquitto/data
      - ./mosquitto/log:/mosquitto/log
    ports:
      - "1883:1883"
      - "9001:9001"
    restart: unless-stopped

  mariadb:
    container_name: mariadb
    image: mariadb:lts
    volumes:
      - ./mariadb/data:/var/lib/mysql
    environment:
      - MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD}
      - MARIADB_DATABASE=homeassistant
      - MARIADB_USER=homeassistant
      - MARIADB_PASSWORD=${MARIADB_HOMEASSISTANT_PASSWORD}
    ports:
      - "127.0.0.1:3306:3306"
    restart: unless-stopped

Secrets in .env. The .env file contains values like:

MARIADB_ROOT_PASSWORD=<generated-strong-password>
MARIADB_HOMEASSISTANT_PASSWORD=<generated-strong-password>
TZ=America/Chicago

The .env file should have restrictive permissions (chmod 600) and should never be committed to public version control if the grower uses git for the configuration.

Mosquitto configuration.

Mosquitto requires a configuration file at ./mosquitto/config/mosquitto.conf. A minimal working configuration:

persistence true
persistence_location /mosquitto/data/

log_dest file /mosquitto/log/mosquitto.log
log_dest stdout

listener 1883
allow_anonymous false
password_file /mosquitto/config/passwd

listener 9001
protocol websockets
allow_anonymous false
password_file /mosquitto/config/passwd

After creating the configuration file, create a Mosquitto user for Home Assistant's MQTT connection:

docker compose run --rm mosquitto mosquitto_passwd -c /mosquitto/config/passwd homeassistant

The command prompts for a password, which Home Assistant will use when connecting to MQTT. Save the password in a password manager.

Starting the stack.

With docker-compose.yml, .env, and mosquitto.conf in place:

cd /opt/graybox
docker compose up -d

up creates and starts the containers. -d runs them in the background (detached).

The first run downloads the container images, which takes a few minutes on a decent internet connection. Subsequent runs start immediately because the images are cached locally.

Check that all containers are running:

docker compose ps

All services should show running or healthy. If any show exited or restarting, check the logs:

docker compose logs homeassistant
docker compose logs mosquitto
docker compose logs mariadb

Common first-run issues include: permission problems on volume mounts (fix with sudo chown on the relevant directory), missing configuration files (Mosquitto in particular needs mosquitto.conf present), and environment variables not loading from .env (check for typos in the file).

First access to Home Assistant.

Home Assistant takes a few minutes to start on the first run: it initializes its database and configuration. After a few minutes, navigate to http://graybox-ip:8123 in a web browser (replace graybox-ip with the host's IP address, or use homeassistant.local:8123 if mDNS works on the network).

The Home Assistant onboarding screen appears. Walk through the steps:

1. Create the admin account (name, username, password). 2. Provide location, timezone, elevation, and unit preferences. Home Assistant uses location for sun calculations, weather integrations, and local time display. 3. Decide whether to share anonymized usage statistics with the Home Assistant project (optional; the collective recommends enabling it: it helps the project prioritize development). 4. Home Assistant scans the local network for discoverable devices. The grower can configure some of them now or later.

Onboarding is complete. The default dashboard appears. The installation is working.

Configuring Home Assistant to use MariaDB.

By default, Home Assistant stores history in a local SQLite database. For a graybox deployment, switching to MariaDB improves performance and allows longer retention. Edit ./homeassistant/config/configuration.yaml and add:

recorder:
  db_url: mysql://homeassistant:the-password-from-env@localhost:3306/homeassistant?charset=utf8mb4
  purge_keep_days: 365

Replace the-password-from-env with the actual MARIADB_HOMEASSISTANT_PASSWORD value from .env. localhost is correct here (not mariadb, the container name) because Home Assistant runs with network_mode: host, outside the Compose network where container-name resolution works.

Before this configuration takes effect, the MariaDB database needs to exist. Connect to MariaDB and create it:

docker compose exec mariadb mariadb -u root -p

Enter the MariaDB root password from .env. At the MariaDB prompt:

CREATE DATABASE homeassistant CHARACTER SET utf8mb4;
CREATE USER 'homeassistant'@'%' IDENTIFIED BY 'the-password-from-env';
GRANT ALL PRIVILEGES ON homeassistant.* TO 'homeassistant'@'%';
FLUSH PRIVILEGES;
EXIT;

Restart Home Assistant to pick up the new recorder configuration:

docker compose restart homeassistant

The Home Assistant logs (docker compose logs homeassistant) should show successful connection to MariaDB. The history going forward stores in MariaDB rather than SQLite.

Adding ESPHome.

ESPHome compiles firmware for ESP32 and ESP8266 devices used as custom sensors and controls. Adding it to the graybox:

  esphome:
    container_name: esphome
    image: ghcr.io/esphome/esphome:stable
    volumes:
      - ./esphome/config:/config
    environment:
      - TZ=${TZ}
    restart: unless-stopped
    network_mode: host

Add this as a new service block under services: in the same docker-compose.yml, alongside homeassistant, mosquitto, and mariadb.

After adding the ESPHome service definition to docker-compose.yml:

docker compose up -d

The ESPHome web interface is then available at http://graybox-ip:6052. ESPHome integration covers the specifics of using it for agricultural devices.

Adding MQTT to Home Assistant.

With Mosquitto running and a Home Assistant MQTT user configured, add the MQTT integration in Home Assistant's interface:

Settings → Devices & Services → Add Integration → MQTT.

Enter the connection details:

  • Broker: the graybox's IP address (or localhost if Home Assistant is running with network_mode: host)
  • Port: 1883
  • Username: the Mosquitto user created above
  • Password: the password for that user

Home Assistant connects to the broker. MQTT is now available for other integrations and for ESPHome devices reporting state.

Adding additional services.

The graybox pattern extends as the operation requires. Common additions:

Zigbee2MQTT. For Zigbee device management. Requires a Zigbee coordinator USB stick. Zigbee integration covers the setup.

Frigate. For camera-based object detection. Benefits substantially from Intel Quick Sync (on most Intel CPUs) or a Coral USB AI accelerator. Frigate and Computer Vision covers the setup.

InfluxDB and Grafana. For time-series analytics alongside Home Assistant. Grafana Integration covers the pattern.

Node-RED. For flow-based automation logic. An alternative and complement to native Home Assistant automations.

Ollama. For local AI capabilities without cloud dependencies. LLM Integrations covers the setup.

Nextcloud. For file sync, including automated backups of sensor data exports.

WireGuard or Tailscale. For secure remote access. Remote Access covers the options.

Each additional service gets its own section in docker-compose.yml and typically its own subdirectory under /opt/graybox/. The pattern is the same: define the service, mount its persistent data directory, set a restart policy, run docker compose up -d.

Updating the stack.

Updating Home Assistant and the other services is a short operation with Docker Compose:

cd /opt/graybox
docker compose pull    # download newer images
docker compose up -d   # recreate containers with new images
docker image prune     # remove old unused images (optional cleanup)

The pull command downloads newer versions of any images that have been updated. The up -d command recreates the containers using the new images, preserving the persistent data in the mounted volumes. The whole process takes a few minutes and results in updated services.

For production deployments, the collective recommends testing updates before applying them to the working graybox. The Updates and Version Management page covers the practices.

Backing up the graybox.

The graybox pattern's persistent data lives in /opt/graybox/. Backing up that directory captures the entire state of the graybox: Home Assistant's configuration and database, Mosquitto's persistence file, MariaDB's database, ESPHome's configurations, and anything else in the tree.

A simple daily backup pattern:

#!/usr/bin/env bash
# Graybox daily backup: stop Home Assistant briefly, archive /opt/graybox/, restart.
set -euo pipefail

GRAYBOX_DIR="/opt/graybox"
BACKUP_DIR="/opt/graybox/backups"
OFFSITE_DIR="/mnt/backup-drive/graybox"   # a second physical location; adjust to fit
TIMESTAMP="$(date +%Y%m%d-%H%M%S)"
ARCHIVE="graybox-backup-${TIMESTAMP}.tar.gz"

cd "$GRAYBOX_DIR"
docker compose stop homeassistant

tar -czf "${BACKUP_DIR}/${ARCHIVE}" --exclude="./backups" .

docker compose start homeassistant

if [ -d "$OFFSITE_DIR" ]; then
  cp "${BACKUP_DIR}/${ARCHIVE}" "$OFFSITE_DIR/"
fi

find "$BACKUP_DIR" -name "graybox-backup-*.tar.gz" -mtime +14 -delete

echo "Backup complete: ${ARCHIVE}"

Only Home Assistant stops during the backup; Mosquitto and MariaDB keep running, so the archive is a live snapshot rather than a perfectly quiesced one, adequate for a daily safety net. Run it from cron or a systemd timer, keep at least one offsite copy, and see Backup and Recovery for the deeper practice of actually testing a restore.

The backup script can be triggered by cron, a systemd timer, or manual invocation. The Backup and Recovery page covers the strategy in depth: local backups, offsite copies, tested recovery, and what to do when things go wrong.

Common issues.

A few problems come up frequently on first deployment.

Permission denied on volume mounts. Docker containers often run as specific users inside the container. When the host directory is owned by a different user, the container cannot write to it. Fix by setting the directory owner to match the container's expected UID (often 1000), or use chmod to allow the container's user access.

Home Assistant cannot find devices. If network_mode: host is not set, Home Assistant's network discovery (mDNS, auto-discovery) does not find devices on the local network. For most graybox deployments, network_mode: host is the right setting.

MariaDB connection refused. If the recorder configuration uses a container name or IP that Home Assistant cannot reach, the connection fails. With network_mode: host on Home Assistant, the database connection uses localhost:3306 (since MariaDB exposes its port to the host).

Containers restart continuously. A container that keeps crashing usually logs the error. docker compose logs <service> shows what went wrong. Common causes: invalid configuration files, missing volumes, insufficient permissions, conflicting port bindings.

Home Assistant not visible at http://graybox-ip:8123. Confirm the container is running (docker compose ps), confirm the firewall allows port 8123 (sudo ufw status, add rule if needed), confirm Home Assistant is actually listening on that port (docker compose logs homeassistant should show the web server starting).

Updates fail or break functionality. Home Assistant's release notes document breaking changes. Before a major update, read the release notes. The collective recommends testing updates in a backup or virtual machine first, especially for updates flagged as containing breaking changes.

Frequently asked questions.

Should I run Home Assistant in Docker?

Docker on Ubuntu Server suits deployments where one capable machine runs a whole stack: Home Assistant plus Mosquitto for MQTT, MariaDB for history, ESPHome, and optionally Zigbee2MQTT or Frigate, all defined in a single Docker Compose file. Containers isolate each service, so updating one does not affect the others. The tradeoff is no Supervisor add-on store; extra services are added as containers instead.

What hardware do I need to run Home Assistant in Docker?

For a multi-service stack, a repurposed business desktop, laptop, or mini PC with at least 16 GB of RAM and a 256 GB SSD, running Ubuntu Server with a static IP or DHCP reservation. A Raspberry Pi or Home Assistant Green is not appropriate for the full multi-container pattern; those devices fit the simpler Home Assistant OS installation. A first-time install takes roughly 60 to 90 minutes.

Why does Home Assistant need host network mode in Docker?

Several integrations depend on it. With host networking the container shares the host's network stack, so mDNS discovery, Bluetooth, and other auto-discovery mechanisms can see devices on the local network. Without it, Home Assistant cannot find many devices. One side effect: container-name addresses stop resolving, so a database on the same machine is reached at localhost, for example localhost:3306 for MariaDB.

How do I update Home Assistant running in Docker?

Three commands from the Compose directory: docker compose pull downloads newer images, docker compose up -d recreates the containers on the new images while keeping data in the mounted volumes, and docker image prune removes old unused images. The whole process takes a few minutes. Read the release notes for breaking changes first, and test major updates on a backup or virtual machine before applying them to a production system.

Should Home Assistant use MariaDB instead of SQLite?

Home Assistant stores history in a local SQLite database by default, which is fine for small systems. Switching the recorder to MariaDB improves performance and allows longer retention, such as a full year of history with purge_keep_days set to 365. The change is a recorder entry in configuration.yaml pointing at a MariaDB database created with its own user and the utf8mb4 character set, followed by a restart.

How do I back up Home Assistant when it runs in Docker?

Docker installations have no Supervisor backup system, so back up at the host level: briefly stop the Home Assistant container, create a tar archive of the whole stack directory containing the configurations and databases, restart the container, and copy the archive to a second physical location. Run the script daily from cron or a systemd timer and prune archives older than about two weeks.