# Multi-stage Dockerfile for WebSSH - Optimized for small image size # Stage 1: Builder - compiles Python packages with build dependencies FROM python:3.11-slim-bookworm AS builder # Install build dependencies needed for compiling cryptography/pynacl on arm/v7 RUN apt-get update && apt-get install -y --no-install-recommends \ gcc \ make \ python3-dev \ libffi-dev \ libssl-dev \ libsodium-dev \ curl \ ca-certificates \ binutils \ && rm -rf /var/lib/apt/lists/* # Install Python dependencies into a virtual environment RUN python -m venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" COPY requirements.txt /tmp/ COPY src/ /tmp/src/ # Prefer prebuilt wheels when available to avoid building larger compiled artifacts RUN SODIUM_INSTALL=system python -m pip install --no-cache-dir --prefer-binary -r /tmp/requirements.txt # Strip compiled extensions and remove tests/pycache in venv to reduce size RUN set -eux; \ # Strip native extensions to reduce size when possible if command -v strip >/dev/null 2>&1; then \ find /opt/venv -type f -name '*.so' -print0 | xargs -0 -r strip --strip-unneeded || true; \ find /opt/venv -type f -executable -print0 | xargs -0 -r file | awk -F: '/ELF/{print $1}' | xargs -0 -r strip --strip-unneeded || true; \ fi; \ # Remove Python caches and compiled bytecode find /opt/venv -type d -name '__pycache__' -print0 | xargs -0 -r rm -rf || true; \ find /opt/venv -type f -name '*.pyc' -print0 | xargs -0 -r rm -f || true; \ # Remove test folders from packages find /opt/venv -type d \( -name 'tests' -o -name 'test' \) -print0 | xargs -0 -r rm -rf || true; \ # Remove static docs and unnecessary package metadata that are not needed at runtime find /opt/venv -type d -name 'doc' -print0 | xargs -0 -r rm -rf || true; \ find /opt/venv -type d -name 'docs' -print0 | xargs -0 -r rm -rf || true; \ # Remove pip/setuptools/wheel from the venv to save space (not needed at runtime) rm -rf /opt/venv/bin/pip* || true; \ find /opt/venv -path '*/site-packages/pip*' -print0 | xargs -0 -r rm -rf || true; \ find /opt/venv -path '*/site-packages/setuptools*' -print0 | xargs -0 -r rm -rf || true; \ find /opt/venv -path '*/site-packages/wheel*' -print0 | xargs -0 -r rm -rf || true; \ true # Download Caddy binary for the build target (multi-arch aware) ARG TARGETARCH ARG TARGETVARIANT RUN set -eux; \ DARCH="${TARGETARCH:-$(dpkg --print-architecture)}"; \ VARIANT="${TARGETVARIANT:-}"; \ case "$DARCH" in \ amd64) ARCH=amd64 ;; \ arm64) ARCH=arm64 ;; \ arm) if [ "${VARIANT}" = "v7" ]; then ARCH=armv7; else ARCH=arm; fi ;; \ *) ARCH=amd64 ;; \ esac; \ echo "Downloading Caddy for arch=${ARCH}"; \ # Try multiple candidate arch names for robustness (especially for arm variants) candidates="${ARCH}"; \ if [ "${ARCH}" = "armv7" ]; then candidates="armv7 arm armv6"; fi; \ if [ "${ARCH}" = "arm" ]; then candidates="arm armv7 armv6"; fi; \ downloaded=0; \ for a in $candidates; do \ echo "Trying caddy download arch=$a"; \ if curl -fsSL "https://caddyserver.com/api/download?os=linux&arch=$a" -o /tmp/caddy_dl; then \ echo "Downloaded caddy for arch=$a"; downloaded=1; break; \ else \ echo "No binary for arch=$a"; fi; \ done; \ if [ "$downloaded" -ne 1 ]; then echo "Failed to download Caddy for any candidate: $candidates" >&2; exit 1; fi; \ # If it's a tar.gz archive, extract it; otherwise treat as raw binary if tar -tzf /tmp/caddy_dl >/dev/null 2>&1; then \ tar -xzf /tmp/caddy_dl -C /tmp; \ else \ mv /tmp/caddy_dl /tmp/caddy_raw || true; \ fi; \ mkdir -p /tmp/caddy_bin_dir; \ # Preferred locations if [ -f /tmp/caddy ]; then mv /tmp/caddy /tmp/caddy_bin_dir/caddy; fi; \ if [ -f /tmp/caddy_linux_${ARCH} ]; then mv /tmp/caddy_linux_${ARCH} /tmp/caddy_bin_dir/caddy; fi; \ if [ -f /tmp/caddy_raw ]; then mv /tmp/caddy_raw /tmp/caddy_bin_dir/caddy; fi; \ # Fallback: find any executable named 'caddy' under /tmp if [ ! -f /tmp/caddy_bin_dir/caddy ]; then \ found=$(find /tmp -type f -name caddy -perm /111 -print -quit || true); \ if [ -n "$found" ]; then mv "$found" /tmp/caddy_bin_dir/caddy; fi; \ fi; \ if [ ! -f /tmp/caddy_bin_dir/caddy ]; then echo "Caddy binary not found after download" >&2; exit 1; fi; \ chmod +x /tmp/caddy_bin_dir/caddy; \ # Strip Caddy to reduce size if command -v strip >/dev/null 2>&1; then strip --strip-unneeded /tmp/caddy_bin_dir/caddy || true; fi # Stage 2: Final - minimal runtime image FROM python:3.11-slim-bookworm # Install only runtime dependencies (no build tools) RUN apt-get update && apt-get install -y --no-install-recommends \ gosu \ libsodium23 \ libffi8 \ && rm -rf /var/lib/apt/lists/* # Copy Python packages from builder COPY --from=builder /opt/venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" # Security: Run as non-root user RUN groupadd -r webssh && useradd -r -g webssh webssh WORKDIR /app/src # Copy application and static files (owned by webssh in one layer, no separate chown pass) COPY --chown=webssh:webssh src/ /app/src/ COPY deployment/docker-entrypoint.sh /usr/local/bin/ # Create .ssh directory for host keys RUN mkdir -p /app/src/.ssh && chown webssh:webssh /app/src/.ssh # Copy entrypoint RUN chmod +x /usr/local/bin/docker-entrypoint.sh # Copy the Caddy binary prepared in the builder stage COPY --from=builder /tmp/caddy_bin_dir/caddy /usr/local/bin/caddy RUN chmod +x /usr/local/bin/caddy || true # Copy Caddyfile and static assets after package installation to avoid dpkg conffile prompts COPY deployment/Caddyfile /etc/caddy/Caddyfile COPY src/index.html /srv/ COPY src/help.html /srv/ COPY src/static/ /srv/static/ # Expose host-facing port for Caddy and internal service ports EXPOSE 8080 8081 8082 8083 # Health check via Caddy (serving root) using Python (no curl required) HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD python -c "import sys, urllib.request; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8080/', timeout=5).getcode() == 200 else 1)" # Use shell form to see entrypoint errors ENTRYPOINT /usr/local/bin/docker-entrypoint.sh