A production-ready local Go module proxy server that caches downloaded packages and modules on disk. This proxy implements the Go module proxy protocol and serves as a caching layer between your Go toolchain and upstream module proxies.
- ✅ Full Go module proxy protocol implementation
- ✅ Disk-based caching for all module artifacts
- ✅ Thread-safe cache operations
- ✅ Atomic file writes to prevent corruption
- ✅ Graceful shutdown handling
- ✅ Configurable upstream proxy (single or multiple with fallback)
- ✅ HTTP client with proper timeouts and connection pooling
- ✅ Content-Length headers for HTTP compliance
- ✅ Comprehensive logging
- ✅ Download progress: live-updating progress block at the bottom (TTY) or throttled lines (Docker/logs) with speed, ETA for zip files
- ✅ Cache TTL: SQLite-backed last-used tracking; automatic cleanup of modules unused beyond configurable age (default 30 days)
- ✅ Environment variable and CLI flag support
- ✅ Request deduplication: concurrent requests for the same module version share a single upstream fetch
- ✅ Retry with backoff: automatic retries on transient failures (5xx, 429, network errors) with exponential backoff
- ✅ Checksum verification: validates zip files via sum.golang.org; skip with GONOSUMDB for private modules
- ✅ Multiple upstreams: ordered fallback list (e.g. proxy.golang.org → goproxy.cn)
- ✅ Private module support: route requests by module pattern to custom upstreams with auth (Basic, Bearer, custom headers)
- ✅ Config file: YAML or JSON config (auto-detected by extension);
-configflag orGOPROXY_CONFIGenv - ✅ Parallel zip downloads: multiple HTTP connections (Range requests) for faster large module fetches
- ✅ Resumable zip downloads: after a restart, single-connection downloads resume from
.partvia Range; parallel downloads reuse completed chunks in.tmp
┌─────────────┐
│ Go Toolchain│
└──────┬──────┘
│ HTTP Request
▼
┌──────────────────┐
│ Proxy Server │
│ (Port 12345) │
└──────┬──────┬────┘
│ │
│ │ Cache Miss
│ ▼
│ ┌──────────────┐
│ │ Upstream │
│ │ Proxy │
│ └──────┬───────┘
│ │
│ │ Fetch
│ ▼
│ ┌──────────────┐
│ │ Cache Disk │
│ └──────────────┘
│
│ Cache Hit
▼
┌──────────────┐
│ Cache Disk │
└──────────────┘
- Go 1.21 or later
- Git (for cloning)
git clone <repository-url>
cd goproxy
go mod download
go build -o goproxyStart the proxy server with default settings:
./goproxyThis will:
- Listen on port
12345 - Use
./cacheas the cache directory - Use
https://proxy.golang.orgas the upstream proxy
./goproxy -port 3000 -cache /path/to/cache -upstream https://proxy.golang.orgOptions:
-port: Port to listen on (default:12345)-cache: Cache directory path (default:./cache)-upstream: Upstream proxy URL(s), comma-separated for fallback (default:https://proxy.golang.org)-config: Path to YAML or JSON config file (overrides defaults; flags override config)-max-cache-age: Remove cached modules unused for this duration (default:0s= unlimited; e.g.,720h= 30 days)-cleanup-interval: How often to run cache cleanup (default:24h)-gosumdb: Checksum database URL (default:off; set to e.g.sum.golang.orgto enable verification)-gonosumdb: Comma-separated module patterns to skip checksum verification (e.g.*.corp.example.com,github.com/myorg/*)-retry-attempts: Number of retries for upstream requests on transient failures (default:3)-retry-backoff: Initial backoff duration for retries (default:100ms; exponential backoff applies)-download-connections: Number of parallel HTTP connections for zip downloads (default:4; set to1to disable)-max-concurrent-downloads: Maximum number of packages (zip files) downloading concurrently (default:4;0= unlimited)
Environment variables override config file; flags override environment:
export PORT=3000
export CACHE_DIR=/path/to/cache
export UPSTREAM_PROXY=https://proxy.golang.org,https://goproxy.cn
export GOPROXY_CONFIG=./config.yaml
# Optional: enable checksum verification (default is disabled)
# export GOSUMDB=sum.golang.org
export GONOSUMDB="*.corp.example.com"
export DOWNLOAD_CONNECTIONS=4
export MAX_CONCURRENT_DOWNLOADS=4
# Optional: enable cache TTL cleanup (default is unlimited)
export MAX_CACHE_AGE=720h
export CLEANUP_INTERVAL=24h
./goproxyThe proxy tracks when each cached module was last used (via SQLite at cache/.usage.db). When a positive max-cache-age is configured, a background job periodically removes modules that have not been accessed within that age. By default (0s), TTL cleanup is disabled and cached modules are kept indefinitely.
Example: remove modules unused for 30 days, run cleanup every 24 hours:
./goproxy -max-cache-age 720h -cleanup-interval 24hOr via environment:
export MAX_CACHE_AGE=720h
export CLEANUP_INTERVAL=24h
./goproxyFor zip (module) downloads, the proxy can use multiple HTTP connections in parallel when the upstream supports Range requests. This can significantly speed up large module downloads.
- Default: 4 parallel connections for zips larger than 512 KB.
- Disable: set
-download-connections=1orDOWNLOAD_CONNECTIONS=1. - Tune: e.g.
-download-connections=8ordownload_connections: 8in config. - Resume: If the proxy is stopped mid-download and restarted, the next request for the same zip will resume (single-connection from
.partvia Range, or parallel by reusing completed chunks in.tmp).
./goproxy -download-connections 8Set the GOPROXY environment variable:
export GOPROXY=http://localhost:12345,directOr for a specific Go command:
GOPROXY=http://localhost:12345,direct go get example.com/moduleThe ,direct fallback ensures that if the proxy doesn't have a module, Go will fetch it directly from the source.
You can use a config file for all settings. Format is auto-detected by extension (.yaml, .yml, or .json).
./goproxy -config config.yamlOr via environment:
export GOPROXY_CONFIG=./config.yaml
./goproxyLoad order: defaults → config file → environment → flags (flags have highest priority)
Example config.yaml (enables 30-day TTL cleanup):
port: "12345"
cache_dir: "./cache"
upstreams:
- https://proxy.golang.org
- https://goproxy.cn
private_upstreams:
- pattern: "github.com/myorg/*"
url: "https://myproxy.corp.com"
auth:
type: bearer
value: "ghp_xxx"
- pattern: "*.corp.example.com"
url: "https://nexus.corp.example.com"
auth:
type: basic
value: "user:pass"
gosumdb: "off"
gonosumdb: "*.corp.example.com,github.com/myorg/*"
retry_attempts: 3
retry_backoff: 100ms
download_connections: 4
max_concurrent_downloads: 4
max_cache_age: 720h # use 0s for unlimited (no TTL cleanup)
cleanup_interval: 24hUse comma-separated upstream URLs for fallback when one fails (404/410) or on transient errors (after retries):
./goproxy -upstream "https://proxy.golang.org,https://goproxy.cn"Or in config: upstreams: [url1, url2, ...]
By default, checksum verification is disabled (equivalent to -gosumdb off), which avoids contacting a checksum database.
To enable verification against the public checksum database:
./goproxy -gosumdb sum.golang.orgTo skip verification for specific private modules while verification is enabled:
./goproxy -gosumdb sum.golang.org -gonosumdb "*.corp.example.com,github.com/myorg/*"Route requests for certain module path prefixes to custom upstreams with authentication. Use the config file for private_upstreams; patterns use Go path.Match globs.
Auth types: basic (value: user:pass), bearer (value: token), header (name: header name, value: header value).
See the config file example above.
The proxy supports HTTP, HTTPS, and SOCKS5 proxies to bypass restrictions and work in restricted environments.
# HTTP/HTTPS proxy
./goproxy -proxy http://proxy-server:8080
# SOCKS5 proxy
./goproxy -proxy socks5://socks5-server:1080
# SOCKS5 with hostname resolution on proxy side
./goproxy -proxy socks5h://socks5-server:1080The proxy checks environment variables in this order:
HTTP_PROXY- HTTP proxy URLHTTPS_PROXY- HTTPS proxy URLSOCKS5_PROXY- SOCKS5 proxy URL
# HTTP proxy
export HTTP_PROXY=http://proxy-server:8080
./goproxy
# SOCKS5 proxy
export SOCKS5_PROXY=socks5://socks5-server:1080
./goproxyPriority: Command-line flag (-proxy) takes precedence over environment variables.
If proxy.golang.org is blocked, try alternative mirrors:
# Chinese mirror (may be accessible from restricted regions)
./goproxy -upstream https://goproxy.cn
# Or use environment variable
export UPSTREAM_PROXY=https://goproxy.cn
./goproxyhttp://- HTTP proxyhttps://- HTTPS proxy (treated as HTTP)socks5://- SOCKS5 proxy (DNS resolution on client)socks5h://- SOCKS5 proxy (DNS resolution on proxy)
./goproxy -proxy socks5://your-socks5-server:1080 -upstream https://goproxy.cnThe proxy supports multiple DNS protocols for domain name resolution, useful when DNS is blocked or you want to use encrypted DNS queries.
Standard DNS (UDP)
- Format:
8.8.8.8:53orudp://8.8.8.8:53 - Default port: 53
- Examples:
- Google DNS:
8.8.8.8:53 - Cloudflare DNS:
1.1.1.1:53 - Quad9:
9.9.9.9:53
- Google DNS:
DNS-over-HTTPS (DoH)
- Format:
https://doh-server/dns-query - Uses JSON format (application/dns-json)
- Examples:
- Cloudflare:
https://cloudflare-dns.com/dns-query - Google:
https://dns.google/resolve - Cloudflare IP:
https://1.1.1.1/dns-query
- Cloudflare:
DNS-over-TLS (DoT)
- Format:
tls://dns-server:853 - Default port: 853
- Examples:
- Cloudflare:
tls://1.1.1.1:853 - Google:
tls://8.8.8.8:853 - Quad9:
tls://9.9.9.9:853
- Cloudflare:
DNS-over-QUIC (DoQ)
- Format:
quic://dns-server:853 - Default port: 853
- Examples:
- AdGuard:
quic://dns.adguard.com:853 - Cloudflare:
quic://1.1.1.1:853
- AdGuard:
# Standard DNS (UDP)
./goproxy -dns 8.8.8.8:53
# DNS-over-HTTPS
./goproxy -dns https://cloudflare-dns.com/dns-query
# DNS-over-TLS
./goproxy -dns tls://1.1.1.1:853
# DNS-over-QUIC
./goproxy -dns quic://dns.adguard.com:853export DNS_SERVER=8.8.8.8:53
./goproxy
# Or use DoH
export DNS_SERVER=https://cloudflare-dns.com/dns-query
./goproxyPriority: Command-line flag (-dns) takes precedence over environment variable.
# Custom DNS + SOCKS5 proxy
./goproxy -dns 8.8.8.8:53 -proxy socks5://socks5-server:1080
# DoH + HTTP proxy + alternative upstream
./goproxy -dns https://cloudflare-dns.com/dns-query -proxy http://proxy:8080 -upstream https://goproxy.cn
# DoT + SOCKS5 proxy
./goproxy -dns tls://1.1.1.1:853 -proxy socks5://socks5-server:1080Standard DNS (UDP)
- Google DNS:
8.8.8.8:53or8.8.4.4:53 - Cloudflare DNS:
1.1.1.1:53or1.0.0.1:53 - Quad9:
9.9.9.9:53 - OpenDNS:
208.67.222.222:53or208.67.220.220:53
DNS-over-HTTPS (DoH)
- Cloudflare:
https://cloudflare-dns.com/dns-query - Google:
https://dns.google/resolve - Quad9:
https://dns.quad9.net/dns-query
DNS-over-TLS (DoT)
- Cloudflare:
tls://1.1.1.1:853 - Google:
tls://8.8.8.8:853 - Quad9:
tls://9.9.9.9:853
Note: If port is not specified for UDP DNS, it defaults to :53. For TLS/QUIC, it defaults to :853.
docker build -t goproxy .Option 1: Using a bind mount (persist cache on host)
docker run -d \
--name goproxy \
-p 12345:12345 \
-v $(pwd)/cache:/app/cache \
goproxyOption 2: Using a named Docker volume (managed by Docker)
# Create a named volume
docker volume create goproxy-cache
# Run container with named volume
docker run -d \
--name goproxy \
-p 12345:12345 \
-v goproxy-cache:/app/cache \
goproxyThe cache directory (/app/cache) is declared as a volume in the Dockerfile, so Docker will automatically create a volume if none is specified.
docker run -d \
--name goproxy \
-p 12345:12345 \
-v $(pwd)/cache:/app/cache \
-e UPSTREAM_PROXY=https://proxy.golang.org \
goproxy# HTTP proxy
docker run -d \
--name goproxy \
-p 12345:12345 \
-v $(pwd)/cache:/app/cache \
-e HTTP_PROXY=http://proxy-server:8080 \
-e UPSTREAM_PROXY=https://proxy.golang.org \
goproxy
# SOCKS5 proxy
docker run -d \
--name goproxy \
-p 12345:12345 \
-v $(pwd)/cache:/app/cache \
-e SOCKS5_PROXY=socks5://socks5-server:1080 \
-e UPSTREAM_PROXY=https://goproxy.cn \
goproxy# Standard DNS
docker run -d \
--name goproxy \
-p 12345:12345 \
-v $(pwd)/cache:/app/cache \
-e DNS_SERVER=8.8.8.8:53 \
goproxy
# DNS-over-HTTPS
docker run -d \
--name goproxy \
-p 12345:12345 \
-v $(pwd)/cache:/app/cache \
-e DNS_SERVER=https://cloudflare-dns.com/dns-query \
goproxy
# DNS + SOCKS5 proxy
docker run -d \
--name goproxy \
-p 12345:12345 \
-v $(pwd)/cache:/app/cache \
-e DNS_SERVER=8.8.8.8:53 \
-e SOCKS5_PROXY=socks5://socks5-server:1080 \
goproxyThe easiest way to run the proxy is using Docker Compose:
docker-compose up -ddocker-compose downdocker-compose logs -fdocker-compose up -d --buildTo use a bind mount (persist cache on host filesystem), edit docker-compose.yml and change:
volumes:
- goproxy-cache:/app/cacheto:
volumes:
- ./cache:/app/cacheThe docker-compose.yml includes:
- Named volume for cache persistence
- Health check endpoint (
/health) - Automatic restart policy
- Port mapping (12345:12345)
- Environment variable support
To use a proxy and/or DNS with Docker Compose, edit docker-compose.yml and uncomment/modify the environment section:
environment:
# HTTP proxy
HTTP_PROXY: http://proxy-server:8080
# Or SOCKS5 proxy
# SOCKS5_PROXY: socks5://socks5-server:1080
# Alternative upstream (if proxy.golang.org is blocked)
# UPSTREAM_PROXY: https://goproxy.cn
# DNS configuration
DNS_SERVER: 8.8.8.8:53
# Or use DoH
# DNS_SERVER: https://cloudflare-dns.com/dns-query
# Or use DoT
# DNS_SERVER: tls://1.1.1.1:853
# Cache TTL (remove modules unused for 30 days, cleanup every 24h):
# MAX_CACHE_AGE: 720h
# CLEANUP_INTERVAL: 24hThen restart the service:
docker-compose down
docker-compose up -dThe proxy implements the following Go module proxy protocol endpoints:
GET /<module>/@v/list- Lists available versions of a moduleGET /<module>/@v/<version>.info- Returns version metadata (JSON)GET /<module>/@v/<version>.mod- Returns the go.mod file for a versionGET /<module>/@v/<version>.zip- Returns the module zip file
- Client sends request to proxy
- Proxy checks local cache (with read lock)
- If cached:
- Serve cached content immediately
- Log cache hit
- If not cached:
- Fetch from upstream proxy
- Validate response (JSON for .info endpoints)
- Cache response atomically (using temp file + rename)
- Serve response to client
- Log cache miss
The cache directory structure mirrors the proxy URL structure:
cache/
├── .usage.db # SQLite DB for last-used timestamps (cache TTL)
├── github.com/
│ └── user/
│ └── repo/
│ └── @v/
│ ├── list
│ ├── v1.0.0.info
│ ├── v1.0.0.mod
│ └── v1.0.0.zip
The proxy uses a properly configured HTTP client with:
- Total request timeout: 30 minutes
- Connection timeout: 5 seconds
- TLS handshake timeout: 5 seconds
- Response header timeout: 10 seconds
- Idle connection timeout: 90 seconds
- Max idle connections: 100
- Max idle connections per host: 10
This ensures efficient connection reuse and prevents hanging requests.
The server handles SIGINT and SIGTERM signals for graceful shutdown:
- Receives shutdown signal
- Stops accepting new requests
- Waits up to 10 seconds for in-flight requests to complete
- Shuts down cleanly
The proxy logs:
- All incoming requests with client IP and path
- Cache hits and misses
- Errors with context
- Startup configuration
- Shutdown events
- Download progress for zip files. In an interactive terminal (TTY), progress is rendered using the
mpbmulti-progress bar library, showing per-module percentage, ETA, and speed in a compact bar UI at the bottom of the screen while regular logs scroll above. When stderr is not a TTY (for example Docker logs or CI), progress bars are disabled to avoid flooding logs with control sequences, and you only see the usual request/cache logs.
Example log output:
2024/01/01 12:00:00 Starting Go module proxy server
2024/01/01 12:00:00 Port: 12345
2024/01/01 12:00:00 Cache directory: ./cache
2024/01/01 12:00:00 Upstreams: [https://proxy.golang.org]
2024/01/01 12:00:00 Set GOPROXY=http://localhost:12345,direct
2024/01/01 12:00:05 [127.0.0.1] GET github.com/example/module/@v/list
2024/01/01 12:00:05 [CACHE MISS] github.com/example/module/@v/list
2024/01/01 12:00:06 [127.0.0.1] GET github.com/example/module/@v/v1.0.0.info
2024/01/01 12:00:06 [CACHE HIT] github.com/example/module/@v/v1.0.0.info
When downloading zip files (cache miss) in a TTY, each active download gets its own mpb bar with the module path and labeled stats: percentage complete, ETA (time remaining), and transfer speed (e.g. 11% | ETA 49m56s | 30.44 KiB/s). The bar stays confined to stderr and does not affect HTTP responses.
go build -o goproxygo test ./...Run with verbose output:
go test -v ./...The test suite covers:
- cache.go:
cachePath,readCache,writeCache(including empty data, atomic write),cacheExists - usage.go:
pathToModule,RecordUsage,LoadStaleModules,DeleteModule,NewUsageStore,RunCleanup/cleanupOnce(stale module deletion), empty module path, nonexistent dir on delete - proxy.go:
formatBytes, health check, method validation, list/info/mod/zip handlers (cache hit and miss), upstream errors (404, 500), invalid JSON (handleInfo), zip with unknown Content-Length, mpb-based progress integration - download progress: integration with
mpbin TTY environments and verification that progress handling does not interfere with HTTP responses or caching - resumable downloads: single-connection resume from
.part(Range request), fallback to full GET when server does not support Range; parallel resume by skipping completed chunks in.tmp; no resume when.tmphas wrong size - edge cases:
Proxy.Shutdownwith nil usageStore
After adding dependencies:
go mod tidyIf port 12345 is already in use, specify a different port:
./goproxy -port 8080Ensure the cache directory is writable:
chmod 755 ./cacheOr specify a different cache directory:
./goproxy -cache /tmp/goproxy-cacheIf you see upstream proxy errors, check:
- Network connectivity
- Upstream proxy URL is correct
- Firewall/proxy settings
- SSL certificate issues
If you suspect cache corruption, delete the cache directory:
rm -rf ./cacheThe proxy will recreate it on startup.
- The proxy does not implement authentication - consider placing it behind a reverse proxy with authentication if needed
- Cache files are stored with 0644 permissions (readable by all)
- Consider implementing cache size limits and cleanup policies for production use
- Monitor logs for unusual activity
- Config file: YAML/JSON support via
-configorGOPROXY_CONFIG - Multiple upstreams: Comma-separated fallback list via
-upstream - Retry with backoff: Configurable retries on 5xx, 429, and network errors
- Request deduplication: In-flight requests for the same path share a single upstream fetch
- Checksum verification: Zip validation via GOSUMDB; skip with GONOSUMDB for private modules
- Private upstreams: Per-module routing with Basic, Bearer, or custom header auth
- New flags:
-config,-gosumdb,-gonosumdb,-retry-attempts,-retry-backoff,-download-connections
MIT