Repository navigation
Expand file tree
/
Copy pathdockerfile
More file actions
175 lines (150 loc) · 8.49 KB
/
Copy pathdockerfile
File metadata and controls
175 lines (150 loc) · 8.49 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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# Multi-stage, multi-arch-capable build (linux/amd64, linux/arm64).
#
# Recommended: build each arch natively on its own host and join with
# `docker buildx imagetools create` (this is what .circleci/config.yml does).
# Cross-building a single image with `buildx --platform amd64,arm64` from an
# amd64 host also works but runs the arm64 leg under QEMU, which is ~10x
# slower (the Go compile alone takes ~10 min emulated).
#
# Local single-arch build (native):
# docker build -t plurality .
#
# Local cross-arch build (slow on non-native legs):
# docker buildx build --platform linux/amd64,linux/arm64 -t <tag> --push .
#
# The Flutter stage is pinned to $BUILDPLATFORM because its output (web
# assets) is arch-independent — no point emulating the toolchain.
# Stage 1: Build the Flutter web app
FROM --platform=$BUILDPLATFORM dart:stable AS flutter_builder
# Install Flutter
RUN apt-get update && apt-get install -y curl git unzip xz-utils zip libglu1-mesa
# Get Stable Branch
RUN git clone https://github.com/flutter/flutter.git /flutter && \
git -C /flutter checkout stable
ENV PATH="/flutter/bin:${PATH}"
# Copy the Flutter app source
WORKDIR /app
COPY client/ ./client/
COPY scripts/ ./scripts/
# Sync the bundled version asset with the pubspec version so the web update
# check can't drift into a reload loop (see scripts/version.sh).
RUN sh scripts/version.sh
WORKDIR /app/client
# Build the Flutter web app
RUN flutter pub get
RUN flutter build web --release
# Stage 2: Build the Go server
# Runs on the *target* platform (no --platform override) so CGO links against
# the matching libc/libsqlite3 for that arch. buildx auto-selects the right
# golang:1.25 image variant per platform.
FROM golang:1.25 AS go_builder
ARG TARGETOS
ARG TARGETARCH
# Install build dependencies. libsqlite3-dev provides /usr/include/sqlite3.h, which
# sqlite-vec-go-bindings@v0.1.7-alpha.2 needs at CGO compile time — the Go
# module zip served by proxy.golang.org does not include sqlite3.h (only
# lib.go, sqlite-vec.c, sqlite-vec.h), so the system header is used as a
# fallback. mattn/go-sqlite3 still provides the actual sqlite implementation.
RUN apt-get update && apt-get install -y --no-install-recommends \
bash curl git gcc libc6-dev libsqlite3-dev python3 python3-pip python3-venv && \
rm -rf /var/lib/apt/lists/*
# Copy the Go app source
WORKDIR /app
COPY server/ ./server/
WORKDIR /app/server
# Build the Go application (build.sh sets its own CGO_CFLAGS).
# GOOS/GOARCH come from buildx's per-target args.
# CACHEBUST: pass --build-arg CACHEBUST=<git rev> so the Go binary is always
# recompiled from the current source (prevents a stale cached 'go build' from
# shipping an old server binary even when the checkout is up to date).
ARG CACHEBUST=latest
RUN chmod +x build.sh
# The RUN command below LITERALLY references ${CACHEBUST} so a changed
# build-arg value changes the RUN instruction's command string and
# therefore INVALIDATES the layer cache. A bare `ARG CACHEBUST` with no
# reference does NOT bust anything (Docker only invalidates from the
# first USE of the arg) -- that was the bug: CI passed CACHEBUST=<sha>
# but the go build layer was served from cache, so the image shipped a
# STALE binary built from an old checkout (observed: `latest` image
# built from #49 still contained the Sep-30 19:28 binary with none of
# the #37-#49 fixes).
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
echo "Building Plurality server @ CACHEBUST=${CACHEBUST}" && \
GOOS=${TARGETOS} GOARCH=${TARGETARCH} ./build.sh
# Copy litellm requirements for installation in final stage
RUN mkdir -p build/litellm && cp litellm_requirements.txt build/litellm/
# Stage 3: Create the final image
FROM debian:bookworm-slim
# Install runtime dependencies (Python needed for LiteLLM proxy;
# nodejs/npm needed for npx-based MCP servers; git needed for MCP servers
# that clone repos / for go install-style tooling).
# Node 22 LTS via NodeSource — Debian bookworm's packaged nodejs is 18.x,
# which is past end-of-life and too old for several MCP servers.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl gnupg git python3 python3-venv python3-pip tini && \
mkdir -p /etc/apt/keyrings && \
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" > /etc/apt/sources.list.d/nodesource.list && \
apt-get update && apt-get install -y --no-install-recommends nodejs && \
rm -rf /var/lib/apt/lists/*
# Pre-install Playwright MCP server and its Chromium browser with system deps.
# Doing this at build time avoids a multi-hundred-MB download on first MCP call.
RUN npm install -g @playwright/mcp@latest playwright && \
npx --yes playwright install --with-deps chromium && \
npm cache clean --force
WORKDIR /app
# Copy the compiled Go binary and litellm files from the builder stage
COPY --from=go_builder /app/server/build/ /app/
# Build LiteLLM venv using runtime Python (avoids glibc version mismatch)
# Stub out pyroscope-io (needs Rust/cargo to build, not needed at runtime)
RUN mkdir -p /tmp/dummy-pyroscope && \
printf '[project]\nname = "pyroscope-io"\nversion = "99.0.0"\n' > /tmp/dummy-pyroscope/pyproject.toml && \
echo 'pyroscope-io>=99.0.0' > /tmp/pip-constraints.txt && \
python3 -m venv /app/litellm/litellm_venv && \
/app/litellm/litellm_venv/bin/pip install --no-cache-dir /tmp/dummy-pyroscope && \
PIP_CONSTRAINT=/tmp/pip-constraints.txt \
/app/litellm/litellm_venv/bin/pip install --no-cache-dir -r /app/litellm/litellm_requirements.txt && \
rm -rf /tmp/dummy-pyroscope /tmp/pip-constraints.txt
# Copy the Flutter web build to the static directory
RUN mkdir -p /app/web
COPY --from=flutter_builder /app/client/build/web /app/web
# Create directories for volumes
RUN mkdir -p /app/users-data /app/data
# Declare runtime data volumes. These are ALWAYS explicitly mounted by the
# host/compose (see deployment.md) — do NOT add anonymous VOLUME entries here:
# a VOLUME /root (or any dir that holds build tooling / binaries) makes Docker
# create a persistent anonymous volume that SHADOWS that path on every
# container start. A stale anonymous volume survives image rebuilds, so a
# freshly layered /app/Plurality.bin can be masked by an old one from a
# previous container, and /root tooling (go cache, npx, playwright) silently
# drifts. Data dirs only, declared, not materialized.
VOLUME /app/users-data
VOLUME /app/data
# Expose the port the server listens on
EXPOSE 8090
# Run the server under the crash-capture supervisor (still under tini as PID 1,
# so zombies are reaped and `docker stop` signals are forwarded). The
# supervisor logs the exact exit code/signal of every server death to the
# persistent /app/data/crash.log and auto-restarts the server, so a crash is
# observable and self-healing instead of an invisible outage.
#
# The real binary is placed at /app/Plurality.bin and /app/Plurality is a shell
# SHIM that execs the supervisor: some launchers OVERRIDE the ENTRYPOINT/CMD and
# start /app/Plurality directly — in that case the ENTRYPOINT is bypassed and
# the supervisor would never run. Making /app/Plurality the shim guarantees the
# supervised entrypoint runs no matter how the container is started.
COPY server/supervisor.sh /app/supervisor.sh
RUN chmod +x /app/supervisor.sh
ARG CACHEBUST_BIN=latest
# the binary COPY happens in the build stage; here we wrap it. This must run
# AFTER /app/Plurality (real binary) is copied from the builder stage.
RUN sh -c 'mv /app/Plurality /app/Plurality.bin && printf "#!/bin/sh\nexec /app/supervisor.sh\n" > /app/Plurality && chmod +x /app/Plurality && chmod +x /app/Plurality.bin'
ENTRYPOINT ["/usr/bin/tini", "--", "/app/supervisor.sh"]
# Healthcheck for orchestrators / autoheal watchers (e.g. willfarrell/autoheal),
# which restart any container lacking a HEALTHCHECK every cycle. The server
# exposes /health (returns 200 when the HTTP stack is up); the supervisor keeps
# the server child alive and restarts it on real crashes, so a healthy HTTP
# endpoint is the correct liveness signal. 20s interval, 3 retries, 5s timeout.
HEALTHCHECK --interval=20s --timeout=5s --start-period=30s --retries=3 \
CMD curl -fsS http://127.0.0.1:8090/health >/dev/null 2>&1 || exit 1