Repository navigation
Expand file tree
/
Copy pathContainerfile
More file actions
123 lines (115 loc) · 7.3 KB
/
Copy pathContainerfile
File metadata and controls
123 lines (115 loc) · 7.3 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
121
122
123
# Manifest-list digests reviewed 2026-10-28 via registry Docker-Content-Digest headers.
FROM python:3.14.10-slim-bookworm@sha256:516b51cb9db2ef061e465c803e50f64361b82cefd91e525b889b967b83e83527 AS python-base
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:${PATH}" \
UV_LINK_MODE=copy \
UV_NO_CACHE=1 \
UV_PROJECT_ENVIRONMENT=/opt/venv
COPY --from=ghcr.io/astral-sh/uv:0.13.6@sha256:af22a52f5eafef3d78e551599d405c02df61919d18028f0cea8eda93bc9986d0 /uv /usr/local/bin/uv
# uv installs everything into /opt/venv, which does not see the base image's
# site-packages, so the base image's pip and the pip wheel that ensurepip
# bundles are never used. Their vendored msgpack, urllib3 and setuptools lag
# behind security releases, so remove both.
#
# The image has no Tk libraries, so tkinter cannot even import; it goes with
# IDLE and turtledemo, which need it. It has no C compiler either, so nothing
# builds against Python's C headers in /usr/local/include. The copyright files
# in /usr/share/doc stay.
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates ffmpeg libsndfile1 libpcre2-8-0 perl-base libssl3 openssl \
&& rm -rf /var/lib/apt/lists/* \
&& python -m pip uninstall --yes --no-input --root-user-action ignore pip \
&& rm -rf /usr/local/lib/python3.*/ensurepip \
/usr/local/lib/python3.*/idlelib /usr/local/lib/python3.*/tkinter \
/usr/local/lib/python3.*/turtledemo /usr/local/lib/python3.*/turtle.py \
/usr/local/lib/python3.*/lib-dynload/_tkinter.* /usr/local/bin/idle* \
/usr/local/include
WORKDIR /app
COPY pyproject.toml uv.lock README.md LICENSE ./
COPY container/torch-cpu.txt ./container/
# TORCH_VARIANT chooses the PyTorch build: cpu (the default) or cuda. Either way every version
# comes from uv.lock, and UV_NO_CACHE keeps uv's download cache out of the image.
#
# cpu: uv.lock resolves Linux torch from PyPI, which is the CUDA build and pulls in several GB of
# nvidia-*, cuda-* and triton wheels. The CPU image has no GPU, so the locked sync skips the torch
# family and those wheels, then the CPU builds of the same locked torch, torchaudio and
# torchvision versions come from PyTorch's CPU index, hash-checked against container/torch-cpu.txt.
# The build stops if that file's versions drift from uv.lock.
#
# cuda: the locked sync installs everything uv.lock names: PyPI's CUDA build of torch and the
# nvidia-*, cuda-* and triton wheels it needs, each checked against the lock's hashes. Those wheels
# carry the CUDA runtime libraries, so no nvidia/cuda base image is needed; the host's driver comes
# in through the NVIDIA Container Toolkit. uv.lock is its only pin, so when torch moves, updating
# uv.lock and container/torch-cpu.txt together covers both images.
#
# Bare-metal installs are unaffected and keep the locked PyPI builds.
#
# Both images then drop what no runtime path loads:
# - every tests and test folder in site-packages. Only test runners and other tests import them;
# the testing modules, such as numpy.testing and torch.testing, are real modules and stay. In the
# CPU build this includes torch/test, PyTorch's C++ unit-test programs.
# - torch/include, the C++ headers that torch.compile and torch.utils.cpp_extension build against.
# AutoDJ never compiles C++, and the image has no compiler to do it.
ARG TORCH_VARIANT=cpu
RUN set -eu; \
case "$TORCH_VARIANT" in \
cpu|cuda) ;; \
*) echo "TORCH_VARIANT must be cpu or cuda, not '$TORCH_VARIANT'" >&2; exit 1 ;; \
esac; \
if [ "$TORCH_VARIANT" = cuda ]; then \
uv sync --frozen --no-dev --extra index --no-install-project; \
else \
python -c 'import re, tomllib; lock = {p["name"]: p["version"] for p in tomllib.load(open("uv.lock", "rb"))["package"]}; pins = dict(re.findall(r"^(torch|torchaudio|torchvision)==(\S+)\+cpu ", open("container/torch-cpu.txt").read(), re.M)); want = {n: lock[n] for n in ("torch", "torchaudio", "torchvision")}; assert pins == want, f"container/torch-cpu.txt {pins} != uv.lock {want}"'; \
skipped="$(python -c 'import re, tomllib; names = sorted(p["name"] for p in tomllib.load(open("uv.lock", "rb"))["package"]); print(" ".join(f"--no-install-package {n}" for n in names if re.fullmatch(r"(nvidia|cuda)-.+|triton|torch|torchaudio|torchvision", n)))')"; \
uv sync --frozen --no-dev --extra index --no-install-project $skipped; \
uv pip install --python /opt/venv/bin/python --no-deps --require-hashes \
--index-url https://download.pytorch.org/whl/cpu -r container/torch-cpu.txt; \
fi; \
site_packages="$(/opt/venv/bin/python -c 'import sysconfig; print(sysconfig.get_path("purelib"))')"; \
find "$site_packages" -type d \( -name tests -o -name test \) -not -path '*.dist-info/*' -prune \
-exec rm -rf {} +; \
rm -rf "$site_packages/torch/include"
FROM python-base AS package
COPY src ./src
RUN uv export --frozen --only-group build --no-emit-project --format requirements-txt --output-file /tmp/build-constraints.txt \
&& uv build --wheel --out-dir /tmp/dist --build-constraints /tmp/build-constraints.txt --require-hashes
FROM python-base AS runtime
WORKDIR /app
COPY --from=package /tmp/dist /tmp/dist
RUN uv pip install --python /opt/venv/bin/python --no-deps /tmp/dist/*.whl \
&& rm -rf /tmp/dist \
&& groupadd --gid 10001 autodj \
&& useradd --uid 10001 --gid 10001 --create-home --shell /usr/sbin/nologin autodj \
&& install -d -o 10001 -g 10001 /app/.cache /index /models
ENV HOME=/home/autodj \
XDG_CACHE_HOME=/app/.cache \
HF_HOME=/models/huggingface \
AUTODJ_HOST=0.0.0.0 \
AUTODJ_PORT=8080 \
AUTODJ_INDEX_DIR=/index \
AUTODJ_MODEL_DIR=/models
# The commit this image was built from, for the web page's build stamp; the
# build context leaves out .git and the image has no git. Pass it with
# AUTODJ_COMMIT=$(git rev-parse --short HEAD) docker compose build. The
# server shows and links it only when it is 7 to 40 hex digits. The update
# channel of a published image, stable or development, is set by the workflow
# that publishes it; a locally built image leaves it empty and follows the
# checkout it is built from. The variant, cpu or cuda, keeps an update on
# the same variant's tags. Declared last so a new commit rebuilds only
# these layers.
ARG AUTODJ_COMMIT=""
ARG AUTODJ_IMAGE_CHANNEL=""
ARG TORCH_VARIANT=cpu
ENV AUTODJ_COMMIT=${AUTODJ_COMMIT} \
AUTODJ_IMAGE_CHANNEL=${AUTODJ_IMAGE_CHANNEL} \
AUTODJ_IMAGE_VARIANT=${TORCH_VARIANT}
USER 10001:10001
EXPOSE 8080
# Probe the local listener over HTTP, then HTTPS for a server with TLS set in
# config.toml. The certificate is not checked: it names the public host, and
# this only asks the server on this machine whether it is up.
HEALTHCHECK --interval=10s --timeout=5s --start-period=20s --retries=6 \
CMD ["/opt/venv/bin/python", "-c", "import os, ssl, urllib.request\nhost = os.environ['AUTODJ_HOST'].strip('[] ')\nprobe = '[::1]' if ':' in host else '127.0.0.1'\nport = int(os.environ['AUTODJ_PORT'])\nopener = urllib.request.build_opener(urllib.request.ProxyHandler({}), urllib.request.HTTPSHandler(context=ssl._create_unverified_context()))\ntry:\n opener.open(f'http://{probe}:{port}/healthz', timeout=2).read()\nexcept Exception:\n opener.open(f'https://{probe}:{port}/healthz', timeout=2).read()"]
ENTRYPOINT ["/opt/venv/bin/autodj"]
CMD ["serve"]