The Situation
Your Go program shells out to an external command β maybe gcc, ffmpeg, git, or some CLI tool. It runs fine in your terminal. Deploy it as a systemd service, drop it in Docker, or hand it off to CI β and suddenly:
exec: "gcc": executable file not found in $PATH
Same binary. Same machine. Different environment. The binary didn't disappear β your process just can't see it.
What's Actually Happening
When os/exec runs a command by name (not full path), it searches $PATH to find the binary. It searches its own $PATH β not yours.
Your terminal session carries a rich PATH like /usr/local/bin:/usr/bin:/home/user/.local/bin:/usr/local/go/bin. A systemd service, cron job, or Docker container starts with almost nothing β often just /usr/bin:/bin.
So gcc at /usr/local/bin/gcc, or go at /usr/local/go/bin/go, simply doesn't exist as far as your process is concerned.
Step 1: Confirm the Binary Exists
Rule out the obvious first β is the binary actually installed?
which gcc
# or
type gcc
# If not found:
sudo apt install gcc # Debian/Ubuntu
sudo yum install gcc # RHEL/CentOS
brew install gcc # macOS
which returning a path means the binary exists. You have a PATH visibility problem, not an installation problem.
Step 2: Debug What Your Go Process Sees
Drop this snippet into your program to see exactly what $PATH looks like at runtime:
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
// Print the PATH your process actually has
fmt.Println("PATH:", os.Getenv("PATH"))
// Try to resolve the binary
path, err := exec.LookPath("gcc")
if err != nil {
fmt.Println("LookPath error:", err)
} else {
fmt.Println("Found gcc at:", path)
}
}
Run it under the same conditions where the error occurs β as the service user, inside Docker, or via sudo -u myuser ./binary. The truncated PATH will make the problem obvious.
Quick Fix: Use the Full Binary Path
Bypass PATH lookup entirely. Pass the absolute path to exec.Command:
cmd := exec.Command("/usr/bin/gcc", "-o", "output", "main.c")
out, err := cmd.CombinedOutput()
if err != nil {
log.Fatalf("gcc failed: %v\n%s", err, out)
}
Works regardless of $PATH. The catch: binaries live in different places on different machines β /usr/local/bin/gcc on Homebrew macOS, /usr/bin/gcc on Ubuntu. Hardcoded paths break on the next box.
Better Fix: Resolve Path at Startup, Fail Fast
Resolve the path once at startup and store it. A missing binary then crashes immediately with a readable error β not silently three goroutines deep:
package main
import (
"fmt"
"log"
"os/exec"
)
var gccPath string
func init() {
var err error
gccPath, err = exec.LookPath("gcc")
if err != nil {
log.Fatalf("Required binary 'gcc' not found in PATH: %v", err)
}
}
func compileFile(src, out string) error {
cmd := exec.Command(gccPath, "-o", out, src)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("gcc error: %w\n%s", err, output)
}
return nil
}
The program either starts clean or dies immediately with a message that tells you exactly what's wrong. No surprises at 2am.
Permanent Fix: Pass the Right PATH to the Child Process
Hardcoded paths too brittle? Extend the subprocess's PATH directly in code. Handy when tools are scattered across /usr/local/bin, ~/.local/bin, or language-specific directories like /usr/local/go/bin:
package main
import (
"os"
"os/exec"
"strings"
)
func runWithExtendedPath(name string, args ...string) ([]byte, error) {
cmd := exec.Command(name, args...)
// Inherit current env, but extend PATH
env := os.Environ()
for i, e := range env {
if strings.HasPrefix(e, "PATH=") {
env[i] = e + ":/usr/local/bin:/usr/local/go/bin:/home/ubuntu/.local/bin"
break
}
}
cmd.Env = env
return cmd.CombinedOutput()
}
Or set the complete environment explicitly when you know exactly what's needed:
cmd := exec.Command("gcc", "-o", "output", "main.c")
cmd.Env = append(os.Environ(),
"PATH=/usr/local/bin:/usr/bin:/bin",
)
out, err := cmd.CombinedOutput()
Fix for systemd Services
Systemd services start with a minimal environment β your user's PATH doesn't carry over. Set it explicitly in the unit file:
[Service]
Environment="PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/go/bin"
ExecStart=/opt/myapp/myapp
Prefer keeping variables in a separate file? Use EnvironmentFile=/etc/myapp/env instead. Either way, reload after editing:
sudo systemctl daemon-reload
sudo systemctl restart myapp
Fix for Docker Containers
Docker images start fresh β the binary must be installed and reachable from PATH. Verify both in your Dockerfile:
FROM golang:1.22-alpine
RUN apk add --no-cache gcc musl-dev
# Verify gcc is reachable before the build
RUN which gcc
COPY . /app
WORKDIR /app
RUN go build -o server .
CMD ["/app/server"]
Verify the Fix
Test under the exact conditions where the error was happening:
# For a service:
sudo systemctl restart myapp
journalctl -u myapp -n 50
# For Docker:
docker build -t myapp . && docker run myapp
# Quick sanity check in code:
path, err := exec.LookPath("gcc")
fmt.Printf("gcc resolved to: %s (err: %v)\n", path, err)
No error, subprocess exits cleanly? Ship it.
Summary
- The error means
os/execcan't find the binary in the process's$PATHβ not that the binary doesn't exist. - Services, containers, and cron jobs don't inherit your interactive shell's PATH.
- Use
exec.LookPathto debug exactly what your Go process sees at runtime. - Quick fix: pass the absolute binary path to
exec.Command. - Cleaner fix: resolve at startup with
LookPath, crash immediately with a clear error. - For services: set
Environment=in the systemd unit file.

