← Back to davecheney/viper

How to Deploy & Use davecheney/viper

# 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 etcd3 provider string
  • Verify network policies allow egress to config store

Precedence Confusion

Viper merges sources in this priority order (highest to lowest):

  1. viper.Set() explicit calls
  2. Command line flags (if using with Cobra)
  3. Environment variables
  4. Config files
  5. External key/value stores (etcd/Consul)
  6. 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()
    }
}()