-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDockerfile
More file actions
69 lines (65 loc) · 3.54 KB
/
Copy pathDockerfile
File metadata and controls
69 lines (65 loc) · 3.54 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
# Stage 1: Build React frontend
FROM node:26-alpine AS frontend-builder
WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json ./
# Cache mount keeps npm's download cache out of the layer but warm across
# builds, so a lockfile change re-downloads only what actually changed.
RUN --mount=type=cache,target=/root/.npm npm ci
COPY frontend/ ./
RUN npm run build
# Stage 2: Python backend serving built frontend as static files.
# Alpine over slim: Debian's base layer ships dozens of no-fix CVEs (perl-base,
# libc6, …) that scanners flag forever; musl's ~10-package base scans clean.
FROM python:3.14-alpine
LABEL org.opencontainers.image.title="Bluebird" \
org.opencontainers.image.description="Map-based weather window finder for hikers and mountaineers" \
org.opencontainers.image.source="https://github.com/zimmertr/bluebird" \
org.opencontainers.image.url="https://bluebirdforecast.com" \
org.opencontainers.image.licenses="PolyForm-Noncommercial-1.0.0"
# Unbuffered so logs reach the container runtime immediately; no .pyc writes
# because the app dir is root-owned and read-only to the runtime user anyway.
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
WORKDIR /app
COPY backend/requirements.txt ./
# Upgrade pip first: the version bundled with the base image trails pip's own
# security fixes (e.g. CVE-2025-8869 tar link-following), and scanners flag it.
# Deliberately unpinned (DL3013): a pin here would sit outside Dependabot's
# view and go stale; floating rides each rebuild to the current fix.
# hadolint ignore=DL3042,DL3013
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --upgrade pip && \
pip install -r requirements.txt
COPY backend/app/ ./app/
COPY --from=frontend-builder /app/frontend/dist/ ./static/
# Swagger UI's assets, vendored so /docs renders without reaching out to a CDN.
# Taken straight from the builder's node_modules rather than through a Vite
# plugin: frontend/package.json is "type": "module", so a plugin doing this in
# CommonJS would break, and routing 1 MB of vendor JS through Vite's hashing
# buys nothing.
COPY --from=frontend-builder \
/app/frontend/node_modules/swagger-ui-dist/swagger-ui-bundle.js \
/app/frontend/node_modules/swagger-ui-dist/swagger-ui.css \
./static/swagger-ui/
# Nothing needs root at runtime — uvicorn binds 8000 and the app only reads
# baked-in files — so serve as an unprivileged user. Fixed numeric UID/GID so
# Kubernetes runAsNonRoot can verify without resolving names inside the image.
RUN addgroup -S -g 10001 bluebird && adduser -S -u 10001 -G bluebird bluebird
# Build identity for GET /api/version and the OpenAPI info.version, populated by
# release.yml and pr-preview.yml. Deliberately the LAST thing before USER:
# APP_BUILT_AT changes on every build, so declaring it any earlier would
# invalidate the pip-install and COPY layers on every single build.
ARG APP_VERSION=dev
ARG APP_COMMIT=dev
ARG APP_BUILT_AT=dev
ENV APP_VERSION=${APP_VERSION} \
APP_COMMIT=${APP_COMMIT} \
APP_BUILT_AT=${APP_BUILT_AT}
USER 10001:10001
EXPOSE 8000
# Kubernetes ignores HEALTHCHECK (its probes hit /healthz directly); this is
# for plain docker/compose users. Python stdlib rather than busybox wget so the
# check doesn't depend on which base-image flavor is underneath.
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD ["python", "-c", "import sys, urllib.request; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/healthz', timeout=2).status == 200 else 1)"]
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]