From 511b2abd8712265525790a8498573cd42cc9cd3d Mon Sep 17 00:00:00 2001 From: Pymetheus Date: Fri, 13 Feb 2026 22:00:40 +0100 Subject: [PATCH] feat: add Docker support; update CI/CD, docs, and bootstrap Introduce a docker/ directory (multi-stage Dockerfile and docker-compose.yml) and wire containerization into CI/CD and docs. Update GitHub Actions: bootstrap.yml updates package/distribution placeholders and yq version; ci.yml adds a build-docker job and a container smoke test; cd.yml adds attestations permission and includes a commented publish-docker job for GHCR. Adjust .dockerignore to exclude docker context and add repository metadata files. Revise README and docs/ to document containerization, CI/CD changes, and checklist items. Small runtime change: disable writing logs to disk in src package_name/main.py (setup_logging write_to_disk=False). --- .dockerignore | 7 +-- .github/workflows/bootstrap.yml | 11 ++--- .github/workflows/cd.yml | 46 +++++++++++++++++++- .github/workflows/ci.yml | 19 ++++++++- README.md | 16 ++++--- docker/Dockerfile | 37 ++++++++++++++++ docker/docker-compose.yml | 11 +++++ docs/CHECKLIST.md | 4 +- docs/INSTRUCTIONS.md | 75 ++++++++++++++++++++++++--------- src/package_name/main.py | 2 +- 10 files changed, 192 insertions(+), 36 deletions(-) diff --git a/.dockerignore b/.dockerignore index 3879efa..014dc12 100644 --- a/.dockerignore +++ b/.dockerignore @@ -56,6 +56,7 @@ nosetests.xml # We exclude everything not strictly required to run the application .log/ .github/ +docker/ docs/ notebooks/ tests/ @@ -63,9 +64,8 @@ res/ # Ignore everything in config except for the shared templates .config/* -!.config/*.example -!.config/*.example.* !.config/config.dev.toml +!.config/config.*.toml # Data Handling (Exclude data from image context) data/ @@ -73,8 +73,9 @@ data/ # Repository Metadata .git/ .gitignore +.gitattributes .dockerignore .pre-commit-config.yaml +codecov.yml LICENSE.md README.md -pyproject.toml diff --git a/.github/workflows/bootstrap.yml b/.github/workflows/bootstrap.yml index 7bf9619..7bd5c80 100644 --- a/.github/workflows/bootstrap.yml +++ b/.github/workflows/bootstrap.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout + - name: Checkout code uses: actions/checkout@v6 - name: Collect metadata @@ -31,11 +31,9 @@ jobs: echo "EOF" } >> "$GITHUB_OUTPUT" - # python package naming PACKAGE=$(echo "$REPO" | sed -E 's/[- ]+/_/g' | tr 'A-Z' 'a-z') # snake_case for python package DISTRIBUTION=$(echo "$REPO" | sed -E 's/[_ ]+/-/g' | tr 'A-Z' 'a-z') # kebab-case for python distribution - # store values for later steps echo "repo=$REPO" >> $GITHUB_OUTPUT echo "user=$USER" >> $GITHUB_OUTPUT echo "year=$YEAR" >> $GITHUB_OUTPUT @@ -43,7 +41,7 @@ jobs: echo "distribution=$DISTRIBUTION" >> $GITHUB_OUTPUT - name: Install yq - uses: mikefarah/yq@v4.50.1 + uses: mikefarah/yq@v4.52.2 - name: Load bootstrap config id: load_config @@ -71,11 +69,14 @@ jobs: mv src/package_name "src/${{ steps.meta.outputs.package }}" fi - - name: Update pyproject.toml + - name: Update pyproject.toml & Docker run: | sed -i "s/DISTRIBUTION-NAME/${{ steps.meta.outputs.distribution }}/g" pyproject.toml sed -i "s/package_name/${{ steps.meta.outputs.package }}/g" pyproject.toml sed -i "s/AUTHOR@EXAMPLE.COM/${{ steps.load_config.outputs.email }}/g" pyproject.toml + sed -i "s/DISTRIBUTION-NAME/${{ steps.meta.outputs.distribution }}/g" docker/Dockerfile + sed -i "s/blueprint-docker/${{ steps.meta.outputs.distribution }}/g" docker/docker-compose.yml + - name: Replace Billboard README with Project README run: | diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index eb2077b..184e152 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -12,6 +12,7 @@ permissions: packages: write id-token: write actions: read + attestations: write env: PYTHON_VERSION: "3.12" @@ -43,6 +44,49 @@ jobs: exit 1 fi +# publish-docker: +# name: Build and publish Docker Image +# runs-on: ubuntu-latest +# needs: download-distribution +# +# steps: +# - name: Checkout code +# uses: actions/checkout@v6 +# +# - name: Normalize repository name +# id: repo +# run: | +# echo "repo=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT +# +# - name: Extract version +# id: meta +# run: | +# VERSION=$(grep '^version =' pyproject.toml | sed 's/version = "\(.*\)"/\1/') +# echo "version=$VERSION" >> $GITHUB_OUTPUT +# +# - name: Login to GitHub Container Registry +# uses: docker/login-action@v3 +# with: +# registry: ghcr.io +# username: ${{ github.actor }} +# password: ${{ secrets.GITHUB_TOKEN }} +# +# - name: Set up Docker Buildx +# uses: docker/setup-buildx-action@v3 +# +# - name: Build and push +# uses: docker/build-push-action@v6 +# with: +# context: . +# file: docker/Dockerfile +# platforms: | +# linux/amd64 +# linux/arm64 +# push: true +# tags: | +# ghcr.io/${{ steps.repo.outputs.repo }}:latest +# ghcr.io/${{ steps.repo.outputs.repo }}:${{ steps.meta.outputs.version }} +# # publish-test-pypi: # name: Publish to TestPyPI # runs-on: ubuntu-latest @@ -72,7 +116,7 @@ jobs: # github-release: # name: Create GitHub Release # runs-on: ubuntu-latest -# needs: [download-distribution, publish-test-pypi] +# needs: [publish-docker, publish-test-pypi] # # steps: # - name: Checkout code diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67f9839..f30b206 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,8 +124,25 @@ jobs: name: python-package-distributions-${{ github.sha }} path: dist/ + build-docker: + name: Build Docker Image & Smoke Test + runs-on: ubuntu-latest + needs: test + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Build Docker image + run: | + docker build -t ${{ github.repository }}:${{ github.sha }} -f docker/Dockerfile . + + - name: Smoke Test Container + run: | + docker run --rm ${{ github.repository }}:${{ github.sha }} + smoke-test: - name: Smoke Test Installation + name: Smoke Test Distribution runs-on: ubuntu-latest needs: build-distribution steps: diff --git a/README.md b/README.md index f2df665..cb19485 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,10 @@ [use-template-badge]: https://img.shields.io/badge/Use%20this%20template-006222 -A **production-ready Python project template** designed to remove setup friction and enforce best practices from the very first commit. +A **production-ready Python project template** designed to reduce repeated setup tasks and enforce best practices from the very first commit. The **Python Project Blueprint** gives teams a clean, scalable foundation for Python applications and libraries, -with configuration management, structured logging, testing, security scanning, and CI/CD already integrated, so you can focus on building your product, not infrastructure. +with configuration management, structured logging, testing, security scanning, containerization, and CI/CD already integrated, so you can focus more on building your product. > **Quick Start:** Follow the [Checklist](docs/CHECKLIST.md) to start your next Python project in seconds. @@ -84,13 +84,19 @@ By establishing structure, tooling, and automation upfront, it reduces the need - Secret detection via `detect-secrets` - Dependency vulnerability scanning with `Snyk` +- **Containerization** + - Multi-stage `Dockerfile` using `uv` for fast builds + - `APP_ENV` configurable at build time and runtime + - Expandable `docker-compose.yml` for multi-container setups + - Publishing images to `GitHub Container Registry` + - **Automated CI/CD** - `prek` hooks for faster local enforcement - Workflows with fast dependency resolution using `uv` - Pull request gatekeeping workflows - - CI verification and packaging - - Automated CD and GitHub Releases - - Dependabot for dependency updates + - CI verification, image building and packaging + - Automated CD with `TestPyPI`, `GHCR` and `GitHub Release` + - Dependency updates with `Dependabot` - **Governance at Scale** - Issue & PR templates diff --git a/docker/Dockerfile b/docker/Dockerfile index 9c4fa6f..0dbea0f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1 +1,38 @@ # This Dockerfile was generated for [[PACKAGE_NAME]] + +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder + +ENV UV_COMPILE_BYTECODE=1 +ENV UV_LINK_MODE=copy +ENV UV_NO_DEV=1 +ENV UV_PYTHON_DOWNLOADS=0 + +WORKDIR /app + +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --no-install-project + +COPY . /app + +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync + + +FROM python:3.12-slim-bookworm + +RUN groupadd --system --gid 999 nonroot \ + && useradd --system --gid 999 --uid 999 --create-home nonroot + +COPY --from=builder --chown=nonroot:nonroot /app /app + +ENV PATH="/app/.venv/bin:$PATH" + +USER nonroot + +WORKDIR /app + +ARG APP_ENV=DEV +ENV APP_ENV=$APP_ENV + +CMD ["DISTRIBUTION-NAME"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8e378ad..d13e07b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1 +1,12 @@ # This docker-compose.yml was generated for [[PACKAGE_NAME]] + +services: + app: + image: blueprint-docker-image:local + build: + context: .. + dockerfile: docker/Dockerfile + container_name: blueprint-docker-container + environment: + APP_ENV: DEV + working_dir: /app diff --git a/docs/CHECKLIST.md b/docs/CHECKLIST.md index 0878b93..a53a489 100644 --- a/docs/CHECKLIST.md +++ b/docs/CHECKLIST.md @@ -87,7 +87,7 @@ Otherwise, delete the commented-out lines. - [ ] **USER:** Delete `.gitkeep` in `data/`, `notebooks/` and `res/` if applicable, empty directories will disappear from GitHub. - [ ] **USER:** Update text in `docs/DOCUMENTATION.md` if applicable. - [ ] **USER:** Update License type if applicable. -- [ ] **USER:** Update dependencies & [project.optional-dependencies] in `pyproject.toml` if applicable. +- [ ] **USER:** Update dependencies & [dependency-groups] in `pyproject.toml` if applicable. - [ ] **USER:** Update text in `README.md`. @@ -109,6 +109,8 @@ The workflow automatically updates the following sections: ### Setting up `docker/` - [x] **BOOTSTRAP:** Update [[PACKAGE_NAME]] in `docker/docker-compose.yml` & `docker/Dockerfile`. +- [x] **BOOTSTRAP:** Update `DISTRIBUTION-NAME` in `docker/Dockerfile` to reflect correct command. +- [x] **BOOTSTRAP:** Update `blueprint-docker` in `docker/docker-compose.yml` to reflect correct image & container_name. ### Setting up `docs/` - [x] **BOOTSTRAP:** Update [[EMAIL]] in `docs/CODE_OF_CONDUCT.md` to reflect correct contact. diff --git a/docs/INSTRUCTIONS.md b/docs/INSTRUCTIONS.md index 99cde82..0372693 100644 --- a/docs/INSTRUCTIONS.md +++ b/docs/INSTRUCTIONS.md @@ -9,19 +9,20 @@ It reduces setup and automation overhead, allowing teams to focus on application The **Python Project Blueprint** comes with built-in: -| Feature | Description | -|:-----------------------------|:----------------------------------------------------------------------------------------------------------------------------------------| -| **Project Management** | `pyproject.toml` as the centralized configuration for packaging, tooling, metadata and dependency management. | -| **Config Management** | `pydantic-settings` for type-safe environment variable loading and validation. | -| **Structured Logging** | `structlog` integration for JSON streams (production) and colored console output (development). | -| **Professional Layout** | `src/` directory structure to ensure package integrity and prevent local import conflicts. | -| **Comprehensive Testing** | `pytest` and `Codecov` integration with coverage reporting to enforce coverage thresholds and ensure deep visibility into code health. | -| **Static Analysis** | `ruff` for linting/formatting and `mypy` for strict static type checking. | -| **Automated Security** | `Snyk` (OSS) and `bandit` (SAST) integrated to scan for vulnerabilities and leaked secrets. | -| **Standardized Governance** | Automated label synchronization, Issue templates, and Pull Request templates. | -| **Local Automation** | `.pre-commit-config.yaml` for linting, formatting, and security checks with `prek`. | -| **CI/CD Pipeline** | `pr-checks`, `ci`, `cd`, and `security` GitHub Actions workflows. | -| **Instant Bootstrapping** | A `bootstrap.yml` GitHub Action to rebrand and initialize the repository in seconds. | +| Feature | Description | +|:----------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------| +| **Project Management** | `pyproject.toml` as the centralized configuration for packaging, tooling, metadata and dependency management. | +| **Config Management** | `pydantic-settings` for type-safe environment variable loading and validation. | +| **Structured Logging** | `structlog` integration for JSON streams (production) and colored console output (development). | +| **Professional Layout** | `src/` directory structure to ensure package integrity and prevent local import conflicts. | +| **Comprehensive Testing** | `pytest` and `Codecov` integration with coverage reporting to enforce coverage thresholds and ensure deep visibility into code health. | +| **Static Analysis** | `ruff` for linting/formatting and `mypy` for strict static type checking. | +| **Automated Security** | `Snyk` (OSS) and `bandit` (SAST) integrated to scan for vulnerabilities and leaked secrets. | +| **Containerization** | Multi-stage `Docker` build using `uv`, non-root runtime, multi-arch support, and optional GHCR publishing. | +| **Standardized Governance** | Automated label synchronization, Issue templates, and Pull Request templates. | +| **Local Automation** | `.pre-commit-config.yaml` for linting, formatting, and security checks with `prek`. | +| **CI/CD Pipeline** | `pr-checks`, `ci`, `cd`, and `security` GitHub Actions workflows. | +| **Instant Bootstrapping** | A `bootstrap.yml` GitHub Action to rebrand and initialize the repository in seconds. | > **Note on Bootstrapping Process:** > @@ -35,6 +36,7 @@ The **Python Project Blueprint** comes with built-in: - [Logging Management](#logging-management--artifacts) - [Automation & GitHub](#github-actions--automation) - [Data & Environment Management](#data--environment-management) +- [Containerization](#containerization) - [Source Code](#source-code-management) - [Testing Management](#testing-management) - [Project Management with pyproject.toml](#project-management-with-pyprojecttoml) @@ -151,6 +153,8 @@ The application defaults to `dev` mode. To run in `prod` mode, you simply set the `APP_ENV` environment variable. The system will then automatically look for `config.prod.toml` and `.env.prod`. +> **Note:** *In the Docker image, APP_ENV is also configurable at build time and runtime.* + **Example: Running in Production Mode** ```bash # Linux / macOS @@ -218,7 +222,7 @@ Where applicable, CI/CD workflows use `uv` for dependency installation and resol | 1) `.pre-commit-config.yaml` | On every commit | **Local Guard.** First line of defense. | | 2) `pr-checks.yml` | Pull Requests to main | **Gatekeeping.** Blocks broken or messy code from being merged. | | 3) `ci.yml` | Push to main & manual | **Verification.** Proves the package is ready for distribution. | -| 4) `cd.yml` | On completed `CI` workflow run | **Automation.** Handles the releasing (TestPyPI/Releases). | +| 4) `cd.yml` | On completed `CI` workflow run | **Automation.** Handles the releasing (TestPyPI/GHCR/Releases). | | 5) `security.yml` | Pull Requests to main & manual | **Protection.** Scans for vulnerabilities and leaked secrets. | | 6) Dependabot | Scheduled | **Maintenance.** Automated dependency updates. | @@ -336,6 +340,7 @@ jobs: verify-version: # Preventing deployment collisions. test: # Executes the complete test suite with coverage and uploads report to Codecov. build-distribution: # Packages the code into a distributable format. + build-docker: # Builds the image and performs a quick run. smoke-test: # Proves the package is installable and functional. ```` @@ -351,7 +356,7 @@ By triggering only after a completed CI run, it ensures that only fully vetted a > > **When:** On completed `CI` workflow run. > -> **Focus:** Publish package and create release. +> **Focus:** Publish package, push Docker image and create release. ````yaml name: CD @@ -366,6 +371,7 @@ on: jobs: download-distribution: # Retrieves the verified artifacts from the CI pipeline. publish-test-pypi: # Distributes the package to a test registry for validation. + publish-docker: # Pushes the image to the GitHub Container Registry. github-release: # Formalizes the version with a GitHub Release and changelog. ```` > **Note:** *Deployments in `cd.yml` are commented out. Uncomment them once you are ready to deploy.* @@ -429,14 +435,45 @@ By isolating these from the `src/` directory, the project remains clean, portabl * **`notebooks/`**: A dedicated space for exploratory data analysis (EDA), prototyping, and research. Keeping these separate from the source code ensures that "scratchpad" code and heavy output cells do not clutter production logic. * **`res/`**: Short for "Resources." This folder houses static assets required by the repository, for example images. -### Containerization & Environment -* **`docker/`**: Contains the `Dockerfile` and `docker-compose.yml`. Storing these in a dedicated subdirectory rather than the root keeps the workspace organized and allows for multiple environment configurations (e.g., development vs. production) to coexist easily. -* **`.dockerignore` & `.gitignore`**: These files are precision-tuned to ensure only essential source files enter the build context or the repository. They explicitly exclude local artifacts like `.log/`, `data/`, and `__pycache__`. - ### Documentation & Governance * **`docs/`**: The central hub for project knowledge. While the `README.md` serves as the landing page, `docs/` houses deep-dives like `CONTRIBUTING.md`, `SECURITY.md`, and technical specifications. * **`LICENSE.md`**: Defines the legal framework and usage permissions for the code. * **`README.md`**: The "Front Desk" of the project—focused on high-level overviews, quick-start instructions, and architectural summaries. +* **`.dockerignore` & `.gitignore`**: These files are precision-tuned to ensure only essential source files enter the build context or the repository. They explicitly exclude local artifacts like `.log/`, `data/`, and `__pycache__`. + +--- + +## Containerization +The `docker/` directory contains the `Dockerfile` and `docker-compose.yml`, +focused on reproducible builds, reduced image size and alignment with CI/CD workflows. + +### Multi-Stage Docker Build +The `docker/Dockerfile` uses a multi-stage build strategy: + +* **Builder**: Uses an `uv` image for fast dependency resolution. +* **Runtime**: Uses a slim Python image and runs as a non-root user. + +This results in smaller attack surface and secure default container execution. + +### Environment Configuration +The image supports both build-time configuration and runtime overrides for `APP_ENV`. + +```dockerfile +ARG APP_ENV=DEV +ENV APP_ENV=$APP_ENV +``` + +This allows Docker and orchestration platforms to override configuration without modifying the image. + +### Docker Compose +The provided `docker/docker-compose.yml` is intentionally minimal and expandable for additional services such as databases. + +### Multi-Architecture Support +The CD workflow builds images for: +* **linux/amd64** +* **linux/arm64** + +This allows the image to run on both x86 and ARM environments. --- diff --git a/src/package_name/main.py b/src/package_name/main.py index 2bc2a37..2de60b2 100644 --- a/src/package_name/main.py +++ b/src/package_name/main.py @@ -27,7 +27,7 @@ def main() -> None: """ # Initialize logging first so configuration loading is tracked - setup_logging(write_to_disk=True, log_dir=Path("../../.log")) + setup_logging(write_to_disk=False, log_dir=Path("../../.log")) # Load settings settings = Settings.load()