Files
jasonwitty 6711ac030f
Build and Deploy to K3s / test (push) Successful in 1m33s
Build and Deploy to K3s / lint (push) Successful in 1m0s
Build and Deploy to K3s / build-and-push (push) Successful in 5m17s
Build and Deploy to K3s / deploy (push) Successful in 1m9s
Pin socktop apt version (1.60.2-1); bump to 0.3.10
Image 0.3.9 baked socktop 1.60.1 because CI's registry layer cache reused
the apt-install layer from before the 1.60.2 release. 1.60.1 has no
--no-kill flag, so the restricted shell's invocation parsed it as the
positional websocket URL, breaking (and on overwrite, corrupting) the
local profile. Pinning the package version busts the cache and ties the
installed binary to the flags the restricted shell uses.

Also point the manifest's webterm container at the current tag so a
manual kubectl apply cannot roll the image back to 0.2.2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 09:16:29 -07:00

192 lines
6.8 KiB
Docker

# Multi-stage Dockerfile for socktop webterm
# This reduces the final image size significantly by separating build and runtime
# ============================================================================
# Stage 1: Documentation Builder
# ============================================================================
FROM rust:1.95-slim-bookworm AS docs-builder
WORKDIR /build
# Install required tools
RUN apt-get update && \
apt-get install -y --no-install-recommends curl ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Install mdbook first
RUN cargo install mdbook
# Copy documentation source (includes theme files)
COPY docs ./docs
# Download Catppuccin theme CSS if not already present
RUN if [ ! -f docs/theme/catppuccin.css ]; then \
curl -fsSL https://github.com/catppuccin/mdBook/releases/latest/download/catppuccin.css \
-o docs/theme/catppuccin.css && \
echo "Catppuccin CSS downloaded successfully"; \
else \
echo "Catppuccin CSS already present"; \
fi
# Build documentation
RUN cd docs && \
mdbook build && \
ls -la book/
# ============================================================================
# Stage 2: Rust Builder
# ============================================================================
FROM rust:1.95-slim-bookworm AS rust-builder
WORKDIR /build
# Install build dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
pkg-config \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy only dependency files first for better caching
COPY Cargo.toml Cargo.lock ./
# Create dummy source to cache dependencies
RUN mkdir src && \
echo "fn main() {}" > src/server.rs && \
echo "pub fn lib() {}" > src/lib.rs && \
cargo build --release && \
rm -rf src target/release/webterm-server target/release/deps/webterm*
# Copy actual source code
COPY src ./src
COPY templates ./templates
COPY static ./static
COPY build.rs ./build.rs
# Copy built documentation from docs-builder stage
COPY --from=docs-builder /build/docs/book ./static/docs
# Verify documentation was copied
RUN ls -la ./static/docs/ && \
test -f ./static/docs/index.html || (echo "ERROR: Documentation index.html not found!" && exit 1)
# Build the actual application (force rebuild by touching sources)
RUN touch src/server.rs src/lib.rs && \
cargo build --release && \
strip target/release/webterm-server
# ============================================================================
# Stage 3: Node.js Builder
# ============================================================================
FROM node:20-slim AS node-builder
WORKDIR /build
# Copy package files
COPY package.json package-lock.json ./
COPY static ./static
# Install only production dependencies
RUN npm ci --only=production && \
# Copy static files to node_modules for serving
cp static/terminado-addon.js node_modules/ && \
cp static/bg.png node_modules/ && \
cp static/styles.css node_modules/ && \
cp static/terminal.js node_modules/ && \
cp static/favicon.png node_modules/
# ============================================================================
# Stage 4: Runtime Image
# ============================================================================
FROM debian:trixie-slim
# Avoid prompts from apt
ENV DEBIAN_FRONTEND=noninteractive
ENV TERM=xterm-256color
# Install only runtime dependencies
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends \
# Runtime libraries
libssl3 \
ca-certificates \
# For socktop packages
curl \
gnupg2 \
# Shell and utilities
bash \
procps \
# Health check
curl \
&& rm -rf /var/lib/apt/lists/*
# Add socktop APT repository and install packages.
# The version is pinned: the restricted shell passes flags that must exist in
# the installed binary (e.g. --no-kill), and CI's registry layer cache would
# otherwise happily reuse an apt layer from before a socktop release. Bump the
# pin together with any restricted-shell.sh change that uses a new flag.
ARG SOCKTOP_VERSION=1.60.2-1
RUN curl -fsSL https://jasonwitty.github.io/socktop/KEY.gpg | \
gpg --dearmor -o /usr/share/keyrings/socktop-archive-keyring.gpg && \
echo "deb [signed-by=/usr/share/keyrings/socktop-archive-keyring.gpg] https://jasonwitty.github.io/socktop stable main" > /etc/apt/sources.list.d/socktop.list && \
apt-get update && \
apt-get install -y --no-install-recommends socktop=${SOCKTOP_VERSION} socktop-agent=${SOCKTOP_VERSION} && \
rm -rf /var/lib/apt/lists/*
# Create application user (if not already exists from socktop packages)
RUN id -u socktop &>/dev/null || useradd -m -s /bin/bash socktop && \
mkdir -p /home/socktop/.config/socktop && \
chown -R socktop:socktop /home/socktop
# Unprivileged user that per-session shells run as (see docker/session-shell.sh).
# Separate from socktop so a session cannot signal the server, the agent, or
# anything else that matters — the kernel refuses cross-UID signals.
RUN useradd -m -s /usr/sbin/nologin demo
# Set working directory
WORKDIR /app
# Copy built binary from rust-builder
COPY --from=rust-builder /build/target/release/webterm-server /usr/local/bin/webterm-server
# Copy templates and static files
COPY --from=rust-builder /build/templates ./templates
COPY --from=rust-builder /build/static ./static
# Verify documentation is present in static/docs
RUN ls -la ./static/docs/ && \
test -f ./static/docs/index.html || echo "WARNING: Documentation not found in static/docs"
# Copy node_modules from node-builder
COPY --from=node-builder /build/node_modules ./node_modules
# Copy runtime scripts
COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/init-config.sh /init-config.sh
COPY docker/restricted-shell.sh /usr/local/bin/restricted-shell.sh
COPY docker/session-shell.sh /usr/local/bin/session-shell.sh
RUN chmod +x /entrypoint.sh /init-config.sh /usr/local/bin/restricted-shell.sh /usr/local/bin/session-shell.sh
# Expose ports
# 8082 - webterm HTTP server
# 3001 - socktop agent (if used)
EXPOSE 8082 3001
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8082/ || exit 1
# entrypoint.sh handles both cases itself: as root it prepares /home/demo,
# runs the agent as the socktop user, and keeps webterm-server as root so it
# can drop each session to the demo user (CAP_SETUID/CAP_SETGID); as non-root
# it behaves as before (single-UID, no session drop).
RUN ln -sf /entrypoint.sh /docker-entrypoint.sh
# Set entrypoint to the wrapper
ENTRYPOINT ["/docker-entrypoint.sh"]
# Default command - sessions enter via session-shell.sh (per-session privilege
# drop when root) which execs the restricted shell that only allows socktop
CMD ["webterm-server", "--host", "0.0.0.0", "--port", "8082", "--command", "/usr/local/bin/session-shell.sh"]