How to Organize Initialization and System Config Files in Go Projects

Place system-level init scripts in the init/ directory and application configuration templates in the configs/ directory to maintain clean separation between deployment artifacts and runtime settings.

The golang-standards/project-layout repository establishes conventions for organizing Go projects at scale. When managing initialization and system configuration files in Go, following these directory standards ensures that operating system integrations remain decoupled from application source code while keeping default configurations version-controlled and accessible.

Understanding the Canonical Directory Structure

According to the project layout standard, two top-level directories serve distinct purposes for deployment-time artifacts. These locations are documented in init/README.md and configs/README.md respectively.

The init/ Directory

The init/ directory houses system-level initialization scripts and process manager configurations. This includes systemd unit files, upstart scripts, sysvinit scripts, and configurations for runit or supervisord. These files are consumed by the host operating system rather than the Go binary itself, which is why they reside outside the cmd/ and internal/ source trees. Placing these artifacts in init/ makes it straightforward for packaging scripts to locate and install them into system directories like /etc/systemd/system/ or /etc/supervisor/conf.d/.

The configs/ Directory

The configs/ directory stores default configuration file templates such as those used by confd or consul-template. Unlike the files in init/, these templates represent application-level data that the Go binary reads at runtime via libraries like Viper or standard library functions such as os.Getenv. Keeping templates in this dedicated folder simplifies version control of defaults and enables embedding directly into binaries using the //go:embed directive.

Practical Implementation Strategies

Embedding Default Configurations with go:embed

Since Go 1.16, you can embed configuration templates directly into your binary to ensure it ships with sane defaults. Store the template in configs/ and reference it from an internal package:

package config

import (
	"bytes"
	_ "embed"
	"text/template"

	"github.com/spf13/viper"
)

//go:embed ../configs/config.yaml.tmpl
var defaultYAML []byte

// Load reads configuration from a file path; if the file does not exist it falls back
// to the embedded template rendered with runtime values.
func Load(path string) (*viper.Viper, error) {
	v := viper.New()
	v.SetConfigFile(path)
	if err := v.ReadInConfig(); err == nil {
		return v, nil
	}
	t, err := template.New("default").Parse(string(defaultYAML))
	if err != nil {
		return nil, err
	}
	var buf bytes.Buffer
	err = t.Execute(&buf, map[string]string{
		"Port": "8080",
	})
	if err != nil {
		return nil, err
	}
	v.SetConfigType("yaml")
	if err := v.ReadConfig(&buf); err != nil {
		return nil, err
	}
	return v, nil
}

This approach guarantees the binary can start even when external configuration files are missing, while still allowing operators to override settings via files in /etc/ or environment variables.

Creating Systemd Unit Files

Place systemd service definitions in init/ to keep OS-specific wiring separate from source code. A typical unit file at init/myapp.service looks like this:

[Unit]
Description=My Go Application
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yaml
Restart=on-failure
EnvironmentFile=-/etc/myapp/env

[Install]
WantedBy=multi-user.target

Include this file in your packaging logic so deployment scripts can execute systemctl enable myapp on the target host.

Configuring Process Managers

For environments using supervisord, store program definitions in init/myapp.conf:

[program:myapp]
command=/usr/local/bin/myapp --config /etc/myapp/config.yaml
autorestart=true
stderr_logfile=/var/log/myapp.err.log
stdout_logfile=/var/log/myapp.out.log
environment=ENVIRONMENT="production",DEBUG="false"

This configuration leverages the configs/ directory for the application config file while the init script manages process lifecycle concerns like log rotation and auto-restart policies.

Deployment Workflow Integration

A typical CI/CD pipeline integrating these directories follows four stages:

  1. Package: Copy init/*.service or init/*.conf into the appropriate system directories during the packaging phase (e.g., within a dpkg or rpm spec file).
  2. Render Config: If using templating tools like consul-template, place the source template in configs/ and render it to /etc/myapp/config.yaml during deployment.
  3. Install Binary: Build the binary with go build -o /usr/local/bin/myapp ./cmd/myapp, ensuring embedded defaults are included via //go:embed.
  4. Enable Service: Execute systemctl enable --now myapp.service or supervisorctl reread && supervisorctl update to activate the service using the files from init/.

Summary

  • Separate concerns by placing OS init scripts in init/ and application config templates in configs/.
  • Embed defaults using Go's //go:embed directive to create self-contained binaries that reference files from configs/.
  • Version control deployment artifacts alongside source code to enable peer review and rollback capabilities for systemd units and process manager configs.
  • Support multiple deployment strategies (bare-metal, containers, Kubernetes) by maintaining both directories in a single source tree.

Frequently Asked Questions

What's the difference between init/ and configs/ in Go projects?

The init/ directory contains deployment-time artifacts like systemd units and supervisord configs that tell the operating system how to manage the process lifecycle. The configs/ directory contains runtime data templates that the Go application reads via libraries like Viper. This separation ensures OS-specific wiring never mixes with application-level configuration logic.

How do I embed configuration files in a Go binary?

Use the //go:embed directive in a package file to reference templates stored in configs/. For example, //go:embed ../configs/config.yaml.tmpl imports the template as a byte slice that you can parse with text/template and load into Viper. This technique, available since Go 1.16, guarantees your binary ships with working defaults.

Where should I place systemd service files in a Go project?

Place systemd unit files in the init/ directory at the repository root, as defined by the golang-standards/project-layout convention. This location keeps service definitions separate from source code while making them easily discoverable by packaging scripts that install files to /etc/systemd/system/ during deployment.

Can I use these directories with containerized deployments?

Yes. While containers often use entrypoint scripts rather than systemd, the configs/ directory remains valuable for storing default configuration templates that you copy into container images. The init/ directory can contain alternative process supervisor configs or reference material for operators translating systemd units into Kubernetes manifests or Docker Compose files.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →