# Configuring Database Connection and Environment Variables in Go: A Clean Architecture Approach

> Learn to configure Go database connections and environment variables using Viper for seamless configuration management. Centralize your settings with a clean architecture approach.

- Repository: [manato/golang-clean-architecture](https://github.com/manakuro/golang-clean-architecture)
- Tags: how-to-guide
- Published: 2026-03-06

---

**Use Viper to load YAML configuration and automatically overlay environment variables, then construct a GORM MySQL connection using a centralized config struct in [`pkg/config/config.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/config/config.go).**

The `manakuro/golang-clean-architecture` repository demonstrates how to isolate infrastructure concerns from business logic when configuring database connection and environment variables in Go. By combining Viper for configuration management with a global config struct, the codebase ensures that database credentials and environment-specific settings remain secure, flexible, and accessible throughout the application lifecycle.

## Centralizing Configuration with Viper

The repository places all configuration logic in [`pkg/config/config.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/config/config.go), which defines a struct that mirrors the YAML structure in [`pkg/config/config.yml`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/config/config.yml). This approach creates a single source of truth for application settings.

### The Config Struct and YAML Structure

The `config` struct in [`pkg/config/config.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/config/config.go) organizes settings into logical groups such as database and server parameters. The `ReadConfig` function unmarshals the YAML file into this struct and assigns it to the global variable `C`, making configuration available across packages without manual dependency injection.

```go
// pkg/config/config.go
type config struct {
    Database struct {
        User                 string
        Password             string
        Net                  string
        Addr                 string
        DBName               string
        AllowNativePasswords bool
        Params               struct {
            ParseTime string
        }
    }
    Server struct {
        Address string
    }
}

var C config

func ReadConfig() {
    viper.SetConfigName("config")
    viper.SetConfigType("yml")
    viper.AddConfigPath(filepath.Join(rootDir(), "config"))
    viper.AutomaticEnv()
    
    if err := viper.ReadInConfig(); err != nil {
        log.Fatal(err)
    }
    
    if err := viper.Unmarshal(&C); err != nil {
        log.Fatal(err)
    }
    
    spew.Dump(C) // Debug output; remove in production
}

```

### Loading Environment Variables Automatically

Viper's `AutomaticEnv` method enables seamless environment variable integration. When `viper.AutomaticEnv()` is called, Viper checks for environment variables that match struct fields and overrides YAML values accordingly. For example, setting `export DATABASE_PASSWORD=supersecret` in your shell automatically replaces the password defined in [`config.yml`](https://github.com/manakuro/golang-clean-architecture/blob/main/config.yml) before the database connection initializes.

This mechanism keeps sensitive credentials out of version control while allowing developers to override any configuration value at runtime using Docker, CI pipelines, or local shell environments.

## Building the Database Connection

The database infrastructure layer in [`pkg/infrastructure/datastore/db.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/infrastructure/datastore/db.go) consumes the global configuration to construct a GORM-compatible MySQL connection. This separation ensures that database driver details remain isolated from domain logic.

### Constructing the MySQL DSN

The `NewDB` function builds a MySQL Data Source Name (DSN) using parameters from `config.C.Database`. It leverages the `go-sql-driver/mysql` package's `Config` struct to format connection parameters correctly, including network type, address, credentials, and connection options like `parseTime=true`.

```go
// pkg/infrastructure/datastore/db.go
package datastore

import (
    "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
    "github.com/manakuro/golang-clean-architecture/pkg/config"
)

func NewDB() *gorm.DB {
    mySqlConfig := &mysql.Config{
        User:                 config.C.Database.User,
        Passwd:               config.C.Database.Password,
        Net:                  config.C.Database.Net,
        Addr:                 config.C.Database.Addr,
        DBName:               config.C.Database.DBName,
        AllowNativePasswords: config.C.Database.AllowNativePasswords,
        Params: map[string]string{
            "parseTime": config.C.Database.Params.ParseTime,
        },
    }
    
    db, err := gorm.Open("mysql", mySqlConfig.FormatDSN())
    if err != nil {
        panic(err)
    }
    
    return db
}

```

### Initializing GORM

The `gorm.Open` call receives the formatted DSN string and establishes the connection pool. Because `NewDB` depends on `config.C` already being populated, it must be called after `config.ReadConfig()` executes in the application entry point. The resulting `*gorm.DB` instance is then passed to repositories or used directly in infrastructure layers.

## Bootstrapping the Application

Both the HTTP server and database seeding commands follow identical initialization patterns, ensuring consistent configuration across all entry points.

### Entry Point Implementation

The `main` functions in [`cmd/app/main.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/cmd/app/main.go) and [`cmd/seed/main.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/cmd/seed/main.go) demonstrate the required bootstrap sequence. First, they invoke `config.ReadConfig()` to load YAML and environment variables. Then they initialize the database connection using `datastore.NewDB()`.

```go
// cmd/app/main.go
package main

import (
    "log"
    
    "github.com/manakuro/golang-clean-architecture/pkg/config"
    "github.com/manakuro/golang-clean-architecture/pkg/infrastructure/datastore"
)

func main() {
    // Load configuration from YAML and environment variables
    config.ReadConfig()
    
    // Initialize database connection using loaded config
    db := datastore.NewDB()
    defer db.Close()
    
    // Continue with server initialization...
    log.Println("Database connection established")
}

```

This pattern guarantees that `config.C` contains the final, environment-specific values before any infrastructure code attempts to use it.

### Docker Compose Integration

The repository includes a [`docker/docker-compose.yml`](https://github.com/manakuro/golang-clean-architecture/blob/main/docker/docker-compose.yml) file that defines MySQL service credentials. These values should align with the defaults in [`pkg/config/config.yml`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/config/config.yml) for local development, while production deployments override them via environment variables.

```yaml

# docker/docker-compose.yml

version: '3.8'
services:
  mysql:
    image: mysql:8.0.23
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: golang_clean_architecture
    ports:
      - "3306:3306"

```

When running the Go application locally against this Docker container, the default YAML configuration connects successfully. For production, inject the actual database password via `DATABASE_PASSWORD` environment variable without modifying any source files.

## Summary

- **Centralized configuration** lives in [`pkg/config/config.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/config/config.go), where Viper loads [`pkg/config/config.yml`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/config/config.yml) and automatically overlays matching environment variables.
- **Environment variable precedence** is handled by `viper.AutomaticEnv()`, allowing runtime overrides of YAML defaults for secrets and deployment-specific settings.
- **Database connection construction** in [`pkg/infrastructure/datastore/db.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/infrastructure/datastore/db.go) consumes the global `config.C` struct to build a MySQL DSN for GORM.
- **Consistent bootstrapping** across [`cmd/app/main.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/cmd/app/main.go) and [`cmd/seed/main.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/cmd/seed/main.go) ensures configuration loads before infrastructure initializes.

## Frequently Asked Questions

### How does Viper handle environment variable precedence in this architecture?

Viper's `AutomaticEnv()` method checks for environment variables after loading the YAML file but before unmarshaling into the struct. If an environment variable exists that matches a configuration key (e.g., `DATABASE_PASSWORD` maps to `database.password`), Viper uses the environment value, overriding the YAML default. This precedence ensures that sensitive values injected at runtime take priority over version-controlled defaults.

### Where should database credentials be stored in production?

Database credentials should never be stored in [`pkg/config/config.yml`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/config/config.yml) or any version-controlled file in production. Instead, export them as environment variables on the host system or orchestration platform (Kubernetes secrets, AWS ECS task definitions, or Docker Compose secrets). The application reads these via Viper's environment variable integration, keeping credentials out of source code while maintaining configuration flexibility.

### How do I add a new configuration field to the application?

First, add the field to the `config` struct in [`pkg/config/config.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/config/config.go) within the appropriate nested struct (e.g., `Database` or `Server`). Then, add the corresponding default value to [`pkg/config/config.yml`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/config/config.yml). If the field might need environment-specific overrides, ensure the field name follows the pattern that Viper's `AutomaticEnv()` can match (e.g., `NEW_FIELD_NAME` for a field `NewFieldName` or `new.field_name` depending on your struct tags and Viper settings).

### Can I use a .env file instead of exporting environment variables directly?

Yes, though the repository does not include this by default. To add `.env` file support, modify [`pkg/config/config.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/config/config.go) to include `viper.SetConfigFile(".env")` and `viper.ReadInConfig()` before calling `viper.AutomaticEnv()`. This allows Viper to load variables from a `.env` file in addition to system environment variables, which is useful for local development workflows where exporting variables in every shell session is inconvenient.