Pre-Compile ESPHome Devices on Docker Startup for Faster Home Assistant OTA Updates

Pre-Compile ESPHome Devices on Startup for Faster OTA Updates

The Problem

ESPHome compiles firmware on-demand when you trigger an install. Depending on the device, this can take anywhere from a few minutes to over 13 minutes for complex devices like a voice assistant. When a new ESPHome version is released and you have multiple devices to update, that wait time adds up.

The Solution

We can make ESPHome automatically pre-compile all devices in the background whenever it detects a new version, so by the time you go to install an update the binary is already sitting there ready to flash. The install is significantly faster as no compile step is needed.

Container startup is the natural place to trigger this. The only way ESPHome ever gets a new version is when you update the container image — so every time the container starts with a new version, we can immediately begin compiling in the background while the dashboard comes up normally. By the time you notice the update is available and go to install it, the binary is likely already ready.

The approach:

  • A background script runs at container startup
  • It checks the current ESPHome version against the last compiled version
  • If it’s a new version, the stale build cache and all per-device tracking files are wiped
  • Each device is compiled individually — successful devices are flagged so they are skipped on retry
  • Failed devices are retried on the next container restart without recompiling successful ones
  • Uses nice and ionice to run at lowest priority so your NAS stays responsive

In testing, a voice assistant device that previously took 13+ minutes to compile on install now flashes much faster with no compile wait.


Prerequisites

  • ESPHome running in Docker (this guide uses a Synology NAS but paths are easily adapted)
  • A Docker Compose-compatible container manager (Portainer, Synology Container Manager, etc.) or SSH access
  • Your config files live in a mapped volume

Step 1: Create the Auto-Compile Script

Create the following file on your host at /volume1/docker/esphome/autocompile.sh (adjust path for your setup):

#!/bin/bash
export PATH=/usr/local/bin:/usr/bin:/bin

/usr/local/bin/python -m esphome version > /tmp/esphome_ver.txt 2>&1
CURRENT_VER=$(cat /tmp/esphome_ver.txt | tr -d '[:space:]' | sed 's/Version://')
echo "[Auto-Build] Detected version: '$CURRENT_VER'"

if [ -z "$CURRENT_VER" ]; then
  echo "[Auto-Build] Could not determine version, aborting."
  exit 0
fi

# If this is a new ESPHome version, wipe the stale build cache and
# clear all per-device tracking files so everything gets recompiled
if [ ! -f /config/.last_compiled_ver ] || [ "$(cat /config/.last_compiled_ver)" != "$CURRENT_VER" ]; then
  echo "[Auto-Build] New version $CURRENT_VER. Clearing stale build cache and device tracking..."
  rm -rf /cache/esphome_data/build/
  rm -f /config/.compiled_ver_*
  echo "$CURRENT_VER" > /config/.last_compiled_ver
fi

