diff --git a/Dockerfile b/Dockerfile index 5c57897f5..6934bb7c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,44 @@ -FROM debian:13.4 +FROM ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie@sha256:b3c543b6c4f23a5f2df22866bd7857e5d304b67a564f4feab6ac22044dde719b AS uv_source +FROM tianon/gosu:1.19-trixie@sha256:3b176695959c71e123eb390d427efc665eeb561b1540e82679c15e992006b8b9 AS gosu_source +FROM debian:13.4-slim # Disable Python stdout buffering to ensure logs are printed immediately ENV PYTHONUNBUFFERED=1 +# Store Playwright browsers outside the volume mount so the build-time +# install survives the /opt/data volume overlay at runtime. +ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright + # Install system dependencies in one layer, clear APT cache RUN apt-get update && \ apt-get install -y --no-install-recommends \ - build-essential nodejs npm python3 python3-pip ripgrep ffmpeg gcc python3-dev libffi-dev procps && \ + build-essential nodejs npm python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps && \ rm -rf /var/lib/apt/lists/* +# Non-root user for runtime; UID can be overridden via HERMES_UID at runtime +RUN useradd -u 10000 -m -d /opt/data hermes + +COPY --chmod=0755 --from=gosu_source /gosu /usr/local/bin/ +COPY --chmod=0755 --from=uv_source /usr/local/bin/uv /usr/local/bin/uvx /usr/local/bin/ + COPY . /opt/hermes WORKDIR /opt/hermes -# Install Python and Node dependencies in one layer, no cache -RUN pip install --no-cache-dir uv --break-system-packages && \ - uv pip install --system --break-system-packages --no-cache -e ".[all]" && \ - npm install --prefer-offline --no-audit && \ +# Install Node dependencies and Playwright as root (--with-deps needs apt) +RUN npm install --prefer-offline --no-audit && \ npx playwright install --with-deps chromium --only-shell && \ cd /opt/hermes/scripts/whatsapp-bridge && \ npm install --prefer-offline --no-audit && \ npm cache clean --force -WORKDIR /opt/hermes +# Hand ownership to hermes user, then install Python deps in a virtualenv +RUN chown -R hermes:hermes /opt/hermes +USER hermes + +RUN uv venv && \ + uv pip install --no-cache-dir -e ".[all]" + +USER root RUN chmod +x /opt/hermes/docker/entrypoint.sh ENV HERMES_HOME=/opt/data diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 68e3b79c1..dc1edd32c 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -5,6 +5,33 @@ set -e HERMES_HOME="/opt/data" INSTALL_DIR="/opt/hermes" +# --- Privilege dropping via gosu --- +# When started as root (the default), optionally remap the hermes user/group +# to match host-side ownership, fix volume permissions, then re-exec as hermes. +if [ "$(id -u)" = "0" ]; then + if [ -n "$HERMES_UID" ] && [ "$HERMES_UID" != "$(id -u hermes)" ]; then + echo "Changing hermes UID to $HERMES_UID" + usermod -u "$HERMES_UID" hermes + fi + + if [ -n "$HERMES_GID" ] && [ "$HERMES_GID" != "$(id -g hermes)" ]; then + echo "Changing hermes GID to $HERMES_GID" + groupmod -g "$HERMES_GID" hermes + fi + + actual_hermes_uid=$(id -u hermes) + if [ "$(stat -c %u "$HERMES_HOME" 2>/dev/null)" != "$actual_hermes_uid" ]; then + echo "$HERMES_HOME is not owned by $actual_hermes_uid, fixing" + chown -R hermes:hermes "$HERMES_HOME" + fi + + echo "Dropping root privileges" + exec gosu hermes "$0" "$@" +fi + +# --- Running as hermes from here --- +source "${INSTALL_DIR}/.venv/bin/activate" + # Create essential directory structure. Cache and platform directories # (cache/images, cache/audio, platforms/whatsapp, etc.) are created on # demand by the application — don't pre-create them here so new installs