This document is the reference for the static-web command-line interface. It covers the command structure, flag design, installation methods, configuration priority, and implementation notes. It is the authoritative reference for usage, flags, and build integration.
- Goals
- Non-Goals
- Binary Name
- Command Structure
- Subcommands
- Configuration Priority
- Flag Reference
- Usage Examples
- Installation Methods
- Build-Time Version Injection
- Implementation Notes
- Decisions & Rationale
- Zero-friction start.
static-web ./distshould be all a developer needs to type to serve a directory. - No new runtime dependencies. Implement with Go stdlib
flagpackage only — no cobra, urfave/cli, or any external CLI framework. - Flags for the common case; config file for everything else. CLI flags cover the ~10 settings changed most often. The full config surface remains accessible via
config.tomland environment variables. - Installable as a global tool.
go install, Homebrew, pre-built binaries, and a curl one-liner all work. - Consistent with Unix conventions. Flags use
--long-formstyle. Exit 0 on success, non-zero on error.--helpprints to stdout. Errors print to stderr.
- Shell completion scripts (can be added later, not in scope for v1).
- A
start/stop/statusdaemon manager — that is the OS's job (systemd, launchd). - Interactive prompts or wizards.
- A web UI or admin API.
static-web
- Matches the repository name.
- Descriptive and unambiguous.
- Hyphenated names tab-complete correctly on all major shells.
- Avoids collision with system binaries (
httpd,nginx,caddyare already taken).
static-web [command] [flags] [directory]
When no subcommand is given, serve is assumed. This means the most common usage is just:
static-web ./dist
static-web --port 3000 .
The three subcommands are:
| Subcommand | Purpose |
|---|---|
serve |
Start the file server (default when omitted) |
init |
Scaffold a config.toml in the current directory |
version |
Print version, Go version, OS/arch and exit |
Start the static file server.
static-web serve [flags] [directory]
static-web [flags] [directory] # shorthand — serve is the default
Positional argument:
directory — the directory to serve. Defaults to ./public if omitted. Overrides files.root from the config file and the STATIC_FILES_ROOT env var.
static-web # serves ./public on :8080
static-web ./dist # serves ./dist on :8080
static-web --port 3000 ./dist # serves ./dist on :3000Write a config.toml file to the current directory (or to --output), populated with all options and their defaults, ready to edit.
static-web init [--output path]
Flags:
| Flag | Default | Description |
|---|---|---|
--output |
./config.toml |
Path to write the config file |
--force |
false |
Overwrite if file already exists |
Behaviour:
- Writes the same content as
config.toml.example, including all comments. - If the file already exists and
--forceis not set, prints an error and exits 1. - Prints the absolute path of the written file on success.
static-web init # writes ./config.toml
static-web init --output /etc/static-web/config.toml
static-web init --force # overwrite existingPrint version information and exit 0.
static-web version
Output format:
static-web v1.2.3
go: go1.23.4
os: darwin/arm64
commit: a1b2c3d4
Version, commit, and build date are injected at build time via -ldflags (see Build-Time Version Injection). When not injected (e.g. go run), values fall back to dev.
Settings are resolved in this order, highest priority first:
1. CLI flags (--port, --host, --tls-cert, ...)
2. Environment vars (STATIC_SERVER_ADDR, STATIC_FILES_ROOT, ...)
3. Config file (config.toml, or path from --config)
4. Built-in defaults (:8080, ./public, cache=true, ...)
This is the standard Unix/12-factor convention. A flag always wins, even over an env var set in the same shell. The config file is optional — if it does not exist the server starts with defaults.
| Flag | Type | Description |
|---|---|---|
--config |
string | Path to TOML config file (default: ./config.toml) |
--help, -h |
bool | Print help and exit |
Grouped by concern for readability. All flags are optional; unset flags do not override the config file.
| Flag | Type | Default | Config field |
|---|---|---|---|
--host |
string | `` (all interfaces) | server.addr (host part) |
--port, -p |
int | 8080 |
server.addr (port part) |
--redirect-host |
string | — | server.redirect_host |
--tls-cert |
string | — | server.tls_cert |
--tls-key |
string | — | server.tls_key |
--tls-port |
int | 8443 |
server.tls_addr (port part) |
--hostand--portare combined intoserver.addras<host>:<port>. Specifying--hostalone without--portuses the default port (8080), and vice versa.
| Flag | Type | Default | Config field |
|---|---|---|---|
--index |
string | index.html |
files.index |
--404 |
string | — | files.not_found |
| Flag | Type | Default | Config field |
|---|---|---|---|
--no-cache |
bool | false |
cache.enabled = false |
--cache-size |
string | 256MB |
cache.max_bytes (parses 256MB, 64MB, 1GB) |
--preload |
bool | false |
cache.preload — load all files into cache at startup |
--gc-percent |
int | 0 |
cache.gc_percent — Go GC target % (0 = default; try 400 for throughput) |
| Flag | Type | Default | Config field |
|---|---|---|---|
--no-compress |
bool | false |
compression.enabled = false |
| Flag | Type | Default | Config field |
|---|---|---|---|
--cors |
string | — | security.cors_origins (comma-separated, or *) |
--dir-listing |
bool | false |
security.directory_listing |
--no-dotfile-block |
bool | false |
security.block_dotfiles = false |
--csp |
string | — | security.csp |
| Flag | Type | Default | Description |
|---|---|---|---|
--quiet, -q |
bool | false |
Suppress per-request access log lines |
--verbose |
bool | false |
Log config values on startup |
# Serve current directory on port 8080
static-web .
# Serve a build output directory on a custom port
static-web --port 3000 ./dist
# Enable directory listing (e.g. for a local file share)
static-web --dir-listing --no-dotfile-block ~/Downloads
# Serve with TLS (HTTPS on :443, HTTP redirect on :80)
static-web --port 80 --tls-port 443 \
--redirect-host static.example.com \
--tls-cert /etc/ssl/cert.pem \
--tls-key /etc/ssl/key.pem \
./public
# Open CORS (for a public API or CDN-served assets)
static-web --cors '*' ./dist
# CORS for specific origins
static-web --cors 'https://app.example.com,https://staging.example.com' ./dist
# Use a config file for all settings
static-web --config /etc/static-web/config.toml
# Scaffold a config file, then edit and run
static-web init
$EDITOR config.toml
static-web
# Disable caching (useful during local development to see file changes immediately)
static-web --no-cache ./dist
# Maximum throughput: preload all files + tune GC
static-web --preload --gc-percent 400 ./dist
# Print version info
static-web versiongo install github.com/BackendStack21/static-web/cmd/static-web@latestRequires Go 1.26+. Installs to $(go env GOPATH)/bin/static-web. Add $(go env GOPATH)/bin to your PATH if not already there.
brew install BackendStack21/tap/static-webOr with the full tap URL:
brew tap BackendStack21/tap https://github.com/BackendStack21/homebrew-tap
brew install static-webAuto-updates with brew upgrade.
Download a binary for your platform from the GitHub Releases page. Binaries are published for:
| Platform | File |
|---|---|
| macOS (Apple Silicon) | static-web_darwin_arm64.tar.gz |
| macOS (Intel) | static-web_darwin_amd64.tar.gz |
| Linux (x86-64) | static-web_linux_amd64.tar.gz |
| Linux (ARM64) | static-web_linux_arm64.tar.gz |
| Windows (x86-64) | static-web_windows_amd64.zip |
Quick install on Linux/macOS:
# Replace X.Y.Z with the desired version, and PLATFORM/ARCH with your system
curl -fsSL https://github.com/BackendStack21/static-web/releases/download/vX.Y.Z/static-web_linux_amd64.tar.gz \
| tar -xz -C /usr/local/bin static-web
chmod +x /usr/local/bin/static-webcurl -fsSL https://static-web.dev/install.sh | shThe script:
- Detects OS and architecture.
- Downloads the latest release binary from GitHub.
- Installs to
/usr/local/bin(or~/.local/binif/usr/local/binis not writable). - Verifies the SHA256 checksum before installing.
- Prints the installed version on success.
To install a specific version:
curl -fsSL https://static-web.dev/install.sh | sh -s -- --version v1.2.3docker run --rm -p 8080:8080 -v "$(pwd)/dist:/public:ro" ghcr.io/BackendStack21/static-web:latestSee USER_GUIDE.md for full Docker and docker-compose examples.
Version information is injected at link time via -ldflags. The variables live in a new internal/version package:
// internal/version/version.go
package version
var (
Version = "dev"
Commit = "none"
Date = "unknown"
)Build command:
go build \
-ldflags="-X github.com/BackendStack21/static-web/internal/version.Version=v1.2.3 \
-X github.com/BackendStack21/static-web/internal/version.Commit=$(git rev-parse --short HEAD) \
-X github.com/BackendStack21/static-web/internal/version.Date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-o bin/static-web ./cmd/static-webThis is added to the Makefile as the release target and used by GoReleaser in CI.
The CLI was implemented using Go stdlib flag.FlagSet — no external framework. Key implementation details:
- Subcommand dispatch:
os.Args[1]switch inmain(). Unknown first arguments (flags, paths) fall through to the implicitservesubcommand. - Flag isolation: each subcommand owns its own
flag.FlagSetwithflag.ContinueOnError, so flags don't bleed between subcommands. - Config layering:
config.Load()handles defaults + TOML file + env vars.applyFlagOverrides()inmain.goapplies CLI flags on top as the final layer. --host+--portmerging:net.SplitHostPort/net.JoinHostPortused to decompose and reconstructserver.addr.parseBytes(): a small helper that parses256MB,1GB, etc. withB/KB/MB/GBsuffixes (case-insensitive).//go:embed config.toml.example: the example config is embedded incmd/static-web/at compile time. The binary is fully self-contained.--quiet: skips access-log middleware entirely, removing per-request logging overhead.--verbose: callslogConfig(cfg)after all overrides are applied, so you see the final resolved values.- Version injection:
internal/version.Version,Commit,Dateare set via-ldflagsat build time. Default to"dev","none","unknown"forgo run.
cobra and urfave/cli are excellent libraries, but they add a dependency, and this project's explicit design constraint is "stdlib-first, minimal external deps". The subcommand surface is small (3 commands), and flag.FlagSet handles per-subcommand flags cleanly without any framework.
static-web ./dist is significantly more ergonomic than static-web serve ./dist for the majority use case. The serve subcommand is still explicit when needed. This pattern is used by tools like python -m http.server, npx serve, and caddy file-server.
--port 3000 is what every developer reaches for. --addr :3000 is correct but unusual for a CLI tool. We accept --host + --port separately and combine them, which is more intuitive even if slightly more code.
Following the precedent of python -m http.server, caddy file-server --root, npx serve, and ruby -run -e httpd. The directory is the most commonly varied parameter — making it positional reduces typing.
Boolean flags that default to true are awkward as positional booleans in CLI (--cache=false is ugly). The --no-* pattern (used by npm, git, curl) is idiomatic and readable: "I want no caching."
--cache-size 128MB is more readable than --cache-size 134217728. The parser handles B, KB, MB, GB (and lowercase variants). Invalid values print an error and exit 1.
Embedding the example config at compile time means the binary is fully self-contained. Running static-web init works correctly regardless of the current working directory, even if installed as a global binary to /usr/local/bin.
Standard Go practice. Avoids a generated file that would pollute diffs. The internal/version package has zero dependencies and is importable by any future tooling.