Run Go in Production with systemd

Khimananda Oli 7 min read Programming and Languages
Run Go in Production with systemd

By Khimananda Oli | Last reviewed: August 2026

Deploying a compiled Go binary directly via SSH or screen sessions is a common anti-pattern that leads to silent failures and unmanageable uptime. To run Go in production with systemd, you must treat your application as a managed system service rather than an ad-hoc process. This approach integrates your application into the OS lifecycle, providing automatic restarts, structured logging via journald, and strict security sandboxing. For teams managing infrastructure on Ubuntu or RHEL-based systems, this is the foundational layer of reliability before you even consider orchestration tools like Kubernetes.

systemd (PID 1)Service ManagerRestart / WatchdogGo Binary/opt/myapp/bin/appDedicated UserjournaldStructured LogsBinary JournalExecStartSTDOUT/STDERR
Systemd acts as the supervisor for your Go production service, managing execution and routing output to journald.

How do you configure a systemd unit file to run Go in production?

The unit file is the contract between your Go application and the Linux kernel. A common mistake I see in audits is running services as root or missing explicit dependency declarations. When you manage Linux daemons with systemd, precision in the unit file prevents cascading failures during boot or high-load events. Create your service definition at /etc/systemd/system/myapp.service. Never edit files in /lib/systemd/system/ as package updates will overwrite them.

[Unit]
Description=My Go Production API Service
Documentation=https://internal.docs/myapp
After=network.target postgresql.service
Wants=postgresql.service

[Service]
Type=simple
User=myapp
Group=myapp
ExecStart=/opt/myapp/bin/myapp -config /etc/myapp/config.yaml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

# Security Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/myapp /var/log/myapp
PrivateTmp=true
MemoryDenyWriteExecute=true

[Install]
WantedBy=multi-user.target

Critical directives explained

  • Type=simple: Go binaries are long-running foreground processes by default. Do not use Type=forking unless your application explicitly daemonizes itself, which is rare and discouraged in modern Go development.
  • Restart=on-failure: Automatically restarts the process if it exits with a non-zero code or is killed by a signal. Avoid always in production as it can mask configuration errors by infinitely restarting a broken binary.
  • After=network.target: Ensures the network stack is up before starting. If your Go app depends on a specific database, add it here to prevent startup race conditions.
  • SyslogIdentifier: Tags all log entries. Without this, filtering logs via journalctl becomes difficult when multiple services share similar output patterns.

What security hardening is required for Go services on Linux?

Running a web-facing Go application without sandboxing violates basic defense-in-depth principles. In my experience helping teams achieve SOC 2 compliance, auditors specifically look for least-privilege execution. Systemd provides kernel-level isolation that application code cannot bypass. Even if an attacker exploits a vulnerability in your Go HTTP handler, these directives limit the blast radius significantly.

DirectivePurposeProduction Recommendation
NoNewPrivileges=truePrevents the process and children from gaining new privileges via setuid/setgidMandatory for all internet-facing services
ProtectSystem=strictMounts entire filesystem read-only except whitelisted pathsUse with ReadWritePaths for data directories only
PrivateTmp=trueGives the service its own isolated /tmp directoryPrevents symlink attacks and temp file leakage
MemoryDenyWriteExecute=trueBlocks creating memory mappings that are both writable and executableStops many buffer overflow and JIT spray exploits
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIXLimits socket types the process can createBlock netlink/raw sockets unless explicitly needed

Always create a dedicated system user for your Go application. Never run as root or a shared user like www-data. Use useradd --system --no-create-home --shell /usr/sbin/nologin myapp to create a non-login account. This ensures that even if the service is compromised, the attacker cannot easily pivot to other system resources or establish interactive shells.

Untrusted InputHTTP / gRPC RequestsGo ApplicationVulnerable HandlerProtected Resources/etc /home /rootsystemd SandboxProtectSystem + NoNewPrivsBLOCKEDEnforced by KernelAllowed Writes/var/lib/myapp only
Systemd security directives create a kernel-enforced boundary that blocks unauthorized access even after application compromise.

How do you manage logs and observability for Go systemd services?

When you run Go in production with systemd, your application should write to stdout/stderr, not directly to files. Systemd captures this output and routes it to journald. This eliminates the need for application-level log rotation and integrates your Go service into the standard Linux observability stack. For teams already using structured logging best practices, this model works seamlessly with JSON parsers.

Essential journalctl commands

  1. Live tailing: journalctl -u myapp.service -f follows new log entries in real-time, equivalent to tail -f but with metadata enrichment.
  2. Time-bounded queries: journalctl -u myapp.service --since "2026-08-18 09:00:00" --until "2026-08-18 10:00:00" isolates incidents without grepping massive files.
  3. Boot-specific logs: journalctl -u myapp.service -b shows only entries from the current boot cycle, critical for diagnosing startup failures after reboots.
  4. JSON export: journalctl -u myapp.service -o json-pretty outputs full metadata including PID, UID, and cgroup path for forensic analysis.

Configure journald persistence in /etc/systemd/journald.conf by setting Storage=persistent. By default, many distributions store logs in volatile memory, losing them on reboot. For production Go services, you need durable logs for post-incident review and compliance evidence. Set SystemMaxUse=2G to prevent disk exhaustion. If you ship logs to a centralized system like Loki or Elasticsearch, configure ForwardToSyslog=no to avoid double-writing and reduce I/O overhead.

