Masalahnya sederhana tetapi sering menipu: sebuah backend job memanggil binary eksternal, misalnya ffmpeg, python, node, convert, atau CLI internal. Di terminal lokal semuanya berjalan normal, tetapi saat dipanggil dari queue worker, cron, atau service daemon, proses gagal dengan pesan seperti file not found, command not found, exit code non-zero, atau bahkan memakai binary versi yang salah.

Dalam banyak kasus, akar masalahnya bukan pada kode bisnis, melainkan pada PATH worker yang berbeda dari shell interaktif. Shell yang Anda pakai di terminal memuat konfigurasi tambahan dari .profile, .bashrc, .zshrc, tool version manager, atau integrasi editor. Sementara itu, process manager seperti systemd, Supervisor, cron, atau container runtime menjalankan proses dengan environment yang lebih minimal dan lebih deterministik.

Gejala Nyata yang Sering Muncul

Kasus ini biasanya muncul saat backend memanggil proses eksternal melalui API bahasa pemrograman, misalnya exec, spawn, subprocess, atau wrapper process dari framework.

  • Perintah sukses saat dijalankan manual di terminal developer.
  • Worker gagal dengan error seperti ENOENT, command not found, atau No such file or directory.
  • Worker menemukan binary, tetapi versinya berbeda dari yang diharapkan.
  • Cron memakai /bin/sh atau PATH minimal, sehingga script yang bergantung pada alias atau shell init gagal.
  • Di container, perintah bekerja saat docker exec -it ... sh, tetapi gagal saat dijalankan entrypoint atau worker process.

Contoh gejala yang menyesatkan:

local shell:   /home/app/.local/bin/ffmpeg --version   # sukses
worker:        ffmpeg: not found

local shell:   python --version   # Python 3.11
worker:        Python 3.8

cron:          /app/scripts/export.sh
hasil:         exit code 127

Exit code 127 sering mengarah ke command tidak ditemukan pada level shell. Namun di beberapa runtime, Anda bisa mendapat error berbeda seperti ENOENT langsung dari system call saat binary target tidak ada di PATH proses.

Root Cause: PATH Shell Interaktif Tidak Sama dengan PATH Service

Perbedaan login shell, interactive shell, dan service process

Terminal developer biasanya berjalan sebagai shell interaktif, dan kadang juga login shell. Pada mode ini, shell memuat file konfigurasi pengguna yang menambahkan PATH, misalnya:

  • ~/.profile
  • ~/.bash_profile
  • ~/.bashrc
  • ~/.zprofile
  • ~/.zshrc

Di sanalah sering ada tambahan seperti:

export PATH="$HOME/.local/bin:$HOME/bin:/opt/tools/bin:$PATH"

Service backend biasanya tidak memuat file-file tersebut secara otomatis. systemd, cron, Supervisor, atau orchestrator container menjalankan proses dengan environment yang disiapkan secara eksplisit. Akibatnya, binary yang tersedia di terminal belum tentu tersedia bagi worker.

Version manager dan tooling editor memperparah kebingungan

Masalah ini makin sering terjadi jika tim memakai version manager seperti pyenv, nvm, asdf, rbenv, atau path injection dari editor/CLI. Dalam praktik harian, developer terbiasa menjalankan command dari terminal atau editor yang sudah “rapi” environment-nya.

Ini relevan dengan kebiasaan tooling seperti Emacs dalam ekosistem Human Emacs: editor dan shell yang terintegrasi sering membuat environment terasa konsisten dari sudut pandang developer. Namun service backend tidak hidup di dalam sesi editor Anda. Jika editor, shell, atau launcher menambahkan PATH secara implisit, asumsi itu tidak otomatis berlaku pada worker produksi.

Pelajaran pentingnya: jangan menyamakan environment yang nyaman di terminal/editor dengan environment runtime service.

Cara Verifikasi: Buktikan Environment Proses, Jangan Menebak

Kesalahan umum saat debugging adalah hanya membandingkan hasil di terminal lokal. Yang perlu Anda lihat justru environment dari proses worker yang benar-benar gagal.

1. Log PATH, executable, working directory, dan user

Tambahkan logging tepat sebelum proses eksternal dijalankan. Minimal log hal berikut:

  • nilai PATH
  • user efektif
  • working directory
  • command lengkap yang dipanggil
  • absolute path binary jika sudah diketahui

Contoh pseudo-code yang berlaku lintas bahasa:

log.info("spawn external process", {
  command: command,
  args: args,
  cwd: process.cwd(),
  path: env.PATH,
  user: currentUser,
})

Jika framework Anda punya wrapper process, log juga stdout, stderr, dan exit code secara eksplisit. Banyak incident selesai lebih cepat hanya karena stderr ternyata berisi command not found atau menunjukkan binary yang berbeda.

2. Log hasil resolusi binary

Sebelum menjalankan command generik seperti python atau ffmpeg, cek dulu binary mana yang akan dipakai. Di shell kita biasa memakai which atau command -v. Pada aplikasi, lebih aman memakai absolute path hasil konfigurasi, atau melakukan pengecekan awal saat boot.

