Introduction

When using Go's embed package to bundle configuration files into the binary, Viper's default file discovery mechanism cannot find embedded files because it reads from the operating system's file system, not from the embedded embed.FS. The viper.AddConfigPath() and viper.SetConfigFile() methods only work with real file paths. This causes Config File "config" Not Found in "[/app /etc/myapp]" errors even though the config file is embedded in the binary and should be available at runtime.

Symptoms

bash
2024/03/15 10:23:45 Config File "config" Not Found in "[/app /etc/myapp .]"
panic: Config File "config" Not Found in "[/app /etc/myapp .]"

Or:

bash
Fatal error config file: Config File "application" Not Found in "[/app /etc/myapp]"

The binary contains the config file (verified with strings myapp | grep database_url) but Viper cannot find it.

Common Causes

  • Viper reads from OS filesystem, not embed.FS: Viper's ReadInConfig uses os.Open, not fs.Open
  • ConfigPath not set correctly: AddConfigPath points to directories that do not exist in the container
  • Working directory differs from binary location: The binary runs from /app but config is embedded relative to source
  • Multiple config file formats: Viper tries .yaml, .yml, .json, .toml in order and fails on all
  • Build does not include embed directive: Missing //go:embed comment or wrong file pattern

Step-by-Step Fix

Step 1: Read embedded config into Viper manually

```go package main

import ( "embed" "fmt"

"github.com/spf13/viper" )

//go:embed config/*.yaml var configFS embed.FS

func initConfig() error { v := viper.New() v.SetConfigType("yaml")

// Read from embedded FS file, err := configFS.Open("config/default.yaml") if err != nil { return fmt.Errorf("open embedded config: %w", err) } defer file.Close()

if err := v.ReadConfig(file); err != nil { return fmt.Errorf("read embedded config: %w", err) }

// Try to override with external config if it exists v.SetConfigName("application") v.SetConfigType("yaml") v.AddConfigPath("/etc/myapp") v.AddConfigPath(".")

// This will not fail if external config does not exist _ = v.MergeInConfig()

return nil } ```

Step 2: Use environment variable overrides

```go func initConfig() error { v := viper.New() v.SetConfigType("yaml") v.AutomaticEnv() v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))

// Load embedded defaults file, err := configFS.Open("config/default.yaml") if err != nil { return err } defer file.Close()

if err := v.ReadConfig(file); err != nil { return err }

// Environment variables override embedded config // DB_HOST=mydb.example.com overrides database.host in config return nil } ```

Step 3: Embed multiple environment configs

```go //go:embed config/*.yaml var configFS embed.FS

func initConfig(env string) error { v := viper.New() v.SetConfigType("yaml")

// Always load defaults defaults, err := configFS.Open("config/default.yaml") if err != nil { return err } defer defaults.Close()

if err := v.ReadConfig(defaults); err != nil { return err }

// Override with environment-specific config envFile := fmt.Sprintf("config/%s.yaml", env) if envConfig, err := configFS.Open(envFile); err == nil { defer envConfig.Close() if err := v.MergeConfig(envConfig); err != nil { log.Printf("WARNING: could not merge %s config: %v", env, err) } }

return nil }

// Usage initConfig(os.Getenv("APP_ENV")) // "development", "staging", "production" ```

Prevention

  • Always use v.ReadConfig() with an embed.FS file, not v.ReadInConfig()
  • Set v.SetConfigType("yaml") explicitly when reading from a reader
  • Use v.MergeInConfig() for optional external overrides that do not cause errors if missing
  • Embed a minimal default.yaml with sensible defaults and allow environment variables to override
  • Add a startup check that logs all loaded configuration keys for debugging
  • Test the binary without external config files to verify embedded config works