FAILED=0
for f in /config/*.yaml; do
  [ -f "$f" ] || continue
  case "$f" in
    *secrets.yaml*|*common*) continue ;;
  esac
  DEVICE=$(basename "$f" .yaml)
  DEVICE_FLAG="/config/.compiled_ver_${DEVICE}"
  if [ -f "$DEVICE_FLAG" ] && [ "$(cat $DEVICE_FLAG)" = "$CURRENT_VER" ]; then
    echo "[Auto-Build] $DEVICE already compiled for $CURRENT_VER, skipping."
    continue
  fi
  echo "[Auto-Build] Pre-compiling $f..."
  if nice -n 19 ionice -c 3 /usr/local/bin/python -m esphome compile "$f"; then
    echo "$CURRENT_VER" > "$DEVICE_FLAG"
    echo "[Auto-Build] $DEVICE compiled successfully."
  else
    echo "[Auto-Build] $DEVICE FAILED to compile."
    FAILED=1
  fi
done

if [ "$FAILED" -eq 0 ]; then
  echo "[Auto-Build] All devices compiled successfully!"
else
  echo "[Auto-Build] Some devices failed. They will be retried on next startup."
fi

Then make it executable via SSH:

chmod +x /volume1/docker/esphome/autocompile.sh

Step 2: Docker Compose

services:
  esphome:
    container_name: esphome
    image: esphome/esphome:latest
    volumes:
      - /volume1/docker/esphome/config:/config
      - /volume1/docker/esphome/cache:/cache
      - /volume1/docker/esphome/autocompile.sh:/autocompile.sh:ro
      - /etc/localtime:/etc/localtime:ro
    restart: unless-stopped
    privileged: true
    network_mode: host
    cpu_shares: 256  # Low CPU priority relative to other containers (default is 1024)
    environment:
      - ESPHOME_COMPILE_PROCESS_LIMIT=1
      #- ESPHOME_DASHBOARD_USE_PING=true
      - PLATFORMIO_CORE_DIR=/cache/platformio
      - ESPHOME_DATA_DIR=/cache/esphome_data
    entrypoint:
      - /bin/bash
      - -c
      - |
        (/autocompile.sh) &
        exec /entrypoint.sh dashboard /config

How It Works

Cache layout — two folders live under /cache on the host:

Folder Purpose Persisted?
platformio/ Compiler toolchains, ESP frameworks, libraries (~2.7GB) :white_check_mark: Yes — saves re-downloading on every update
esphome_data/build/ Compiled object files per device (~1.4GB for 9 devices) :white_check_mark: Yes — speeds up dashboard compiles, wiped before each new version

Version tracking — the script writes /config/.last_compiled_ver after a successful run. On the next container restart it compares this to the current ESPHome version. If they match, the whole compile phase is skipped.

Failure handling — tracking is per-device. Each device gets its own flag file (/config/.compiled_ver_devicename) written only after a successful compile. If a device fails, its flag is never written, so it will be retried on the next container restart without recompiling devices that already succeeded.

Prioritynice -n 19 ionice -c 3 runs compiles at the absolute lowest CPU and disk I/O priority. Your NAS stays responsive — the compile just uses whatever headroom is available. cpu_shares: 256 (vs the default 1024) further limits the container’s CPU weight relative to other containers on the host, so ESPHome yields to anything else that needs resources.

Dashboard availability — the dashboard starts immediately while compiles run in the background. There’s no delay in accessing ESPHome while it works.


Step 3: Connect Home Assistant to Your Standalone ESPHome Container

If you’re running ESPHome as a standalone Docker container (not the official HA addon), Home Assistant won’t automatically discover it. You need to tell HA where to find the dashboard by creating a file in HA’s .storage folder.

Create /config/.storage/esphome.dashboard with the following contents, replacing the IP with the address of your Docker host:

{
  "version": 1,
  "minor_version": 1,
  "key": "esphome.dashboard",
  "data": {
    "info": {
      "addon_slug": "standalone_docker",
      "host": "192.168.1.100",
      "port": 6052
    }
  }
}

Then restart Home Assistant. The ESPHome integration should now connect to your Docker container and devices will appear as normal.


Notes

  • The script skips any yaml containing secrets or common in the filename — adjust the case statement if your naming convention is different
  • ESPHOME_COMPILE_PROCESS_LIMIT=1 ensures only one device compiles at a time, keeping resource usage predictable
  • The first run after a fresh install will take a while — PlatformIO needs to download toolchains. After that, only the compile time itself applies on future updates
  • This does not affect when Home Assistant shows the update notification — HA will still report an available update as soon as it detects a version mismatch. But by the time you act on it, the binary will almost certainly be ready
1 Like

This is a great idea. Especially for those running things on a low power SBC and/or with many devices.
It would be great (especially for people afraid to mess with containers) if this could be somehow easier to install (HACS maybe?) or, even better, push it directly to ESPHome and make it an option?
Thanks!

1 Like

The best solution would be to have this feature built into esphome itself. There is already a feature request here, so I would recommend voting for it. Auto build device images when a new ESPHome version is deployed · esphome · Discussion #3540 · GitHub