Repository navigation
Expand file tree
/
Copy pathDockerfile
More file actions
153 lines (130 loc) · 6.61 KB
/
Copy pathDockerfile
File metadata and controls
153 lines (130 loc) · 6.61 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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# Build frontend
FROM node:22-bookworm-slim AS frontend-builder
WORKDIR /app/frontend
# Copy package files first for better caching
COPY frontend/package*.json ./
# Use cache mount for npm
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY frontend/ ./
RUN npm run build
# Production image
FROM python:3.13-slim-trixie
WORKDIR /app
# Install system dependencies
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ffmpeg \
gnupg \
gosu \
iproute2 \
libcap2-bin \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Install the Tailscale CLI only (no tailscaled — the daemon runs on the host).
# Grove Control calls `tailscale status` / `tailscale cert` via the host's socket,
# which the user mounts in via docker-compose when they want to enable the
# Tailscale integration for virtual printers. Without the socket mount, the
# binary is harmless — the code logs a hint and falls back to self-signed.
RUN curl -fsSL https://pkgs.tailscale.com/stable/debian/trixie.noarmor.gpg \
-o /usr/share/keyrings/tailscale-archive-keyring.gpg \
&& curl -fsSL https://pkgs.tailscale.com/stable/debian/trixie.tailscale-keyring.list \
-o /etc/apt/sources.list.d/tailscale.list \
&& apt-get update && apt-get install -y --no-install-recommends tailscale \
&& rm -rf /var/lib/apt/lists/*
# Allow binding to privileged ports (e.g. 990/FTPS) as non-root user.
# File capabilities are more reliable than Docker cap_add with user: directive,
# which depends on ambient capability support in the container runtime.
RUN setcap cap_net_bind_service=+ep "$(readlink -f /usr/local/bin/python3)"
# Install Python dependencies with cache mount.
# pip is upgraded to >=26.1 first to close CVE-2026-6357 — the python:3.13-slim
# base image ships pip 26.0.1, which runs its self-update check after installing
# wheels (so a hostile wheel could hijack stdlib imports during install).
COPY requirements.txt ./
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --root-user-action=ignore --upgrade 'pip>=26.1' \
&& pip install --root-user-action=ignore -r requirements.txt
# Copy version metadata and backend
COPY VERSION ./VERSION
# Development images pass the checked-out commit so the version shown in the
# UI identifies the exact source revision. Release builds omit BUILD_COMMIT and
# therefore retain the canonical VERSION value unchanged.
ARG BUILD_COMMIT=""
RUN if [ -n "$BUILD_COMMIT" ]; then \
version="$(tr -d '[:space:]' < VERSION)"; \
printf '%s-#%s\n' "$version" "$BUILD_COMMIT" > VERSION; \
fi
COPY backend/ ./backend/
# Operational recovery tooling is deliberately shipped in the image so it can
# be run against the named data volume without requiring a source checkout.
COPY scripts/rebuild_print_queue.py ./scripts/rebuild_print_queue.py
COPY scripts/rebuild_database.py ./scripts/rebuild_database.py
# Copy built frontend from builder stage
COPY --from=frontend-builder /app/static ./static
# Copy embedded GCode viewer static assets (PrettyGCode + Grove Control adapter).
# Served by the explicit @app.get("/gcode-viewer/{...}") routes in main.py,
# which resolve files under (static_dir.parent / "gcode_viewer") = /app/gcode_viewer/.
# Without this COPY the routes return a bare 404 at request time and the 3D
# Preview iframe shows {"detail":"Not Found"} (see #1218). The directory is
# vendored third-party JS — the Vite build does NOT stage it into static/,
# the dev server serves it via a configureServer middleware that's dev-only.
COPY gcode_viewer/ ./gcode_viewer/
# Create data directories. Ownership is normalised at startup by the
# entrypoint (chowns to PUID:PGID and drops privileges via gosu before
# exec'ing the app), so we don't need a chmod 777 hack here — that was
# the workaround for the previous compose `user: "1000:1000"` model and
# only worked when the volume's perms happened to survive (named volume
# first-create case; bind-mount-source case bit users in #1211 / #668).
#
# The sentinel file is needed so a freshly-created Docker named volume
# isn't "empty" from Docker's POV. On empty volumes Docker resyncs the
# directory metadata (incl. ownership) from the image on every mount,
# which would mean our entrypoint chown gets reverted on every restart
# and re-fired on every start (slow on multi-GB archive dirs). With a
# sentinel inside the volume on first mount, Docker considers the
# volume populated and stops resyncing, so the chown is genuinely
# one-shot.
RUN mkdir -p /app/data /app/logs && \
: >/app/data/.bambuddy && \
: >/app/logs/.bambuddy
# Entrypoint script: handles PUID/PGID + ownership normalisation +
# privilege drop. See deploy/docker-entrypoint.sh for the full rationale.
COPY deploy/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Environment variables
ENV PYTHONUNBUFFERED=1
ENV DATA_DIR=/app/data
ENV LOG_DIR=/app/logs
ENV PORT=8000
# Provide a writable home for libraries that cache generated assets.
ENV HOME=/app
# Matplotlib (imported lazily by the STL thumbnail generator) tries to create
# its font/style cache at $HOME/.config/matplotlib on first import. /app is
# root-owned and not writable by the PUID:PGID the entrypoint drops to,
# which trips an EPERM warning in everyone's logs and forces matplotlib
# to fall back to a per-restart temp dir (paying the font-scan cost on
# every container restart). Pinning the cache dir to /tmp/matplotlib
# silences the warning and keeps the cache alive for the container's
# lifetime. /tmp is writable by any uid, so this works regardless of PUID.
ENV MPLCONFIGDIR=/tmp/matplotlib
EXPOSE 322
EXPOSE 990
EXPOSE 3000
EXPOSE 3002
EXPOSE 6000
EXPOSE 8000
EXPOSE 8883
EXPOSE 50000-50100
# Health check (uses PORT env var via shell)
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
CMD python -c "import urllib.request, os; urllib.request.urlopen(f'http://localhost:{os.environ.get(\"PORT\", \"8000\")}/health')" || exit 1
# Run the application
# Use standard asyncio loop (uvloop has permission issues in some Docker environments)
# Port is configurable via PORT environment variable (default: 8000).
# Keep uvicorn as PID 1 so Docker signals reach the application directly.
# Bound graceful shutdown prevents an open MJPEG response from consuming the
# entire Compose stop grace period and being killed without checkpointing.
ENV UVICORN_TIMEOUT_GRACEFUL_SHUTDOWN=5
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["sh", "-c", "exec uvicorn backend.app.main:app --host 0.0.0.0 --port ${PORT:-8000} --loop asyncio --timeout-graceful-shutdown ${UVICORN_TIMEOUT_GRACEFUL_SHUTDOWN}"]