# Caddy Version Management: How Build-Time Injection and Module Metadata Determine Version Strings

> Learn how Caddy manages version strings using Go build info, VCS metadata, and custom overrides. Discover simple and full version details via caddy.Version().

- Repository: [Caddy/caddy](https://github.com/caddyserver/caddy)
- Tags: internals
- Published: 2026-03-03

---

**Caddy derives its version from Go module build information, embedded VCS metadata, and optional custom overrides set via `-ldflags`, exposing both a short "simple" string and a detailed "full" string through the `caddy.Version()` function.**

Caddy version management operates entirely at build time, generating immutable version strings that the binary reports throughout its lifecycle. According to the caddyserver/caddy source code, the `caddy.Version()` function in [`caddy.go`](https://github.com/caddyserver/caddy/blob/main/caddy.go) orchestrates this process by inspecting Go toolchain data, Git repository state, and user-supplied compiler flags.

## Build-Time Version Injection

Caddy's version is not hardcoded in the source repository. Instead, the Go compiler injects values during the build process, allowing flexible versioning for official releases, development builds, and custom forks.

### The CustomVersion Override Variable

The `CustomVersion` variable provides a mechanism to completely override Caddy's reported version. Defined at [line 946 of [`caddy.go`](https://github.com/caddyserver/caddy/blob/main/caddy.go)](https://github.com/caddyserver/caddy/blob/master/caddy.go#L946), this string supersedes all automatic detection when present.

```go
// CustomVersion is an optional string that overrides Caddy's
// reported version.
// Set this variable during `go build` with `-ldflags`:
//   -ldflags '-X github.com/caddyserver/caddy/v2.CustomVersion=v2.6.2'
var CustomVersion string

```

When `CustomVersion` is set, it overwrites both the simple and full version strings. If other metadata is available, `CustomVersion` is prefixed to the full string rather than replacing it entirely.

### Additional Customization Variables

While they do not affect version numbers, `CustomBinaryName` and `CustomLongDescription` (also set via `-ldflags` or init functions) allow distributors to customize the binary's name and help-text for embedded Caddy builds.

## The Version() Function Implementation

The core logic resides in the `Version()` function at [line 994 of [`caddy.go`](https://github.com/caddyserver/caddy/blob/main/caddy.go)](https://github.com/caddyserver/caddy/blob/master/caddy.go#L994). This function returns two strings: a **simple** version suitable for `User-Agent` headers and a **full** version containing checksums, VCS revisions, and build dates.

### Step 1: Go Module Build Information

The function first attempts to read build metadata populated by the Go toolchain:

```go
func Version() (simple, full string) {
    bi, ok := debug.ReadBuildInfo()
    if !ok {
        if CustomVersion != "" {
            full, simple = CustomVersion, CustomVersion
            return
        }
        full, simple = "unknown", "unknown"
        return
    }
    // ...
}

```

If `debug.ReadBuildInfo()` fails (rare in modern Go builds), the function immediately falls back to `CustomVersion` or the literal string `"unknown"`.

### Step 2: Module and VCS Resolution

When build info is available, the function searches for Caddy's own module entry using the `ImportPath` constant (defined at [line 1320 of [`caddy.go`](https://github.com/caddyserver/caddy/blob/main/caddy.go)](https://github.com/caddyserver/caddy/blob/master/caddy.go#L1320)):

```go
for _, dep := range bi.Deps {
    if dep.Path == ImportPath {
        module = dep
        break
    }
}

```

If found, the function extracts the module's `Version` and `Sum` (checksum). It also handles `replace` directives, which indicate local forks or custom paths:

```go
if module.Replace != nil {
    full += " => " + module.Replace.Path
    if module.Replace.Version != "" {
        simple = module.Replace.Version + "_custom"
        full += "@" + module.Replace.Version
    }
}

```

If module information is missing, the function falls back to VCS metadata embedded by Go 1.18+, parsing settings for `vcs.revision`, `vcs.time`, and `vcs.modified`:

```go
for _, setting := range bi.Settings {
    switch setting.Key {
    case "vcs.revision":
        vcsRevision = setting.Value
    case "vcs.time":
        vcsTime, _ = time.Parse(time.RFC3339, setting.Value)
    case "vcs.modified":
        vcsModified, _ = strconv.ParseBool(setting.Value)
    }
}

```

### Step 3: CustomVersion Application

Finally, the function applies any `CustomVersion` override as a prefix to the full string, or as the sole value if no other data exists. It also sanitizes the simple string to ensure it never returns empty or the placeholder `"(devel)"`:

```go
if full == "" {
    if CustomVersion != "" {
        full = CustomVersion
    } else {
        full = "unknown"
    }
} else if CustomVersion != "" {
    full = CustomVersion + " " + full
}

if simple == "" || simple == "(devel)" {
    if CustomVersion != "" {
        simple = CustomVersion
    } else {
        simple = "unknown"
    }
}

```

## How Caddy Uses Version Information

The version strings generated at build time propagate throughout Caddy's runtime for telemetry, debugging, and protocol compliance.

### ACME User-Agent Headers

When Caddy initializes, it constructs a `User-Agent` string for ACME (Automatic Certificate Management Environment) requests using the simple version. In [[`cmd/main.go`](https://github.com/caddyserver/caddy/blob/main/cmd/main.go)](https://github.com/caddyserver/caddy/blob/master/cmd/main.go#L49-L53), the init function trims the `v` prefix and registers the agent with `certmagic`:

```go
func init() {
    version, _ := caddy.Version()
    cleanModVersion := strings.TrimPrefix(version, "v")
    ua := "Caddy/" + cleanModVersion
    certmagic.UserAgent = ua
}

```

### CLI Version Command

Running `caddy version` invokes the Cobra command layer in [[`cmd/cobra.go`](https://github.com/caddyserver/caddy/blob/main/cmd/cobra.go)](https://github.com/caddyserver/caddy/blob/master/cmd/cobra.go#L128), which prints the full version string:

```go
_, f := caddy.Version()
fmt.Printf("caddy.Version=%s\n", f)

```

This output includes checksums, VCS data, and any custom prefixes, providing maximum diagnostic detail for bug reports.

## Practical Build Examples

### Overriding Version with ldflags

Distributors and custom builders can embed specific version strings during compilation:

```bash
go build -ldflags "-X github.com/caddyserver/caddy/v2.CustomVersion=v2.9.0-custom" -o mycaddy .

```

The resulting binary reports:

```bash
$ ./mycaddy version
caddy.Version=v2.9.0-custom

```

When module data is also available, the full string appears as:

```

v2.9.0-custom v2.9.0 a1b2c3d4 (2024-02-14)

```

### Handling Local Forks with Replace Directives

If you maintain a local fork, add a `replace` directive to your `go.mod`:

```go
replace github.com/caddyserver/caddy/v2 => ./my-fork

```

After building, the full version string reflects the replacement path:

```

v2.8.4 => ./my-fork@v2.8.4-custom abcdef1234

```

The simple version receives the `_custom` suffix to indicate the fork.

## Summary

- **Build-time injection** via `-ldflags` and the `CustomVersion` variable allows complete override of Caddy's reported version.
- **Automatic detection** prefers Go module metadata (`Version`, `Sum`) over VCS data (`vcs.revision`, `vcs.time`), but falls back gracefully when either is unavailable.
- **Dual string output** provides a **simple** version for protocols and headers, and a **full** version for debugging and compliance.
- **Replace directive support** ensures local forks and development builds receive accurate version annotations.

## Frequently Asked Questions

### How does Caddy determine its version when built from source?

When built from a Git checkout, Caddy uses Go 1.18+'s VCS metadata embedding to read `vcs.revision`, `vcs.time`, and `vcs.modified` settings. If the build occurs inside a Go module with full dependency information, it instead uses the module's `Version` and `Sum` fields from `debug.ReadBuildInfo()`.

### What is the difference between the simple and full version strings?

The **simple** string (first return value of `caddy.Version()`) is a short, space-free identifier suitable for `User-Agent` headers and log prefixes. The **full** string contains the module checksum, VCS revision, build date, and any `replace` directive paths, making it ideal for debugging and bug reports.

### How can I override Caddy's reported version during compilation?

Set the `CustomVersion` variable using `-ldflags` during `go build`: `-ldflags "-X github.com/caddyserver/caddy/v2.CustomVersion=your-version"`. This value overwrites the simple string and prefixes the full string when other metadata is present.

### Why does my Caddy binary show "(devel)" or "unknown"?

The string `"(devel)"` appears when Go module information is present but lacks a proper semantic version tag, typically occurring when building from a local directory without tagged releases. `"unknown"` appears only when `debug.ReadBuildInfo()` fails entirely and no `CustomVersion` was supplied, which is rare in standard Go toolchains.