# Viper Deployment & Usage Guide
Complete configuration solution for Go applications supporting 12-Factor app principles.
## Prerequisites
- **Go**: Version 1.23 or higher
- **Go Modules**: Project must be initialized with `go mod init`
- **(Optional) Remote Configuration**: Running etcd, etcd3, Consul, Firestore, or NATS instance for distributed config
- **(Optional) Encryption**: OpenPGP keyring file for encrypted remote configuration
## Installation
Add Viper to your Go project:
```shell
go get github.com/spf13/viper
For remote configuration support (etcd, Consul, etc.), install the remote subpackage:
go get github.com/spf13/viper/remote
Import in your Go code:
import "github.com/spf13/viper"
// For remote config:
import _ "github.com/spf13/viper/remote"
Configuration
Basic File Configuration
Configure Viper to search multiple paths and formats:
viper.SetConfigName("config") // Name without extension
viper.SetConfigType("yaml") // Explicit type (optional if extension exists)
viper.AddConfigPath("/etc/appname/") // System config
viper.AddConfigPath("$HOME/.appname") // User config
viper.AddConfigPath(".") // Working directory
if err := viper.ReadInConfig(); err != nil {
var fileLookupError viper.FileLookupError
if errors.As(err, &fileLookupError) {
log.Fatal("Config file not found:", err)
} else {
log.Fatal("Config file invalid:", err)
}
}
Environment Variables
viper.SetEnvPrefix("MYAPP") // Prefix for env vars (e.g., MYAPP_PORT)
viper.AutomaticEnv() // Bind all env vars automatically
viper.BindEnv("port", "SERVER_PORT") // Explicit binding with alias
Defaults
viper.SetDefault("port", 8080)
viper.SetDefault("database.host", "localhost")
Remote Configuration (etcd/Consul)
// Requires: import _ "github.com/spf13/viper/remote"
viper.AddRemoteProvider("etcd", "http://127.0.0.1:4001", "/config/app")
viper.SetConfigType("json")
err := viper.ReadRemoteConfig()
if err != nil {
log.Fatal("Failed to read remote config:", err)
}
Supported Config Formats
Viper automatically detects formats by extension or explicit type:
- JSON
- TOML
- YAML/YML
- INI
- envfile (dotenv)
- Java Properties
Build & Run
Development Mode
Enable live configuration reloading:
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
slog.Info("Config file changed:", "file", e.Name)
// Reload dependent services here
})
Run your application:
go run main.go
Production Mode
Environment variable override (12-Factor approach):
export MYAPP_PORT=443
export MYAPP_DATABASE_HOST=prod-db.example.com
./myapp
Writing Configuration at Runtime
// Save current config state (creates or overwrites)
viper.WriteConfig()
// Safe write - errors if file exists
viper.SafeWriteConfig()
// Write to specific path
viper.WriteConfigAs("/etc/appname/config.yaml")
Accessing Values
port := viper.GetInt("port")
host := viper.GetString("database.host")
// Unmarshal into struct
type Config struct {
Port int
Database struct {
Host string
}
}
var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
log.Fatal(err)
}
Deployment
Container Deployment (Docker)
Option A: Config file in image
COPY config.yaml /etc/appname/
ENV MYAPP_CONFIG_PATH=/etc/appname
Option B: Environment variables only (Recommended)
# No config file copied - rely on env vars
ENV MYAPP_PORT=8080
ENV MYAPP_LOG_LEVEL=info
Option C: Config via volume mount
# docker-compose.yml
volumes:
- ./config:/etc/appname:ro
Kubernetes Deployment
Using ConfigMap for configuration:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
config.yaml: |
port: 8080
log_level: info
---
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
containers:
- name: app
volumeMounts:
- name: config
mountPath: /etc/appname
volumes:
- name: config
configMap:
name: app-config
Using Secrets for sensitive data:
env:
- name: MYAPP_DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
Remote Configuration in Production
For distributed systems, use centralized configuration:
// etcd example
viper.AddRemoteProvider("etcd", "https://etcd-cluster:2379", "/config/production")
viper.SetConfigType("yaml")
// With encryption
viper.AddSecureRemoteProvider("etcd", "https://etcd-cluster:2379",
"/config/production", "/path/to/keyring.pgp")
Enable watching for dynamic updates without pod restarts:
go func() {
for {
time.Sleep(5 * time.Second)
err := viper.WatchRemoteConfig()
if err != nil {
slog.Error("Failed to watch remote config", "error", err)
continue
}
// Update application state
}
}()
Troubleshooting
Config File Not Found
Error: ConfigFileNotFoundError or file lookup errors
Solutions:
- Verify search paths: Check
viper.AddConfigPath()includes the correct directory - Check permissions: Ensure process has read access to config directory
- Extension handling: If file has no extension, use
viper.SetConfigType("yaml")explicitly
// Debug search paths
slog.Info("Searching config in", "paths", viper.ConfigPathsUsed())
Type Conversion Errors
Issue: GetInt(), GetString() return zero/empty values unexpectedly
Cause: Viper stores everything as interface{} and uses spf13/cast for conversion
Fix: Use explicit type checking or Unmarshal() into typed struct
// Instead of direct access
duration := viper.GetDuration("timeout")
// Validate required fields
if !viper.IsSet("database.host") {
log.Fatal("database.host is required")
}
Case Sensitivity
Behavior: Configuration keys are case-insensitive by default
Issue: database.host and DATABASE.HOST reference the same value
Fix: If you need case sensitivity (rare), access raw map directly or use custom decoder registry
Remote Configuration Failures
Error: UnsupportedRemoteProviderError or connection timeouts
Solutions:
- Verify provider is in supported list:
etcd,etcd3,consul,firestore,nats - Check endpoint format: Use semicolon-separated endpoints for clusters (
"http://node1:2379;http://node2:2379") - For etcd v3, explicitly use
etcd3provider string - Verify network policies allow egress to config store
Precedence Confusion
Viper merges sources in this priority order (highest to lowest):
viper.Set()explicit calls- Command line flags (if using with Cobra)
- Environment variables
- Config files
- External key/value stores (etcd/Consul)
- Defaults
Debug precedence:
// Print final merged config
allSettings := viper.AllSettings()
slog.Info("Final config", "settings", allSettings)
File Watching Issues
Issue: WatchConfig() doesn't detect changes on some platforms (NFS, Docker volumes)
Workaround: Use polling for remote filesystems or implement manual SIGHUP reload:
signal.Notify(c, syscall.SIGHUP)
go func() {
for range c {
viper.ReadInConfig()
}
}()