Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/Dockerfile

43 lines
981 B
Text
Raw Permalink Normal View History

FROM --platform=$BUILDPLATFORM golang:1.27.3-alpine AS builder
WORKDIR /src
# Install build dependencies
RUN apk add --no-cache git
# Copy go mod files first for caching
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build the binary for the target platform
ARG TARGETARCH
2026-11-02 11:37:28 +01:00
ARG VERSION=dev
ARG COMMIT=unknown
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build \
-ldflags="-s -w -X main.Version=${VERSION} -X main.Commit=${COMMIT}" \
-o /proxy ./cmd/proxy
FROM alpine:3.24.3
Serve Git repositories from a local cache (#492) * Serve Git repositories from a local cache Named upstream.git hosts are served read-only over Git's smart HTTP protocol at /git/{name}/{repo}, so git clone and git fetch work against the proxy through an insteadOf rewrite. Each repository is a bare clone at git.cache_dir/{name}-{hash of the upstream URL}/{repo}.git, served by git upload-pack. Every request for refs checks the upstream's branches, tags and HEAD with a protocol v2 ls-refs limited to those prefixes, and fetches only when they changed, atomically and with --prune. Concurrent requests share one refresh. Only branches and tags are cached, and HEAD follows the upstream's default branch. A commit a client asks for by ID that the cache lacks, such as a pull request head, is fetched and kept under a hidden ref until a branch or tag includes it or for 7 days. One the upstream cannot provide is not asked for again for a minute, or an hour when it is larger than git.max_repo_size leaves. This is off for upstreams upstream.auth has credentials for, since GitHub serves commits from any repository in a fork network. git never reaches an upstream itself. Its fetches go through a loopback relay that forwards with the proxy's upstream client, so address checks, upstream.auth and the access log apply, and git never sees a credential. Redirects are not followed, and git runs with an environment of its own. The cached copy is served while the upstream fails or is slower than git.refresh_wait, until git.stale_max_age has passed since the last successful check. Refusals and missing repositories are never answered from the cache. git.max_repo_size cuts a transfer off as it streams, and every fetched pack is kept as a pack, so a cut-off leaves nothing behind. A repository that can no longer be refreshed within the limit is cloned again with only its branches and tags. git.max_size evicts the least recently used repositories, never one in use. At startup only the directory of a repointed upstream's old URL is removed, and Close waits for refreshes in progress. Hosts with upstream.auth credentials need a repos allowlist, and a repository outside it answers 404. Refs #403 * Document setting the Git rewrite through environment variables The Git section of the configuration docs now explains what insteadOf does, with the URL a clone fetches from, and shows the same rewrites set through GIT_CONFIG_COUNT, GIT_CONFIG_KEY_n and GIT_CONFIG_VALUE_n for CI. Package managers that run git for Git dependencies inherit those variables, so their fetches are cached too; npm's GitHub tarball downloads do not go through git and are not. Refs #403 * Note that upstream.auth credentials reach every route to a host Credentials are matched by URL, so a token configured for a Git upstream is also sent by a generic upstream at the same URL, which forwards any path and checks no repos allowlist. With a token for a private GitHub org, /generic/github/acme/private.git/info/refs would list that repository's refs to anyone who can reach the proxy. * Cap concurrent upload-pack processes and request body size Every POST git-upload-pack started a pack-objects with git's defaults, a delta-search thread per CPU and unbounded window memory, and nothing limited how many ran at once, so a burst of CI clones of one large repository could run the proxy out of memory. At most two upload-pack processes per CPU (GOMAXPROCS, so a container's limit) now build packs at once, and further requests wait for a slot while the client is connected. ls-refs builds no pack and does not wait. Serving runs with pack.threads=1 and pack.windowMemory=64m, and every git process with core.bigFileThreshold=8m, so index-pack streams large files instead of holding them in memory. A request body was streamed to upload-pack unbounded, so a small gzip body could feed it an arbitrarily long negotiation for as long as the request may run. Bodies are now limited to 100MB, both as received and decoded, and one past the limit fails with an error instead of being cut short quietly. * Apply refresh_wait to the ref check only refresh_wait cut off the whole refresh, fetch included, so after a push with a large pack a client got the old refs once it passed, though the upstream was fine and the docs say a clone right after a push sees the new commit. It now bounds only the check of the upstream's branches and tags. Once the check finds they changed, requests wait for the fetch; one that fails, or stalls and is abandoned by http.lowSpeedTime, still falls back to the cached copy within stale_max_age. * Lock cache_dir against a second process The cache clears its temporary directory at startup, so a second process given the same cache_dir, such as a second pod on a shared volume, would delete the first one's clones in progress, and each process's eviction knows only its own readers. New now locks {cache_dir}/.gitcache-lock before touching anything and holds it until Close: flock on Unix, a handle opened without sharing on Windows. The OS releases it when the process exits, so a crashed process can restart. A second process fails to start with an error naming the directory. * Evict again once repositories in use are released Eviction skips repositories a request or refresh is using, and only the release of one too large to refresh ran it again, so a cache filled past max_size while its repositories were in use stayed over it once they went idle, until the next fetch. Eviction now records when it is left over max_size, and the next release runs it again. * Cap git.max_repo_size at git.max_size A repository larger than max_size could still be cloned, up to max_repo_size, which defaults to 5GB whatever max_size is. Once released it was evicted to bring the cache back within max_size, and the next request cloned it again. max_size now caps max_repo_size, so such a repository is refused for an hour like any other too large one, with an error that names both limits. * Count fetches in progress toward git.max_size Fetches enforced only max_repo_size, and eviction ran after each one, so up to 16 concurrent fetches could take the cache past max_size by as much as each may download. Each fetch now reserves space within max_size as the relay receives its bytes, a chunk at a time, evicting least recently used idle repositories to make room. When the fetch ends, the repository's measured size replaces the reservation under the cache lock, so its bytes are neither counted twice nor missed. When evicting every idle repository would not make room, none is evicted for the fetch, and it is cut off with ErrCacheFull rather than ErrTooLarge, so the repository is not refused for an hour: a clone answers 507, and a refresh serves the cached copy as when the upstream fails. A fetch that runs out of room while another has just been cut off waits, up to a minute, for that one's space, so two fetches that do not fit together do not both fail. A fetch that was cut off never waits, so no two wait on each other. --------- Co-authored-by: Andrew Nesbitt <andrewnez@gmail.com>
2026-10-21 03:47:16 -07:00
RUN apk add --no-cache ca-certificates git
COPY --from=builder /proxy /usr/local/bin/proxy
# Create non-root user
RUN adduser -D -u 1000 proxy
USER proxy
# Default data directory
WORKDIR /data
EXPOSE 8080
# /health checks the database and storage and can take up to ten seconds.
HEALTHCHECK --interval=30s --timeout=11s --start-period=10s --retries=3 \
CMD wget -q -O /dev/null http://127.0.0.1:8080/health || exit 1
ENTRYPOINT ["proxy"]
CMD ["serve"]