状況の説明
Goプログラムから外部コマンド — gcc、ffmpeg、git、あるいは何らかのCLIツール — を呼び出しているとします。ターミナルでは正常に動作します。ところがsystemdサービスとしてデプロイしたり、Dockerに入れたり、CIに渡したりすると、突然こんなエラーが出ます:
exec: "gcc": executable file not found in $PATH
同じバイナリ。同じマシン。でも環境が違う。バイナリが消えたわけではありません — プロセスがそれを見つけられないだけです。
実際に何が起きているのか
os/execがコマンドをフルパスではなく名前で実行する場合、$PATHを検索してバイナリを探します。ただしそれはそのプロセス自身の$PATHであって、あなたのものではありません。
ターミナルセッションには/usr/local/bin:/usr/bin:/home/user/.local/bin:/usr/local/go/binのような豊富なPATHがあります。一方、systemdサービス、cronジョブ、Dockerコンテナが起動する際のPATHはほぼ何もなく、多くの場合/usr/bin:/binだけです。
そのため/usr/local/bin/gccにあるgccや、/usr/local/go/bin/goにあるgoは、プロセスから見ると存在しないも同然なのです。
ステップ1:バイナリの存在を確認する
まず当たり前のことを確認しましょう — そのバイナリは実際にインストールされていますか?
which gcc
# または
type gcc
# 見つからない場合:
sudo apt install gcc # Debian/Ubuntu
sudo yum install gcc # RHEL/CentOS
brew install gcc # macOS
whichがパスを返すということは、バイナリは存在しています。インストールの問題ではなく、PATHの可視性の問題です。
ステップ2:Goプロセスが何を見ているか調べる
以下のスニペットをプログラムに追加して、実行時に$PATHが実際にどうなっているか確認しましょう:
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
// プロセスが実際に持っているPATHを表示する
fmt.Println("PATH:", os.Getenv("PATH"))
// バイナリを解決しようとする
path, err := exec.LookPath("gcc")
if err != nil {
fmt.Println("LookPath error:", err)
} else {
fmt.Println("Found gcc at:", path)
}
}
エラーが発生するのと同じ条件で実行してみてください — サービスユーザーとして、Docker内で、あるいはsudo -u myuser ./binaryで。切り詰められたPATHを見れば問題は一目瞭然です。
手っ取り早い修正:バイナリのフルパスを使う
PATH検索を完全に回避する方法です。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)
}
$PATHに関係なく動作します。ただし欠点があります:バイナリの場所はマシンによって異なります — Homebrew macOSでは/usr/local/bin/gcc、Ubuntuでは/usr/bin/gccなど。ハードコードされたパスは別の環境で壊れます。
より良い修正:起動時にパスを解決し、即座に失敗させる
起動時に一度だけパスを解決して保存します。バイナリが見つからない場合は、3つもゴルーチンが深く積み重なってからサイレントに失敗するのではなく、わかりやすいエラーとともに即座にクラッシュします:
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
}
プログラムはきれいに起動するか、何が問題かを正確に伝えるメッセージとともに即座に終了するかのどちらかです。深夜2時に驚くことはありません。
恒久的な修正:子プロセスに正しいPATHを渡す
ハードコードされたパスでは脆すぎますか?コード内で直接サブプロセスのPATHを拡張しましょう。ツールが/usr/local/bin、~/.local/bin、あるいは/usr/local/go/binのような言語固有のディレクトリに散在している場合に便利です:
package main
import (
"os"
"os/exec"
"strings"
)
func runWithExtendedPath(name string, args ...string) ([]byte, error) {
cmd := exec.Command(name, args...)
// 現在の環境を継承しつつ、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()
}
必要なものが正確にわかっている場合は、環境全体を明示的に設定することもできます:
cmd := exec.Command("gcc", "-o", "output", "main.c")
cmd.Env = append(os.Environ(),
"PATH=/usr/local/bin:/usr/bin:/bin",
)
out, err := cmd.CombinedOutput()
systemdサービスの修正
systemdサービスは最小限の環境で起動します — ユーザーのPATHは引き継がれません。ユニットファイルで明示的に設定しましょう:
[Service]
Environment="PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/go/bin"
ExecStart=/opt/myapp/myapp
変数を別ファイルで管理したい場合は、代わりにEnvironmentFile=/etc/myapp/envを使ってください。どちらの方法でも、編集後は再読み込みが必要です:
sudo systemctl daemon-reload
sudo systemctl restart myapp
Dockerコンテナの修正
Dockerイメージは新鮮な状態から始まります — バイナリはインストールかつPATHから到達可能でなければなりません。Dockerfileで両方を確認しましょう:
FROM golang:1.22-alpine
RUN apk add --no-cache gcc musl-dev
# ビルド前にgccが到達可能か確認する
RUN which gcc
COPY . /app
WORKDIR /app
RUN go build -o server .
CMD ["/app/server"]
修正を確認する
エラーが発生していたのと全く同じ条件でテストします:
# サービスの場合:
sudo systemctl restart myapp
journalctl -u myapp -n 50
# Dockerの場合:
docker build -t myapp . && docker run myapp
# コード内での簡単な動作確認:
path, err := exec.LookPath("gcc")
fmt.Printf("gcc resolved to: %s (err: %v)\n", path, err)
エラーなし、サブプロセスが正常終了?これでリリースできます。
まとめ
- このエラーは
os/execがプロセスの$PATH内でバイナリを見つけられないことを意味します — バイナリが存在しないわけではありません。 - サービス、コンテナ、cronジョブはインタラクティブシェルのPATHを継承しません。
exec.LookPathを使って、実行時にGoプロセスが何を見ているか正確にデバッグしましょう。- 手っ取り早い修正:
exec.Commandにバイナリの絶対パスを渡す。 - より綺麗な修正:起動時に
LookPathで解決し、明確なエラーとともに即座にクラッシュさせる。 - サービスの場合:systemdユニットファイルに
Environment=を設定する。