How does systemd compare to Docker and Kubernetes for Go deployment?

Engineers often ask whether they should skip systemd entirely and containerize. The answer depends on operational complexity and scale. For single-node deployments, edge servers, or environments where container runtime overhead is unacceptable, systemd remains the gold standard. Understanding this trade-off prevents over-engineering. I have seen startups waste months building Kubernetes clusters for applications that would have been more reliable and cheaper as hardened systemd services on a pair of VPS instances.

Criteriasystemd DirectDocker ComposeKubernetes
Startup OverheadNear-zero (native binary)Low (container runtime)High (kubelet + control plane)
Security Isolationcgroups + namespaces (manual)Container namespaces (default)Pod security + network policies
Auto-RestartRestart=on-failure (instant)restart: unless-stoppedLiveness probes + controller
Log Managementjournald (integrated)JSON file driver / pluginSidecar or node-level agent
Config Updatessystemctl reload (SIGHUP)docker compose up -dRolling deployment / GitOps
Best ForSingle node, edge, low-latencyDev parity, small teamsMulti-node, auto-scaling, SaaS

If you choose systemd, you gain direct access to kernel features without abstraction layers. Debugging becomes simpler because there is no container runtime between you and the process. However, you lose portability and declarative infrastructure. My recommendation: start with systemd for MVPs and internal tools. Migrate to containers only when you need horizontal scaling or multi-cloud portability. Both approaches require the same foundational discipline around Ubuntu security hardening and least-privilege execution.

Scale (Nodes / Replicas)Operational Complexitysystemd1 NodeDocker1-5 NodesK8s5+ NodesLow overhead, fast debugAuto-scale, self-healing
Choose systemd for low-complexity single-node Go deployments; escalate to containers only when scale demands it.

Run Go in Production with systemd Reliably

Treating your Go application as a first-class systemd citizen pays dividends in stability, security, and operational clarity. Start with a hardened unit file, enforce least-privilege execution, and integrate with journald from day one. Resist the urge to containerize prematurely; native systemd services remain the most efficient way to deploy Go on Linux for small-to-medium workloads. When your architecture grows beyond single-node capacity, the discipline you build here translates directly to containerized environments. If you need help designing production-grade deployment pipelines or auditing existing Go services for compliance gaps, reach out to discuss your infrastructure.

Frequently Asked Questions

Create /etc/systemd/system/myapp.service with Unit, Service, and Install sections. Set Type=simple, ExecStart to your binary path, User to a non-root account, then run systemctl daemon-reload and enable the service.

Systemd offers lower overhead without container runtime costs. It provides native process supervision, journal logging integration, and simpler resource limits via cgroups v2, making it ideal for single-binary Go deployments on bare metal or VMs in 2026.

Add Restart=on-failure and RestartSec=5s to the Service section. This tells systemd to restart only on non-zero exit codes, preventing restart loops from configuration errors while ensuring recovery from panics or transient failures.

Use EnvironmentFile=/etc/myapp/env in the service unit rather than inline Environment directives. This keeps secrets out of unit files, allows standard dotenv formatting, and simplifies configuration management across staging and production environments.

Use MemoryMax=512M and CPUQuota=80% in the Service section. These map directly to cgroups v2 controllers, providing hard limits that prevent runaway goroutines or memory leaks from affecting other processes on the host.

Yes. Set TimeoutStopSec=30s and send SIGTERM via KillSignal=SIGTERM. Your Go server must handle this signal to stop accepting new connections and finish in-flight requests before systemd sends SIGKILL after the timeout expires.

Use journalctl -u myapp.service -f for live tailing or --since "1 hour ago" for historical logs. Journald captures stdout/stderr automatically with timestamps and metadata, eliminating the need for separate log file rotation configurations.

Never run as root. Create a dedicated system user with no shell access using useradd --system --no-create-home --shell /usr/sbin/nologin myapp. Set User=myapp and Group=myapp in the service file to enforce least privilege.

Implement SIGHUP handling in your Go app to re-read config files. Then add ExecReload=/bin/kill -HUP $MAINPID to the unit file. Run systemctl reload myapp to trigger the signal without dropping active connections.

Add ProtectSystem=strict, ProtectHome=true, PrivateTmp=true, NoNewPrivileges=true, and CapabilityBoundingSet=~CAP_SYS_ADMIN. These restrict filesystem write access, isolate temporary directories, and prevent privilege escalation attacks against your Go binary.

Use After=postgresql.service in the Unit section for ordering, but implement health checks in your Go code since After only guarantees start order, not readiness. Consider systemd socket activation for true dependency-aware startup sequencing.

Update ExecStart in the unit file and run systemctl daemon-reload followed by systemctl restart myapp. Alternatively, use a stable symlink at /usr/local/bin/myapp pointing to versioned binaries to avoid editing unit files during deploys.

Check systemctl status myapp for exit codes and journalctl -u myapp -n 50 for recent output. Common issues include wrong binary permissions, missing environment variables, or AppArmor denials visible in dmesg or audit logs.

Yes. Set Type=notify and have your Go app call sd_notify("READY=1") after initialization completes. This prevents dependent services from starting prematurely and enables accurate health reporting in orchestration tooling and monitoring dashboards.

Use systemd template units named [email protected] with %i representing the instance name. Start instances via systemctl start myapp@instance1 and reference instance-specific config files or ports using the %I specifier in ExecStart.