Files
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

135 lines
5.2 KiB
Docker

# ============================================================================
# Open Terminal — Slim Image (Debian)
# ============================================================================
#
# A minimal, hardened image for running the Open Terminal API.
# No Node.js, no Docker CLI, no data-science libraries, no sudo.
# Just the terminal API, git, curl, and the egress firewall.
#
# Build:
# docker build -f Dockerfile.slim -t open-terminal:slim .
#
# Run:
# docker run -d -p 8000:8000 -e OPEN_TERMINAL_API_KEY=secret open-terminal:slim
#
# Customize:
# - Want extra apt packages? Add them to the "Runtime packages" section below.
# - Want extra pip packages? Add them to the "pip install" line in the builder.
# - Need Node.js or heavier tools? Use the full image (Dockerfile) instead.
#
# ============================================================================
# --------------------------------------------------------------------------
# Stage 1: Builder
# Compiles Python dependencies in an isolated prefix so we can copy only
# the final result into the slim runtime image — no compilers shipped.
# --------------------------------------------------------------------------
FROM python:3.12.13-slim AS builder
WORKDIR /src
# Build-time system dependencies.
# If a pip package needs extra C libraries to compile, add the -dev
# package here (e.g. "libpq-dev" for psycopg2).
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
gcc \
libffi-dev \
&& rm -rf /var/lib/apt/lists/*
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 slim 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
# The actual image that ships. No compilers, no build tools, no sudo.
# --------------------------------------------------------------------------
FROM python:3.12.13-slim
# ── Runtime system packages ────────────────────────────────────────────────
#
# These are the only apt packages in the final image. The list is kept
# intentionally tiny. If you need something extra, just add it below.
#
# Core: tini (PID 1), gosu (drop privileges)
# Utilities: curl, git, jq, less, procps (ps/top)
# Firewall: iptables, ipset, dnsmasq (egress whitelist)
#
# ➡️ Want more tools? Add them here, one per line for easy diffs:
# e.g. vim \
# sqlite3 \
#
RUN apt-get update && apt-get install -y --no-install-recommends \
tini \
gosu \
curl \
git \
jq \
less \
procps \
iptables \
ipset \
dnsmasq \
libcap2-bin \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Copy the pre-built Python packages from the builder stage.
COPY --from=builder /install /usr/local
WORKDIR /app
COPY . .
# Uncomment to apply security patches beyond what the base image provides.
# Not recommended for reproducible builds; prefer bumping the base image tag.
# RUN apt-get update && apt-get upgrade -y && rm -rf /var/lib/apt/lists/*
# 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 gosu.
#
RUN useradd -m -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 gosu → runs the app as "user"
#
# If you don't need the egress firewall, you can add "USER user" here
# and simplify the entrypoint — but gosu handles it cleanly either way.
#
ENV SHELL=/bin/bash
EXPOSE 8000
# tini is PID 1 — reaps zombies and forwards signals cleanly.
ENTRYPOINT ["/usr/bin/tini", "--", "/app/entrypoint-slim.sh"]
CMD ["run"]