Skip to content

Commit 0b56f52

Browse files
committed
refactor: Utilize multistage builds to improve caching and compatibility
The main changes here are; * using multi-stage builds for better caching and smaller images * explicitly listing the files that should be included / "COPY"able into the containers For the API, I moved the tests outside the `src` folder. That way, we do not include the tests in the finished image. Since we use multi-stage builds, we can install the necessary compilers for running on ARM. The compilers are not part of the "production" image, and we don't need to worry about them taking up more space in the image layers. I also created a user (similar to what's done in `nginx`) so that we can run the tests without running the container as root. When only installing the necessary packages, I noticed that `click` is not explicitly defined, so it wasn't included in `site-packages`. By explicitly listing it in `pyproject.toml`, that issue is resolved.
1 parent df215da commit 0b56f52

38 files changed

+227
-115
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ repos:
2626
- id: mixed-line-ending
2727
exclude: ^.*\.(lock)$
2828
- id: detect-private-key
29-
exclude: api/src/tests/integration/mock_authentication.py
29+
exclude: api/tests/integration/mock_authentication.py
3030
- id: no-commit-to-branch
3131
args: [--branch, main, --branch, master]
3232
stages: [commit-msg]
@@ -80,7 +80,7 @@ repos:
8080
hooks:
8181
- id: pytest
8282
name: pytest-check
83-
entry: sh -c "cd ./api/src/ && poetry run pytest ./tests/"
83+
entry: sh -c "cd ./api/ && poetry run pytest ./tests/"
8484
language: system
8585
pass_filenames: false
8686
always_run: true

api/.dockerignore

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1-
.venv
2-
.pytest_cache
3-
.mypy_cache
1+
# Ignore all by default
2+
*
3+
!pyproject.toml
4+
!poetry.lock
5+
6+
!src/**/*.py
7+
!src/*.py
8+
!src/init.sh
9+
10+
!tests/**/*.py
11+
!tests/test_data/

api/Dockerfile

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,76 @@
1-
FROM --platform=linux/amd64 python:3.12-slim AS base
1+
# syntax=docker/dockerfile:1
2+
FROM python:3.13.2-alpine3.21 AS base
3+
ENV XDG_CACHE_HOME=/var/lib/
4+
ENV SITE_PACKAGES=/usr/local/lib/python3.13/site-packages
5+
6+
ENV USER="api-user"
7+
8+
RUN adduser \
9+
--disabled-password \
10+
--home /code \
11+
--gecos "" \
12+
--uid 1000 \
13+
"$USER"
14+
215
WORKDIR /code
316
CMD ["/code/src/init.sh", "api"]
417
EXPOSE 5000
518

619
ENV PYTHONUNBUFFERED=1
7-
ENV PYTHONPATH=/code
20+
ENV PYTHONPATH=/code/src
21+
22+
FROM base AS dependencies
23+
ENV POETRY_VERSION=1.8.5
24+
25+
ENV TO_BE_REMOVED="/usr/local/share/to-be-removed.txt"
26+
# Enumerate files that should not be present in dependencies-slim
27+
# that way, the relevant folders can be COPY-ed without incurring the size cost of already existing items
28+
RUN <<EOF
29+
#!/usr/bin/env sh
30+
set -eu
831

9-
RUN pip install --upgrade pip && \
10-
pip install poetry && \
32+
find /usr/local/bin -mindepth 1 > "$TO_BE_REMOVED"
33+
find "$SITE_PACKAGES" -maxdepth 1 -mindepth 1 >> "$TO_BE_REMOVED"
34+
35+
EOF
36+
37+
ENV PATH="/root/.local/bin:$PATH"
38+
RUN \
39+
--mount=type=cache,target="$XDG_CACHE_HOME/pip" \
40+
pip install --upgrade pip && \
41+
pip install --user "poetry==$POETRY_VERSION" && \
1142
poetry config virtualenvs.create false
1243

13-
COPY pyproject.toml pyproject.toml
14-
COPY poetry.lock poetry.lock
44+
RUN --mount=type=cache,target=/var/cache/apk \
45+
apk add \
46+
linux-headers \
47+
musl-dev \
48+
gcc
49+
50+
COPY --chown="$USER:$USER" \
51+
pyproject.toml poetry.lock ./
52+
RUN --mount=type=cache,target="$XDG_CACHE_HOME/pip" \
53+
poetry install --no-dev
54+
55+
FROM dependencies AS dependencies-slim
56+
RUN <<EOF
57+
#!/usr/bin/env sh
58+
set -eu
59+
60+
# Remove all files / directories that are (already) present in the final image
61+
cat "$TO_BE_REMOVED" | while IFS= read -r path; do
62+
rm -rf "$path"
63+
done
64+
EOF
1565

16-
FROM base AS development
17-
RUN poetry install
18-
WORKDIR /code/src
19-
COPY src .
66+
FROM dependencies AS development
67+
RUN --mount=type=cache,target="$XDG_CACHE_HOME/pip" \
68+
poetry install --with dev
69+
COPY --chown="$USER:$USER" . .
2070
USER 1000
2171

2272
FROM base AS prod
23-
RUN poetry install --without dev
24-
WORKDIR /code/src
25-
COPY src .
73+
COPY --from=dependencies-slim "$SITE_PACKAGES" "$SITE_PACKAGES"
74+
COPY --from=dependencies-slim /usr/local/bin/ /usr/local/bin/
75+
COPY --chown="$USER:$USER" src src
2676
USER 1000

api/poetry.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/pyproject.toml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pydantic-extra-types = "^2.10"
2020
azure-monitor-opentelemetry = "^1.6.2"
2121
opentelemetry-instrumentation-fastapi = "^0.48b0"
2222
cryptography = "^44.0.0"
23+
click = "^8.1.8"
2324

2425
[tool.poetry.group.dev.dependencies]
2526
pre-commit = ">=3"
@@ -65,7 +66,7 @@ strict = true
6566

6667
[tool.ruff]
6768

68-
src = ["src"]
69+
src = [".", "src"]
6970
target-version = "py311"
7071
line-length = 119
7172

@@ -87,7 +88,7 @@ ignore = [
8788

8889
[tool.ruff.lint.per-file-ignores]
8990
"__init__.py" = ["E402"] # Ignore `E402` (import violations) in all `__init__.py` files
90-
"src/tests/*" = ["S101"] # Allow the use of ´assert´ in tests
91+
"tests/*" = ["S101"] # Allow the use of ´assert´ in tests
9192

9293
[tool.codespell]
9394
skip = "*.lock,*.cjs"
@@ -100,6 +101,9 @@ markers = [
100101
"integration: mark a test as integration test."
101102
]
102103
testpaths = [
103-
"src/tests/unit",
104-
"src/tests/integration"
104+
"tests/unit",
105+
"tests/integration"
106+
]
107+
pythonpath = [
108+
"src"
105109
]

api/src/init.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
#!/bin/sh
1+
#!/usr/bin/env sh
22
set -eu
33

4+
ROOT_DIR="$(cd "$(dirname -- "$0")" && pwd -P)"
5+
readonly ROOT_DIR
6+
47
if [ "$1" = 'api' ]; then
5-
python3 ./app.py run
8+
python3 "$ROOT_DIR/app.py" run
69
else
710
exec "$@"
811
fi
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)