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
niceandioniceto 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) | |
esphome_data/build/ |
Compiled object files per device (~1.4GB for 9 devices) |
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.
Priority — nice -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
secretsorcommonin the filename — adjust thecasestatement if your naming convention is different ESPHOME_COMPILE_PROCESS_LIMIT=1ensures 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