Flux source URL, flux-git rotator verify repo, review-bot REPOS, pipeline API, clone and push URLs, the heating-can source label and the review-bot and kuma-sync Go module paths. Merge right after the transfer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
123 lines
6.1 KiB
Docker
123 lines
6.1 KiB
Docker
# Heating system over the Viessmann E3 CAN bus: open3e (Vitocal heat pump and
|
|
# Vitocharge VX3) and E3onCAN (E380 grid energy meter).
|
|
#
|
|
# One image, two services. They read the same physical installation over the
|
|
# same bus and are versioned together, so they are one artifact -- but they run
|
|
# as two containers from this image so either can crash and restart on its own
|
|
# without a supervisor as PID 1.
|
|
#
|
|
# Why an image at all: both were pip-installed into venvs on the host, open3e
|
|
# straight from a git URL. pip cannot compare an installed VCS dist against a
|
|
# requested one without fetching it, so `pip install` re-cloned and rebuilt on
|
|
# every Ansible converge -- reporting changed, restarting the service, and
|
|
# putting a gap in heating telemetry each time. Pinning the SHA did not fix
|
|
# that (verified: pip still clones and rebuilds a pinned VCS requirement). A
|
|
# tagged image does: `docker compose up -d` on an unchanged tag is a genuine
|
|
# no-op.
|
|
#
|
|
# It also gives the weekly update-audit a normal artifact to track instead of
|
|
# two awkward special cases -- open3e publishes no tags at all, and E3onCAN is
|
|
# pinned to an untagged commit between releases.
|
|
#
|
|
# Runtime needs host networking: SocketCAN interfaces live in the host network
|
|
# namespace, so can0 is only reachable with network_mode: host. That is also
|
|
# how the local MQTT broker on 127.0.0.1:1883 is reached. CAN_RAW sockets need
|
|
# no capabilities, so both run unprivileged.
|
|
FROM python:3.15-slim@sha256:2da676046630d7bf14c6764ae1143dc7fa0377a53f94bf5a642f0b214aca5bce AS build
|
|
|
|
# Pins live here, in the artifact, rather than in Ansible variables.
|
|
#
|
|
# open3e: a commit, because the project publishes NO tags (its tags API returns
|
|
# an empty array). Self-reported package version at this commit: 0.7.5.
|
|
ARG OPEN3E_REF=6553c10453a6bae1210908f38a5af2481a46a3e5
|
|
# E3onCAN: an untagged commit on the default branch (2026-06-21, "Fixed: MQTT
|
|
# client ID collision caused stale retained values"). Later than the repo's
|
|
# only tag, v0.5.1.
|
|
ARG E3ONCAN_REF=461dcbdf97e8d72d2edb9929c63b39382af6c3a1
|
|
|
|
# git is needed to FETCH the sources and exists only in this stage. The runtime
|
|
# stage below copies the built venv and the E3onCAN source out, so neither git
|
|
# nor the apt cache ends up in the published image.
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends git ca-certificates \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
RUN python -m venv /opt/venv
|
|
ENV PATH="/opt/venv/bin:$PATH"
|
|
|
|
RUN pip install --no-cache-dir supervisor \
|
|
&& pip install --no-cache-dir "git+https://github.com/open3e/open3e.git@${OPEN3E_REF}"
|
|
|
|
# E3onCAN is not packaged -- it is run as a script out of its checkout, so the
|
|
# source has to travel into the runtime image alongside its dependencies.
|
|
RUN git clone --quiet https://github.com/MyHomeMyData/E3onCAN.git /opt/e3oncan \
|
|
&& git -C /opt/e3oncan checkout --quiet "${E3ONCAN_REF}" \
|
|
&& pip install --no-cache-dir -r /opt/e3oncan/requirements.txt \
|
|
&& rm -rf /opt/e3oncan/.git
|
|
|
|
|
|
FROM python:3.15-slim@sha256:2da676046630d7bf14c6764ae1143dc7fa0377a53f94bf5a642f0b214aca5bce
|
|
|
|
ARG OPEN3E_REF=6553c10453a6bae1210908f38a5af2481a46a3e5
|
|
ARG E3ONCAN_REF=461dcbdf97e8d72d2edb9929c63b39382af6c3a1
|
|
|
|
LABEL org.opencontainers.image.title="heating-can" \
|
|
org.opencontainers.image.description="open3e + E3onCAN, Viessmann E3 CAN bus readers" \
|
|
org.opencontainers.image.source="https://gitea.renner.uno/konairius/homelab" \
|
|
uno.renner.open3e.ref="${OPEN3E_REF}" \
|
|
uno.renner.e3oncan.ref="${E3ONCAN_REF}"
|
|
|
|
# Everything is installed at build time. The runtime image carries the finished
|
|
# venv and source only -- no git, no compiler, no package index.
|
|
COPY --from=build /opt/venv /opt/venv
|
|
COPY --from=build /opt/e3oncan /opt/e3oncan
|
|
ENV PATH="/opt/venv/bin:$PATH"
|
|
|
|
# Both services log to stdout, which docker's journald driver forwards to the
|
|
# journal and edge-log-shipper ships to Loki. open3e's systemd unit did NOT set
|
|
# this, so its output was block-buffered and effectively never reached the
|
|
# journal -- a long-standing blind spot that this fixes for both.
|
|
ENV PYTHONUNBUFFERED=1
|
|
|
|
# One container, two services, so PID 1 is a supervisor. It restarts either
|
|
# reader on its own if it dies, reaps children and forwards shutdown -- which a
|
|
# bare process as PID 1 cannot do for a sibling.
|
|
#
|
|
# Configuration arrives as environment variables and supervisord expands them,
|
|
# so nothing outside this image constructs open3e's or E3onCAN's argv. Those
|
|
# CLIs are an implementation detail here; a compose file spelling them out
|
|
# would break whenever either upstream renamed a flag.
|
|
COPY supervisord.conf /etc/supervisord.conf
|
|
|
|
# Build-time self-test. Fails the build if either service cannot actually run,
|
|
# which is exactly what shipped before: supervisord pointed at
|
|
# /opt/e3oncan/src/E3onCANcollect.py, a path that does not exist -- the file is
|
|
# at the repo root. The image built green and would have run open3e while
|
|
# e3oncan crash-looped, the E380 meter silently absent.
|
|
#
|
|
# Done here rather than as a CI step because this needs no registry
|
|
# credentials and no runner able to pull a private image -- and because a
|
|
# broken image should never be published in the first place.
|
|
RUN open3e --help > /dev/null \
|
|
&& python /opt/e3oncan/E3onCANcollect.py --help > /dev/null \
|
|
&& supervisord --help > /dev/null \
|
|
&& grep -q '^\[program:open3e\]' /etc/supervisord.conf \
|
|
&& grep -q '^\[program:e3oncan\]' /etc/supervisord.conf \
|
|
&& grep -q '^\[supervisorctl\]' /etc/supervisord.conf \
|
|
&& supervisorctl --help > /dev/null \
|
|
&& echo "self-test OK: both services execute; supervisord has both programs"
|
|
|
|
# CAN_RAW needs no capabilities, so nothing here runs as root.
|
|
RUN useradd --system --no-create-home --shell /usr/sbin/nologin open3e
|
|
|
|
# A writable working directory, owned by the service user. udsoncan writes its
|
|
# log file into the CWD, and the native install this replaces gave open3e's
|
|
# directory `owner: open3e` for exactly that reason. /opt is root-owned from the
|
|
# COPY layers, so running there would fail that write.
|
|
RUN install -d -o open3e -g open3e -m 0755 /var/lib/open3e
|
|
|
|
USER open3e
|
|
WORKDIR /var/lib/open3e
|
|
|
|
ENTRYPOINT ["supervisord", "-c", "/etc/supervisord.conf"]
|