Skip to content

Commit

Permalink
Merge pull request #5 from max-pfeiffer/feature/dockerfile_optimisations
Browse files Browse the repository at this point in the history
Feature/dockerfile optimisations
  • Loading branch information
max-pfeiffer authored Dec 27, 2023
2 parents 364064d + 49ce121 commit 02dbc3a
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 25 deletions.
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,30 @@
[![codecov](https://codecov.io/gh/max-pfeiffer/uvicorn-poetry-project-template/branch/main/graph/badge.svg?token=WQI2SJJLZN)](https://codecov.io/gh/max-pfeiffer/uvicorn-poetry-project-template)
![pipeline workflow](https://github.com/max-pfeiffer/uvicorn-poetry-project-template/actions/workflows/pipeline.yml/badge.svg)
# uvicorn-poetry-project-template
[Cookiecutter](https://github.com/cookiecutter/cookiecutter) project template for the
[uvicorn-poetry Docker image](https://github.com/max-pfeiffer/uvicorn-poetry).
[Cookiecutter](https://github.com/cookiecutter/cookiecutter) project template for starting a containerized Fast API project.

It uses [Poetry](https://python-poetry.org/) for managing dependencies and setting up a virtual environment locally and in the container.

The project is set up to produce a Docker image to run your application with [Uvicorn](https://github.com/encode/uvicorn) on [Kubernetes](https://kubernetes.io/) container orchestration system.

This Docker image provides a platform to run Python applications with [Uvicorn](https://github.com/encode/uvicorn) on [Kubernetes](https://kubernetes.io/) container orchestration system.
It provides [Poetry](https://python-poetry.org/) for managing dependencies and setting up a virtual environment in the container.

## Quick Start
[Install Cookiecutter](https://cookiecutter.readthedocs.io/en/latest/installation.html) on your machine. Then:
```shell
cookiecutter https://github.com/max-pfeiffer/uvicorn-poetry-project-template
```

### Run with Poetry
In project directory install dependencies:
```shell
poetry install
```
Run application in project directory:
```shell
poetry run uvicorn --workers 1 --host 0.0.0.0 --port 80 app.main:app
poetry run uvicorn --workers 1 --host 0.0.0.0 --port 8000 app.main:app
```

### Build and run Docker image
Build the production Docker image:
```shell
docker build --tag my-application:1.0.0 .
Expand Down
2 changes: 1 addition & 1 deletion cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"project_description": "",
"author_name": "",
"author_email": "",
"python_version": ["3.12.0", "3.10.13", "3.10.13"],
"python_version": ["3.12.0", "3.11.6", "3.10.13"],
"operating_system_variant": ["slim-bookworm", "bookworm"],
"_extensions": ["cookiecutter.extensions.SlugifyExtension"]
}
18 changes: 6 additions & 12 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,6 @@ def docker_client() -> docker.client:
return docker.client.from_env()


@pytest.fixture(scope="session")
def docker_file_name() -> str:
return "Dockerfile"


@pytest.fixture(scope="session")
def docker_image_test_project(cookies_session):
result: Result = cookies_session.bake(
Expand All @@ -29,27 +24,26 @@ def docker_image_test_project(cookies_session):
}
)

# Install project dependencies with Poetry
# Create Poetry lock file for building Docker container
completed_process: CompletedProcess = run(
["poetry", "install"], cwd=result.project_path
["poetry", "lock"], cwd=result.project_path
)
if completed_process.returncode > 0:
raise Exception("Dependencies were not installed with Poetry.")
raise Exception("Lock file could not be created with Poetry.")

return result


@pytest.fixture(scope="session")
def production_image(
docker_client, docker_image_test_project, docker_file_name
) -> str:
def production_image(docker_client, docker_image_test_project) -> str:
path: str = str(docker_image_test_project.project_path)
tag: str = str(uuid4())

image: Image = docker_client.images.build(
path=path,
dockerfile=docker_file_name,
dockerfile="Dockerfile",
tag=tag,
target="production-image",
)[0]
image_tag: str = image.tags[0]
yield image_tag
Expand Down
5 changes: 4 additions & 1 deletion tests/test_custom_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def test_custom_config(cookies) -> None:
assert result.project_path.name == "seriously-silly-project-name"
assert result.project_path.is_dir()

# Check if pyproject.toml became expanded correctly
pyproject_toml_file: Path = result.project_path / "pyproject.toml"
toml_data: dict = toml.load(pyproject_toml_file)

Expand All @@ -39,7 +40,9 @@ def test_custom_config(cookies) -> None:
"Jane Doe <[email protected]>"
]

# Check if Dockerfile became expanded correctly
dockerfile: Path = result.project_path / "Dockerfile"
dfp = DockerfileParser(path=str(dockerfile))

assert "3.10.13-bookworm" in dfp.baseimage
assert dfp.is_multistage
assert all(["3.10.13-bookworm" in image for image in dfp.parent_images])
5 changes: 4 additions & 1 deletion tests/test_default_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def test_default_config(cookies) -> None:
assert result.project_path.name == "project-name"
assert result.project_path.is_dir()

# Check if pyproject.toml became expanded correctly
pyproject_toml_file: Path = result.project_path / "pyproject.toml"
toml_data: dict = toml.load(pyproject_toml_file)

Expand All @@ -24,7 +25,9 @@ def test_default_config(cookies) -> None:
assert toml_data["tool"]["poetry"]["description"] == ""
assert toml_data["tool"]["poetry"]["authors"] == []

# Check if Dockerfile became expanded correctly
dockerfile: Path = result.project_path / "Dockerfile"
dfp = DockerfileParser(path=str(dockerfile))

assert "3.12.0-slim-bookworm" in dfp.baseimage
assert dfp.is_multistage
assert all(["3.12.0-slim-bookworm" in image for image in dfp.parent_images])
File renamed without changes.
57 changes: 52 additions & 5 deletions {{cookiecutter.project_slug}}/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,59 @@
# Be aware that you need to specify these arguments before the first FROM
# see: https://docs.docker.com/engine/reference/builder/#understand-how-arg-and-from-interact
FROM pfeiffermax/uvicorn-poetry:3.2.0-python{{ cookiecutter.python_version }}-{{ cookiecutter.operating_system_variant }}
# Using an image for dependency build stage which provides Poetry
# see: https://github.com/max-pfeiffer/python-poetry/blob/main/build/Dockerfile
FROM pfeiffermax/python-poetry:1.8.0-poetry1.7.1-python{{ cookiecutter.python_version }}-{{ cookiecutter.operating_system_variant }} as dependencies-build-stage
ENV POETRY_VIRTUALENVS_IN_PROJECT=true \
POETRY_CACHE_DIR="/application_root/.cache" \
PYTHONPATH=/application_root

# Set the WORKDIR to the application root.
# https://www.uvicorn.org/settings/#development
# https://docs.docker.com/engine/reference/builder/#workdir
WORKDIR ${PYTHONPATH}

# install [tool.poetry.dependencies]
# this will install virtual environment into /.venv because of POETRY_VIRTUALENVS_IN_PROJECT=true
# see: https://python-poetry.org/docs/configuration/#virtualenvsin-project
COPY --chown=python_application:python_application ./poetry.lock ./pyproject.toml /application_root/
COPY ./pyproject.toml ${PYTHONPATH}
RUN poetry install --no-interaction --no-root --without dev

# Using the standard Python image here to have the least possible image size
FROM python:{{ cookiecutter.python_version }}-{{ cookiecutter.operating_system_variant }} as production-image
ARG APPLICATION_SERVER_PORT=8000

# https://docs.python.org/3/using/cmdline.html#envvar-PYTHONUNBUFFERED
ENV PYTHONUNBUFFERED=1 \
# https://docs.python.org/3/using/cmdline.html#envvar-PYTHONDONTWRITEBYTECODE
PYTHONDONTWRITEBYTECODE=1 \
PYTHONPATH=/application_root \
VIRTUAL_ENVIRONMENT_PATH="/application_root/.venv" \
APPLICATION_SERVER_PORT=$APPLICATION_SERVER_PORT

# Adding the virtual environment to PATH in order to "activate" it.
# https://docs.python.org/3/library/venv.html#how-venvs-work
ENV PATH="$VIRTUAL_ENVIRONMENT_PATH/bin:$PATH"

# Principle of least privilege: create a new user for running the application
RUN groupadd -g 1001 python_application && \
useradd -r -u 1001 -g python_application python_application

# Set the WORKDIR to the application root.
# https://www.uvicorn.org/settings/#development
# https://docs.docker.com/engine/reference/builder/#workdir
WORKDIR ${PYTHONPATH}
RUN chown python_application:python_application ${PYTHONPATH}

# Copy virtual environment
COPY --from=dependencies-build-stage --chown=python_application:python_application ${VIRTUAL_ENVIRONMENT_PATH} ${VIRTUAL_ENVIRONMENT_PATH}

# Copy application files
COPY --chown=python_application:python_application /app /application_root/app/
COPY --chown=python_application:python_application /app ${PYTHONPATH}/app/

# Document the exposed port
# https://docs.docker.com/engine/reference/builder/#expose
EXPOSE ${APPLICATION_SERVER_PORT}

# Use the unpriveledged user to run the application
USER 1001

# Run the uvicorn application server.
CMD exec uvicorn --workers 1 --host 0.0.0.0 --port $APPLICATION_SERVER_PORT app.main:app
2 changes: 2 additions & 0 deletions {{cookiecutter.project_slug}}/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# {{ cookiecutter.project_name }}
{{ cookiecutter.project_description }}
1 change: 1 addition & 0 deletions {{cookiecutter.project_slug}}/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ fastapi = "0.104.1"
[tool.poetry.dev-dependencies]
black = "23.11.0"
coverage = "7.3.3"
httpx = "0.26.0"
pre-commit = "3.6.0"
pytest = "7.4.3"
pytest-cov = "4.1.0"
Expand Down

0 comments on commit 02dbc3a

Please sign in to comment.