ESPHome Device Builder Dashboard
Status: stable. The dashboard reached its 1.0 release and ships
as the default builder in the official ESPHome add-on (as of ESPHome
2026.6.0). Issues
and feedback welcome — please check existing issues / the
project board
first, and join the Discord channel
for live discussion.
A new dashboard for ESPHome — a guided interface for composing device configs, exploring components and boards, managing automations, and pushing firmware updates.
Try it
Running it behind a reverse proxy?
Skip ahead to Behind a reverse proxy
for the nginx / --trusted-domains setup.
The dashboard runs by default in the official Home Assistant add-on (as of ESPHome 2026.6.0) and ships as an opt-in backend in ESPHome Desktop. Pick the path that matches how you run ESPHome today:
Home Assistant add-on
As of ESPHome 2026.6.0, the ESPHome add-on (Stable, Beta, or Dev) runs the Device Builder by default; install or update the add-on and open it from the Home Assistant sidebar. The Device Builder ships inside the add-on and serves the dashboard over Home Assistant Ingress, so there's no toggle to set.
The add-on's data layout stays the same (/config/esphome/ for YAMLs,
/data/ for build artefacts) so updating doesn't move or duplicate any
state.
ESPHome Desktop (macOS / Windows / Linux)
Install ESPHome Desktop v0.7.0 or later, then click the system-tray icon and pick **Backend → ESPHome Builder (stable) or ESPHome Builder (beta)**. The daemon restarts under the chosen backend and the tray badge updates to reflect which one is running. Switch back to Classic ESPHome Dashboard the same way.
Windows build location. On native Windows the dashboard puts its
build tree and PlatformIO toolchain under a short, space-free root,
C:\esphb\(where\ is the first 8 characters of the
dashboard id; per-dashboard roots nest under one C:\esphb folder),
rather than under your config / profile dir, so deep ESP-IDF build paths
stay under the 260-char MAX_PATH limit and clear of spaces in your
profile name (C:\Users\First Last\…). The native ESP-IDF toolchain
lives at C:\esphb\idf, shared by all dashboards on the machine: its
compiler probes paths reaching ~245 characters below that folder, so the
prefix must stay very short to clear MAX_PATH. Nothing here is removed
on uninstall, so a reinstall keeps the warm toolchain; delete C:\esphb
by hand to reclaim the disk space. This applies only to native Windows;
running in a Linux Docker container (or the HA add-on) uses the normal
data dir.
Standalone (PyPI)
For developers, headless servers, or anyone running outside the add-on / Desktop / Docker shapes:
python -m venv .venv && source .venv/bin/activate
pip install 'esphome-device-builder[esphome]'
esphome-device-builder ~/esphome-configs
The [esphome] extra pulls in the upstream esphome package the
dashboard needs to compile, flash, and discover devices. The HA
add-on / Desktop builds ship esphome separately, so the bare
pip install esphome-device-builder (no extra) is for those
contexts only.
For the beta channel, pass --pre to opt the resolver into
prereleases — e.g. pip install --pre 'esphome-device-builder[esphome]'
for a fresh install, or
pip install --upgrade --pre 'esphome-device-builder[esphome]'
to pull the newest beta on top of an existing install. --pre only
opts the current command into prereleases; rerun the upgrade
command to refresh.
The server starts on http://localhost:6052. Run with --help for
the full flag set.
Install from a GitHub release
Every build is published to PyPI, so the install above is the preferred path. The same wheels are mirrored on the GitHub releases page — handy as a fallback if PyPI is unreachable.
python -m venv .venv && source .venv/bin/activate
Replace <version> with a release tag (X.Y.Z stable, X.Y.ZbN beta).
`[esphome] @ <url> carries the optional extra through the
direct-URL install so the dashboard finds esphome at startup.
pip install "esphome-device-builder[esphome] @ https://github.com/esphome/device-builder/releases/download/<version>/esphome_device_builder-<version>-py3-none-any.whl"
esphome-device-builder ~/esphome-configs
From source (contributors)
Requires uv:
git clone https://github.com/esphome/device-builder
cd device-builder
script/setup
source .venv/bin/activate
esphome-device-builder ./configs --log-level debug --dev
--dev serves index.html with Cache-Control: no-cache so a
re-deployed frontend wheel isn't masked by a browser-cached SPA
shell pointing at a now-deleted hashed bundle. Hashed bundles
themselves stay immutable regardless. Skip --dev in production —
the browser's default heuristic is fine when you're not rebuilding
every few minutes.
Standard ESPHome container image
As of ESPHome 2026.6.0, the standard ghcr.io/esphome/esphome image
bundles the Device Builder and runs it as the dashboard; there's no env
var or extra setup. The image's default command (dashboard /config)
launches esphome-device-builder against your config dir:
docker run -p 6052:6052 -v "$PWD":/config ghcr.io/esphome/esphome
The dashboard serves /config on port 6052. Every other subcommand
(compile, run, logs, ...) still runs the classic esphome CLI, so
direct command-line use is unchanged.
Username / password authentication
The three install paths each handle network reach and the auth gate
differently. The Home Assistant add-on runs ingress-only with no
password under the new preview, so access goes through the HA
sidebar and port 6052 stays unbound; ESPHome Desktop binds only
to 127.0.0.1, so there's nothing on the LAN to authenticate
against in the first place; the standalone PyPI install binds
0.0.0.0:6052, is LAN-reachable by default, and prints a startup
banner warning that there is no auth gate until you configure one.
Configuring a username and password is only wired up for the standalone install today. The HA add-on doesn't ship a knob for it (see below for the rationale, this is by design) and Desktop is localhost-only, so the knob doesn't add anything there. The backend itself accepts credentials anywhere it runs; it's the packaging around the HA add-on and Desktop that doesn't pass any in.
Home Assistant add-on
On the Home Assistant add-on, the dashboard is reached through Home
Assistant itself: you open it from the HA sidebar in your browser, and
the Home Assistant Companion App opens it the same way. Both paths come
in over Home Assistant Ingress, so
the dashboard is already protected by your Home Assistant login;
there's no separate username or password to configure for the
add-on, and port 6052 stays unbound to keep the dashboard off the
LAN by default.
This is the supported way to use the add-on. The classic dashboard
also let you forward your Home Assistant username and password
through the supervisor /auth endpoint to gate an exposed port
6052; that endpoint has no rate limiting or lockout, so it turned
the dashboard into an open brute-force target against every account
on the Home Assistant instance, and we don't carry that path forward
(see
device-builder issue #85).
The classic dashboard's other option, "Disable external
authentication" (leave_front_door_open), is honored. With it on
and port 6052 mapped in the add-on's Network options, the
dashboard binds the port on the LAN with no authentication at all,
for example so the VS Code ESPHome plugin can reach it. Both opt-ins
are required, matching the classic add-on; the dashboard logs a loud
banner because this leaves the dashboard wide open to anyone on your
network. Turn the option off, or unmap the port, for ingress-only
access. For password-gated LAN access instead, run the standalone
PyPI install on the same network with its own dashboard-managed
password.
Standalone (PyPI)
Set the credentials through environment variables before launching the dashboard:
export ESPHOME_USERNAME=admin
export ESPHOME_PASSWORD='<pick a strong password>'
esphome-device-builder ~/esphome-configs
Both values must be set together; setting only one fails the
credential check at startup. The env var names are ESPHOME_USERNAME
and ESPHOME_PASSWORD, not the legacy dashboard's bare USERNAME /
PASSWORD, because the bare names collide with the OS-supplied
$USERNAME on Windows and most login shells.
The --username / --password CLI flags are still accepted for
parity with the legacy dashboard's CLI, but please avoid
--password in particular: command-line arguments show up in
process listings (ps output on Linux / macOS, Task Manager on
Windows, plus /proc/ on Linux unless hidepid is
locked down), so another local user on the host can usually read
the dashboard password. Use the env vars instead (or a .env /
systemd EnvironmentFile= that's only readable by the dashboard's
user).
Behind a reverse proxy
The dashboard rejects browser WebSocket handshakes whose
Origin doesn't match the server's Host header. When a
proxy fronts the dashboard under a different hostname (nginx,
Caddy, Traefik, nginx-proxy-manager, ...), the browser sends
Origin: https://dashboard.example.com but the upstream sees
Host: localhost:6052 — those don't match and the handshake
gets 403'd. Same gate, same fix, regardless of whether the
dashboard has a password set; it applies to every public-site
deployment.
Two ways to make it work:
- Configure the proxy to forward the public Host (cleanest):
location / {
proxy_pass http://localhost:6052;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400s; # keep WS connections alive
}
With proxy_set_header Host $host;, the dashboard sees
Host: dashboard.example.com (the hostname the browser
asked for), Origin matches, and the handshake passes.
- If the proxy rewrites Host (default nginx, some
:
esphome-device-builder /config \
--trusted-domains dashboard.example.com,proxy.example.com
Or via the env var (
$ESPHOME_TRUSTED_DOMAINS, same name
the legacy dashboard used):
ESPHOME_TRUSTED_DOMAINS=dashboard.example.com esphome-device-builder /config
The list is comma-separated, case-insensitive, port-tolerant.
IPv6 addresses work bracketed or bare (
::1 and [::1]
both match). Use * as the only entry to disable the Host
restriction entirely (handy when the Host varies per request
— but then operator-supplied auth becomes the only gate).
Listening on a UNIX socket (
--socket)
Instead of a TCP port, the public site can listen on a UNIX
socket — the usual choice when a reverse proxy runs on the
same host and you don't want the dashboard reachable over
TCP at all:
esphome-device-builder /config --socket /run/esphome/dashboard.sock
--host and --port are then ignored for binding but still
feed the dashboard's mDNS advertisement, so point them at
where the proxy ultimately listens.
The socket file is created with the default permissions your
process
umask yields; the dashboard does not manage its
mode. **Put the socket in a directory readable only by the
dashboard and the proxy** (for example a dedicated
/run/esphome/ owned by the dashboard user with the proxy's
user granted access via group or ACL) — directory permissions
are the reliable access control here, since socket-file mode
bits are not enforced on every platform. Anything that can
connect to the socket gets the same surface a TCP client
would, gated by the same auth and Origin/Host checks.
nginx upstream example:
proxy_pass http://unix:/run/esphome/dashboard.sock;
Subpath mounts (X-Forwarded-Prefix)
If you run the standalone dashboard behind an HTTP reverse proxy
(nginx, Traefik, Caddy, nginx-proxy-manager, …) at a subpath (for
example
https://example.com/esphome/), the proxy must forward the
mount prefix to the backend using the X-Forwarded-Prefix header so
the server can render the SPA's correctly per request.
The header value does not need a trailing slash — the server
normalises the prefix internally.
Minimal nginx example:
location /esphome/ {
proxy_pass http://localhost:6052/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /esphome;
proxy_read_timeout 86400s;
}
Notes:
- Home Assistant ingress deployments do not need this because the
supervisor sets X-Ingress-Path automatically; the dashboard
prefers X-Ingress-Path when present.
- Traefik and Caddy provide equivalent ways to forward or strip
prefixes (look for X-Forwarded-Prefix or stripPrefix-style
options in their docs).
- For the exact precedence and normalization logic, see
_resolve_base_href() in the backend source:
https://github.com/esphome/device-builder/blob/main/esphome_device_builder/device_builder.py
CLI tools and the Home Assistant integration omit
Origin
entirely, so they're never affected — the gate is browser-only.
The HA Ingress site (the --ingress-host listener the
supervisor proxies to) skips both checks because the supervisor
handles auth upstream. It binds only loopback plus the supervisor
gateway (127.0.0.1 + 172.30.32.1), never all interfaces, and a
peer guard rejects any source other than loopback or the supervisor
(172.30.32.2) — so the no-auth site is never reachable from the LAN
or another add-on even though the add-on runs host-network. This
matches the legacy add-on's nginx allow/deny ACL.
See docs/ARCHITECTURE.md § Authentication
for the deep dive on the trust model.
Send builds to another dashboard
Compiling ESPHome firmware is CPU-heavy, especially for ESP-IDF
targets. If your dashboard runs on a low-power host, say the Home
Assistant add-on on a Raspberry Pi or HA Green, you can pair it to
a beefier dashboard on the same LAN, for example ESPHome Desktop
running on a workstation, and offload compiles there. The firmware
bytes still install from the original dashboard; only the build
runs elsewhere.
Remote build is designed for dashboards on the same local
network. Running the peer link across a VPN, Tailscale, or any
slow or high-latency uplink is best effort: it may work, but the
timing assumptions favor LAN-class throughput, slow links aren't
a design target, and issues that only reproduce over a slow link
won't be pursued.
Two roles:
- Build server, the dashboard that lends its CPU. Surfaced
under Settings → Build server. Accepts pair requests,
compiles incoming jobs, returns artefacts.
- Send builds, the dashboard that delegates compiles.
Surfaced under Settings → Send builds. Lists dashboards
the LAN discovered and the ones you've paired with.
A single dashboard can play both roles at once. The Home Assistant
add-on defaults to send-only, since it doesn't accept inbound
build jobs without opt-in, which is the sensible default for a
typically-shared host; ESPHome Desktop and standalone installs
default to both roles on.
Pairing in four steps
The receiving dashboard only accepts new pair requests while its
Pairing requests screen is open — open that screen before
clicking Pair on the sending side, and keep it open until you've
clicked Accept. Step 2 is the prerequisite, not the wrap-up.
- Start both dashboards on the same subnet, or with a working
mDNS reflector between subnets. Outside the Home Assistant
add-on, a dashboard advertises itself over mDNS as soon as it
starts, independently of whether Build server is enabled.
The receiver's peer-link port lands in the same TXT record
only once Build server is enabled and the listener has
bound; a dashboard without Build server enabled still
appears in Known dashboards but can't be paired with until
the receiving side flips that toggle. (HA add-on instances
stay silent on the network; two add-on dashboards on the same
LAN need the manual-entry flow below.)
!Enabling Build server on the receiving dashboard
- On the receiving dashboard, open **Settings → Pairing
requests**. This opens the pairing window; the receiver will
refuse any pair request that arrives while this screen isn't
mounted. The window shows as Open alongside a countdown,
and Extend restarts it if you run out of time. Leave the
screen open through step 4.
- On the sending dashboard, open **Settings → Send builds →
Known dashboards**, find the receiver in the list, and click
Pair. Confirm receiver opens with the receiver's
identity fingerprint, a name for your own paired-receivers
list, and the name this dashboard introduces itself by; click
Send pair request. Pair request sent then shows this
dashboard's own fingerprint rendered as an emoji grid.
Leave it on screen for step 4. Hex bytes are tucked behind a
Show hex bytes disclosure if you prefer that form, but
the emoji grid is the primary verification surface.
!Sending a pair request from the sending dashboard
- Back on the receiving dashboard's still-open **Pairing
requests** screen, the new request now shows up with the
sender's name and originating IP — click Review. Compare
the OOB fingerprint in that dialog against the grid the
sending dashboard is still showing; the two must match for
the pairing to be safe to accept. Then click Accept. The
pairing persists on both sides and survives restarts.
!Reviewing and accepting the pair request
Once accepted, the receiver appears under **Paired build
servers on the sending dashboard marked Connected**, with
the ESPHome version each side runs:
!A connected build server on the sending dashboard
If a dashboard you expected to show up doesn't appear in
Known dashboards, run
esphome-device-builder-discover on
the sending host before troubleshooting the UI. The CLI browses
the same mDNS service the dashboard does and prints what it
sees, including the receiver's peer-link port and identity
fingerprint:
Status |Name |Address:Port |Server |ESPHome |RB Port |Pin (sha256)
-------+-----+--------------------+---------+----------+--------+--------------
ONLINE |mac |192.168.1.75:6052 |0.1.0b39 |2026.4.5 |6055 |3968ef58…
If the receiver shows up in the CLI but not in the UI, the
discovery layer is fine and the gap is somewhere downstream; if
neither side sees the other, mDNS isn't crossing the network
(different subnet without a reflector, container without host
networking, firewall blocking 5353/udp).
After pairing, clicking Install on a device automatically routes
through the paired receiver as soon as one is online. The
scheduler prefers an idle receiver, but if every paired receiver
is busy it queues the install behind the in-flight work rather
than silently building locally; that keeps the toolchain warm and
the artefacts coming from one place. The install dialog shows a
"Building on
{receiver}" sub-line so you can see which side is
doing the work. You can override per-install via the **Build
locally instead** link in the install dialog, or disable
auto-routing entirely from **Settings → Send builds →
Auto-route installs to remote build**.
Manual entry (no mDNS)
If the dashboards are on different subnets, or if either side is
running as the Home Assistant add-on (which doesn't advertise
itself on mDNS), use the Pair with another dashboard section
beneath Known dashboards. Open the receiving dashboard's
Pairing requests screen first (same prerequisite as the
discovered-dashboard flow above), then click **Pair with a build
server** on the sending side, type the receiver's hostname and
port, and submit; the pairing flow runs identically to the
discovered-dashboard case from there. The peer-link is a
WebSocket served at
/remote-build/peer-link over TCP port
6055 by default; if a reverse proxy or firewall sits between
the two dashboards it needs to allow WebSocket upgrades on
that path. The wire is Noise-encrypted regardless of how you
reach it, and the emoji-fingerprint comparison still gates
pairing the same way.
Headless build server (
--remote-build-only)
A machine whose only job is lending CPU can run as a
dedicated build server with no dashboard UI:
esphome-device-builder --remote-build-only /var/lib/esphome-builder
The config-dir argument holds the server's identity and build
state — keep it persistent. On first run it prints a
fingerprint and a one-time pairing key to the console:
📌 🍎 🐙 ☎️ 🚀 🌏 🐰
...
8MC5-KAXV-NN6N-PWAA
To pair, on your main dashboard open **Settings → Send builds
→ Pair with a build server**, enter the headless server's
hostname and peer-link port (default 6055), and Continue.
Check the emoji fingerprint matches the console; the dialog
detects it's a headless server and shows a pairing-key field —
enter the printed key and send. That's it — the server pairs
and starts serving builds. Re-run the server if you don't pair
within the window it prints.
--allow-pairing-source
optionally restricts which address may pair.
Install coverage
Remote build runs the compile on a paired receiver for **every
install type** — OTA over Wi-Fi or Ethernet, and serial /
USB-attached flashes — across every chip family ESPHome's OTA
component supports: ESP32, ESP8266, RP2040 / RP2350, the
LibreTiny family (BK72xx, RTL87xx, LN882x), and the nRF52 line.
For a serial install the receiver compiles the full
bootloader / partitions / firmware image set and ships it back;
the USB flash itself still runs on the sending host, since
that's where the device is plugged in.
Receiver / sender ESPHome-version drift is governed by a
version-match policy on the sending side (Settings → Send
builds):
any, release (major version must match), exact,
or exact_required — the last refuses to build rather than
falling back to a local compile when no version-compatible
receiver is paired.
Version history
The dashboard keeps a local, git-backed history of your device
configs. Every change to a YAML in the config directory is committed
automatically shortly after it lands on disk, whether the edit came
from the dashboard, an external editor, a script, or an AI agent
working in the directory. The history powers viewing, diffing, and
restoring earlier versions of a config, including bringing back a
deleted one. Today those are exposed as the
version_history/*
commands in docs/API.md; a recovery UI in the
dashboard is planned. Recording starts now precisely so that when
that UI ships, your history is already there.
Why it's on by default. A history only helps if it already
exists the moment you need it, and that moment is always right
after the bad edit or the accidental delete, never before. Making
it opt-in would protect exactly the people who least need it: those
who already knew the feature existed. For everyone else, the first
bad edit would also be the moment they learn there was no history.
So it stays on, quietly, like a backup should.
What it does on disk:
- If the config directory is not already a git repository, one is
created for it, with a .gitignore that keeps secrets.yaml out
of history.
- If the directory already is a git work tree (or sits inside one,
as /config/esphome commonly does), it is adopted rather than
re-initialised. Automatic commits are scoped to exactly the files
that changed, so your own staged work is never swept into one,
and your .gitignore is never modified. Device Builder's own
machine state (sidecars, keys, pairing files) is kept out of
history via the repo-local .git/info/exclude.
- Your secrets file is never committed.
- Commits are authored as
ESPHome Device Builder , with hooks
and commit signing skipped (--no-verify,
-c commit.gpgsign=false). This is deliberate: these commits
happen unattended in the background, and an unattended commit
must never hang on a signing passphrase prompt or trip an
interactive hook. The identity is passed per invocation; your
global and per-repo git configuration is never written to.
- If
git isn't installed, the feature quietly stays off.
Turning it off. Settings → enable Expert mode → turn off
Save version history. (Programmatically: the
version_history_enabled preference via config/set_preferences.)
Turning it off stops new commits and creates no repository; an
existing history stays readable and restorable. If your config
directory lives in a git repository you manage yourself (your own
identity, signing, hooks, branch protection) and you don't want
automatic commits alongside yours, this toggle is for you.
Policy note. The default has been discussed and decided:
version history ships on. New issues asking to flip the default,
or relitigating the commit identity / signing / hook behaviour
described above, will be closed with a pointer to this section.
Bug reports about the feature misbehaving are always welcome.
Device status and
name_add_mac_suffix
Configs that keep
name_add_mac_suffix: true are out of scope for
status tracking; they show as offline or stay unknown. The
option is a provisioning feature: one YAML flashed to a whole batch of
devices, each announcing itself as so the
units can be told apart out of the box. The dashboard tracks one device
per YAML file, keyed on esphome.name, so a suffixed broadcast can't
be safely linked back to a config — several units may be running the
same YAML at once, and guessing promotes an arbitrary unit's address
and version onto the card. Adopting the device is the supported path:
the adopt flow bakes the suffixed name into esphome.name as a literal
and turns the option off, which is the intended production state. If
you manage the YAML by hand instead, give each device a unique literal
name.
Policy note. Mapping suffixed broadcasts back to a base-named
config has been discussed and declined
(issue #2480).
New issues asking for it will be closed with a pointer to this
section.
Roadmap
- ✅ Standalone backend with WS-first API, persistent compile queue, mDNS device discovery
- ✅ Curated board + component catalogs (nightly catalog sync from upstream ESPHome)
- ✅ Functional parity with the legacy dashboard
(one intentional decline: the HA Supervisor /auth POST flow —
the new backend's HA add-on path is ingress-only by design, see
issue #85)
- ✅ Bundled as the default dashboard in the Home Assistant add-on
(Stable, Beta, and Dev channels; as of ESPHome 2026.6.0)
- ✅ Backend selector in ESPHome Desktop
≥ v0.7.0 (system tray → Backend)
- ✅ Bundled in the standalone ESPHome Docker image
(ghcr.io/esphome/esphome ≥ 2026.6.0; the dashboard command runs the
Device Builder)
- 🗺️ See the
project backlog
for in-progress work and what's planned next
Documentation
- docs/ARCHITECTURE.md — controllers, event bus,
firmware queue, catalog sync, deployment.
the internals of the pair flow, peer-link transport, and build
scheduler behind the "Send builds" feature above.
- docs/API.md — every WebSocket command, request/response
shapes, event types.
board (and component) contributor guide: manifest schema plus the
workflow for adding or updating a board (edit the manifest, then run
python script/update_board.py to regenerate and validate).
Contributing
Contributions welcome — board definitions especially
(definitions/README.md).
Every PR needs exactly one label from this set so it lands in the right
release-notes section:
breaking-change, new-feature, enhancement,
bugfix, refactor, docs, maintenance, ci, dependencies. CI enforces
the rule via pr-labels.yaml`.
Bugs / feature ideas: open an issue and the chooser will route you to the right venue (this repo for dashboard bugs, esphome core for compile/firmware issues, org Discussions for ideas, Discord for chat).
License
Apache-2.0 — Maintained by Open Home Foundation.