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:
- Package: Copy
init/*.serviceorinit/*.confinto the appropriate system directories during the packaging phase (e.g., within adpkgorrpmspec file). - Render Config: If using templating tools like
consul-template, place the source template inconfigs/and render it to/etc/myapp/config.yamlduring deployment. - Install Binary: Build the binary with
go build -o /usr/local/bin/myapp ./cmd/myapp, ensuring embedded defaults are included via//go:embed. - Enable Service: Execute
systemctl enable --now myapp.serviceorsupervisorctl reread && supervisorctl updateto activate the service using the files frominit/.
Summary
- Separate concerns by placing OS init scripts in
init/and application config templates inconfigs/. - Embed defaults using Go's
//go:embeddirective to create self-contained binaries that reference files fromconfigs/. - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →