The snapshot that saves builds
You push code to CI. The build passes on your machine. It fails in the pipeline. The error points to a dependency that updated its API while you were sleeping. Or the network to the module proxy is flaky, and the build hangs for twenty minutes waiting for a download that never completes. You need a way to lock your project to a snapshot of the world, not a moving target.
go mod vendor copies all your module dependencies into a vendor directory within your project. It creates a local copy of the code your project needs. When you build with the vendor directory, Go ignores the network and the module proxy. It uses only what lives in vendor. This gives you reproducible builds. If the code works with the vendored dependencies, it will work anywhere, even offline.
Vendoring is a snapshot. The world changes; your snapshot stays still.
How the tool works
The go.mod file lists your direct dependencies. go.sum locks the cryptographic hashes of every module version used in your project. go mod vendor reads these files, resolves the full dependency graph, and copies the source code into vendor. It also generates vendor/modules.txt. This file maps module paths to their locations inside vendor and records the exact versions.
The Go toolchain uses modules.txt to resolve imports when vendoring is enabled. If you import github.com/example/pkg, Go checks vendor/modules.txt to find the version and location. If the file is missing or out of sync, the build fails.
Here's the basic workflow. Clean the module graph, then vendor.
# go mod tidy removes unused deps and adds missing ones
go mod tidy
# go mod vendor copies resolved deps to ./vendor
go mod vendor
Always run go mod tidy before go mod vendor. tidy ensures go.mod and go.sum match the code. If you skip tidy, vendor might copy stale dependencies or miss new ones. The convention is strict: tidy first, vendor second.
Run go mod tidy first. A clean module graph produces a clean vendor directory.
The vendor directory structure
Inside vendor, you see directories named after module paths. If you depend on github.com/gin-gonic/gin, you'll find vendor/github.com/gin-gonic/gin. The structure mirrors the import path. This allows Go to resolve imports by simple file lookup.
The vendor/modules.txt file is critical. It contains lines that map modules to versions. A typical entry looks like this:
# github.com/gin-gonic/gin v1.9.1
## explicit; go 1.18
github.com/gin-gonic/gin
github.com/gin-gonic/gin/binding
The comment line records the module path and version. The ## explicit tag marks direct dependencies. Lines without explicit are transitive dependencies. Go uses this file to verify that the directory contents match the expected versions.
If you edit vendor manually, the map gets out of sync. The build fails with vendor/modules.txt is out of date. Fix it by running go mod vendor again. Never edit vendor by hand. The tool owns the directory.
Vendoring includes transitive dependencies. If your code imports A, and A imports B, and B imports C, the vendor directory contains A, B, and C. You don't need to list B and C in your go.mod. The tool resolves the graph and copies everything required. This ensures the entire dependency tree is locked locally.
If your go.mod contains a replace directive, go mod vendor respects it. The vendor directory will contain the replaced module, not the original. This is useful for testing local forks or overriding a broken dependency. The modules.txt file records the replacement, so the build remains consistent.
Never edit vendor by hand. The tool owns the directory.
Building with vendor
To use the vendor directory, you must tell Go to look there. The flag -mod=vendor forces the toolchain to use only the local vendor folder. Without this flag, Go might fall back to the module proxy, which defeats the purpose of vendoring.
Here's how you build with vendoring enabled.
# -mod=vendor tells Go to ignore the proxy and use ./vendor
go build -mod=vendor ./...
The flag enforces isolation. If a dependency is missing from vendor, the build fails immediately. This catches configuration errors early. You won't accidentally build against a newer version from the proxy.
If you forget the flag, Go uses the default mode. In default mode, Go checks vendor first, but falls back to the proxy if something is missing. This can lead to subtle version mismatches. The compiler doesn't warn you about this; it just builds against the wrong version. Always use -mod=vendor in CI and when you want strict reproducibility.
Some projects set GOFLAGS=-mod=vendor in their environment to avoid typing the flag every time. This is a common practice in teams that rely heavily on vendoring.
The flag enforces isolation. Without it, Go falls back to the proxy.
CI/CD and Docker
In CI/CD environments, vendoring guarantees that every build uses the exact same dependency versions. This eliminates "works on my machine" issues caused by dependency drift. It also speeds up builds by avoiding network downloads.
Teams have two main strategies. You can commit the vendor directory to the repository, or you can cache dependencies in CI without committing.
Committing vendor increases repository size. Every clone downloads the dependencies. This is slower for developers and uses more disk space. However, it guarantees reproducibility. The repository contains everything needed to build. No network access is required.
Caching dependencies keeps the repository small. You run go mod download in CI to populate a cache. Subsequent builds reuse the cache. This approach relies on the network and the module proxy. If the proxy goes down or a version gets yanked, the build might fail.
Here's a Dockerfile that leverages vendoring for a clean build.
# Builder stage compiles the binary
FROM golang:1.22-alpine AS builder
# Set working directory
WORKDIR /src
# Copy go.mod and go.sum first for layer caching
COPY go.mod go.sum ./
# Copy vendor directory to ensure dependencies are available
COPY vendor ./vendor
# Copy the rest of the source code
COPY . .
# Build the binary using vendor directory
RUN go build -mod=vendor -o /bin/app ./cmd/app
# Runtime stage contains only the binary
FROM alpine:3.19
# Copy binary from builder stage
COPY --from=builder /bin/app /app
# Run the application
CMD ["/app"]
The Dockerfile copies vendor into the builder stage. This ensures the build has all dependencies. The -mod=vendor flag in the RUN command forces the build to use them. The final image contains only the binary. No Go toolchain, no source code, no vendor directory. This pattern is robust and secure.
If you choose to cache instead of commit, you can skip copying vendor and run go mod download in the Dockerfile. This reduces the image size during build but requires network access.
Commit the vendor directory to guarantee reproducibility. Cache dependencies to save space. Choose based on your risk tolerance.
Pitfalls and errors
Vendoring introduces a few pitfalls. The most common is an out-of-sync vendor directory. If you update go.mod but forget to run go mod vendor, the build fails with vendor/modules.txt is out of date. The fix is simple: run go mod vendor again.
Another error is build requires ... which is not in vendor. This happens when you import a new package but haven't vendored it yet. Run go mod tidy to add the dependency to go.mod, then run go mod vendor to copy it.
You cannot selectively exclude a package from vendor if your code imports it. The tool vendors everything required. If you want to avoid vendoring a dependency, remove the import. The exclude directive in go.mod blocks specific versions of a module, it does not skip packages. Some teams use go mod tidy to remove unused dependencies before vendoring, which keeps the directory lean.
Test dependencies are included in vendor if they are required by the main module's tests. If you run go test with -mod=vendor, Go uses the vendored test dependencies. This is usually what you want. If you need to exclude test dependencies, you can't do it easily with vendoring. The tool treats test imports as part of the dependency graph.
Vendoring does not replace go.sum. You still need go.sum to verify the integrity of the modules. go mod vendor uses go.sum to check hashes before copying. If a hash mismatches, the command fails. This prevents corrupted or tampered code from entering your vendor directory. Always commit go.sum alongside vendor.
Trust the tool. Run go mod vendor. Don't touch vendor.
Decision matrix
Use go mod vendor when you need reproducible builds that work offline or behind a firewall.
Use go mod vendor when your CI environment has unreliable network access to the module proxy.
Use go mod vendor when you want to commit dependencies to the repository to guarantee exact versions across all environments.
Use go mod vendor when you are auditing dependencies and need a local copy of the source code for review.
Use the module proxy without vendoring when repository size is a constraint and your CI has stable network access.
Use go mod download with a local cache when you want to speed up CI builds without increasing repository size.
Vendoring trades disk space for certainty. Pick what your team needs.