openwebui-open-terminal/Dockerfile.alpine
Timothy Jaeryang Baek 2da589100c fix: port detection broken since v0.11.2 (#85, #63)
setcap cap_setgid+ep on the system Python binary made all Python
processes non-dumpable, blocking /proc/[pid]/fd/ access needed by
_pid_from_inode() to resolve socket inodes to PIDs.

Fix: copy the Python binary to python3-ot and setcap only the copy.
The open-terminal server uses python3-ot (has CAP_SETGID for
multi-user os.setgroups()), while user-spawned python3 stays
capability-free and dumpable.

Slim/Alpine: removed setcap entirely (multi-user mode requires sudo,
which only the full image has). Kept libcap packages installed.

README: corrected Image Variants table — multi-user mode is
full-image only.
2026-03-19 17:42:17 -05:00

140 lines
5.4 KiB
Text

# ============================================================================
# Open Terminal — Alpine Image
# ============================================================================
#
# The smallest possible Open Terminal image. Built on Alpine Linux with
# musl libc, this image is ~100 MB — great for edge deployments, CI
# runners, and environments where every megabyte counts.
#
# Build:
# docker build -f Dockerfile.alpine -t open-terminal:alpine .
#
# Run:
# docker run -d -p 8000:8000 -e OPEN_TERMINAL_API_KEY=secret open-terminal:alpine
#
# Customize:
# - Want extra apk packages? Add them to the "Runtime packages" section below.
# - Want extra pip packages? Add them to the "pip install" line in the builder.
# - Need glibc or heavier tools? Use the slim (Debian) or full image instead.
#
# ⚠️ Alpine uses musl libc. Most pure-Python packages work fine, but
# some C-extension wheels (numpy, pandas, etc.) may need to be compiled
# from source, which is slower. If you hit issues, try the slim image.
#
# ============================================================================
# --------------------------------------------------------------------------
# Stage 1: Builder
# Compiles Python dependencies in an isolated prefix. Alpine needs
# musl-dev and gcc for most C extensions.
# --------------------------------------------------------------------------
FROM python:3.12.13-alpine AS builder
WORKDIR /src
# Build-time system dependencies.
# ➡️ If a pip package needs extra C headers, add the -dev package here
# (e.g. "postgresql-dev" for psycopg2, "libxml2-dev" for lxml).
RUN apk add --no-cache \
build-base \
gcc \
musl-dev \
libffi-dev
COPY pyproject.toml README.md uv.lock* ./
COPY open_terminal/ open_terminal/
# Install the app + dependencies into /install so we can cherry-pick them.
# ➡️ To add extra pip packages to the alpine image, append them here:
# e.g. pip install --no-cache-dir --prefix=/install . httpx polars
RUN pip install --no-cache-dir --prefix=/install .
# --------------------------------------------------------------------------
# Stage 2: Runtime
# Tiny Alpine runtime. No compilers, no build tools, no sudo.
# --------------------------------------------------------------------------
FROM python:3.12.13-alpine
# ── Runtime system packages ────────────────────────────────────────────────
#
# These are the only apk packages in the final image. Kept intentionally
# tiny. If you need something extra, just add it below.
#
# Core: tini (PID 1), su-exec (drop privileges, Alpine's gosu)
# Utilities: curl, git, jq, less, procps (ps/top)
# Firewall: iptables, ipset, dnsmasq (egress whitelist)
# Shell: bash (the terminal API spawns bash sessions)
#
# ➡️ Want more tools? Add them here, one per line for easy diffs:
# e.g. vim \
# sqlite \
#
RUN apk add --no-cache \
tini \
su-exec \
curl \
git \
jq \
less \
procps \
bash \
iptables \
ipset \
dnsmasq \
libcap \
libcap-utils \
ca-certificates
# Copy the pre-built Python packages from the builder stage.
COPY --from=builder /install /usr/local
WORKDIR /app
COPY . .
# Alpine patches ship fast. Upgrade base packages to pick up security fixes
# that landed after the pinned base image was published.
RUN apk upgrade --no-cache
# Install the app itself (fast — all deps are already present).
#
# Cleanup removes ~20 MB of unnecessary files:
# - pip itself (not needed at runtime)
# - __pycache__ / .pyc / .pyo bytecode (regenerated on first import)
# - test directories inside packages
#
RUN pip install --no-cache-dir --no-deps . \
&& pip cache purge 2>/dev/null || true \
&& pip uninstall -y pip setuptools 2>/dev/null || true \
&& find /usr/local/lib/python3.12 -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true \
&& find /usr/local/lib/python3.12 -type f -name "*.pyc" -o -name "*.pyo" -delete 2>/dev/null || true \
&& find /usr/local/lib/python3.12 -type d -name "tests" -o -name "test" | xargs rm -rf 2>/dev/null || true
# ── Non-root user ─────────────────────────────────────────────────────────
#
# The app runs as "user" (UID 1000) with NO sudo access.
# The entrypoint handles any root-level setup (iptables, chown) via su-exec.
#
RUN adduser -D -s /bin/bash -u 1000 user
COPY entrypoint-slim.sh /app/entrypoint-slim.sh
RUN chmod +x /app/entrypoint-slim.sh
# ── NOTE: We do NOT set "USER user" here ──────────────────────────────────
#
# The container starts as root so the entrypoint can:
# 1. Fix /home/user ownership on bind mounts
# 2. Set up iptables egress rules (if configured)
# 3. Drop privileges via su-exec → runs the app as "user"
#
# If you don't need the egress firewall, you can add "USER user" here
# and simplify the entrypoint — but su-exec handles it cleanly either way.
#
ENV SHELL=/bin/bash
EXPOSE 8000
# tini is PID 1 — reaps zombies and forwards signals cleanly.
ENTRYPOINT ["/sbin/tini", "--", "/app/entrypoint-slim.sh"]
CMD ["run"]