From 8a253b2e212be04c170080278ae8989bd9fe9702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yurii=20=7C=20monok8i=20=F0=9F=A6=8B?= Date: Sat, 7 Jun 2025 19:29:42 +0000 Subject: [PATCH 1/9] build: update Dockerfile, Makefile, and .dockerignore for improved build process - Refine Dockerfile for better multi-stage builds and environment setup - Update Makefile to streamline build and development commands - Adjust .dockerignore to optimize Docker context and image size --- .dockerignore | 12 +++++++++++- Dockerfile | 4 +++- Makefile | 23 ++++++++++++++++------- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.dockerignore b/.dockerignore index 093126f..dc4f0fd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -16,6 +16,7 @@ __pycache__/ .hypothesis/ pip-log.txt pip-delete-this-directory.txt +requirements.txt # Editor directories and OS files .idea/ @@ -41,4 +42,13 @@ mkdocs.yml # Project files you don't want in image LICENSE -Makefile \ No newline at end of file +Makefile + +# Github +.devcontainer/ +.github/ + +# Docker files +Dockerfile +docker-compose.yml +.dockerignore \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2448820..46c41d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,8 @@ COPY . /app RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --locked --no-dev +# Remove the uv lock file and pyproject.toml, so that the final image does not +RUN rm -f pyproject.toml uv.lock .python-version # Then, use a final image without uv FROM python:3.12-slim-bookworm @@ -36,4 +38,4 @@ WORKDIR /app # Place executables in the environment at the front of the path ENV PATH="/app/.venv/bin:$PATH" -ENTRYPOINT ["/bin/sh", "./docker/docker-entrypoint.sh"] +CMD ["/bin/sh", "./docker/docker-entrypoint.sh"] diff --git a/Makefile b/Makefile index 8309264..9ad4f63 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,7 @@ PROJECT_NAME = python-boilerplate +CONTAINER_NAME = py-blplt +d = docker +dc = docker compose run: uv run __main__.py @@ -16,25 +19,31 @@ typecheck: uv run mypy --config-file=pyproject.toml --explicit-package-bases ./src/ docker-build: - docker build -t $(PROJECT_NAME) . + $(d) build -t $(PROJECT_NAME) . + +docker-bash: + $(d) run --rm -it --env-file .env $(PROJECT_NAME) /bin/bash docker-run: - docker run --rm -it --env-file example.env $(PROJECT_NAME) + $(d) run --rm -it --env-file .env $(PROJECT_NAME) + +docker-logs: + $(d) logs -f $(CONTAINER_NAME) compose-build: - docker compose up --build -d + $(dc) up --build -d compose-up: - docker compose up -d + $(dc) up -d compose-stop: - docker compose stop + $(dc) stop compose-down: - docker compose down + $(dc) down clean: find . -type d -name '__pycache__' -exec rm -rf {} + rm -rf .mypy_cache .ruff_cache .pytest_cache -.PHONY: run test lint format typecheck check docker-build docker-run compose-build compose-up compose-stop compose-down clean +.PHONY: run test lint format typecheck check docker-build docker-bash docker-run compose-build compose-up compose-stop compose-down clean From 3474c0924f38cdac14798caca809ec09ce3e9653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yurii=20=7C=20monok8i=20=F0=9F=A6=8B?= Date: Sat, 7 Jun 2025 21:57:26 +0000 Subject: [PATCH 2/9] build: setup docker for running dev and prod dockerfiles --- .dockerignore | 3 ++- .gitignore | 3 +++ Makefile | 42 ++++++++++++++++++++++------------- dev.Dockerfile | 17 ++++++++++++++ docker-compose.yml | 23 +++++++++---------- example.env | 6 +++++ Dockerfile => prod.Dockerfile | 2 +- 7 files changed, 66 insertions(+), 30 deletions(-) create mode 100644 dev.Dockerfile rename Dockerfile => prod.Dockerfile (96%) diff --git a/.dockerignore b/.dockerignore index dc4f0fd..97abb08 100644 --- a/.dockerignore +++ b/.dockerignore @@ -49,6 +49,7 @@ Makefile .github/ # Docker files -Dockerfile +dev.Dockerfile +prod.Dockerfile docker-compose.yml .dockerignore \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3453a7c..4a1db89 100644 --- a/.gitignore +++ b/.gitignore @@ -129,6 +129,9 @@ celerybeat.pid # Environments .env +.env.development +.env.test +.env.production .venv env/ venv/ diff --git a/Makefile b/Makefile index 9ad4f63..6c78000 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,11 @@ PROJECT_NAME = python-boilerplate -CONTAINER_NAME = py-blplt +DEV_NAME = dev +PROD_NAME = prod d = docker dc = docker compose +# ============ Commands for local development + run: uv run __main__.py @@ -18,32 +21,39 @@ format: typecheck: uv run mypy --config-file=pyproject.toml --explicit-package-bases ./src/ -docker-build: - $(d) build -t $(PROJECT_NAME) . - -docker-bash: - $(d) run --rm -it --env-file .env $(PROJECT_NAME) /bin/bash +dev-logs: + $(d) logs -f $(DEV_NAME) -docker-run: - $(d) run --rm -it --env-file .env $(PROJECT_NAME) +dev-exec: + $(d) exec -it $(DEV_NAME) /bin/bash -docker-logs: - $(d) logs -f $(CONTAINER_NAME) +dev-bash: + $(d) run --rm -it --env-file .env.development $(PROJECT_NAME):dev /bin/bash -compose-build: - $(dc) up --build -d +dev-build: + $(dc) --env-file=.env.development build -compose-up: - $(dc) up -d +dev-up: + $(dc) --env-file=.env.development up -d -compose-stop: +dev-stop: $(dc) stop -compose-down: +dev-down: $(dc) down clean: find . -type d -name '__pycache__' -exec rm -rf {} + rm -rf .mypy_cache .ruff_cache .pytest_cache + +# ============ Commands to check prod image + +prod-build: + $(d) build -t $(PROJECT_NAME):prod -f prod.Dockerfile . + +prod-run: + $(d) run -it --env-file .env.production --name $(PROD_NAME) $(PROJECT_NAME):prod + + .PHONY: run test lint format typecheck check docker-build docker-bash docker-run compose-build compose-up compose-stop compose-down clean diff --git a/dev.Dockerfile b/dev.Dockerfile new file mode 100644 index 0000000..ca20e0d --- /dev/null +++ b/dev.Dockerfile @@ -0,0 +1,17 @@ +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim + +WORKDIR /app + +# Copy all files (can be made more selective) +COPY . /app + +# Install all dependencies, including dev +RUN uv sync + +# Add dev tools if you need anything else +# RUN apt-get update && apt-get install -y vim git + +ENV PATH="/app/.venv/bin:$PATH" + +# For interactive work - bash or shell +CMD ["/bin/sh", "docker/docker-entrypoint.sh"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index b53991b..c0243a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,17 @@ -# rename to your project name name: python-boilerplate - -# rename to your network networks: project-network: - name: project-network + name: ${DOCKER_NETWORK_NAME} driver: bridge services: python-boilerplate: - image: python-boilerplate - container_name: py-blplt + image: ${DOCKER_PROJECT_NAME}:${DOCKER_IMAGE_TAG} + container_name: ${DOCKER_CONTAINER_NAME} build: - context: ./ - dockerfile: Dockerfile + context: . + dockerfile: dev.Dockerfile develop: watch: @@ -22,10 +19,8 @@ services: - action: sync path: . target: /app - # Exclude the project virtual environment ignore: - app/.venv/ - # Rebuild the image on changes to the `pyproject.toml` - action: rebuild path: ./app/pyproject.toml @@ -33,7 +28,11 @@ services: ports: - "8080:8000" restart: on-failure + + # Можливо, краще монтувати все в /app (якщо твій код у src, скоригуй) volumes: - - ./src/:/src/ + - ./docs/:/app/docs + - ./tests/:/app/tests + - ./src/:/app/src/ networks: - - project-network \ No newline at end of file + - project-network diff --git a/example.env b/example.env index e69de29..8be451d 100644 --- a/example.env +++ b/example.env @@ -0,0 +1,6 @@ +DOCKER_PROJECT_NAME=python-boilerplate +DOCKER_NETWORK_NAME=project-network +DOCKER_IMAGE_TAG=dev +DOCKER_CONTAINER_NAME=dev + +APP_STAGE=development \ No newline at end of file diff --git a/Dockerfile b/prod.Dockerfile similarity index 96% rename from Dockerfile rename to prod.Dockerfile index 46c41d6..d4669c4 100644 --- a/Dockerfile +++ b/prod.Dockerfile @@ -38,4 +38,4 @@ WORKDIR /app # Place executables in the environment at the front of the path ENV PATH="/app/.venv/bin:$PATH" -CMD ["/bin/sh", "./docker/docker-entrypoint.sh"] +ENTRYPOINT ["/bin/sh", "./docker/docker-entrypoint.sh"] From 9991438b01352a5955d4ad1368644aee27477d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yurii=20=7C=20monok8i=20=F0=9F=A6=8B?= Date: Sat, 7 Jun 2025 22:09:53 +0000 Subject: [PATCH 3/9] feat: add loading env variables based on app stage --- __main__.py | 5 +++++ dev.Dockerfile | 1 + prod.Dockerfile | 1 + pyproject.toml | 9 +++++---- src/config/env.py | 13 +++++++++++-- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/__main__.py b/__main__.py index 06cdd5b..5d6a21f 100644 --- a/__main__.py +++ b/__main__.py @@ -1,9 +1,14 @@ +from src.config.env import APP_STAGE, ENV_FILE_MAP, env from src.utils.logging import setup_logging def main() -> None: logger = setup_logging() logger.info("Starting the application...") + logger.info(f"App stage: {env.str('APP_STAGE')}") + logger.info( + f"Environment variables loaded from: {ENV_FILE_MAP[APP_STAGE]}" + ) if __name__ == "__main__": diff --git a/dev.Dockerfile b/dev.Dockerfile index ca20e0d..cbf045d 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -12,6 +12,7 @@ RUN uv sync # RUN apt-get update && apt-get install -y vim git ENV PATH="/app/.venv/bin:$PATH" +ENV APP_STAGE="development" # For interactive work - bash or shell CMD ["/bin/sh", "docker/docker-entrypoint.sh"] \ No newline at end of file diff --git a/prod.Dockerfile b/prod.Dockerfile index d4669c4..4e553d0 100644 --- a/prod.Dockerfile +++ b/prod.Dockerfile @@ -37,5 +37,6 @@ WORKDIR /app # Place executables in the environment at the front of the path ENV PATH="/app/.venv/bin:$PATH" +ENV APP_STAGE="production" ENTRYPOINT ["/bin/sh", "./docker/docker-entrypoint.sh"] diff --git a/pyproject.toml b/pyproject.toml index 3c3c7dc..8fdf298 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "0.8.0" description = "A template for quickly starting new projects in Python " readme = "README.md" requires-python = ">=3.12" -dependencies = [] +dependencies = ["environs>=14.2.0"] [dependency-groups] dev = [ @@ -28,7 +28,7 @@ lint.select = [ "N", # pep8-naming # "D", # pydocstyle "UP", # pyupgrade - "I", # isort + "I", # isort ] # Ignore some rules that may be too strict for general use @@ -73,12 +73,13 @@ section-order = [ [tool.ruff.format] quote-style = "double" # Use double quotes indent-style = "space" # Use spaces for indentation -line-ending = "auto" # Auto-detect line endings +line-ending = "auto" # Auto-detect line endings # ================================= MYPY [tool.mypy] -strict = true # Enable strict type checking +strict = true # Enable strict type checking explicit_package_bases = true +ignore_missing_imports = true # Ignore missing imports for third-party libraries # ================================= COMMITIZEN [tool.commitizen] diff --git a/src/config/env.py b/src/config/env.py index 1095b40..9846f63 100644 --- a/src/config/env.py +++ b/src/config/env.py @@ -1,8 +1,17 @@ +import os from pathlib import Path from typing import Final +from environs import Env + ABS_PATH: Final[Path] = Path(__file__).resolve().parent.parent.parent -ENV_PATH: Final[Path] = ABS_PATH / ".env" +APP_STAGE: Final[str] = os.getenv("APP_STAGE", "development") + +ENV_FILE_MAP: Final[dict[str, Path]] = { + "development": ABS_PATH / ".env.development", + "production": ABS_PATH / ".env.production", +} -print(ENV_PATH) +env = Env() +env.read_env(ENV_FILE_MAP[APP_STAGE]) From 65a5b7f6535ff513b74dbe0fa662a02266e359db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yurii=20=7C=20monok8i=20=F0=9F=A6=8B?= Date: Sat, 7 Jun 2025 22:18:38 +0000 Subject: [PATCH 4/9] fix: move getting correct path to env file to another variable --- __main__.py | 6 ++---- src/config/env.py | 4 +++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/__main__.py b/__main__.py index 5d6a21f..83b1c5b 100644 --- a/__main__.py +++ b/__main__.py @@ -1,4 +1,4 @@ -from src.config.env import APP_STAGE, ENV_FILE_MAP, env +from src.config.env import ENV_PATH, env from src.utils.logging import setup_logging @@ -6,9 +6,7 @@ def main() -> None: logger = setup_logging() logger.info("Starting the application...") logger.info(f"App stage: {env.str('APP_STAGE')}") - logger.info( - f"Environment variables loaded from: {ENV_FILE_MAP[APP_STAGE]}" - ) + logger.info(f"Environment variables loaded from: {ENV_PATH}") if __name__ == "__main__": diff --git a/src/config/env.py b/src/config/env.py index 9846f63..f637ba2 100644 --- a/src/config/env.py +++ b/src/config/env.py @@ -13,5 +13,7 @@ "production": ABS_PATH / ".env.production", } +ENV_PATH: Final[Path] = ENV_FILE_MAP[APP_STAGE] + env = Env() -env.read_env(ENV_FILE_MAP[APP_STAGE]) +env.read_env(ENV_PATH) From 3191080c51f55ac2d54e3f92749f046285752c02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yurii=20=7C=20monok8i=20=F0=9F=A6=8B?= Date: Sat, 7 Jun 2025 22:18:51 +0000 Subject: [PATCH 5/9] =?UTF-8?q?bump:=20version=200.8.0=20=E2=86=92=200.9.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 11 +++++++++++ pyproject.toml | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d527e3..1422345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 0.9.0 (2025-06-07) + +### Feat + +- add loading env variables based on app stage + +### Fix + +- move getting correct path to env file to another variable +- fix name of workflow + ## 0.8.0 (2025-06-05) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 8fdf298..a34fe09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-boilerplate" -version = "0.8.0" +version = "0.9.0" description = "A template for quickly starting new projects in Python " readme = "README.md" requires-python = ">=3.12" From f5c1ace3cc3147b5da2ca20399fbfd9d0f45b2e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yurii=20=7C=20monok8i=20=F0=9F=A6=8B?= Date: Sat, 7 Jun 2025 22:24:47 +0000 Subject: [PATCH 6/9] fix: fix command to run container and add logs command to check prod container logs --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 6c78000..ca36325 100644 --- a/Makefile +++ b/Makefile @@ -53,7 +53,9 @@ prod-build: $(d) build -t $(PROJECT_NAME):prod -f prod.Dockerfile . prod-run: - $(d) run -it --env-file .env.production --name $(PROD_NAME) $(PROJECT_NAME):prod + $(d) run -d --env-file .env.production --name $(PROD_NAME) $(PROJECT_NAME):prod +prod-logs: + $(d) logs -f $(PROD_NAME) -.PHONY: run test lint format typecheck check docker-build docker-bash docker-run compose-build compose-up compose-stop compose-down clean +.PHONY: run test lint format typecheck dev-logs dev-exec dev-bash dev-build dev-up dev-stop dev-down clean prod-build prod-run From 3c7ae7f658f3601cad757f0308ede4cd542fa469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yurii=20=7C=20monok8i=20=F0=9F=A6=8B?= Date: Sat, 7 Jun 2025 22:37:07 +0000 Subject: [PATCH 7/9] fix: fix adding all env files to dev image --- .dockerignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index 97abb08..4ee79bb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,8 @@ __pycache__/ .venv/ .venv .env/ +.env.development +.env.production .Python .mypy_cache/ .ruff_cache/ From edba36a327c0df2126f4db79e0072031403820d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yurii=20=7C=20monok8i=20=F0=9F=A6=8B?= Date: Sat, 7 Jun 2025 22:42:53 +0000 Subject: [PATCH 8/9] build: add multistage building to dev image --- dev.Dockerfile | 38 ++++++++++++++++++++++++++++---------- prod.Dockerfile | 3 ++- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/dev.Dockerfile b/dev.Dockerfile index cbf045d..19bed90 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -1,18 +1,36 @@ -FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim +# Example taken from https://github.com/astral-sh/uv-docker-example/blob/main/multistage.Dockerfile +# An example using multi-stage image builds to create a final image without uv. -WORKDIR /app +# First, build the application in the `/app` directory. +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder +ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy -# Copy all files (can be made more selective) +# Disable Python downloads, because we want to use the system interpreter +# across both images. If using a managed Python version, it needs to be +# copied from the build image into the final image; see `standalone.Dockerfile` +# for an example. +ENV UV_PYTHON_DOWNLOADS=0 + +WORKDIR /app +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 COPY . /app +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --locked -# Install all dependencies, including dev -RUN uv sync +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim + +# Copy the application from the builder +COPY --from=builder --chown=app:app /app /app -# Add dev tools if you need anything else -# RUN apt-get update && apt-get install -y vim git +WORKDIR /app +# Place executables in the environment at the front of the path ENV PATH="/app/.venv/bin:$PATH" -ENV APP_STAGE="development" -# For interactive work - bash or shell -CMD ["/bin/sh", "docker/docker-entrypoint.sh"] \ No newline at end of file +# Set the application stage to production +ENV APP_STAGE="production" + +CMD ["/bin/sh", "./docker/docker-entrypoint.sh"] \ No newline at end of file diff --git a/prod.Dockerfile b/prod.Dockerfile index 4e553d0..cba2416 100644 --- a/prod.Dockerfile +++ b/prod.Dockerfile @@ -2,7 +2,6 @@ # An example using multi-stage image builds to create a final image without uv. # First, build the application in the `/app` directory. -# See `Dockerfile` for details. FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy @@ -37,6 +36,8 @@ WORKDIR /app # Place executables in the environment at the front of the path ENV PATH="/app/.venv/bin:$PATH" + +# Set the application stage to production ENV APP_STAGE="production" ENTRYPOINT ["/bin/sh", "./docker/docker-entrypoint.sh"] From 87ac77a42159f2acdbb85c5b4ea5aaae3a7bfc69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yurii=20=7C=20monok8i=20=F0=9F=A6=8B?= Date: Sat, 7 Jun 2025 22:44:56 +0000 Subject: [PATCH 9/9] fix: set the correct app stage for dev image --- __main__.py | 4 ++-- prod.Dockerfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/__main__.py b/__main__.py index 83b1c5b..dcf6790 100644 --- a/__main__.py +++ b/__main__.py @@ -1,11 +1,11 @@ -from src.config.env import ENV_PATH, env +from src.config.env import APP_STAGE, ENV_PATH from src.utils.logging import setup_logging def main() -> None: logger = setup_logging() logger.info("Starting the application...") - logger.info(f"App stage: {env.str('APP_STAGE')}") + logger.info(f"App stage: {APP_STAGE}") logger.info(f"Environment variables loaded from: {ENV_PATH}") diff --git a/prod.Dockerfile b/prod.Dockerfile index cba2416..31ddcee 100644 --- a/prod.Dockerfile +++ b/prod.Dockerfile @@ -38,6 +38,6 @@ WORKDIR /app ENV PATH="/app/.venv/bin:$PATH" # Set the application stage to production -ENV APP_STAGE="production" +ENV APP_STAGE="development" ENTRYPOINT ["/bin/sh", "./docker/docker-entrypoint.sh"]