Containerization
Overview
Software containers have become an integral part of the modern software development landscape. They bundle an application with its runtime and dependencies into a portable unit that runs consistently across environments. Container engines such as Docker and Podman build and run these images on developer machines, CI runners, and cloud platforms.
The Depsight project leverages containerization for its terminal application by packaging the CLI into a portable Open Container Initiative (OCI) image. The top-level Dockerfile is the build recipe that installs depsight into a slim Python base image, which is then published to Docker Hub alongside the wheel on PyPI.
Container Ecosystem
Docker Official Images
Docker maintains a curated library of Official Images on Docker Hub. These images follow best practices, receive regular updates, and serve as trusted base layers for application containers. For Python projects, the most commonly used variants are the full python:<version> image and its leaner counterpart python:<version>-slim, which ships a minimal Debian installation with only the packages required to run CPython.
Depsight builds on top of python:3.12-slim. The slim variant keeps the image small while still providing the standard library, pip, and the shared libraries needed to install compiled packages. Using an official image also means that security patches flow in through regular upstream rebuilds without requiring manual intervention.
Dockerfile Architecture
Container Build Primitives
The Dockerfile is composed of a small set of instructions that together define the build and runtime behavior of the image. The following subsections describe the primitives used in the Depsight Dockerfile.
ARG
ARG declares a build-time variable that can be passed via --build-arg on the command line. It only exists during the build and is not available at container runtime. Depsight uses ARG to parameterize the Python version, the uv version, and the non-root user identity so that the CI pipeline or a developer can pin exact versions without editing the Dockerfile.
| Argument | Default | Purpose |
|---|---|---|
PYTHON_VERSION |
3.12 |
Base Python image tag |
UV_VERSION |
0.11.1 |
uv installer version |
USER_ID |
1000 |
UID for the non-root runtime user |
USER_NAME |
depsight |
Username for the non-root runtime user |
RUN
RUN executes a command inside the build container and commits the resulting filesystem change as a new image layer. Each RUN instruction creates one layer, so chaining related commands with && reduces layer count and image size. In the builder stage, RUN installs system packages, downloads uv, and runs uv sync to install dependencies and the project.
COPY
COPY transfers files from the build context (or from a previous stage via --from) into the image. Depsight copies pyproject.toml and uv.lock before copying src/ so that Docker can cache the dependency layer independently. In the final stage, COPY --from=builder selectively pulls the virtual environment and uv binaries without carrying over the build-time toolchain.
ENTRYPOINT
ENTRYPOINT sets the default executable for the container. When a user runs docker run depsight:local --help, Docker invokes the entrypoint binary with --help as its argument. Depsight sets ENTRYPOINT ["depsight"] so the container behaves like a direct invocation of the CLI.
Multi-Stage Builds
Introduction
A multi-stage build splits a Dockerfile into multiple FROM stages. Each stage starts from its own base image and can selectively copy artifacts from a previous stage. The main advantage is image size. Build-time tools, compilers, and intermediate files never end up in the final image because they are discarded when the stage completes.
Depsight uses two stages. The builder stage starts from python:3.12, installs uv and all project dependencies, and then installs depsight itself. The final stage starts from a fresh python:3.12-slim image and copies only the runtime artifacts (the uv binaries, the virtual environment, and the plugin source) from the builder. This keeps build-time dependencies such as curl and the full source tree out of the shipped image.
flowchart LR
subgraph Builder["Builder Stage"]
B1["Base: python:3.12"]
B2["Install uv"]
B3["Install dependencies"]
B4["Install Depsight"]
end
subgraph Final["Final Stage"]
F1["Base: python:3.12-slim"]
F2["Copy uv + .venv + src"]
F3["Non-root runtime user"]
F4["depsight entrypoint"]
end
B1 --> B2 --> B3 --> B4
F1 --> F2 --> F3 --> F4
B4 -. runtime artifacts .-> F2
Builder Stage
The builder stage starts from python:3.12, installs [uv`](https://docs.astral.sh/uv/) via its official installer script, and then builds the project inside a virtual environment.
ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION} AS builder
ARG UV_VERSION=0.11.1
RUN curl -LsSf https://astral.sh/uv/${UV_VERSION}/install.sh | UV_INSTALL_DIR=/usr/local/bin sh
WORKDIR /depsight
Dependencies are installed before copying the source code. Docker caches each layer independently, so as long as pyproject.toml, uv.lock, README.md and CONTRIBUTING.md have not changed, the dependency layer is reused and only the final project install is re-run.
# Install dependencies (Cached Layer)
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
# Install the actual project
COPY src/ src/
COPY README.md ./
# --no-editable ensures the code is physically moved into site-packages
RUN uv sync --frozen --no-dev --no-editable
Layer Caching
Splitting uv sync into two steps — dependencies first, then the project — means that a source-only change rebuilds only the last layer. This significantly speeds up iterative builds during development.
Runtime Stage
The final stage starts from a fresh python:3.12-slim image and copies only what is needed at runtime.
FROM python:${PYTHON_VERSION}-slim
WORKDIR /depsight
# Create non-root user
ARG USER_ID=1000
ARG USER_NAME=depsight
RUN apt-get update && \
apt-get upgrade -y && \
rm -rf /var/lib/apt/lists/* && \
groupadd -g ${USER_ID} ${USER_NAME} && \
useradd -u ${USER_ID} -g ${USER_NAME} -m -s /bin/bash ${USER_NAME}
# Copy the virtual environment ONLY
# Because we used --no-editable, the code lives inside this folder now.
COPY --from=builder --chown=${USER_NAME}:${USER_NAME} /depsight/.venv /depsight/.venv
# Copy uv binaries ONLY if Depsight needs to call 'uv' commands at runtime
COPY --from=builder /usr/local/bin/uv /usr/local/bin/uvx /usr/local/bin/
The container runs as a non-root user (depsight) and exposes the CLI as its entrypoint.
RUN mkdir -p /home/${USER_NAME}/.depsight/logs /home/${USER_NAME}/.depsight/data && \
chown -R ${USER_NAME}:${USER_NAME} /depsight /home/${USER_NAME}
USER ${USER_NAME}
ENV PATH="/depsight/.venv/bin:$PATH"
ENV PYTHONPATH="/depsight/src"
ENV PYTHONUNBUFFERED=1
ENTRYPOINT ["depsight"]
Docker Command Primitives
The docker build command reads the Dockerfile in the current directory, executes both stages, and tags the resulting image as depsight:local. This image exists only on the local machine and is not pushed to any registry.
docker build -t depsight:local .
The docker run command creates a short-lived container from the local image. The --rm flag removes the container automatically after it exits. Any arguments after the image name are forwarded to the depsight entrypoint, so --help and uv scan --help print the CLI and subcommand usage respectively.
docker run --rm depsight:local --help
docker run --rm depsight:local uv scan --help