diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..06cf516 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.venv +.vscode +.idea +.mypy_cache +__pycache__ +LICENSE +CODE_OF_CONDUCT.md +README.md +.pre-commit-config.yaml +mypy.ini +ruff.toml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index e6dde27..d7acbf0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,68 @@ -FROM python:3.12-slim +# --- STAGE 1: Builder --- +# We use a specific SHA to ensure the build is deterministic and reproducible. +FROM python:3.12.10-slim-bookworm@sha256:fd95fa221297a88e1cf49c55ec1828edd7c5a428187e67b5d1805692d11588db AS builder # Install uv directly from its official image COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /bin/uv -# Set the working directory inside the container +# UV Configuration: +# 1. Compile to bytecode for faster startup +# 2. Use 'copy' mode to avoid hardlink issues in some Docker filesystems +# 3. Disable python downloads to force use of the base image's Python +ENV UV_COMPILE_BYTECODE=1 \ + UV_LINK_MODE=copy \ + UV_PYTHON_DOWNLOADS=0 \ + UV_NO_DEV=1 + WORKDIR /app -# Copy dependency files first. -# This allows Docker to cache the installed packages layer. -# If you change your code but not your dependencies, this step will be skipped. +# Copy only dependency definitions to leverage Docker's layer caching. +# If these files don't change, Docker skips the expensive 'uv sync' step. COPY pyproject.toml uv.lock ./ -# Install dependencies into the system python environment. -RUN uv sync --frozen --no-dev +# Install dependencies using a cache mount for the uv cache directory. +# We use a bind mount for the lockfiles to avoid creating extra layers. +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --locked --no-install-project --no-dev + +# Copy the actual application source code +COPY . /app + +# Final sync to install the project itself into the virtual environment +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --locked --no-dev + + +# --- STAGE 2: Runtime --- +# We use the exact same base image to ensure binary compatibility. +FROM python:3.12.10-slim-bookworm@sha256:fd95fa221297a88e1cf49c55ec1828edd7c5a428187e67b5d1805692d11588db -# This makes 'gunicorn' and 'flask' available globally in the container -ENV PATH="/app/.venv/bin:$PATH" +# Security: Create a non-privileged user to run the application. +# Setup a non-root user +RUN groupadd --system --gid 1000 nonroot \ + && useradd --system --gid 1000 --uid 1000 --create-home nonroot -# Copy the rest of the application code -COPY . . +# Copy only the necessary files from the builder (app + .venv) +# This keeps the final image size small +COPY --from=builder --chown=nonroot:nonroot /app /app + +# Use the non-root user for all subsequent operations +USER nonroot + +# Environment Setup: +# 1. Add the virtual environment's bin folder to the PATH +# 2. Ensure Python logs are sent straight to the terminal without buffering +ENV PATH="/app/.venv/bin:$PATH" \ + PYTHONUNBUFFERED=1 + +WORKDIR /app -# Expose the port the app runs on +# Expose the application port EXPOSE 5000 -# Run the production server -# -w 4: Run 4 worker processes -# -b 0.0.0.0:5000: Listen on all interfaces on port 5000 +# Run the application using Gunicorn. +# -w 4: 4 worker processes for concurrency +# -b 0.0.0.0: Listen on all network interfaces CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "application:app"] \ No newline at end of file