Make Hugo Module Builds Reproducible with hugo mod vendor
A build that works today and fails next week because a module's upstream cache changed is exactly the problem hugo mod vendor solves. It copies Hugo's module dependencies out of its cache and into a project-local _vendor directory, giving the project a visible, reviewable copy to build from. This guide uses Hugo 0.123.7, the version installed on this machine.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about 10 minutes for a small site, longer if dependencies still need downloading. You need a Hugo module project, write access to its directory, and a clean enough working tree to review generated files. Root privileges are not normally needed: don't use sudo for a project in your home directory, it just leaves the new files owned by root.
Before you start
Run these checks from the directory containing the site's go.mod file. If it's elsewhere, change to the module root first.
$ pwd
$ test -f go.mod && echo "module root: yes"
$ hugo version
hugo v0.123.7+extended linux/amd64 BuildDate=2026-03-17T19:51:14Z VendorInfo=ubuntu:0.123.7-1ubuntu0.3+esm2
This is a subcommand of Hugo, not a separate executable. Check its locally installed options before borrowing a command from a different Hugo release:
$ hugo mod vendor --help
Vendor all module dependencies into the _vendor directory.
The installed interface accepts --baseURL, --cacheDir, --contentDir and --theme, plus the inherited configuration and source options. Upstream documentation may list more options in newer releases, so treat this machine's help output as the contract for this installation.
1. Inspect what Hugo thinks the project imports
Before changing the tree, look at the module graph. This separates a missing dependency from a vendoring problem, and gives you something to compare against afterwards.
$ hugo mod graph
$ git status --short
An empty graph can be entirely correct for a module with no imported dependencies. If the command reports a malformed module file, an unavailable version, or a replacement path that doesn't exist, fix that project configuration first: vendoring cannot repair an invalid dependency graph.
2. Record the starting state
Vendoring writes files, so capture the starting state before running it. This also makes it easy to tell generated changes apart from work already in progress.
$ git status --short
$ test ! -e _vendor || find _vendor -maxdepth 2 -type f -print | sort | head -40
If _vendor already contains hand-edited files, stop and preserve them before continuing. Hugo's documentation recommends overriding vendored content from the project root rather than editing the copy inside _vendor directly. A normal source-control commit is the safest record; a separate copy outside the project works when you can't commit yet.
3. Vendor the dependencies
Run the command from the module tree. The ordinary form needs no flags:
$ hugo mod vendor
A successful run can produce no terminal output at all, which is not a failure. The useful result is the _vendor directory itself and its copied module files. Confirm both the directory and the source-control change:
$ test -d _vendor && echo "vendor directory: present"
$ find _vendor -maxdepth 3 -type f -print | sort | sed -n '1,40p'
$ git status --short
Don't assume every directory under themes will show up in _vendor. Hugo's module documentation says modules inside the themes directory are not vendored. If a theme seems to be missing, check the graph and the project mounts rather than copying files into an invented path.
4. Build using the vendored tree
Run the same build command your project already uses and check the result. Hugo looks in _vendor for dependencies whenever a module is vendored.
$ hugo --destination /tmp/example-hugo-public
$ test -f /tmp/example-hugo-public/index.html && echo "build: present"
That destination is a disposable example path; use your real build destination in a deployment script. --destination is inherited by hugo mod vendor, but it controls where Hugo writes rendered output, not where _vendor lives. Keep those two concerns separate in your head.
5. Use extra options only when needed
Most projects should stick to the default command. If a project keeps its configuration somewhere non-default, pass the inherited configuration option explicitly:
$ hugo mod vendor --config /path/to/site/config.yaml
If several configured themes need selecting for the operation, the command also exposes a repeatable --theme option. Use the theme names exactly as they exist below the configured themes directory:
$ hugo mod vendor --theme example-theme
Those paths and names are placeholders: don't paste them unchanged. An incorrect configuration or theme name can make a perfectly valid module look like it's missing.
Recovery and safe removal
Vendoring is reversible only if you haven't made unique edits inside _vendor. Review the diff before deleting anything:
$ git diff --stat
$ git diff -- _vendor go.mod go.sum
Recovery
If the generated directory is wrong, restore it through your normal source-control operation, or move it aside after checking for local edits. Once removed, rerun hugo mod vendor to recreate it. Never remove a shared or system directory, and don't run a broad recursive deletion from a working directory you're not certain about.
Done means
- Version and help match.
hugo versionidentifies the installed release andhugo mod vendor --helpmatches the command you used. - Graph resolves cleanly. The module graph resolves without errors.
- Vendor directory present.
_vendorexists and contains the expected dependency files. - Build succeeds. A normal Hugo build completes while using the vendored dependencies.
- Diff is clean. The source-control diff contains only changes you understand and intend to keep.