How to Set Up a Private Go Module Proxy

Configure the GOPROXY environment variable to point to your private Go module proxy URL.

The network won't let you build

You spin up a fresh CI runner or join a corporate network. You run go build and the terminal hangs. Then it errors out. The Go toolchain is trying to reach proxy.golang.org, but the firewall blocks it. Or your security policy forbids fetching code from the public internet. Or you just want to cache dependencies locally so your team stops hitting GitHub rate limits.

You need a private module proxy. A private proxy is a server inside your network that stores copies of Go modules. Go fetches dependencies from it instead of the public proxy. Setting it up is mostly about environment variables, but getting it right requires understanding how Go resolves modules, handles private packages, and verifies checksums.

The proxy becomes the single gatekeeper for your dependencies. Configure it once, and every machine on the network builds from the same verified source.

What a module proxy actually does

Go modules use a proxy protocol. It is a simple HTTP interface. When Go needs a module, it sends a request to the proxy. The proxy returns the module archive, usually a zip file. Go downloads it, verifies the hash, and uses it.

Think of a proxy as a local warehouse. Instead of ordering every component from the factory every time, you keep a stock locally. If the warehouse has the part, you grab it instantly. If not, the warehouse orders from the factory, keeps a copy, and hands you the part. Next time, it is already there.

The protocol itself is deliberately minimal. Go asks for three things per module version: a .info file containing metadata, a .mod file containing the go.mod contents, and a .zip file containing the actual source code. The proxy serves these endpoints. If the proxy does not have the version, it fetches it from the upstream source, caches it, and returns it to you.

A private proxy adds control. You can audit what code enters your network. You can cache dependencies to speed up builds. You can serve modules in air-gapped environments where no external network access exists. The proxy becomes the single source of truth for your team dependencies.

Do not treat the proxy as a magic cache. Treat it as a controlled distribution point.

Pointing Go at your proxy

The GOPROXY environment variable tells Go where to find modules. It is not just a single URL. It is a comma-separated list of proxies. Go tries them in order. If the first proxy fails, Go moves to the second. If all proxies fail, Go stops and reports an error.

Here is the basic configuration to point Go to a private proxy:

# Set GOPROXY to use the internal proxy, falling back to direct VCS access
# The comma separates the proxy URL from the direct fallback
# Go will attempt the internal server first before trying anything else
export GOPROXY="https://proxy.corp.example.com,direct"

Run go env to verify the setting. The output shows the effective configuration. If GOPROXY appears in the list, Go is using your proxy.

# output:
GOPROXY="https://proxy.corp.example.com,direct"

Convention aside: modern Go workflows prefer go env -w GOPROXY="..." over shell exports. The -w flag writes the setting to your go.env file, which persists across terminal sessions and does not clutter your shell configuration. Most teams standardize on go env -w for module-related variables.

The keyword direct is special. It is not a URL. It tells Go to skip the proxy and fetch the module directly from the version control system, like Git or SSH. You almost always want direct as the last item in the list. It acts as a fallback. If your private proxy is down or missing a module, Go can still build by reaching the source repository.

The default value for GOPROXY is proxy.golang.org,direct. This means Go tries the public proxy first, then falls back to direct VCS access. You override this default by setting GOPROXY in your environment.

Set the variable once. Let the toolchain handle the rest.

How the resolution chain works

Go caches downloaded modules in the module cache, usually $GOPATH/pkg/mod. Once a module is in the cache, Go does not contact the proxy again unless you run go clean -modcache. The proxy speeds up the first fetch and keeps the cache consistent across machines.

When you run go build or go get, Go follows a strict sequence. It checks the local module cache first. If the exact version is already there, Go uses it immediately. If the cache is empty or you request a new version, Go queries the proxy. The proxy returns the .info, .mod, and .zip files. Go extracts the zip, places it in the cache, and compiles.

This caching behavior is intentional. Go prioritizes reproducibility and speed. The module cache is content-addressed, meaning the directory structure includes the module path, version, and a hash of the contents. This prevents accidental overwrites and makes builds deterministic.

Convention aside: never manually edit files inside $GOPATH/pkg/mod. The cache is managed by the toolchain. If you need to patch a dependency, use a replace directive in your go.mod or fork the module. Direct cache edits break reproducibility and cause silent mismatches across machines.

If you update a module in your repository, Go might still use the cached version. Run go clean -modcache to clear the cache. This forces Go to re-fetch all modules. Be careful in shared environments. Clearing the cache affects all projects on the machine.

The proxy handles the network. The cache handles the speed. Keep them aligned.

Handling private code and checksums

A private proxy often coexists with private modules. Private modules live in internal repositories that are not accessible to the public. If you set GOPROXY to a private server, Go assumes every module should go through that proxy. This breaks private modules. The proxy cannot fetch them because it lacks the SSH keys or authentication tokens. Or the proxy simply does not host them.

