Repository navigation
Expand file tree
/
Copy pathDockerfile
More file actions
104 lines (87 loc) · 4.51 KB
/
Copy pathDockerfile
File metadata and controls
104 lines (87 loc) · 4.51 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
# syntax=docker/dockerfile:1
#
# One image for every deployment. What differs between running this locally and
# running it on Fargate is environment variables and mounts, nothing else.
#
# Configuration, credentials and output are mounted rather than baked in:
# config.yml and policy.yml are personal, secrets are secret, and log/ and
# .state/ are written at run time.
# Pinned by digest: a tag is mutable, and a base image that changes under a
# build is a supply-chain hole of exactly the kind the lockfile closes.
ARG PYTHON_IMAGE=python:3.14-slim@sha256:cad9a2c871761c413caa6fdd6441c783451e740a48aaeba60ae62a8b53525ef6
# uv is installed from PyPI rather than copied from its own image. That keeps
# the build to one registry and one credential path — and uv is a build-time
# tool that never reaches the runtime layer, while the supply chain that
# actually ships is pinned by uv.lock's per-package hashes.
ARG UV_VERSION=0.12.5
# ---------------------------------------------------------------------------
# builder — resolves and installs into a virtualenv that is copied out whole
# ---------------------------------------------------------------------------
FROM ${PYTHON_IMAGE} AS builder
ARG UV_VERSION
RUN pip install --no-cache-dir "uv==${UV_VERSION}"
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_PYTHON_DOWNLOADS=never \
UV_PROJECT_ENVIRONMENT=/opt/venv
WORKDIR /build
# Dependencies before source, so editing code does not re-resolve them.
# `--frozen`: a lockfile that does not match pyproject.toml fails the build
# rather than being quietly resolved around.
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev --no-install-project --extra classify --extra aws
COPY src/ ./src/
COPY README.md ./
# `--no-editable`: uv installs the project as an editable link by default, which
# would point the runtime layer at a /build/src that does not exist there.
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev --no-editable --extra classify --extra aws
# ---------------------------------------------------------------------------
# runtime — no compiler, no uv, no build cache
# ---------------------------------------------------------------------------
FROM ${PYTHON_IMAGE} AS runtime
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PATH="/opt/venv/bin:${PATH}" \
TZ=Asia/Tokyo
# Two things the pinned base does not do for us:
#
# * Debian's security updates. The digest fixes what the image starts from, but
# the official image is rebuilt on its own schedule, and fixes published in
# between would ship unpatched — CI's vulnerability scan fails on exactly
# those. Upgrading here takes them without unpinning the base.
# * Removing pip. Nothing installs packages at runtime, and pip's vendored
# libraries (msgpack, setuptools' pkg_resources) carry advisories of their own
# into an image that never uses them.
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/* \
&& python -m pip uninstall --yes --root-user-action=ignore pip
# A fixed UID and GID so a bind-mounted log/ or .state/ has predictable
# ownership on any host. Both are pinned: `useradd` alone picks the group's GID
# itself, and only happens to pick 10001 today.
RUN groupadd --gid 10001 epc \
&& useradd --uid 10001 --gid 10001 --create-home --shell /usr/sbin/nologin epc
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
# The generic prompt templates are part of the program. Personal rules are not:
# policy.yml is mounted, which is what lets these stay publishable.
COPY prompts/ /app/prompts/
# Mount points. Created here so a read-only root filesystem still has somewhere
# to write once these are mounted.
RUN mkdir -p /app/log /app/.state && chown -R epc:epc /app/log /app/.state
# /tmp as a declared volume, world-writable with the sticky bit. On Fargate, a
# task volume mounted at a path the image declares as a VOLUME is seeded from
# the image, permissions included. Mounted anywhere else it is root-owned 0755,
# which UID 10001 cannot write to — and a dry run on ECS writes its plan there.
RUN chmod 1777 /tmp
VOLUME ["/tmp"]
# Numeric, not the name: it is the number the kernel checks against a bind
# mount, and the one a host or an orchestrator can resolve without this image's
# /etc/passwd.
USER 10001:10001
# `epc` rather than a script path, so `docker run … run --dry-run` reads the
# same as the command you would type locally.
ENTRYPOINT ["epc"]
CMD ["run", "--dry-run"]