On this page (7)
  1. Verified compose file
  2. Install steps
  3. Measured resources
  4. Container vs HA OS: what you lose
  5. Gotchas, upgrade and backup
  6. What we did not measure
  7. Related

This Home Assistant Docker Compose file runs the official Container image, ghcr.io/home-assistant/home-assistant 2026.10.2, with host networking, privileged mode and one config volume, as in the Home Assistant install docs. We first verified it on 2026.9.4 on Oct 7, 2026, and re-measured it on 2026.10.2 on Oct 21 as the median of three runs on a GitHub Actions runner. The web server answered 4.2 s after docker compose up. The container idled at 309 MiB (range 305 to 310) and peaked at 309 MiB (range 309 to 313) with 122 entities and eight automations producing about 7 state changes a second on a 2-vCPU lab host, so by our sizing rule it needs 1 GB. Four things to know first: host networking replaces ports:, a Container install has no apps (add-ons), a USB radio needs a device mapping that we could only research, and updates and backups are yours to run.

Measured readings

Test box: Home Assistant 2026.10.2

Measured in our lab on : 3 full runs, each from a fresh stack. Each figure is the median, with the range over the runs under it. The numbers come from the raw result file.

Idle RAM
309 MiBrange 305–310 · 3 runs all containers, median of 3 runs; each run is the mean over 30 s after the stack settled
Startup peak RAM
342 MiBrange 337–347 · 3 runs highest total between healthy and settled
Peak RAM
309 MiBrange 309–313 · 3 runs under load, median of the run peaks
Settled after
141 ssame when rounded in all 3 runs after the first healthy response
Idle CPU
0 %range 0–0.1 · 3 runs of one core, lab host
Startup
4.2 srange 3.7–4.4 · 3 runs to first healthy response
App version
2026.10.2
Tested
, 3 runs (compose file verified the same day)
How the figures are made
Each run starts a fresh stack and goes through setup, settling, the idle window and the load step. We publish the median over the runs and the lowest and highest value, because the lab host and some apps vary from run to run. Runs, median and range
Lab host
AMD EPYC 9V45 96-Core Processor, 2 cores, 7.7 GiB RAM, x86_64.Ubuntu 24.04.5 LTS, Docker 28.0.4.
Disk for the images
About 3.4 GB with the containerd image store, the default on fresh installs of Docker Engine 29 and later (our estimate: unpacked plus compressed size, plus 10%; not measured on a containerd host). About 2.4 GB with the older overlay2 store (the unpacked column, as measured on this lab host). Plan for the larger figure. How we size disk
How idle was measured
After the stack was healthy and set up, we sampled total RAM until it stayed within max(2% of its mean, 10 MiB) over 60 s (at least 120 s, at most 600 s), then measured idle over the next 30 s. Rule in full
Peak RAM measured
Under load. Eight automations that fire all the time, plus one client. A Python script (standard library only, on the host network) logs in as the owner and creates the automations through the config API the automation editor uses (POST /api/config/automation/config/<id>). Six time_pattern automations (every 2, 3, 4, 5, 6 and 7 seconds) toggle or set the demo lights, switches, fans, a cover, a humidifier and a thermostat (temperatures in degrees F, the lab's unit system), one automation reacts to a light's state change by firing an event, and one writes a persistent notification once a minute. The script then keeps one WebSocket subscription to state_changed events for 120 seconds and counts them, and reads the server's error log at the end to count automation errors (it reports the number; the run expects 0). Every state change and event is written to the recorder's SQLite database. No dashboard is open, no mobile app, no voice, no real devices.
Per-container figures for Home Assistant: median over 3 runs, with the range
ContainerIdle RAMPeak RAM (under load)Image, compressed (download)Image, unpacked (disk, overlay2 store)
homeassistant309 MiBrange 305–310 · 3 runs309 MiBrange 309–313 · 3 runs653 MB2.4 GB
Every run (3)
Total RAM, startup and CPU model of each run of Home Assistant
RunIdle RAMPeak RAMStartup peakStartupLab host CPU
1310 MiB313 MiB342 MiB4.4 sAMD EPYC 9V45 96-Core Processor
2305 MiB309 MiB347 MiB4.2 sAMD EPYC 9V45 96-Core Processor
3 (closest to median idle)309 MiB309 MiB337 MiB3.7 sAMD EPYC 9V45 96-Core Processor
Total RAM after the stack became healthy (10 s averages, run 3 of 3)
Total RAM across all containers in 10 s averages, from the moment the stack was healthy until the end of the idle window (180 s). Run 3 of 3, the one closest to the median idle. Highest 10 s average 309 MiB at 170 s, last 309 MiB at 180 s. Dashed line: the stack counted as settled at 141 s.

0 s to 180 s after healthy; the line spans 0 to 309 MiB. Dashed line: settled.

Timeline as a table
Total RAM, 10 s average ending at each time, every 30 s after healthy
Seconds after healthyTotal RAM
30309 MiB
60308 MiB
90309 MiB
120309 MiB
150309 MiB
180309 MiB

RAM is MiB (220 bytes); image sizes are MB (106 bytes). The unpacked column is what our lab host's Docker reported; see Disk for the images for other setups. Per-container peaks happen at different moments, so they do not add up to the total peak. Medians of separate columns need not add up either.

Lab notes (5)
  • Image: the official `ghcr.io/home-assistant/home-assistant`, Home Assistant Container, `privileged: true` and `network_mode: host` as in the docs' compose file. No add-ons (apps) and no Supervisor in this install type. No USB device, Bluetooth, Zigbee or Z-Wave hardware in the lab: those are not tested.
  • Host networking: the harness health check and steps run in throwaway containers with `--network host`, because the Home Assistant container has no address on the Compose network. localhost in test.yaml is the daemon host (the VPS in remote mode).
  • Onboarding creates the default integrations google_translate, met (weather), radio_browser and shopping_list, as the first run of the frontend does. The lab host (GitHub Actions runner) has direct internet access. In the earlier sandbox lab a TLS-intercepting proxy made the ones that fetch data log certificate errors (observed 2026-10-07, Home Assistant 2026.9.4); the result still shows what the container idles at.
  • Adding `demo:` needs a restart (it is a YAML integration). The step waits until Home Assistant reports RUNNING and then restarts it (a restart request during the first seconds, state NOT_RUNNING, is ignored). The restart happens in the setup step, so the startup peak figure includes it.
  • The load step counts state_changed events with a WebSocket subscription; that client is part of the load.

Verified compose file

One service, homeassistant, from the official image. The file follows the Docker Compose tab of the Linux install page and differs from it in these ways:

  • Pinned tag. 2026.10.2, not stable. The lab wrote the image digest it pulled (sha256:33afb065...) into the file’s comment. stable moves to the next monthly release when it ships, so a docker compose pull on a stable file can move you a whole release: it pointed at 2026.9.4 when we first tested on Oct 7 and at 2026.10.0 on Oct 8 (we did not check where it pointed on Oct 15 or Oct 21).
  • A named volume for /config. The docs use a folder path (/PATH_TO_YOUR_CONFIG:/config). A folder lets you edit configuration.yaml straight from the host; a named volume keeps the file free of host paths. Either works with the commands below.
  • Left out: container_name (so two projects can run side by side, which the restore test needs), and the /run/dbus mount, which the docs call “optional but required if you plan to use the Bluetooth integration”. We had no Bluetooth adapter to test it with.
  • Kept as documented: privileged: true, network_mode: host, restart: unless-stopped, stop_grace_period: 60s, a TZ and the read-only /etc/localtime. The docs explain the grace period, and the Gotchas section shows what we saw.

The comments in the file are the lab’s.

compose.yamlVerified 2026-10-21 · Home Assistant 2026.10.2Download compose.yaml for Home Assistant
# Verified compose for Home Assistant, tested 2026-10-21 by the SelfHostBench lab (3 runs; the figures are the median).
# Lab host: AMD EPYC 9V45 96-Core Processor, 2 vCPU, 7.7 GiB RAM, x86_64, Docker 28.0.4.
# Floating image tags below were replaced by the version resolved at test time;
# the registry digest is in the comment on each image line.
# Measurements: home-assistant.json (schema 3).

# Home Assistant Container (official image), as in the "Linux" install page of the Home Assistant docs, Docker Compose tab:
# https://www.home-assistant.io/installation/linux/
# Differences from that example, all marked below: a named volume for /config instead of a folder path, no container_name,
# no /run/dbus mount (it is only needed for the Bluetooth integration), and a pinned version instead of `stable`.
# There is no TLS in this file. Put a reverse proxy in front before you expose it (see the article).
name: home-assistant

services:
  homeassistant:
    # Pin a release tag. `stable` follows every monthly release (2026.9 -> 2026.10 ...), so a `docker compose pull` can move you a whole release.
    image: ghcr.io/home-assistant/home-assistant:2026.10.2  # sha256:33afb0659d786213264c2211f8d91c5fe87461c56eba6a3a546eb05fc71e89f4  (version 2026.10.2, from label:org.opencontainers.image.version)
    restart: unless-stopped
    # From the docs: "Home Assistant can need longer than [10 s] to close its database". Without this, Docker kills it after
    # 10 s and the next start can log "The system could not validate that the sqlite3 database ... was shutdown cleanly".
    stop_grace_period: 60s
    # The docs' compose file and docker run command both set this. It gives the container every host device and all capabilities.
    # We kept it as documented. In our lab, onboarding and the demo integration ran the same without it; USB radios and Bluetooth
    # were not tested either way (see the article).
    privileged: true
    # The container shares the host's network stack, so there is no `ports:` (it would be ignored). Web UI on host port 8123.
    # Discovery protocols such as mDNS (Zeroconf) and SSDP look for devices on the local network. Not tested in our lab (no LAN devices).
    network_mode: host
    environment:
      # Your time zone, a tz database name (docs: "TZ=America/Los_Angeles"). Used for automations and the history.
      TZ: America/New_York
    volumes:
      # Settings, users, automations, the recorder database (home-assistant_v2.db) and the backups Home Assistant creates (/config/backups).
      # This is your install: back it up, and keep a copy of the backups somewhere else.
      # The docs use a folder path here (/PATH_TO_YOUR_CONFIG:/config). A named volume keeps this file free of host paths.
      - config:/config
      - /etc/localtime:/etc/localtime:ro
    # The image has no HEALTHCHECK. The lab waits for the web server to answer on http://localhost:8123.

volumes:
  config:

Install steps

We ran these steps on our earlier sandbox lab host on Oct 7, 2026 with Home Assistant 2026.9.4 (Docker 29.6.2, Compose 5.3.1). We did not repeat them on 2026.10.2 or on the Actions runner. The headline numbers on this page come from the lab run in home-assistant.json (2026.10.2, Oct 21). The rest come from manual checks on 2026.9.4, and the raw output of every one is in lab/notes/home-assistant-2026-10-07.md in the lab repository. The manual steps ran with their own Compose project names (-p), which also name the volumes; leave -p out and the file’s name: home-assistant applies.

  1. Install Docker Engine with the Compose plugin. The docs require Docker Engine 23.0.0 or later and say that Docker Desktop will not work.

  2. Make a folder and save the file above as compose.yaml in it. The download link is on the block.

    bash
    mkdir homeassistant
    cd homeassistant
  3. Set TZ in the file to your time zone, a tz database name such as America/Chicago or America/Toronto. We left America/New_York.

  4. Start it. The first start downloads the image: 652.7 MB compressed for 2026.10.2 (table below).

    bash
    docker compose up -d
  5. Check the container and the log. Our output of the first command on 2026.9.4, for a stack named hanet:

    bash
    docker compose ps
    docker compose logs --tail 5 homeassistant
    text
    NAME                    IMAGE                                            COMMAND   SERVICE         CREATED         STATUS         PORTS
    hanet-homeassistant-1   ghcr.io/home-assistant/home-assistant:2026.9.4   "/init"   homeassistant   8 seconds ago   Up 7 seconds

    The PORTS column is empty because of host networking (next section). At its default log level the container prints a few s6 startup lines and then only warnings and errors, so a quiet log is normal.

  6. Open http://<host>:8123 in a browser, where <host> is the machine’s name or IP, and follow the onboarding screens. Not run in our lab: the browser. The docs list five steps: open the address, create or restore an account, set the home location (time zone, unit system and currency), choose whether to share anonymous data, and finish (onboarding docs). The lab did the same through the web server’s onboarding API: it created the owner user, finished the core config, analytics and integration steps, and saved a name, location, time zone, units and currency. Those endpoints are not in the user docs; they are in the onboarding source at the 2026.9.4 tag. A source diff to 2026.10.0 showed the same routes and required fields, with only a validation-library rename (lab/notes/home-assistant-2026-10-08.md); the lab’s own onboarding calls worked on 2026.10.0 (Oct 8), 2026.10.1 (Oct 15) and 2026.10.2 (Oct 21). After onboarding the status endpoint answers 404, so a script that polls it must not wait for it forever.

  7. If the page does not load from another machine, open TCP port 8123 in the host’s firewall. The docs give this for UFW systems such as Debian. Not run in our lab. This serves plain HTTP to every machine that can reach the host, so we would open it to your home network only and not forward it from your router; for access from outside, put HTTPS in front of it (see Reverse proxy below).

    bash
    sudo ufw allow 8123/tcp

To stop the stack, docker compose down removes the container and keeps the named volume unless you add -v, which deletes it, with your config, users and history. Not run in our lab: the plain command on a long-lived stack. The lab’s own runs used down -v at the end.

Measured resources

Source: home-assistant.json, three runs on Oct 21, 2026, each from a fresh stack, on the lab host (a GitHub Actions ubuntu-latest runner: 2 vCPU AMD EPYC 9V45, 7.7 GiB RAM, Docker 28.0.4). Every figure below is the median of the three runs, with the range (lowest to highest run) in brackets where it matters (why we repeat runs). In each run idle is the mean of 16 samples over 30 s, taken after the total memory had settled (141 s after the stack answered its first health check, in all three runs; rule on the methodology page). At that point the owner existed, the location was set, and the built-in demo integration had added 122 entities (40 domains, demo: in configuration.yaml, then one restart). No automation was running and nobody was connected.

The load was eight automations that fire all the time, plus one client. A Python script on the host network logged in as the owner and created the automations through the config API that the automation editor uses. Six time-pattern automations (every 2, 3, 4, 5, 6 and 7 s) toggle or set the demo lights, switches, fans, a cover, a humidifier and a thermostat. One more reacts to a light’s state change by firing an event, and one writes a persistent notification once a minute. The script then held one WebSocket subscription to state_changed events for 120 s and counted them: 845 to 850 per run, 7.0 a second, all written to the recorder’s SQLite database. It found no automation error in the error log in any run. There was no dashboard open, no phone app, no voice assistant and no real device.

Container Idle RAM, settled (MiB) Startup peak (MiB) Peak RAM under load (MiB) Image compressed (MB) Image unpacked (MB)
homeassistant 308.76 (304.82 to 310.25) 342.10 (336.80 to 347.20) 309.20 (309.00 to 313.00) 652.7 2,360

Each cell is the median of the three runs with the range in brackets. The unpacked size is what Docker reports on the lab host, which has Docker 28.0.4 with the classic overlay2 image store. Docker 29 and later uses the containerd image store on fresh installs, and that store also keeps the compressed layers, so the same image takes more disk there: the earlier host (Docker 29.6.2, containerd store) showed 3,440 MB for the 2026.9.4 image. For disk we plan for unpacked plus compressed size plus 10%, about 3.4 GB for this image against 2.4 GB on overlay2 (how we size disk). CPU: the mean is the median of the three runs’ means, and the highest sample is the highest of any run.

Phase CPU, mean (% of one core, lab host) CPU, highest sample (% of one core, lab host)
Idle 0.04 0.4
Load, 8 automations 1.1 26

What the numbers say:

  • Idle RAM was 309 MiB, and the load added 0.4 MiB to the median (4 MiB at most in a single run). The startup peak, taken from healthy until the memory settled, was higher, 342 MiB (337 to 347); that window includes the restart that loads the demo integration. By our sizing rule (the highest figure of any run, 347.2 MiB, times 1.5, rounded up to 1, 2, 4, 8, 16 or 32 GB) the stack needs 1 GB.
  • Against 2026.10.1 (Oct 15) the idle figure is 56 MiB lower (365 MiB before) and the start-up peak 68 MiB lower (410 MiB before). The ranges do not overlap: all three new idle runs (305 to 310 MiB) are below all three old ones (353 to 365 MiB). Version and host changed in the same step. The Oct 15 runs were on an AMD EPYC 7763 with kernel 6.17.0 and runner image 20261012.334.1; these runs were on an EPYC 9V45 with kernel 7.0.0 and image 20261018.339.1 (the machine type, Docker 28.0.4 and the overlay2 store are the same). So we cannot credit the drop to 2026.10.2, and we cannot credit it to the host. Earlier figures on this page: 358 MiB on 2026.10.0 (Oct 8, same CPU model as Oct 15) and 311 MiB on 2026.9.4 (the earlier sandbox host, 4 vCPU Intel Xeon at 2.80GHz, 15.7 GiB, Docker 29.6.2). The 2026.10 release notes name no memory, recorder or database change that we found when we read the backward-incompatible section in full (lab/notes/home-assistant-2026-10-08.md).
  • What the new runs show about the 309 MiB, and what we do not know. The lab now saves each container’s memory.stat. Right after the idle window it read 288 to 293 MiB of anonymous memory and 43 to 45 MiB of file cache, all of it inactive cache, which docker stats leaves out of its figure. The Oct 15 result predates that field, so we cannot say whether the anonymous part, the cache or something else moved. The one cross-check we have is another container. The same Valkey image (same digest) in the Immich stacks idled at 9.7 to 11.7 MiB (medians) in the five Oct 15 stacks and at 9.6 to 11.0 MiB in the three Oct 21 runs of one stack, all on kernel 6.17 runner images, and at 4.8 to 6.4 MiB in 11 of 12 Oct 21 runs on the kernel 7.0 image (10.7 MiB in the twelfth), on three CPU models. So the runner image can change what docker stats reports for an unchanged container, by about 5 MiB for Valkey. That does not show that it moved Home Assistant by 56 MiB, and Postgres, also unchanged, moved by between -40 and +9 MiB across the five stacks with no consistent direction, so it settles nothing either way. Not tested: 2026.10.1 on the current runner image, or 2026.10.2 on the old one. Until one of those is run, treat 309 MiB as the 2026.10.2 figure on this runner image and 365 MiB as the 2026.10.1 figure on the old one.
  • That is a quiet install. 122 entities and one WebSocket client is not a home with a Zigbee network, a camera, a voice pipeline or years of history. We did not measure those, so treat 309 MiB as the number for this setup, not as a forecast for yours.
  • The web server answered /api/onboarding 4.2 s after docker compose up (3.7 to 4.4 s, image already pulled). That is the lab’s health check, and it is early: in a separate probe on 2026.9.4 on a fresh volume on the sandbox host the server answered at 6.0 s and the state was RUNNING at 10.8 s. The memory took 141 s after the first health check to settle in all three runs, because the lab waits for flat memory, not for a ready screen.
  • Idle CPU was 0.04% of one core (0.01 to 0.07 over the three runs). Under the load the mean was 1.1% (0.4 to 1.2 over the runs). Two of the three runs had one sample near 26% about 90 s into the load (26.2% and 25.6%; the neighbouring samples stayed under 4%) with no change in memory; we did not look into what caused it, and the highest sample in the Oct 15 runs was 3.6%. These are for the lab host only.
  • Internet calls: finishing onboarding sets up four default integrations: Google Translate text-to-speech, the Met weather service, Radio Browser and the shopping list (onboarding source, 2026.9.4). Our earlier sandbox host sat behind a TLS-inspecting proxy, so those lab runs mounted the proxy’s CA into the container; without it, our manual runs there logged certificate errors for Radio Browser and for Home Assistant’s alerts feed (observed Oct 7, 2026, 2026.9.4), and Home Assistant started and ran regardless. The Actions runner has direct internet access and the 2026.10.1 and 2026.10.2 runs mounted no CA bundle.

Database growth

The recorder writes states and events to home-assistant_v2.db in /config (SQLite, the default; recorder docs). We measured it on 2026.9.4 on the sandbox host, on a separate stack with the same setup and the same eight automations, run for 300 s so the growth would show (lab/notes/home-assistant-2026-10-07.md, section 4). Not repeated on 2026.10.1 or 2026.10.2. The database runs in WAL mode, which makes it two files, so we report the logical size, page_size times page_count, which counts both.

Moment Logical size (KiB) states rows events rows
Load start 396 229 1,049
After 112 s, about the lab’s 120 s load 624 1,014 1,550
After 306 s 968 2,398 2,427

The first 112 s of the load added 228 KiB. All 306 s added 572 KiB, about 165 bytes per new row in states and events (an average over everything the database stores), or 1.9 KiB a second at about 7 state changes a second. On disk the main file was 820 KiB and the WAL 4,140 KiB at the end, since the WAL stays near 4 MB. A straight-line extrapolation of this synthetic load, which we did not run that long, comes to about 165 MB a day and 1.65 GB over the default 10 days of history (purge_keep_days, default 10). Your rate is your own: count your state changes. The recorder docs ask for at least 2.5 times the database size in free disk space for the case where SQLite has to recover from corruption.

Container vs HA OS: what you lose

Home Assistant offers two installation types, and recommends Home Assistant OS (“the recommended installation type for most users”, install overview). Container means you bring the operating system, the container runtime and the updates. We tested the Container side only. The HA OS column is the docs’ statement; we ran no HA OS.

Feature HA OS Container Source
Automations, dashboards, integrations, blueprints Yes Yes Docs, read Oct 7, 2026
Apps (called add-ons before the 2026.2 release) Yes No: “does not have access to apps” Docs, read Oct 7, 2026
Supervisor Included Not loaded: no hassio component among the 177 that loaded (2026.9.4) Docs for HA OS (Linux install page, read Oct 7, 2026); Tested for Container
One-click updates Yes No: docker compose pull and up -d Docs; the commands Tested
Backups Yes Yes: create and restore both worked in our lab Docs; Tested for Container
Thread and Z-Wave Controlled by apps “No out-of-the-box support” Docs, read Oct 7, 2026
Operating system and Docker maintenance Handled by HA OS Yours Docs (“bring your own system”)
Docker Desktop Not applicable Does not work; use Docker Engine Docs, read Oct 7, 2026

The practical loss is the apps. The docs describe them as “additional standalone third-party software packages that can be installed on Home Assistant OS”. In a Container install, any software like that is something you run yourself, as another container or on the host, and Home Assistant does not manage it. We ran none of it. Our Jellyfin and Nextcloud guides show the same single-host Compose pattern for other services.

Gotchas, upgrade and backup

The manual checks in this section ran on Home Assistant 2026.9.4 on our earlier sandbox host (Oct 7, 2026). We did not repeat them on 2026.10.2 or on the Actions runner. The 2026.10 release notes’ backward-incompatible section names nothing about host networking, the HTTP integration, backups or the Docker image (we read it in full), so we have no reason to expect a change, but we did not test one.

Host networking and port 8123

network_mode: host puts the container in the host’s network stack. The Docker docs say published ports are discarded in host mode and the container gets no IP address of its own. What we saw on 2026.9.4 (lab/notes/home-assistant-2026-10-07.md, section 7):

  • docker compose ps shows an empty PORTS column, docker port prints nothing, and docker inspect shows the network host and no port bindings.
  • Port 8123 is listening on all of the host’s addresses (00000000:1FBB in /proc/net/tcp; 0x1FBB is 8123).
  • Inside the container the network interfaces are the host’s own: lo, eth0, docker0 and two ifb devices, the same list as on the host. A container on Docker’s default bridge sees only lo and eth0 with a 172.17.0.x address.
  • A file with ports: ["8200:8123"] next to network_mode: host still starts. Compose prints “Published ports are discarded when using host network mode”, and nothing listens on 8200.

Why host networking: the docs’ compose file sets it and does not say why. The reason that fits is discovery, our inference and not a docs statement: Home Assistant’s Zeroconf (mDNS) and SSDP integrations look for devices on the local network, and a container on Docker’s default bridge network is not on your LAN. Not tested in our lab: discovery itself, because the lab has no LAN devices. The cost follows from the shared network namespace: any other service on the host that wants port 8123 conflicts with it, and the container sits in the same network namespace as the host’s own services. To use another port, set one in the HTTP server settings (see Reverse proxy below), not in ports:.

Privileged mode

The docs’ compose file and docker run command both set privileged: true. In our lab it gave the container every host device and all capabilities: 111 entries in /dev instead of 14, and a capability mask of 000001fffeffffff instead of 00000000a80425fb (notes, section 1). A privileged container gets “nearly all the same access to the host as processes running outside containers” (Docker’s runtime privilege docs, which say to use the flag “with caution”), so a flaw in Home Assistant or in anything you add to it is a flaw on the host. We kept it because the docs do. Without it, onboarding, the demo integration and the restart ran the same, with the same 122 entities. We did not test Bluetooth, USB radios or anything else that needs a device, with or without it, so we cannot say what removing it breaks for you.

Clean shutdown

The docs warn that Home Assistant “can need longer than [10 seconds] to close its database”, and that when Docker stops it early, the next start logs “Ended unfinished session” and “could not validate that the sqlite3 database … was shutdown cleanly”. That is why the file has stop_grace_period: 60s. On our stack, with a database of about half a megabyte, docker compose stop took 5.3 s with the 60 s limit in place. A forced stop after 1 s (docker stop -t 1, exit code 137) did not produce those warnings at the next start. So we could not reproduce the problem the docs describe; they tie it to a database that takes longer to close than the timeout, and ours is tiny. We kept the 60 s as documented. The same two warnings did appear after our restore tests below, where Home Assistant swaps the database files and restarts.

USB devices (researched, not run)

We have no USB radio, so this section is researched from the docs and not run. The Linux install page says that for Zigbee and other integrations that need a device you map it into the container, make sure the user running the container may access the /dev/tty* file, and add the mapping to the compose file:

Not run in our lab:

yaml
services:
  homeassistant:
    # ...
    devices:
      - /dev/ttyUSB0:/dev/ttyUSB0

Use the path of your own adapter. What we did run is the mechanism, with the lab VM’s only serial port as a stand-in: the file without privileged, plus devices: [/dev/ttyS0:/dev/ttyS0]. The container then listed /dev/ttyS0 and 14 other entries, nothing more (notes, section 2). That shows the devices: key maps one node into an unprivileged container. It does not show that a Zigbee or Z-Wave stick works. Thread and Z-Wave are controlled by apps in HA OS and have no out-of-the-box support in Container installs, per the install overview.

Reverse proxy

Home Assistant refuses requests that arrive through a proxy until you tell it to trust the proxy. Since 2026.8 the web server settings are in the UI: Settings, System, Network, HTTP server. Turn on Trust X-Forwarded-For and add the proxy under Trusted proxies (HTTP integration docs, which say that “requests from reverse proxies are blocked if these options are not set”). A network mask must be the network address, such as 192.168.1.0/24, not a host address. Saving restarts Home Assistant and asks an administrator to confirm within 5 minutes; if nobody does, it returns to the previous settings. Before 2026.8 these were use_x_forwarded_for and trusted_proxies under http: in configuration.yaml, and the first start after the upgrade imports them into the UI and raises a repair issue (2026.8 release notes). Not tested in our lab: that import. Our test update started from 2026.8.3, which is already past it. We did not re-run the proxy test below on any 2026.10 release.

What we ran on 2026.9.4, on the same sandbox machine: Caddy 2.11.4 on the host network in front of the verified file.

bash
docker run -d --name hapx-caddy --network host caddy:2.11.4 caddy reverse-proxy --from :8081 --to 127.0.0.1:8123

Before any change, a request through Caddy came back as 400 Bad Request, and Home Assistant logged “A request from a reverse proxy was received from 127.0.0.1, but your HTTP integration is not set-up for reverse proxies”. We then set Trust X-Forwarded-For and the trusted proxy 127.0.0.1 through the WebSocket admin commands that store the HTTP settings (http/config/configure, then http/config/promote to confirm; they are in the HTTP source). The lab has no browser, so we did not click the settings screen. Home Assistant restarted, and a request through Caddy returned 200. Not run in our lab: the settings screen itself, HTTPS, WebSocket traffic through the proxy, Nginx or Traefik, and a proxy in a container on a bridge network, where the address to trust would be that network’s gateway. For Caddy with automatic HTTPS in a Compose file, see the Vaultwarden Docker Compose guide; it is a different app, so only the Caddy part carries over. The docs also say a proxy or CDN must not cache /api/, /auth/, HTML pages or the service worker files.

To serve Home Assistant on another port, change Server port in the same settings, or set SETUP_PORT, which the docs say overrides the defaults at startup. In our restore test a copy with SETUP_PORT: "8124" and a stored port that was already taken fell back to 8124 and answered there, so the variable works as a fallback (see Back up and restore below).

Back up and restore

What to back up is the config volume: settings, users, automations, the recorder database and the backups Home Assistant makes (/config/backups). Those backups land inside the same volume, so copy them off it, or a lost volume takes them along. We ran three procedures on 2026.9.4, all in separate Compose projects (lab/notes/home-assistant-2026-10-07.md, section 8). The commands below keep the 2026.9.4 tag we ran; use the tag in your own file.

Home Assistant’s own backup. The Backup integration “creates and restores backups across all installation types”, and the update page asks you to store the backup and its emergency kit somewhere safe. Not run in our lab: the Backups screen, encrypted backups with the emergency kit, automatic backups, and the Home Assistant Cloud and network locations. The lab has no browser, so we called the backup.create action, which the integration page lists for starting a backup yourself, through the API. It wrote a 174 KB .tar into /config/backups. The file held a backup.json and a homeassistant.tar.gz with the config, .storage and the recorder database. It was not encrypted ("protected": false).

  • Restore on the same running stack. We created an automation after the backup, then restored through the WebSocket command backup/restore. Home Assistant restarted itself inside the container; Docker’s StartedAt and restart count did not change. Afterward the new automation was gone, 8 automations and 130 entities were back, and the owner still logged in. The log then had the two warnings from the shutdown section (“could not validate … shutdown cleanly” and “Ended unfinished session”). They did not appear after our stop tests, only after the restores.
  • Restore into a new instance. The docs say a backup can be used during onboarding to migrate to a new device. We removed the old stack, started a new container on an empty volume, and sent the backup file to the onboarding restore endpoints (/api/onboarding/backup/upload and /api/onboarding/backup/restore, in the backup source). The endpoints answered 404 for the first seconds and then 500 Backup manager busy: blocked until the manager reported idle, 12 s after the container started in our run. After the restore the new instance had version 2026.9.4, the old owner’s password, 130 entities, 8 automations and the old history.

Copying the volume. The other way is a copy of the volume. We made one from the running stack, with the volume mounted read-only. The volume is named <project>_config; docker volume ls shows yours.

bash
mkdir -p backup
docker run --rm --entrypoint tar -v halive_config:/config:ro -v "$PWD/backup":/backup ghcr.io/home-assistant/home-assistant:2026.9.4 czf /backup/ha-config.tgz -C /config .

tar exited 0 and wrote a 313 KiB archive of 49 entries. We took one copy this way, and it restored; we did not test whether a copy of a database in the middle of a write can fail, nor a copy with the container stopped. To restore into a separate project beside the running one, we used a second port, with an override file:

yaml
services:
  homeassistant:
    environment:
      SETUP_PORT: "8124"
bash
docker compose -p harestore1 -f compose.yaml -f override-8124.yaml create
docker run --rm --entrypoint tar -v harestore1_config:/config -v "$PWD/backup":/backup:ro ghcr.io/home-assistant/home-assistant:2026.9.4 xzf /backup/ha-config.tgz -C /config
docker compose -p harestore1 -f compose.yaml -f override-8124.yaml up -d

To avoid it, run this between the tar and the second up -d above. It is what we ran: it sets the stored port in the copy’s volume to the port of the override.

bash
docker run --rm -v harestore1_config:/config python:3.12-slim python -c "
import json
p='/config/.storage/http'
d=json.load(open(p))
d['data']['stable']['server_port']=8124
json.dump(d,open(p,'w'),indent=2)
print('stable server_port set to', d['data']['stable']['server_port'])"

During the volume-copy tests the original stack kept running: same start time, no restarts. Then we removed the copies with down -v. The restore into a new instance came last, after we had removed the original stack, so it never ran beside it.

Update

A Container install does not update itself, and stable moves when a release ships: the release notes index lists one feature release a month (2026.9 on Sep 2, 2026 and 2026.10 on Oct 7, 2026), and 2026.9.4, a patch, came out on Sep 27 (2026.9.4 release). The docs say to back up first and to read the release notes between your version and the new one for “Backward-incompatible changes”, then run:

bash
docker compose pull homeassistant
docker compose up -d

We ran this from 2026.8.3, the last 2026.8 release, to 2026.9.4. The stack had the owner, 122 entities, eight automations and 90 s of load history. We edited the tag in compose.yaml and ran the two commands. The image was already in the lab’s cache, so the pull took 1.9 s; on a new host expect the download (646.5 MB for 2026.9.4, 651.5 MB for 2026.10.1, 652.7 MB for 2026.10.2). up -d recreated the container in 7.0 s, and the state was RUNNING about 30 s after it started. Afterward:

  • The version was 2026.9.4, the owner’s password still logged in, all eight automations and 130 entities were there, and the history of one light kept its first point from before the update and gained new ones.

  • The recorder’s schema version stayed at 53, so there was no database migration. The log had no migration lines and no “unfinished session” warnings, and the repairs list was empty.

  • The docs’ config check passed on both versions, with exit code 0. The docs write it as docker exec homeassistant ...; our file has no container_name, so we ran it through Compose (with -T added, because our script has no terminal; leave it out in a shell):

    bash
    docker compose exec homeassistant python -m homeassistant --script check_config --config /config

The release notes for 2026.9 list backward-incompatible changes for Flexit Nordic (BACnet), KNX, LLM APIs, Persistent Notification, UniFi Protect, Update, Vacuum and Z-Wave JS (2026.9 notes). By their headings these are changes to individual integrations; we did not read each one against a Container install, and our demo setup uses none of those integrations, so this test says nothing about them. Read the list for the integrations you run.

Rolling back. We put the tag back to 2026.8.3 on the volume that 2026.9.4 had used. It started, reached RUNNING, and showed the same automations, entities and history, with no downgrade message in the log. That is one test of one minor step in which the database schema did not change, so it is not a promise for other releases. The pre-update backup that the docs ask for is the way back. Not run in our lab: updates over more than one release, from before 2026.8, and to a later patch.

On 2026.10. 2026.10.0 came out on Oct 7, 2026; this page’s numbers are for the patch release 2026.10.2. We did not run the update from 2026.9.4 to 2026.10.0, 2026.10.1 or 2026.10.2; the lab started fresh stacks. The 2026.10 release notes (read in full on Oct 8, lab/notes/home-assistant-2026-10-08.md) list these backward-incompatible items that could matter to a Container install, none of which touches what the lab exercises:

  • Authentication. “The deprecated legacy mode of the Home Assistant authentication provider has been removed. Usernames are now always handled in their normalized form: lowercase, and without spaces at the start or end.” Our owner user, labowner, was already in that form. If your usernames have capitals or spaces, read the note before you update.
  • State conditions. Conditions that combine for with a state attribute, a list of states or a state from another entity now fail validation. Our eight automations have no conditions, so this test says nothing about yours.
  • Integrations. Non-admin users now get “Unauthorized” for some actions in the MQTT and Synology DSM integrations. We run neither.

The notes name nothing about onboarding, the demo integration, the recorder, the HTTP integration and its proxy settings, backups, the REST API or the Docker image. A source diff of the calls our lab script makes (2026.9.4 against 2026.10.0) found that the login flow gained an optional PKCE step that our script does not use, and that the service-call API now returns HTTP 400 or 500 for some errors; the script needed no change, and the eight automations ran without an error in the log in all three runs. Read the full list for the integrations you run.

The 2026.10 changelog lists 2026.10.1, dated Oct 10, with 69 changes (read on Oct 15, 2026; titles only, we did not read the code changes; lab/notes/release-check-2026-10-15.md). One removes the user-configurable polling interval from the omnilogic integration, which we do not run; the GitHub release page labels it breaking-change (read through a web-fetch tool, which returns a summary, so unverified). Two change TLS verification: one restores unverified TLS for stream sources and adds a verify_ssl option, the other skips verification for UniFi Protect RTSPS streams. The lab uses neither. No title mentions memory, the recorder, the database, a migration or the Docker image.

The 2026.10.2 release page (released Oct 16, 2026; 50 titles, read on Oct 21, 2026 through a web-fetch tool, which returns a summary, so unverified; lab/notes/patch-batch-2026-10-21.md) has no item labelled breaking and lists no CVE. No title mentions memory, the recorder, a migration or the Docker image (one title is about the KNX integration’s own SQLite telegram store). Three titles touch behavior you may rely on: “Revoke refresh tokens when removing a user”, “Guard owner and existing credentials in auth provider create command”, and “Allow a list of states for state conditions with a duration” (the 2026.10.0 notes made that combination fail validation). Several are dependency bumps, among them aiohttp 3.14.5 and the frontend. Our lab uses none of those paths, and we did not test them.

What we did not measure

  • Real devices: Zigbee, Z-Wave, Bluetooth, Matter, cameras, and discovery on a real LAN.
  • Apps (add-ons) and Home Assistant OS, which we did not run at all.
  • A phone app, a browser dashboard, voice assistants, or any integration beyond the demo and the defaults that onboarding adds.
  • A real history: weeks of data, the purge job, a MariaDB or PostgreSQL recorder, and long-term statistics.
  • ARM hosts and other CPUs, a VPS, and memory growth over days.

The RAM figure in the module below comes from the memory we measured with 122 entities and the eight busy automations, and it says nothing about the network. Real installs add integrations. Home Assistant’s Zeroconf and SSDP integrations look for devices on the local network, so a rented VPS that is not on your LAN may not see them. We did not test a VPS.

Where to run it

  • Hardware. Home Assistant idled at 309 MiB and peaked at 309 MiB under load (medians of 3 runs) in our lab. The highest reading in any run was 347 MiB, counting the startup spike, which was higher than the load peak. Plan for at least 1 GB of RAM, sized from the highest run.
  • VPS. The smallest plan that runs this comfortably has 1 GB of RAM, plus disk for your data and the images. Plan for about 3.4 GB for the images (Docker 29 and later with the containerd image store, the default on fresh installs; 2.4 GB with the older overlay2 store).
  • Keep it safe. Back up the data volume before you rely on it, and put the app behind a reverse proxy with HTTPS before you expose it to the internet. See backing up self-hosted apps.

How we size: recommended RAM is the highest reading in any of our runs (under load or during startup, not the median) times 1.5, rounded up to the next of 1, 2, 4, 8, 16 or 32 GB. The extra half covers the operating system, Docker, file cache and spikes our sampling can miss. Your own data and extra apps need more. Disk for images: Docker Engine 29 and later uses the containerd image store on fresh installs, which keeps each image twice (as downloaded and unpacked); older installs use overlay2, which keeps it unpacked only. We plan for the larger figure: unpacked plus compressed size, plus 10%, rounded up. Your data volumes and growth come on top.

Update log

  • Linked the new Uptime Kuma Docker Compose page from Related. No measurements changed.
  • Re-tested on Home Assistant 2026.10.2 (was 2026.10.1), median of 3 runs on the same runner type but a different CPU model (AMD EPYC 9V45; was EPYC 7763) and a newer runner image (kernel 7.0.0; was 6.17.0); no config changes (image tag and digest only). Idle 309 MiB (range 305 to 310; was 365), peak 309 MiB (range 309 to 313; was 369), startup peak 342 MiB (was 410), web server up after 4.2 s (was 5.6). The sizing figure uses the highest run (347 MiB; was 418) and is still 1 GB. Version and host changed together, so we do not know why idle fell by 56 MiB; what we can and cannot say is under Measured resources. Image download 652.7 MB (was 651.5), unpacked 2,360 MB (was 2,350). The 2026.10.2 release page (50 titles, read through a web-fetch tool, so unverified) has no item labelled breaking, no CVE, and no title naming memory, the recorder, a migration or Docker. The manual findings were observed on 2026.9.4 and were not repeated.
  • Linked the new Authentik vs Authelia page from Related (login servers for a reverse proxy, measured on one lab host). No measurements changed.
  • Re-tested on Home Assistant 2026.10.1 (was 2026.10.0), median of 3 runs on the same runner type and CPU model (AMD EPYC 7763); no config changes (image tag and digest only). Idle 365 MiB (range 353 to 365; was 358), peak 369 MiB (range 353 to 369; was 366), startup peak 410 MiB (was 414), web server up after 5.6 s (was 5.7). The sizing figure uses the highest run (418 MiB; was 420) and is still 1 GB. The Test box note about internet access now comes from the corrected test definition, so the sentence about its old wording was removed. The 2026.10.1 changelog (69 titles) lists no memory, recorder or Docker change. The manual findings were observed on 2026.9.4 and were not repeated.
  • Linked the new AdGuard Home vs Pi-hole v6 page from Related. No measurements changed.
  • Disk sizing now covers both Docker image stores: plan for about 3.4 GB for the image (unpacked plus compressed size plus 10%, for Docker 29 and later with the containerd store), 2.4 GB on overlay2; was 2.4 GB only. Also dated the sandbox-era proxy notes. No new measurements.
  • Re-tested on Home Assistant 2026.10.0 (was 2026.9.4) on our new standard lab host (GitHub Actions runner, 2 vCPU / 8 GB), median of 3 runs. The earlier numbers came from a sandbox host with 4 vCPU and 15.7 GiB, so version and host changed together. Idle 358 MiB (range 350 to 373; was 311), peak 366 MiB (range 351 to 377; was 312), startup peak 414 MiB (was 331), web server up after 5.7 s (was 8.6). The sizing figure uses the highest run (420 MiB) and is still 1 GB. The 2026.10 release notes name no memory, recorder, HTTP or backup change, and our lab script needed no change. The manual findings (host networking, restore and the port 8123 trap, proxy, update from 2026.8.3) were observed on 2026.9.4 on the earlier sandbox host and were not repeated; the page now says so where they appear.
  • First published. Home Assistant 2026.9.4 (Container install) verified in the lab as the median of 3 runs. Tested: onboarding through the API, the demo integration with 8 busy automations, a native backup created and restored, a config-volume copy restored into a separate Compose project, an update from 2026.8.3, a rollback, host networking, and a Caddy proxy test. USB devices are researched from the Home Assistant docs and not run.