-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathDockerfile
More file actions
120 lines (104 loc) · 6.27 KB
/
Copy pathDockerfile
File metadata and controls
120 lines (104 loc) · 6.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# syntax=docker/dockerfile:1
#
# The image is the product for anyone who is not writing Python: a Go or Ruby
# team runs partition maintenance as a CronJob and never installs this library.
# So it is built the way such a team would judge it -- small, with nothing in
# it that is not needed to run one command and exit.
#
# Two stages. The builder is a full Python image with uv, and everything that
# installs, compiles or strips happens there. The runtime is distroless: glibc,
# OpenSSL, CA certificates, a timezone database, and nothing else -- no shell,
# no package manager, no pip. The interpreter, its standard library and the
# virtualenv are copied over, plus exactly the shared libraries the extension
# modules we keep link against.
# The base images are written out rather than taken from build arguments:
# Dependabot reads FROM lines, and a rebuild on a base-image fix is what it is for.
FROM ghcr.io/astral-sh/uv:0.12.9 AS uv
FROM python:3.14-slim-bookworm AS builder
# binutils for strip; nothing else is installed into the image being built.
RUN apt-get update -qq \
&& apt-get install -y -qq --no-install-recommends binutils >/dev/null \
&& rm -rf /var/lib/apt/lists/*
COPY --from=uv /uv /bin/uv
ENV UV_PROJECT_ENVIRONMENT=/opt/venv \
UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_PYTHON_DOWNLOADS=never
WORKDIR /src
COPY pyproject.toml uv.lock README.md LICENSE CHANGELOG.md ./
COPY pg_partsmith ./pg_partsmith
# Installed from the lock file and nothing else: the image contains the
# versions the test suite ran against, not whatever the index served today.
# The project itself goes in as a real package, not an editable link to /src,
# and is always rebuilt: the cache keys built wheels by version, and the
# version does not change between two commits of the same release.
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --no-editable --extra cli --python /usr/local/bin/python3 \
--reinstall-package pg-partsmith
# What the runtime image will not have: rich and its dependencies exist for
# typer's coloured --help, which a CronJob log never sees (typer falls back to
# plain help without them). Debug symbols in the compiled extensions
# are most of their size. Then the parts of the standard library nobody runs
# in a container -- the IDE, the GUI toolkit, the test suite, the pager data,
# the base image's own pip, which would otherwise ride along with the standard
# library -- and the extension modules whose libraries the runtime leaves out.
RUN cd /opt/venv/lib/python3.*/site-packages \
&& rm -rf rich rich-*.dist-info pygments pygments-*.dist-info markdown_it markdown_it_py-*.dist-info \
mdurl mdurl-*.dist-info sqlalchemy/testing greenlet/tests \
/opt/venv/bin/pygmentize /opt/venv/bin/markdown-it \
&& find /opt/venv -name '*.so' -exec strip --strip-unneeded {} + \
&& find /opt/venv \( -name '*.pyx' -o -name '*.pxd' -o -name '*.c' -o -name '*.h' -o -name '*.pyi' \) -delete \
&& cd /usr/local/lib/python3.* \
&& rm -rf idlelib tkinter turtledemo turtle.py test pydoc_data lib2to3 ensurepip config-3.* \
site-packages/pip site-packages/pip-*.dist-info site-packages/setuptools* site-packages/wheel* \
lib-dynload/_tkinter* lib-dynload/_curses* lib-dynload/readline* lib-dynload/_dbm* \
lib-dynload/_gdbm* lib-dynload/_sqlite3* lib-dynload/_test* lib-dynload/xxlimited* \
lib-dynload/_ctypes_test* lib-dynload/_xxtestfuzz* lib-dynload/_zstd* \
&& strip --strip-unneeded /usr/local/lib/libpython3.*.so.1.0 lib-dynload/*.so \
&& python3 -m compileall -q -j0 --invalidation-mode unchecked-hash /usr/local/lib/python3.*
# Everything the runtime takes from the builder, staged under the paths it
# will occupy so one COPY places it: the shared libraries the kept extension
# modules need beyond what distroless provides (glibc, libgcc, libstdc++,
# OpenSSL), the interpreter, its shared library and its standard library. The
# multiarch directory comes from the interpreter, so the arm64 build stages
# arm64 libraries; the Python version comes from the base image, so nothing
# below names it.
RUN set -eu \
&& arch="$(python3 -c 'import sysconfig; print(sysconfig.get_config_var("MULTIARCH"))')" \
&& mkdir -p "/staging/usr/lib/${arch}" /staging/usr/local/bin /staging/usr/local/lib \
&& for lib in libz.so.1 libbz2.so.1.0 liblzma.so.5 libffi.so.8 libuuid.so.1; do \
cp "/usr/lib/${arch}/${lib}" "/staging/usr/lib/${arch}/"; \
done \
&& cp -a /usr/local/bin/python3 /usr/local/bin/python3.* /staging/usr/local/bin/ \
&& rm -f /staging/usr/local/bin/python3*-config \
&& cp -a /usr/local/lib/libpython3*.so.1.0 /staging/usr/local/lib/ \
&& cp -a /usr/local/lib/python3.* /staging/usr/local/lib/
FROM gcr.io/distroless/cc-debian12:nonroot AS runtime
ARG VERSION=0.0.0
LABEL org.opencontainers.image.title="pg-partsmith" \
org.opencontainers.image.description="PostgreSQL partition lifecycle management with a plan you can read before it runs" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.source="https://github.com/bedrock-python/pg-partsmith" \
org.opencontainers.image.documentation="https://bedrock-python.github.io/pg-partsmith/guide/cli/" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.base.name="gcr.io/distroless/cc-debian12:nonroot"
COPY --from=builder /staging/ /
COPY --from=builder /opt/venv /opt/venv
# TYPER_USE_RICH=0: typer assumes rich is installed unless told otherwise, and
# rich is one of the things this image leaves out. Plain --help is what a
# CronJob log shows anyway.
ENV PATH="/opt/venv/bin:/usr/local/bin:${PATH}" \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONHOME=/usr/local \
TYPER_USE_RICH=0
# distroless' nonroot is 65532 -- the same fixed UID this image has always
# run as, so a runAsUser that named it keeps naming it.
USER 65532:65532
WORKDIR /home/nonroot
# The entrypoint is the command itself, so a Compose service or a CronJob names
# only what it wants done: ["plan", "-c", "/etc/partitions.yaml", "--check"].
# There is no shell to fall back to; --write and --ok-if-locked cover the two
# things a wrapper used to do.
ENTRYPOINT ["/opt/venv/bin/python", "-m", "pg_partsmith.cli"]
CMD ["--help"]