diff --git a/Dockerfile b/Dockerfile index 6fc73ce..5330891 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,45 @@ # syntax=docker/dockerfile:1 # ubuntunoble stopped receiving builds in June 2026; its older Selkies rejects h264enc -FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute +FROM ghcr.io/linuxserver/baseimage-selkies:ubunturesolute AS base + +# Strip what ConnectHub never uses from the base: Docker-in-Docker, compilers, all but one +# locale, CJK serif fonts (~800 MB). Deleting files in a later layer can't shrink an +# image, so this stage's filesystem is copied into a fresh one below. CI's layer cache +# keeps this stage, and so that copied layer, unchanged until the base image changes, +# so updates still only download ConnectHub's own layers. +FROM base AS slim +COPY scripts/slim_base.sh /tmp/slim_base.sh +RUN sh /tmp/slim_base.sh && env | sort > /connecthub-base.env + +FROM scratch +COPY --from=slim / / + +# The base image's configuration, which copying its files doesn't carry over. +# check_base_env.sh below fails the build if this drifts from the base image. +ENV HOME=/config \ + LANGUAGE=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + TERM=xterm \ + SHELL=/bin/bash \ + S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0 \ + S6_VERBOSITY=1 \ + S6_STAGE2_HOOK=/docker-mods \ + DISPLAY=:1 \ + PERL5LIB=/usr/local/bin \ + PULSE_RUNTIME_PATH=/defaults \ + SELKIES_INTERPOSER=/usr/lib/selkies_input_interposer.so \ + SELKIES_WEBCAM_INTERPOSER=/usr/lib/selkies_v4l2_interposer.so \ + NVIDIA_DRIVER_CAPABILITIES=all \ + DISABLE_DRI3=false \ + SELKIES_ENCODER=h264enc,h265enc,vp8enc,vp9enc,av1enc,jpeg \ + SELKIES_ENABLE_BASIC_AUTH=false \ + SELKIES_VIDEO_STREAMING_MODE=false \ + SELKIES_ALLOWED_ORIGINS=* \ + SELKIES_RATE_CONTROL_MODE=crf,cbr \ + __GL_SYNC_TO_VBLANK=0 \ + TITLE=Selkies +# Docker was removed above; the base's svc-docker only sleeps with this false +ENV START_DOCKER=false LABEL maintainer="kodi" LABEL description="ConnectHub - WebRTC remote desktop gateway for RDP, VNC and SSH, multi-GPU encoding (NVIDIA/AMD/Intel/CPU), Authentik SSO & Basic Auth, and WebUI File Sharing." @@ -9,7 +48,11 @@ ENV DEBIAN_FRONTEND=noninteractive \ PIP_DISABLE_PIP_VERSION_CHECK=1 \ PYTHONUNBUFFERED=1 \ VIRTUAL_ENV=/app/venv \ - PATH="/app/venv/bin:$PATH" + PATH="/app/venv/bin:/lsiopy/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +COPY scripts/check_base_env.sh /tmp/check_base_env.sh +RUN sh /tmp/check_base_env.sh /connecthub-base.env \ + && rm -f /tmp/check_base_env.sh /connecthub-base.env # Install FreeRDP 3, GPU drivers for VA-API, Python 3, and utilities RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -19,7 +62,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libva-drm2 \ vainfo \ python3 \ - python3-pip \ python3-venv \ procps \ curl \ @@ -83,6 +125,13 @@ RUN /command/s6-rc-compile /tmp/s6-rc-check \ /etc/s6-overlay/s6-rc.d \ && rm -rf /tmp/s6-rc-check +# What the session needs still runs after the slimming above +RUN /lsiopy/bin/python3 -c "import selkies, pixelflux, pcmflux" \ + && for bin in Xvfb nginx pulseaudio openbox xdotool xclip xterm xfreerdp3 xtigervncviewer sshpass s6-rc; do \ + command -v "$bin" > /dev/null || { echo "missing after slimming: $bin" >&2; exit 1; }; \ + done \ + && locale -a | grep -qix 'en_US.utf8' + # Release version, set by CI. Declared last so a new version doesn't invalidate # the cached package and pip layers above. ARG APP_VERSION=0.0.0-dev @@ -93,3 +142,5 @@ VOLUME ["/config", "/shared"] # Port 3000 (HTTP) and 3001 (HTTPS, self-signed) serve the WebUI, API, and Selkies stream EXPOSE 3000 3001 + +ENTRYPOINT ["/init"] diff --git a/README.md b/README.md index 422b2f6..485d905 100644 --- a/README.md +++ b/README.md @@ -309,6 +309,12 @@ ConnectHub/ --- +## Image Size + +The image is built on LinuxServer's Selkies base, minus the parts ConnectHub never uses: Docker-in-Docker, the C/C++ compilers and build tools, every locale except `en_US.UTF-8` (and `C.UTF-8`), and the CJK serif fonts (the sans fonts remain, so SSH terminals still show CJK text). That saves about 800 MB. Setting another `LANG` or `LC_*` locale in the container therefore has no effect; it doesn't change anything on the remote machine either way. + +--- + ## Troubleshooting The **Connection Logs & Diagnostics** panel on the dashboard has three tabs, each keeping its last 1,000 lines: diff --git a/scripts/check_base_env.sh b/scripts/check_base_env.sh new file mode 100755 index 0000000..e5c1cd8 --- /dev/null +++ b/scripts/check_base_env.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# Fails the build when the environment re-declared in the Dockerfile has drifted from +# the base image's. The image is rebuilt FROM scratch (see slim_base.sh), so it doesn't +# inherit the base's ENV, and a variable the base adds or changes later would otherwise +# go missing without anyone noticing. +# +# $1: `env` as seen in a build stage of the base image, one VAR=value per line. +set -eu + +base_env=$1 +# Set per process or per build step (BuildKit's tracing context differs on every RUN), +# or overridden on purpose in the Dockerfile +skip='^(HOSTNAME|PWD|OLDPWD|SHLVL|_|TRACEPARENT|TRACESTATE|PATH|VIRTUAL_ENV|START_DOCKER|DEBIAN_FRONTEND)=' + +status=0 +# Every mismatch is reported, so one failed build shows all that needs updating +mismatches=$(grep -Ev "$skip" "$base_env" | while IFS= read -r line; do + name=${line%%=*} + expected=${line#*=} + actual=$(printenv "$name" || true) + if [ "$actual" != "$expected" ]; then + echo "check_base_env: $name is '$actual' here but '$expected' in the base image" + fi +done) +if [ -n "$mismatches" ]; then + printf '%s\n' "$mismatches" >&2 + status=1 +fi + +# PATH may only add to the front of the base image's +base_path=$(grep '^PATH=' "$base_env" | cut -d= -f2-) +case "$PATH" in + *":$base_path"|"$base_path") ;; + *) echo "check_base_env: PATH '$PATH' does not end with the base image's '$base_path'" >&2; status=1 ;; +esac + +if [ "$status" -ne 0 ]; then + echo "check_base_env: update the ENV block in the Dockerfile to match the base image" >&2 +fi +exit "$status" diff --git a/scripts/slim_base.sh b/scripts/slim_base.sh new file mode 100755 index 0000000..05115c5 --- /dev/null +++ b/scripts/slim_base.sh @@ -0,0 +1,48 @@ +#!/bin/sh +# Removes the parts of baseimage-selkies that ConnectHub never uses. Runs in a build +# stage whose filesystem is then copied into a fresh image (see the Dockerfile): +# deleting files in a later layer of the same image would not make it any smaller. +set -eu + +# Docker-in-Docker (installed on amd64 only), the C/C++ toolchain and build tools, +# and every locale. Only the ones actually installed are named, so arm64 builds, +# which have no Docker, don't fail on unknown packages. +PURGE="" +for pkg in docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin \ + gcc g++ cmake make git locales-all; do + if dpkg-query -W -f='${db:Status-Status}' "$pkg" 2>/dev/null | grep -qx installed; then + PURGE="$PURGE $pkg" + fi +done + +if [ -n "$PURGE" ]; then + # apt also removes whatever depends on these. Refuse if that would take any package + # the base image installed on purpose, rather than silently losing it. + tmp=$(mktemp -d) + apt-get -s purge --auto-remove $PURGE | awk '/^Purg /{print $2}' | sed 's/:.*//' | sort -u > "$tmp/removed" + apt-mark showmanual | sed 's/:.*//' | sort -u > "$tmp/manual" + printf '%s\n' $PURGE | sort -u > "$tmp/purge" + # removed AND installed on purpose AND not one we asked for + unexpected=$(comm -12 "$tmp/removed" "$tmp/manual" | comm -23 - "$tmp/purge") + if [ -n "$unexpected" ]; then + echo "slim_base: purging$PURGE would also remove packages the base installs on purpose:" >&2 + printf ' %s\n' $unexpected >&2 + exit 1 + fi + echo "slim_base: removing$PURGE ($(wc -l < "$tmp/removed") packages in total)" + rm -rf "$tmp" + apt-get purge -y --auto-remove $PURGE +fi + +rm -f /etc/apt/sources.list.d/docker.list /usr/share/keyrings/docker.asc /usr/local/bin/dind + +# The base compiles hundreds of locales into one 180 MB archive; the image runs in en_US.UTF-8 +rm -f /usr/lib/locale/locale-archive +localedef -i en_US -f UTF-8 en_US.UTF-8 + +# CJK serif faces (54 MB). The sans faces stay, so SSH terminals still show CJK text. +rm -f /usr/share/fonts/opentype/noto/NotoSerifCJK-*.ttc +fc-cache -f >/dev/null + +apt-get clean +rm -rf /var/lib/apt/lists/* /var/cache/apt/* /var/log/apt /var/log/dpkg.log /tmp/* /var/tmp/*