Jika perlu, buat endpoint debug internal atau command admin yang menampilkan:

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
resolved python=/usr/bin/python3
resolved ffmpeg=/usr/local/bin/ffmpeg
cwd=/srv/app
uid=1001

3. Periksa environment service secara langsung

Untuk service yang dikelola systemd, inspeksi unit file dan environment efektifnya. Intinya, cari tahu apakah PATH memang diset eksplisit atau tidak. Untuk cron, ingat bahwa PATH sering minimal dan shell default bisa berbeda dari yang Anda pakai di terminal.

Pada container, bandingkan:

  • environment saat image dibangun
  • environment pada entrypoint/cmd
  • environment saat Anda masuk dengan docker exec

Masuk interaktif ke container tidak selalu merepresentasikan environment worker yang sebenarnya, terutama jika shell interaktif memuat file init tambahan.

4. Verifikasi user dan home directory

Banyak binary user-space dipasang di bawah home directory, misalnya ~/.local/bin atau direktori version manager. Jika worker berjalan sebagai user lain, atau HOME-nya berbeda, PATH yang Anda andalkan di terminal tidak lagi valid.

Contoh kasus klasik:

  • Developer menginstal tool di /home/deploy/.local/bin.
  • Worker berjalan sebagai user www-data atau app.
  • Binary tidak ada, atau ada tetapi tidak bisa diakses.

Studi Kasus Singkat

Sebuah job backend men-generate thumbnail video dengan memanggil ffmpeg. Saat diuji manual di server melalui SSH, perintah sukses. Namun job queue gagal dengan stderr kosong dan exit code gagal.

Setelah logging ditambah, terlihat:

PATH=/usr/bin:/bin
user=app
command=ffmpeg -i input.mp4 output.jpg

Sementara di shell developer pada server:

PATH=/home/deploy/.local/bin:/usr/local/bin:/usr/bin:/bin
$ command -v ffmpeg
/usr/local/bin/ffmpeg

Worker ternyata tidak melihat /usr/local/bin. Ketika aplikasi memanggil ffmpeg tanpa absolute path, resolusi binary gagal. Pada host yang lain, lebih rumit lagi: worker menemukan /usr/bin/python, tetapi aplikasi sebenarnya bergantung pada interpreter berbeda yang hanya tersedia melalui version manager di shell interaktif.

Intinya: kode aplikasi benar, tetapi asumsi terhadap environment salah.

Perbaikan yang Aman

Prinsip umumnya ada dua:

  1. Prefer explicit over implicit: gunakan absolute path atau environment eksplisit.
  2. Samakan runtime secara sengaja, jangan berharap shell init pengguna akan ikut termuat.

Opsi 1: Gunakan absolute path ke binary

Ini pendekatan paling deterministik jika binary memang diketahui lokasinya dan dikelola oleh deployment.

/usr/local/bin/ffmpeg -i input.mp4 output.jpg

Kelebihan:

  • Tidak bergantung pada PATH proses.
  • Perilaku lebih konsisten antar worker.
  • Mengurangi risiko memakai binary versi yang salah.

Kekurangan:

  • Kurang portabel jika path berbeda antar host.
  • Perlu konfigurasi yang jelas saat deploy.

Praktik yang baik adalah menjadikan path binary sebagai konfigurasi aplikasi, bukan hardcode tersebar di banyak file.

Opsi 2: Set PATH eksplisit pada service

Jika beberapa command memang perlu ditemukan lewat PATH, set PATH langsung pada definisi service. Ini lebih baik daripada mengandalkan .bashrc pengguna.

Contoh systemd:

[Service]
User=app
WorkingDirectory=/srv/app
Environment="PATH=/usr/local/bin:/usr/bin:/bin"
ExecStart=/srv/app/bin/worker

Kenapa ini bekerja? Karena systemd akan memberikan environment tersebut langsung ke proses worker, tanpa bergantung pada shell login. PATH menjadi bagian dari kontrak runtime service, bukan efek samping dari sesi interaktif.

Catatan: hindari memasukkan path yang hanya valid untuk user developer tertentu kecuali memang service berjalan sebagai user itu dan path-nya dikelola secara resmi.

Opsi 3: Set environment eksplisit di Supervisor

Jika worker dijalankan oleh Supervisor atau process manager serupa, tentukan environment pada konfigurasi program.

[program:app-worker]
command=/srv/app/bin/worker
directory=/srv/app
user=app
environment=PATH="/usr/local/bin:/usr/bin:/bin",HOME="/srv/app"

Pastikan Anda tidak menyembunyikan masalah lain, misalnya binary sesungguhnya berada di home user pribadi hasil instalasi manual yang tidak seharusnya dipakai di produksi.

Opsi 4: Untuk cron, definisikan shell dan PATH secara eksplisit

Cron sering menjadi sumber kejutan karena environment-nya minimal. Jangan asumsikan ia akan memuat shell startup yang sama seperti sesi SSH Anda.

