Introduction

Go 1.16's embed package allows embedding configuration files directly into the binary, which is ideal for Docker deployments. However, when using Viper with embed.FS, common issues include the embedded path not matching Viper's expected path format (no leading / in embedded paths), Viper not detecting the config type from embedded content, and environment variable overrides not working as expected. The error Unsupported Config Type "" occurs when Viper cannot determine the file format from the embedded content.

Symptoms

bash
Unsupported Config Type ""
  File "/app/config.yaml" could not be read from embedded FS

Or:

bash
open /config/config.yaml: file does not exist
# Path mismatch between go:embed directive and Viper search path

Or:

bash
Viper did not load any config
# Silent failure - no config loaded, defaults used

Common Causes

  • Leading slash in embed path: go:embed /config.yaml fails -- must be config.yaml
  • Config type not detected: Embedded content does not have extension Viper can parse
  • Wrong FS path separator: Using OS path separators instead of forward slashes
  • Multiple config files in directory: Embedding entire directory but Viper reads wrong file
  • SetConfigType not called: Viper needs explicit type when reading from io.Reader
  • Environment variable override not working: Viper env prefix not configured

Step-by-Step Fix

Step 1: Correct embedded config setup

```go package config

import ( "embed" "fmt" "strings"

"github.com/spf13/viper" )

// IMPORTANT: No leading slash in embed path // //go:embed config.yaml var configFS embed.FS

func Load() (*viper.Viper, error) { v := viper.New()

// Read embedded file - path must match go:embed exactly data, err := configFS.ReadFile("config.yaml") if err != nil { return nil, fmt.Errorf("read embedded config: %w", err) }

// Viper needs explicit type when reading from bytes v.SetConfigType("yaml")

if err := v.ReadConfig(strings.NewReader(string(data))); err != nil { return nil, fmt.Errorf("parse config: %w", err) }

// Enable environment variable overrides v.SetEnvPrefix("APP") v.AutomaticEnv()

return v, nil } ```

Step 2: Fallback to file system for development

```go func LoadConfig() (*viper.Viper, error) { v := viper.New() v.SetConfigName("config") v.SetConfigType("yaml") v.AddConfigPath(".") v.AddConfigPath("./config") v.AddConfigPath("$HOME/.myapp")

// Try file system first (for development) if err := v.ReadInConfig(); err != nil { // Fall back to embedded (for production) if _, ok := err.(viper.ConfigFileNotFoundError); ok { data, err := configFS.ReadFile("config.yaml") if err != nil { return nil, fmt.Errorf("embedded config not found: %w", err) } v.SetConfigType("yaml") if err := v.ReadConfig(strings.NewReader(string(data))); err != nil { return nil, fmt.Errorf("parse embedded config: %w", err) } } else { return nil, fmt.Errorf("read config file: %w", err) } }

v.SetEnvPrefix("APP") v.AutomaticEnv() return v, nil } ```

Step 3: Embed entire directory correctly

```go // IMPORTANT: Embed directory contents // //go:embed all:configs var configsFS embed.FS

func LoadEnvConfig(env string) (*viper.Viper, error) { v := viper.New()

// Path uses forward slashes, no leading slash path := fmt.Sprintf("configs/%s.yaml", env) data, err := configsFS.ReadFile(path) if err != nil { return nil, fmt.Errorf("load config for %s: %w", env, err) }

v.SetConfigType("yaml") if err := v.ReadConfig(strings.NewReader(string(data))); err != nil { return nil, err }

return v, nil } ```

Prevention

  • Never use leading / in go:embed paths -- paths are relative to source file
  • Always call v.SetConfigType() when reading from io.Reader or bytes
  • Use configFS.ReadFile() with exact path matching the embedded file
  • Test embedded config loading in CI by building with go build and running binary
  • Use environment variable overrides for deployment-specific configuration
  • Add config validation after loading to catch missing required values early
  • Document the config file location and embedding strategy for new developers