# syntax=docker/dockerfile:1.7 FROM node:24-slim AS builder RUN apt-get update \ && apt-get install -y --no-install-recommends python3 make g++ \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY package.json pnpm-lock.yaml ./ COPY patches ./patches RUN --mount=type=cache,target=/root/.local/share/pnpm/store \ corepack enable \ && pnpm install --frozen-lockfile # Copy only what the JS build (vite + tsup) and the final server image consume, # so a cmd/-only change (the Go downloader) doesn't bust this layer and force a # full server rebuild. Keep in sync with the build inputs + the final-stage COPYs. COPY index.html vite.config.ts tsconfig.json ./ COPY src ./src COPY server ./server COPY shared ./shared COPY public ./public COPY migrations ./migrations COPY scripts ./scripts COPY agent-skills ./agent-skills # .git is excluded from the build context, so git describe cannot run here. # The release workflow passes the tag via APP_VERSION and the commit SHA via # APP_COMMIT; resolveAppVersion/resolveAppCommit read them. ARG APP_VERSION=dev ENV ZPAN_APP_VERSION=${APP_VERSION} ARG APP_COMMIT= ENV ZPAN_APP_COMMIT=${APP_COMMIT} RUN pnpm build:node \ && pnpm prune --prod --ignore-scripts # Build on the native build platform and cross-compile to the target arch, so # the arm64 image doesn't go through slow QEMU emulation. CGO is off, so Go # cross-compiles cleanly. Build/mod caches make incremental rebuilds fast. FROM --platform=$BUILDPLATFORM golang:1.25 AS cli-builder ARG TARGETOS ARG TARGETARCH WORKDIR /app/cmd COPY cmd/go.mod cmd/go.sum ./ RUN --mount=type=cache,target=/go/pkg/mod go mod download COPY cmd ./ RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \ CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -trimpath -ldflags="-s -w" -o /out/zpan . FROM debian:bookworm-slim AS geoip-db ARG GEOIP_DB_MONTH= ARG GEOIP_DB_URL= RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates curl gzip \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /out \ && month="${GEOIP_DB_MONTH:-$(date -u +%Y-%m)}" \ && url="${GEOIP_DB_URL:-https://download.db-ip.com/free/dbip-city-lite-${month}.mmdb.gz}" \ && if ! curl -fsSL --retry 5 --retry-delay 3 --retry-connrefused "$url" -o /tmp/geoip.mmdb.gz; then \ if [ -n "$GEOIP_DB_MONTH" ] || [ -n "$GEOIP_DB_URL" ]; then exit 1; fi; \ previous_month="$(date -u -d '1 month ago' +%Y-%m)"; \ curl -fsSL --retry 5 --retry-delay 3 --retry-connrefused \ "https://download.db-ip.com/free/dbip-city-lite-${previous_month}.mmdb.gz" \ -o /tmp/geoip.mmdb.gz; \ fi \ && gzip -dc /tmp/geoip.mmdb.gz > /out/geoip.mmdb \ && rm -f /tmp/geoip.mmdb.gz FROM debian:bookworm-slim AS cli RUN apt-get update \ && apt-get install -y --no-install-recommends aria2 ca-certificates qbittorrent-nox \ && rm -rf /var/lib/apt/lists/* \ && addgroup --system zpan \ && adduser --system --ingroup zpan --home /home/zpan zpan \ && mkdir -p /home/zpan/.local/share/zpan COPY --from=cli-builder /out/zpan /usr/local/bin/zpan COPY --from=geoip-db /out/geoip.mmdb /home/zpan/.local/share/zpan/geoip.mmdb RUN mkdir -p /home/zpan/.config/zpan /home/zpan/.local/state/zpan/downloader /data /downloads \ && chown -R zpan:zpan /data /home/zpan /downloads USER zpan ENV HOME=/home/zpan WORKDIR /downloads ENTRYPOINT ["zpan"] CMD ["downloader", "up"] FROM node:24-slim WORKDIR /app RUN apt-get update \ && apt-get install -y --no-install-recommends aria2 ca-certificates qbittorrent-nox \ && rm -rf /var/lib/apt/lists/* \ && addgroup --system zpan \ && adduser --system --ingroup zpan --home /home/zpan zpan \ && mkdir -p /home/zpan/.local/share/zpan COPY --from=builder /app/dist ./dist COPY --from=builder /app/dist-server ./dist-server COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/migrations ./migrations COPY --from=builder /app/scripts/docker-entrypoint.sh /app/scripts/docker-entrypoint.sh COPY --from=cli-builder /out/zpan /usr/local/bin/zpan COPY --from=geoip-db /out/geoip.mmdb /home/zpan/.local/share/zpan/geoip.mmdb RUN mkdir -p /data /home/zpan/.config/zpan /home/zpan/.local/state/zpan/downloader \ && chown -R zpan:zpan /data /home/zpan USER zpan ENV NODE_ENV=production ENV HOME=/home/zpan ENV PORT=8222 # Lets the app report its deployment platform as "docker" (Cloud Run overrides # this via K_SERVICE, which entry-node checks first). ENV ZPAN_RUNTIME=docker EXPOSE 8222 ENTRYPOINT ["/app/scripts/docker-entrypoint.sh"] CMD ["node", "dist-server/entry-node.js"]