Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 54 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \

Check warning on line 19 in Dockerfile

View workflow job for this annotation

GitHub Actions / build

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "SELKIES_ENABLE_BASIC_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/

Check warning on line 19 in Dockerfile

View workflow job for this annotation

GitHub Actions / build

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ENV "SELKIES_ENABLE_BASIC_AUTH") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
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."
Expand All @@ -9,7 +48,11 @@
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 \
Expand All @@ -19,7 +62,6 @@
libva-drm2 \
vainfo \
python3 \
python3-pip \
python3-venv \
procps \
curl \
Expand Down Expand Up @@ -83,6 +125,13 @@
/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
Expand All @@ -93,3 +142,5 @@

# Port 3000 (HTTP) and 3001 (HTTPS, self-signed) serve the WebUI, API, and Selkies stream
EXPOSE 3000 3001

ENTRYPOINT ["/init"]
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
40 changes: 40 additions & 0 deletions scripts/check_base_env.sh
Original file line number Diff line number Diff line change
@@ -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"
48 changes: 48 additions & 0 deletions scripts/slim_base.sh
Original file line number Diff line number Diff line change
@@ -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/*
Loading