You must tell Go which modules are private. The GOPRIVATE environment variable takes a list of glob patterns. When a module path matches a pattern, Go skips the proxy and uses direct access.

Here is a realistic configuration that handles both public and private modules:

# Point Go to the internal proxy for public modules
# The proxy will cache and serve all external dependencies
export GOPROXY="https://proxy.corp.example.com,direct"

# Tell Go to skip the proxy for internal packages
# Go will fetch these directly via git or ssh using local credentials
export GOPRIVATE="*.corp.example.com,github.com/my-company/*"

When you run go get, Go checks the module path against GOPRIVATE. If it matches, Go bypasses the proxy and contacts the VCS directly. If it does not match, Go uses the proxy. This split allows your team to use the proxy for public dependencies while still importing internal libraries.

Convention aside: GOPRIVATE patterns use shell-style globs. *.corp.example.com matches any subdomain. github.com/my-org/* matches all modules under that organization. Be specific. Overly broad patterns like * disable the proxy for everything, which defeats the purpose. Most teams mirror their internal domain structure exactly in the pattern.

Go verifies every module against a checksum database. By default, this is sum.golang.org. The database stores hashes of all public modules. When Go downloads a module, it computes the hash and compares it to the database. If the hashes differ, Go rejects the module. This prevents supply chain attacks where a compromised server serves malicious code.

Private modules are not in the public checksum database. If you fetch a private module, Go cannot verify the hash. The build fails with a verification error.

You need to disable checksum verification for private modules. The GONOSUMCHECK environment variable takes glob patterns, similar to GOPRIVATE. When a module matches, Go skips the checksum check.

Here is the full configuration for a corporate environment:

# Use the internal proxy with direct fallback
# This ensures all public dependencies pass through your controlled server
export GOPROXY="https://proxy.corp.example.com,direct"

# Skip proxy for internal modules
# Go will use local git/ssh credentials to fetch these directly
export GOPRIVATE="*.corp.example.com"

# Skip checksum verification for internal modules
# The public sum database does not host private code, so verification would fail
export GONOSUMCHECK="*.corp.example.com"

Convention aside: GONOSUMCHECK and GOPRIVATE usually have the same patterns. If a module is private, it is likely not in the public sum database. Set both variables to the same values to avoid confusion. The Go team designed them to mirror each other for exactly this reason.

This setup is secure enough for most internal workflows. You trust your internal proxy and your internal repositories. The checksum database protects you against compromised public modules. The private rules protect you from verification errors on internal code.

Trust the proxy for public code. Bypass it for private code. Verify what you can, skip what you must.

Where things break

Setting up a proxy is simple. Making it work reliably requires attention to detail. Here are the common failure modes.

Forgetting GOPRIVATE is the most frequent mistake. You set GOPROXY and then try to import a private module. Go sends the request to the proxy. The proxy returns a 404 or an authentication error. The build fails. The compiler reports module not found. The real issue is that Go should not have asked the proxy at all. Add the module path to GOPRIVATE.

Forgetting GONOSUMCHECK causes verification errors. You set GOPRIVATE correctly, so Go fetches the module via Git. But Go still tries to check the hash against sum.golang.org. The hash is missing. Go rejects the module with verifying module: checksum mismatch. Add the pattern to GONOSUMCHECK.

Proxy authentication can trip you up. Some private proxies require tokens. You pass the token via the URL or a separate environment variable. If the token expires, Go fails with a 401 Unauthorized error. Check your proxy documentation for authentication details. The Go toolchain does not handle auth automatically. You must configure it in the proxy URL or use a wrapper script.

Using GOPROXY=off breaks builds that depend on the proxy. This setting disables all proxies. Go only uses direct access. If your network blocks VCS access, the build fails. Use GOPROXY=off only in fully isolated environments where all modules are vendored or local.

The module cache can cause stale data. If you update a module in your repository, Go might still use the cached version. Run go clean -modcache to clear the cache. This forces Go to re-fetch all modules. Be careful in shared environments. Clearing the cache affects all projects on the machine.

Convention aside: run go mod tidy after changing proxy settings or updating private modules. The command synchronizes your go.mod and go.sum files with the actual dependencies. It catches missing checksums and unused imports before they reach production.

Read the error message. It tells you exactly which variable is missing. Fix the pattern. Move on.

Choosing the right setup

Use a private proxy when you need to cache dependencies, filter code, or build in air-gapped environments. Use GOPRIVATE when your project imports modules from internal repositories that the proxy cannot access. Use GONOSUMCHECK when your private modules are not registered with the public checksum database. Use direct as the last element in GOPROXY when you want Go to fall back to VCS if the proxy fails. Use GOPROXY=off when you are in a fully isolated environment with no network access and all modules are vendored or local.

GOPROXY is a chain. Direct is the escape hatch. Private modules need private rules. Set GOPRIVATE or watch the build fail. Trust the proxy, verify the hash.

Where to go next