build: multi-stage Docker image with uv and BuildKit caching (#1261)

* build: multi-stage Docker image with uv and BuildKit caching

Builder/runtime split keeps the toolchain out of the runtime image
(standalone ~1.8GB -> ~1.2GB). Dependencies install with uv from
uv.lock in a cache-mounted, source-independent layer. Runtime keeps
the editable install because akkudoktoreos.core.version needs the
src/ layout. Also fixes the io.hass.version label and adds a
HEALTHCHECK.

* build: address Docker image review feedback

- Healthcheck honours EOS_SERVER__PORT instead of hardcoding 8503.
- BUILD_VERSION defaults to "dev" rather than the literal "VERSION";
  docker-compose passes the real version, and the redundant
  org.opencontainers.image.version label is dropped (CI sets it via
  docker/metadata-action).

* fix: use resolved server port for container healthcheck

---------

Co-authored-by: Normann <github@koldrack.com>
Co-authored-by: Normann Koldrack <normann.koldrack@desy.de>
This commit is contained in:
Robert Neumann
2026-09-07 08:13:09 +02:00
committed by GitHub
co-authored by Normann Normann Koldrack
parent e9bd55caac
commit 2d18547aaa
5 changed files with 128 additions and 43 deletions
+69 -43
View File
@@ -1,19 +1,67 @@
# syntax=docker/dockerfile:1.7
# Dockerfile
# Support both Home Assistant builds and standalone builds
# Only Debian based images are supported (no Alpine)
# Support both Home Assistant builds and standalone builds.
# Only Debian based images are supported (no Alpine).
ARG BUILD_FROM
ARG PYTHON_VERSION=3.13.15
# If BUILD_FROM is set (Home Assistant), use it; otherwise use python-slim
FROM ${BUILD_FROM:-python:${PYTHON_VERSION}-slim}
# Builder and runtime share the same base so the copied virtualenv is ABI-safe.
# If BUILD_FROM is set (Home Assistant), use it; otherwise use python-slim.
FROM ${BUILD_FROM:-python:${PYTHON_VERSION}-slim} AS builder
# uv: pinned, copied as a static binary (no extra Python packages installed).
COPY --from=ghcr.io/astral-sh/uv:0.12.7 /uv /bin/uv
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_PYTHON_DOWNLOADS=never \
UV_PROJECT_ENVIRONMENT=/opt/venv \
VIRTUAL_ENV=/opt/venv \
PATH="/opt/venv/bin:$PATH"
WORKDIR /opt/eos
# Build toolchain for the numpy/scipy/pandas/matplotlib stack. python3 is
# explicit because the Home Assistant base image ships without it.
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
gcc g++ gfortran \
libopenblas-dev liblapack-dev \
&& rm -rf /var/lib/apt/lists/*
# Resolve and install dependencies from the lock file first. This layer stays
# cached as long as pyproject.toml / uv.lock are unchanged.
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev --no-install-project
# Project sources and generated version (pyproject reads version from version.txt).
COPY src/ ./src
COPY scripts/get_version.py ./scripts/get_version.py
RUN python scripts/get_version.py > version.txt
# Install the project itself. Editable, because akkudoktoreos.core.version
# requires the src/akkudoktoreos layout at runtime; the runtime stage copies
# src/ back in.
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
FROM ${BUILD_FROM:-python:${PYTHON_VERSION}-slim} AS runtime
# Supplied by Home Assistant, docker-compose and CI; "dev" marks an
# unversioned local build. CI stamps org.opencontainers.image.version via
# docker/metadata-action, so it is not set here.
ARG BUILD_VERSION=dev
LABEL \
io.hass.version="VERSION" \
io.hass.version="${BUILD_VERSION}" \
io.hass.type="addon" \
io.hass.arch="aarch64|amd64" \
source="https://github.com/Akkudoktor-EOS/EOS"
source="https://github.com/Akkudoktor-EOS/EOS" \
org.opencontainers.image.source="https://github.com/Akkudoktor-EOS/EOS" \
org.opencontainers.image.licenses="Apache-2.0"
ENV EOS_DIR="/opt/eos"
# Create persistent data directory similar to home assistant add-on
@@ -21,6 +69,8 @@ ENV EOS_DIR="/opt/eos"
# - MPLCONFIGDIR: user customizations to Mathplotlib
ENV EOS_DATA_DIR="/data"
ENV EOS_CACHE_DIR="${EOS_DATA_DIR}/cache"
# Written by EOS after resolving its startup configuration.
ENV EOS_HEALTHCHECK_PORT_FILE="${EOS_CACHE_DIR}/eos-healthcheck-port"
ENV EOS_OUTPUT_DIR="${EOS_DATA_DIR}/output"
ENV EOS_CONFIG_DIR="${EOS_DATA_DIR}/config"
ENV MPLCONFIGDIR="${EOS_DATA_DIR}/mplconfigdir"
@@ -43,53 +93,29 @@ ENV PATH="$VENV_PATH/bin:$PATH"
WORKDIR ${EOS_DIR}
# Create eos user and data directories with eos user permissions
RUN apt-get update && apt-get install -y --no-install-recommends adduser \
&& adduser --system --group --no-create-home eos \
&& mkdir -p "${EOS_DATA_DIR}" \
&& chown -R eos:eos "${EOS_DATA_DIR}" \
&& mkdir -p "${EOS_CACHE_DIR}" "${EOS_OUTPUT_DIR}" "${EOS_CONFIG_DIR}" "${MPLCONFIGDIR}" \
&& chown -R eos:eos "${EOS_CACHE_DIR}" "${EOS_OUTPUT_DIR}" "${EOS_CONFIG_DIR}" "${MPLCONFIGDIR}"
# Install build dependencies (Debian)
# - System deps
# Runtime shared libraries only (no -dev packages, no compilers). Create the eos
# user and the persistent data directories with eos ownership.
RUN apt-get update && apt-get install -y --no-install-recommends \
python3-venv \
gcc g++ gfortran \
libopenblas-dev liblapack-dev \
adduser python3 libopenblas0 liblapack3 \
&& adduser --system --group --no-create-home eos \
&& mkdir -p "${EOS_DATA_DIR}" "${EOS_CACHE_DIR}" "${EOS_OUTPUT_DIR}" "${EOS_CONFIG_DIR}" "${MPLCONFIGDIR}" \
&& chown -R eos:eos "${EOS_DATA_DIR}" \
&& rm -rf /var/lib/apt/lists/*
# - Copy project metadata first (better Docker layer caching)
COPY pyproject.toml .
# - Create venv
RUN python3 -m venv ${VENV_PATH}
# - Upgrade pip inside venv
RUN pip install --upgrade pip setuptools wheel
# Install EOS/ EOSdash
# - Copy source
COPY --from=builder /opt/venv /opt/venv
# Editable install: the venv only points at the source tree, so it must be present.
COPY src/ ./src
# - Create version information
COPY scripts/get_version.py ./scripts/get_version.py
RUN python scripts/get_version.py > ./version.txt
RUN rm ./scripts/get_version.py
RUN echo "Building Akkudoktor-EOS with Python $PYTHON_VERSION"
# - Install akkudoktoreos package in editable form (-e)
# - pyproject-toml will read the version from version.txt
RUN pip install --no-cache-dir -e .
ENTRYPOINT []
EXPOSE 8504
EXPOSE 8503
EXPOSE 8504
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD ["python", "-m", "akkudoktoreos.server.container_healthcheck"]
# Ensure EOS and EOSdash bind to 0.0.0.0
# EOS is started with root provileges. EOS will drop root proviledges and switch to user eos.
# EOS is started with root privileges. EOS will drop root privileges and switch to user eos.
CMD ["python", "-m", "akkudoktoreos.server.eos", "--host", "0.0.0.0", "--run_as_user", "eos"]
# Persistent data