Dockerfile
Rama main · réplica de lectura del repositorio original.
# syntax=docker/dockerfile:1.7@sha256:a57df69d0ea827fb7266491f2813635de6f17269be881f696fbfdf2d83dda33e
ARG NODE_IMAGE=node:24-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd
FROM ${NODE_IMAGE} AS image-library
ARG TARGETARCH
COPY app/package.json app/package-lock.json /build/app/
COPY container/build-image-library.sh /build/build-image-library.sh
RUN mkdir -p /opt/hauser-vips /build/app/node_modules/sharp && \
if [ "$TARGETARCH" = amd64 ]; then sh /build/build-image-library.sh; fi
FROM ${NODE_IMAGE} AS image-library-runtime
ARG TARGETARCH
RUN if [ "$TARGETARCH" = amd64 ]; then \
apk add --no-cache glib expat libjpeg-turbo libpng libwebp libwebpmux libwebpdemux libheif \
libheif-aom libheif-dav1d tiff lcms2 pango librsvg; \
fi
COPY --from=image-library /opt/hauser-vips /opt/hauser-vips
ENV HMI_SHARP_BASELINE_BINDING=/opt/hauser-vips/lib/sharp.node
FROM image-library-runtime AS build
WORKDIR /build/app
COPY app/package.json app/package-lock.json ./
RUN npm ci
COPY app/ ./
COPY design-tokens/ /build/design-tokens/
# AGPL section 13: the built frontend embeds the exact revision it was built
# from and the URL of its corresponding source. There is no .git inside the
# build context, so both arrive as build arguments; HAUSER_RELEASE=1 turns a
# missing or unusable value into a build failure instead of a published image
# that claims a source it cannot back. Declared after `npm ci` so a new
# revision does not invalidate the dependency layer.
ARG HAUSER_REVISION=""
ARG HAUSER_SOURCE_URL=""
ARG HAUSER_RELEASE=""
ENV HAUSER_REVISION=${HAUSER_REVISION} \
HMI_SOURCE_URL=${HAUSER_SOURCE_URL} \
HAUSER_RELEASE=${HAUSER_RELEASE}
RUN npm run build && \
./node_modules/.bin/tsc \
--ignoreConfig \
src/lib/config/build-info.ts \
src/lib/config/household-config.ts \
src/lib/config/household-config-migration.ts \
src/lib/config/household-runtime-data.ts \
src/lib/config/hotel-mode-policy.ts \
src/lib/config/legacy-household-config.ts \
src/lib/config/legacy-household-data.ts \
--outDir /build/server-contract \
--rootDir src/lib/config \
--module nodenext \
--moduleResolution nodenext \
--target es2022 \
--skipLibCheck \
--rewriteRelativeImportExtensions true \
--declaration false \
--sourceMap false \
--noEmitOnError true && \
./node_modules/.bin/tsc \
--ignoreConfig \
src/lib/room-images/sharp-runtime.ts \
src/lib/room-images/room-image-transform-policy-v1.ts \
src/lib/room-images/room-image-prompt-policy-v1.ts \
src/lib/room-images/room-image-phone-variants.ts \
src/lib/room-images/room-image-phone-derivation-policy-v1.ts \
--outDir /build/room-image-contract \
--rootDir src/lib/room-images \
--module nodenext \
--moduleResolution nodenext \
--target es2022 \
--skipLibCheck \
--rewriteRelativeImportExtensions true \
--declaration false \
--sourceMap false \
--noEmitOnError true && \
npm prune --omit=dev
# Fernzugriff (Plan 21, Stufe 2): der tsnet-Sidecar ist ein statisches
# Go-Binary; Userspace-Netz, keine Rechte, kein TUN. Fehlt es, läuft Hauser
# im LAN unverändert.
FROM golang:1.26-alpine AS tunnel
WORKDIR /build/tunnel
COPY tools/tunnel/go.mod tools/tunnel/go.sum ./
RUN go mod download
COPY tools/tunnel/ ./
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/hauser-tunnel .
FROM image-library-runtime AS runtime
ARG HAUSER_VERSION=0.50.0
ARG HAUSER_REVISION=""
ARG HAUSER_SOURCE_URL=""
LABEL org.opencontainers.image.title="Hauser" \
org.opencontainers.image.description="Local-first smart home control surface" \
org.opencontainers.image.source="https://github.com/ralleur/hauser" \
org.opencontainers.image.licenses="AGPL-3.0-only" \
org.opencontainers.image.version="${HAUSER_VERSION}" \
org.opencontainers.image.revision="${HAUSER_REVISION}"
ENV NODE_ENV=production \
HMI_HOST=0.0.0.0 \
HMI_PORT=4173 \
HMI_REVISION=${HAUSER_REVISION} \
HMI_SOURCE_URL=${HAUSER_SOURCE_URL} \
HMI_AI_CUSTOMIZING_ENABLED=0 \
HMI_HOUSEHOLD_CONFIG_PATH=/config/household.json \
HMI_HOUSEHOLD_CONFIG_MODE=active \
HMI_CONFIG_PATH=/data/config.json \
HMI_FAMILY_DATA_PATH=/data/family-data.json \
HMI_SONG_LIBRARY_DIR=/data/songs \
HMI_ROOM_IMAGE_AUTH_MODE=direct \
HMI_REQUIRED_WRITABLE_DIRS=/config,/data,/assets \
HMI_SERVER_CONTRACT=compiled \
HMI_TUNNEL_BIN=/opt/hauser/bin/hauser-tunnel \
HMI_TUNNEL_STATE=/data/tunnel
WORKDIR /opt/hauser
COPY --from=build --chown=node:node /build/app/dist ./dist
COPY --from=build --chown=node:node /build/app/node_modules ./node_modules
COPY --from=build --chown=node:node /build/server-contract ./server-contract
COPY --from=build --chown=node:node /build/room-image-contract ./room-image-contract
COPY --chown=node:node app/server.mjs app/package.json ./
COPY --chown=node:node app/server/ ./server/
COPY --chown=node:node container/healthcheck.mjs container/start.mjs ./container/
COPY --from=tunnel --chown=node:node /out/hauser-tunnel ./bin/hauser-tunnel
RUN mkdir -p /config /data/songs /assets && chown -R node:node /opt/hauser /config /data /assets
VOLUME ["/config", "/data", "/assets"]
EXPOSE 4173
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s --retries=3 \
CMD ["node", "container/healthcheck.mjs"]
# Entrypoint, not `node server.mjs`: bind-mounted volumes arrive owned by the
# host (root under Home Assistant OS) and replace the ownership set above. The
# entrypoint repairs that as root and drops to `node` before serving. Run the
# container with `--user node` to keep it unprivileged end to end — the
# entrypoint then skips the preparation.
CMD ["node", "container/start.mjs"]