This commit captures both the prior accumulated work-in-progress
(framework migration web/→svelte/, postgres storage, conversation
viewer, dashboard auth, OpenAPI spec, integration tests) AND today's
operational improvements layered on top. History wasn't checkpointed
incrementally; happy to split it via interactive rebase if a reviewer
wants smaller commits.
Today's changes (in addition to the older WIP):
1. Configurable upstream response-header timeout
- ANTHROPIC_RESPONSE_HEADER_TIMEOUT env (default 300s)
- Replaces hardcoded 300s in provider/anthropic.go that was firing
on opus + 1M-context + extended thinking non-streaming requests
- Files: internal/config/config.go, internal/provider/anthropic.go
2. Structured forward-error diagnostic logging
- When a forward to Anthropic fails, log a single key=value line
with request_id, model, stream, body_bytes, has_thinking,
anthropic_beta, query, elapsed, ctx_err — alongside the existing
human-readable error line for back-compat
- Files: internal/handler/handlers.go (logForwardFailure)
3. Full SSE protocol passthrough + Flusher fix
- handler/handlers.go: forward all SSE lines verbatim (event:, id:,
retry:, : comments, blank-line terminators), not only data:.
Previous code produced malformed SSE for strict parsers.
- middleware/logging.go: explicit Flush() method on responseWriter.
Embedding http.ResponseWriter (interface) does not auto-promote
Flush(), so every w.(http.Flusher) check in the streaming
handler was returning ok=false and SSE writes buffered in net/http
until the body closed.
4. Non-streaming → streaming demotion (feature-flagged)
- ANTHROPIC_DEMOTE_NONSTREAMING env (default false)
- When enabled and the routed provider is anthropic, force stream=true
upstream for clients that asked for stream=false. Receive SSE,
accumulate via accumulateSSEToMessage (handles text, tool_use with
partial_json reassembly, thinking, signature, citations_delta,
usage merge), and synthesize a single non-streaming JSON response.
- Eliminates the ResponseHeaderTimeout class of failure entirely.
- Body rewrite uses json.Decoder + UseNumber() to preserve integer
precision in unknown nested fields (tool inputs from prior turns).
- Files: internal/config/config.go, internal/handler/handlers.go,
cmd/proxy/main.go, cmd/proxy/main_test.go
5. Live operational state: /livez gauge + graceful drain
- New internal/runtime package: atomic in-flight counter + draining flag
- New middleware/inflight.go: increments runtime gauge, applied to
/v1/* subrouter so Messages, ChatCompletions, and ProxyPassthrough
are all counted
- /v1/* moved to a gorilla/mux subrouter so the InFlight middleware
applies surgically; /health, /livez, /openapi.* remain on parent
router (unauthenticated, uncounted)
- Health handler returns 503 draining when runtime.IsDraining() is
true, so Traefik stops routing to a slot before drain begins
- New /livez handler returns {status, in_flight, draining, timestamp}
- SIGTERM handler in main.go: SetDraining(true), poll for in_flight==0
with 32-min ceiling and 1s tick (logs every 10s), then srv.Shutdown
- Auth bypass list extended with /livez
- Files: internal/runtime/runtime.go (new),
internal/middleware/inflight.go (new),
internal/middleware/auth.go,
internal/handler/handlers.go (Health, Livez, runtime import),
cmd/proxy/main.go (subrouter, drain loop)
6. OpenAPI spec updates
- Document Health 503 response and new DrainingResponse schema
- Add /livez path with LivezResponse schema
- Files: internal/handler/openapi.go
Verified: go build ./... clean, go test ./... all pass, go vet clean.
Three rounds of codex peer review across changes 1-5; all feedback
addressed (citations_delta, json.Number precision, drain-loop logging
via lastLog timestamp, PathPrefix tightened to "/v1/").
153 lines
5.3 KiB
Docker
153 lines
5.3 KiB
Docker
# syntax=docker/dockerfile:1
|
|
# Multi-stage Dockerfile for Claude Code Proxy
|
|
# Builds both Go proxy server and SvelteKit frontend in a single container
|
|
#
|
|
# Targets:
|
|
# - (default): Production runtime image
|
|
# - dev: Development image with hot-reload tooling
|
|
|
|
# ============================================================================
|
|
# Stage: go-builder — compile Go proxy binary
|
|
# ============================================================================
|
|
FROM golang:1.26-alpine AS go-builder
|
|
|
|
WORKDIR /app/proxy
|
|
|
|
# Install build dependencies including gcc for CGO (sqlite)
|
|
RUN apk add --no-cache git gcc musl-dev sqlite-dev
|
|
|
|
# Copy Go modules first (cache layer)
|
|
COPY proxy/go.mod proxy/go.sum ./
|
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
|
go mod download
|
|
|
|
# Copy Go source code and build
|
|
COPY proxy/ ./
|
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
|
--mount=type=cache,target=/root/.cache/go-build \
|
|
CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o /app/bin/proxy cmd/proxy/main.go
|
|
|
|
# ============================================================================
|
|
# Stage: svelte-deps — install SvelteKit dependencies (cached)
|
|
# ============================================================================
|
|
FROM node:20-alpine AS svelte-deps
|
|
|
|
WORKDIR /app/svelte
|
|
COPY svelte/package*.json ./
|
|
RUN --mount=type=cache,target=/root/.npm \
|
|
npm ci
|
|
|
|
# ============================================================================
|
|
# Stage: svelte-builder — build SvelteKit frontend
|
|
# ============================================================================
|
|
FROM svelte-deps AS svelte-builder
|
|
|
|
COPY svelte/ ./
|
|
# shared/ is referenced by vite.config.ts (../shared/frontend/backend)
|
|
COPY shared/ /app/shared/
|
|
RUN npm run build
|
|
|
|
# ============================================================================
|
|
# Stage: svelte-prod — production SvelteKit deps only
|
|
# ============================================================================
|
|
FROM node:20-alpine AS svelte-prod
|
|
|
|
WORKDIR /app/svelte
|
|
COPY svelte/package*.json ./
|
|
RUN --mount=type=cache,target=/root/.npm \
|
|
npm ci --omit=dev
|
|
|
|
# ============================================================================
|
|
# Stage: dev — development image with hot-reload
|
|
# ============================================================================
|
|
# CGO is required for mattn/go-sqlite3. To avoid slow first-build times
|
|
# at container start, we pre-build the binary (and warm the build cache)
|
|
# during image build. CompileDaemon then does fast incremental rebuilds.
|
|
FROM golang:1.26-alpine AS dev
|
|
|
|
# Copy Node.js 20 from official image (Alpine's repos ship Node 24 which has
|
|
# breaking module-resolution changes that break SvelteKit's virtual modules)
|
|
COPY --from=node:20-alpine /usr/local/bin/node /usr/local/bin/node
|
|
COPY --from=node:20-alpine /usr/local/lib/node_modules /usr/local/lib/node_modules
|
|
RUN ln -sf /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
|
|
&& ln -sf /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
|
|
|
|
# Install build deps and runtime tools
|
|
RUN apk add --no-cache \
|
|
libstdc++ \
|
|
git gcc musl-dev sqlite-dev \
|
|
wget su-exec postgresql-client
|
|
|
|
# Install CompileDaemon for Go hot-reload
|
|
RUN go install github.com/githubnemo/CompileDaemon@latest
|
|
|
|
WORKDIR /app
|
|
|
|
# Pre-install Go dependencies and do initial build to warm cgo cache
|
|
COPY proxy/go.mod proxy/go.sum ./proxy/
|
|
RUN cd proxy && go mod download
|
|
|
|
COPY proxy/ ./proxy/
|
|
RUN cd proxy && CGO_ENABLED=1 go build -o /tmp/proxy-bin/proxy cmd/proxy/main.go
|
|
|
|
# Pre-install Node dependencies for svelte (layer cache)
|
|
COPY svelte/package*.json ./svelte/
|
|
RUN cd svelte && npm install
|
|
|
|
# Copy the dev entrypoint
|
|
COPY docker-entrypoint.dev.sh ./
|
|
RUN chmod +x docker-entrypoint.dev.sh
|
|
|
|
ENV PORT=3001
|
|
ENV SVELTE_PORT=5174
|
|
ENV CGO_ENABLED=1
|
|
|
|
EXPOSE 3001 5174
|
|
|
|
ENTRYPOINT ["./docker-entrypoint.dev.sh"]
|
|
|
|
# ============================================================================
|
|
# Stage: (default) — production runtime
|
|
# ============================================================================
|
|
FROM node:20-alpine
|
|
|
|
WORKDIR /app
|
|
|
|
# Install runtime dependencies (sqlite for legacy, postgresql-client for healthcheck)
|
|
RUN apk add --no-cache sqlite wget su-exec postgresql-client
|
|
|
|
# Copy built Go binary
|
|
COPY --from=go-builder /app/bin/proxy ./bin/proxy
|
|
RUN chmod +x ./bin/proxy
|
|
|
|
# Copy built SvelteKit application with production deps
|
|
COPY --from=svelte-builder /app/svelte/build ./svelte/build
|
|
COPY --from=svelte-prod /app/svelte/package*.json ./svelte/
|
|
COPY --from=svelte-prod /app/svelte/node_modules ./svelte/node_modules
|
|
|
|
# Create data directory for SQLite database
|
|
RUN mkdir -p /app/data && chown -R node:node /app
|
|
|
|
# Copy startup script
|
|
COPY docker-entrypoint.sh ./
|
|
RUN chmod +x docker-entrypoint.sh
|
|
|
|
# Environment variables with defaults
|
|
ENV PORT=3001
|
|
ENV SVELTE_PORT=5174
|
|
ENV READ_TIMEOUT=600
|
|
ENV WRITE_TIMEOUT=600
|
|
ENV IDLE_TIMEOUT=600
|
|
ENV ANTHROPIC_FORWARD_URL=https://api.anthropic.com
|
|
ENV ANTHROPIC_VERSION=2023-06-01
|
|
ENV ANTHROPIC_MAX_RETRIES=3
|
|
ENV DB_PATH=/app/data/requests.db
|
|
|
|
EXPOSE 3001 5174
|
|
|
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
|
CMD wget -qO- http://localhost:3001/health > /dev/null || exit 1
|
|
|
|
# Entrypoint handles privilege drop — compose overrides user to root at start
|
|
ENTRYPOINT ["./docker-entrypoint.sh"]
|
|
CMD []
|