SHELL=/bin/sh
PATH=/usr/local/bin:/usr/bin:/bin

*/5 * * * * /srv/app/scripts/run-job.sh

Jika script Anda membutuhkan bash feature tertentu, nyatakan dengan jelas di shebang script atau konfigurasi shell. Jangan mengandalkan kebetulan bahwa di mesin tertentu command dijalankan lewat shell yang sesuai.

Opsi 5: Untuk Docker, bake dependency ke image dan tetapkan ENV

Di container, perbaikan terbaik biasanya bukan menambah path lewat shell profile, tetapi memastikan binary ada di image dan PATH ditentukan pada image atau runtime container.

ENV PATH="/usr/local/bin:/usr/bin:/bin"

Jika aplikasi membutuhkan tool eksternal, instal tool tersebut saat build image. Hindari bergantung pada modifikasi interaktif setelah container berjalan. Container yang benar harus reproducible tanpa sesi manual.

Hal yang Sebaiknya Tidak Dilakukan

Jangan memperbaiki service dengan menjalankan shell interaktif sebagai pembungkus tanpa alasan kuat

Misalnya pola seperti:

/bin/bash -lc 'source ~/.bashrc && worker'

Ini kadang terlihat “menyelesaikan” masalah, tetapi membawa beberapa risiko:

  • runtime menjadi bergantung pada file profil user
  • lebih sulit direproduksi dan diaudit
  • perubahan shell config personal bisa merusak service
  • menyembunyikan dependency yang seharusnya dideklarasikan eksplisit

Pendekatan ini hanya masuk akal untuk kasus transisi jangka pendek, bukan solusi operasional yang bersih.

Jangan andalkan alias atau fungsi shell

Alias seperti python=python3 atau fungsi shell hanya berlaku di shell tertentu. Process worker yang memanggil executable tidak tahu apa-apa tentang alias shell Anda.

Jangan anggap docker exec -it sama dengan runtime worker

Sesi debug interaktif berguna, tetapi bukan sumber kebenaran utama. Selalu validasi environment dari proses yang benar-benar dijalankan oleh orchestrator atau process manager.

Logging yang Perlu Ditambah Saat Memanggil Proses Eksternal

Jika aplikasi Anda memang bergantung pada external process, tambahkan observability sejak awal. Minimal, simpan informasi berikut pada level debug atau error:

  • binary yang dipanggil
  • argumen penting
  • absolute path binary jika tersedia
  • PATH proses
  • cwd
  • uid/gid atau user service
  • exit code
  • stdout dan stderr terpotong aman bila terlalu panjang
  • waktu eksekusi

Ini membantu membedakan beberapa kelas masalah:

  • binary tidak ditemukan
  • binary ditemukan tetapi dependency lain tidak ada
  • binary versi salah
  • permission issue
  • cwd salah sehingga file input/output tidak ditemukan

Sering kali masalah yang awalnya tampak sebagai PATH ternyata kombinasi dari PATH, working directory, dan user yang berbeda.

Mengapa Ini Sering Terjadi pada Tim yang Tooling-nya Mapan

Semakin nyaman setup developer, semakin mudah asumsi keliru terbentuk. Shell modern, plugin, version manager, dan editor seperti Emacs dapat membuat command-line terasa seragam dan selalu siap pakai. Referensi seperti Human Emacs menekankan kenyamanan workflow personal, yang bagus untuk produktivitas. Tetapi dari sudut pandang operasi backend, kenyamanan itu bisa menutupi fakta bahwa production worker hidup dalam konteks environment yang jauh lebih ketat.

Artinya, masalah ini bukan karena editor tertentu. Penyebabnya adalah ketergantungan implisit pada environment sesi pengguna yang tidak pernah diformalisasi ke runtime service.

Checklist Pencegahan

  1. Gunakan absolute path untuk binary penting jika memungkinkan.
  2. Jika mengandalkan PATH, set PATH eksplisit di systemd, Supervisor, cron, atau container.
  3. Jangan bergantung pada .bashrc, .zshrc, alias, atau function shell untuk service produksi.
  4. Pastikan user service, HOME, dan working directory sesuai dengan asumsi aplikasi.
  5. Log PATH, command, cwd, user, stdout, stderr, exit code saat memanggil proses eksternal.
  6. Verifikasi binary dengan absolute path atau startup check, bukan hanya uji manual di terminal.
  7. Di Docker, instal dependency ke image dan deklarasikan environment secara reproducible.
  8. Di cron, definisikan SHELL dan PATH secara eksplisit.
  9. Waspadai version manager seperti pyenv, nvm, asdf, rbenv; jangan asumsikan worker mewarisi konfigurasi shell developer.
  10. Saat incident terjadi, bandingkan environment proses yang gagal dengan environment shell interaktif, bukan menebak berdasarkan kebiasaan lokal.

Jika sebuah perintah sukses di terminal tetapi gagal di worker, mulailah dari asumsi bahwa environment berbeda. Dalam debugging backend, itu sering lebih produktif daripada langsung mencurigai framework, queue, atau library process wrapper.