crontab에서만 command not found? cron PATH부터 보세요
터미널에서는 잘 되던 스크립트가 예약 시각에 command not found로 멈춘다면, 작업 내용보다 실행 환경을 먼저 의심해야 합니다. 사이트 감시와 예약 글 점검을 cron에 맡길 때 생기는 이 문제를 Debian 13에서 재현했습니다. 로그인 셸과 예약 실행의 환경 차이를 드러낸 다음 경로를 고쳤고, 작업이 제대로 도는지는 예약 실행 로그로 가렸습니다. 수동 실행 결과와 예약 실행 로그를 처음부터 따로 남긴 것도 그래서입니다. 아래에서는 PATH 지정과 절대 경로 호출을 비교하고, 날짜 처리와 잠금까지 필요한 작업은 래퍼 스크립트로 묶습니다.
터미널의 성공과 예약 실행의 차이를 확인합니다
가장 먼저 본 것은 작업 내용이 아니라 명령을 찾는 환경입니다. 로그인 셸에서 demo-tool이 돈다는 사실만 믿고 cron도 같은 명령을 찾을 거라 여기면 원인을 놓칩니다. 이번에는 일반 계정 crondemo의 사용자 crontab에서 환경을 파일로 떨어뜨리고, 실패한 명령과 고친 명령의 로그를 나란히 놓았습니다. 실제 운영 작업은 crontab 맨 위에 PATH를 두고, 비밀값은 래퍼 스크립트에서 불러오는 방식으로 정리했습니다.
| 항목 | 테스트 환경 |
|---|---|
| OS | Debian GNU/Linux 13 (trixie) |
| 주요 도구 버전 | Debian cron 패키지(/usr/sbin/cron)를 사용합니다. 세부 도구 버전 번호 대신 실행 환경과 출력으로 비교합니다. |
| 실습 사용자 | 일반 계정 crondemo를 사용합니다. |
| 실습 명령 | /home/crondemo/.local/bin/demo-tool: echo를 수행하는 셸 스크립트를 사용합니다. |
| 확인일 | 2026-10-15 KST |
대상 계정은 crontab -u crondemo로 고정했습니다. 환경 덤프에는 HOME, PATH, SHELL을 남기고, 작업 출력과 에러는 로그로 보냅니다. cron이 어떻게 동작하는지는 Debian cron(8) 매뉴얼 (확인일 2026-10-15)을 기준으로 삼았고, 아래 해석은 모두 이 서버에서 관찰한 환경에 근거합니다.
재현 로그에서 원인을 찾고 실행 경로를 지정합니다
로그인 셸과 cron에서 같은 명령을 실행합니다
로그인 셸에서는 PATH를 찍은 다음 demo-tool을 부릅니다. 사용자 crontab에는 환경을 기록하는 명령과 demo-tool 호출을 한 줄에 같이 올렸습니다. 재현에 쓴 작업 줄은 아래와 같고, 다음 예약 실행이 지나간 뒤 환경 파일과 job.log를 읽어 비교합니다.
* * * * * env > /home/crondemo/logs/cron_env.txt; demo-tool >> /home/crondemo/logs/job.log 2>&1
이 작업 줄은 환경 덤프와 명령 실행 결과를 서로 다른 파일로 나눠 남깁니다. 출력을 보면 로그인 셸은 demo-tool ok 23:28:43을 보여 주는데, 예약 실행은 demo-tool: not found로 끝납니다. 다음 실행까지 19초를 기다렸다가 읽은 환경에는 PATH=/usr/bin:/bin이 적혀 있습니다. 같은 계정의 작업인데도 명령을 찾는 조건이 다르다는 것이 이 비교 하나로 드러납니다.
$ sudo -u crondemo bash -lc "echo \$PATH; demo-tool"
/home/crondemo/.local/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games
demo-tool ok 23:28:43
$ sudo crontab -u crondemo -l
* * * * * env > /home/crondemo/logs/cron_env.txt; demo-tool >> /home/crondemo/logs/job.log 2>&1
(다음 분 실행까지 19초 대기)
$ sudo grep -E "^(PATH|SHELL|HOME)=" $L/cron_env.txt
HOME=/home/crondemo
PATH=/usr/bin:/bin
SHELL=/bin/sh
$ sudo cat $L/job.log
/bin/sh: 1: demo-tool: not found
재현 캡처는 로그인 셸과 cron의 환경을 한 화면에 나란히 놓았습니다. 로그인 셸의 성공 시각은 23:28:43이고, 예약 실행 로그에는 /bin/sh: 1: demo-tool: not found가 남았습니다. HOME=/home/crondemo는 그대로인데 PATH=/usr/bin:/bin에는 사용자 도구 디렉터리가 없습니다. 계정이 맞는지 확인하는 데서 멈추면 안 되고 PATH와 SHELL까지 비교해야 하는 이유가 이것입니다.

로그인 셸의 PATH에는 /home/crondemo/.local/bin이 들어 있고 demo-tool의 성공 메시지도 찍힙니다. 반면 cron이 넘긴 PATH는 /usr/bin:/bin이고, job.log에는 /bin/sh: 1: demo-tool: not found가 남습니다. HOME은 같은 사용자 디렉터리를 가리키지만 명령을 찾는 PATH가 다릅니다. HOME만 보고 환경이 같다고 판단하면 이 문제는 풀리지 않습니다.
원인은 cron이 .profile이나 .bashrc 같은 로그인 셸 초기화 파일을 읽어 같은 환경을 만들어 주지 않고, 최소 환경으로 /bin/sh를 띄운다는 데 있습니다. 이번 PATH에서는 사용자 도구 디렉터리와 /usr/local/bin이 빠졌습니다. 이 상태에서 명령 이름만 적으면 그 디렉터리의 프로그램은 찾지 못합니다. 실행 시각을 만지작거리기 전에, 실제 작업 환경에 필요한 경로가 들어 있는지부터 봐야 합니다.
첫 번째 처방: crontab 첫 줄에 PATH 적기
여러 작업이 같은 도구 경로를 쓴다면 crontab 맨 위에 검색 경로를 못 박아 둡니다. 해결 스크립트는 사용자 도구 디렉터리, /usr/local/bin, /usr/bin, /bin을 적고 그 아래에 작업을 둡니다. 이 줄을 빼고 기본 환경으로 돌아가면 demo-tool을 찾을 경로가 없어 같은 실패가 되풀이됩니다. 필요한 경로가 crontab에 적혀 있으면, 운영자가 로그인 셸 설정을 뒤져 가며 실행 조건을 짐작할 일이 줄어듭니다.
PATH=/home/crondemo/.local/bin:/usr/local/bin:/usr/bin:/bin
* * * * * demo-tool >> /home/crondemo/logs/job.log 2>&1
이 방식은 여러 작업이 쓰는 명령 검색 경로를 한곳에서 관리하고, 작업 줄에는 명령 이름만 남아 읽기 편합니다. 대신 사용자 도구의 설치 디렉터리가 바뀌면 PATH 설정도 같이 고쳐야 하니, 경로 목록을 관리할 책임은 여전히 남습니다.
두 번째 처방: 실행 파일을 절대 경로로 부르기
부를 파일이 분명하다면 명령 이름 대신 전체 경로를 씁니다. 아래 작업은 demo-tool의 위치를 직접 적었습니다. 경로가 짧고 잘 관리되는 단순 작업이라면 고칠 곳이 한눈에 보입니다. 해결 스크립트는 PATH를 지정한 작업과 절대 경로로 부른 작업을 같은 crontab에 올려 결과를 함께 남깁니다.
* * * * * /home/crondemo/.local/bin/demo-tool >> /home/crondemo/logs/abs.log 2>&1
crontab만 보고도 어떤 실행 파일을 부르는지 바로 알 수 있고, 그 파일을 찾는 데 PATH 검색이 끼어들지 않는다는 것이 장점입니다. 그 대신 실행 파일을 옮기면 작업 줄도 고쳐야 하고, 스크립트 안에서 다른 명령을 이름으로 부른다면 그 내부 환경은 따로 점검해야 합니다.
등록된 crontab을 먼저 읽어 PATH 설정과 실행 파일 위치를 맞춰 보고, 예약 실행이 지나간 뒤 job.log와 abs.log를 함께 읽었습니다. 두 로그 모두 demo-tool ok 23:30:01을 남겼습니다. 경로를 바꿨다면 예약 작업 자신이 남긴 성공 기록까지 봐야 수정이 끝났다고 말할 수 있습니다.
$ sudo crontab -u crondemo -l
PATH=/home/crondemo/.local/bin:/usr/local/bin:/usr/bin:/bin
* * * * * demo-tool >> /home/crondemo/logs/job.log 2>&1
* * * * * /home/crondemo/.local/bin/demo-tool >> /home/crondemo/logs/abs.log 2>&1
(다음 분 실행까지 56초 대기)
$ sudo cat $L/job.log $L/abs.log
demo-tool ok 23:30:01
demo-tool ok 23:30:01
해결 쪽 화면에는 PATH를 지정한 작업과 절대 경로로 부른 작업의 결과가 같이 나옵니다. job.log와 abs.log 모두 demo-tool ok 23:30:01입니다. 다음 실행까지 56초를 기다려 얻은 결과라서, 손으로 부른 성공과 예약 작업의 성공이 섞이지 않습니다. 같은 crontab의 PATH 설정이 함께 적용된 실행이므로, 스크립트 내부 명령의 환경은 따로 봐야 합니다.

job.log와 abs.log 모두 demo-tool ok 23:30:01을 남겼으니, PATH를 명시한 예약 실행과 절대 경로 호출 모두 성공한 것입니다. 다만 절대 경로 작업도 PATH가 설정된 같은 crontab 안에서 돌았습니다. 이 결과로 말할 수 있는 범위는 출력에 나타난 성공까지이고, 절대 경로만 쓰면 모든 내부 명령의 환경 문제가 풀린다고까지 넓히지는 않습니다.
세 번째 처방: 래퍼 스크립트에 환경과 절차를 모으기
날짜 처리와 중복 실행 방지까지 필요하다면 래퍼 스크립트로 묶습니다. 점검 스크립트에서 쓴 /home/crondemo/job_wrapper.sh는 PATH를 export하고, flock -n으로 중복 실행을 막은 뒤 날짜와 demo-tool 결과를 찍습니다. 잠금을 못 얻으면 그대로 종료하는 구조라서, 실행을 건너뛴 경우와 정상적으로 수행한 경우를 구분해서 봐야 합니다. 이 내용은 뒤의 실행 기록에도 그대로 나오니 환경 준비와 실행 절차를 한 번에 대조하면 됩니다.
#!/bin/bash
export PATH=/home/crondemo/.local/bin:/usr/local/bin:/usr/bin:/bin
exec 9>/tmp/crondemo_job.lock; flock -n 9 || exit 0
echo "$(date +%F) $(demo-tool)"
crontab에는 래퍼의 절대 경로와 로그 위치만 남깁니다. 환경을 준비하는 부분과 실제 명령이 한 스크립트 안에 모여 있으니 추적하기도 편합니다. 날짜 형식은 스크립트 안에서 처리하므로 아래 예약 줄에는 퍼센트 기호가 들어가지 않습니다. 출력과 에러를 wrap.log로 모아 두면 예약 실행 결과를 그 작업의 기록에서 바로 찾습니다.
* * * * * /home/crondemo/job_wrapper.sh >> /home/crondemo/logs/wrap.log 2>&1
환경 설정, 잠금, 날짜 처리를 한 파일에서 관리하고 crontab의 작업 줄은 짧게 유지된다는 점이 래퍼의 강점입니다. 반대로 crontab 말고 래퍼 파일도 같이 관리해야 하고, 문제가 생기면 작업 줄과 스크립트 내용을 둘 다 열어 봐야 합니다.
PATH를 고친 뒤에는 날짜 문법과 로그를 점검합니다
날짜 형식의 퍼센트 기호를 구분합니다
PATH를 고쳤는데도 crontab의 명령 해석 때문에 로그가 아예 안 생기는 일이 있습니다. crontab에서 이스케이프하지 않은 퍼센트 기호는 줄바꿈으로 바뀌고, 그 뒤의 내용은 표준 입력으로 넘어갑니다. 날짜 명령을 그대로 넣으면 셸이 받는 명령이 의도와 달라지는 것입니다. 환경 설정과 명령 문법은 Debian crontab(5) 매뉴얼 (확인일 2026-10-15)에 정리되어 있습니다.
점검 스크립트는 날짜 명령을 그대로 넣은 작업, 백슬래시로 이스케이프한 작업, 래퍼를 부르는 작업을 나란히 돌립니다. 출력에는 pct.log가 없고 esc.log와 wrap.log만 있습니다. 이스케이프한 작업은 2026-10-15를, 래퍼는 같은 날짜와 demo-tool ok 23:31:01을 남겼습니다. 같은 예약 시점의 결과를 견주면 도구를 못 찾은 실패와 날짜 문법 문제가 깔끔하게 갈립니다.
$ sudo crontab -u crondemo -l
* * * * * echo "$(date +%F) direct" >> /home/crondemo/logs/pct.log 2>&1
* * * * * echo "$(date +\%F) escaped" >> /home/crondemo/logs/esc.log 2>&1
* * * * * /home/crondemo/job_wrapper.sh >> /home/crondemo/logs/wrap.log 2>&1
(다음 분 실행까지 56초 대기)
$ sudo ls $L; sudo cat $L/pct.log; sudo cat $L/esc.log $L/wrap.log
esc.log wrap.log
cat: /home/crondemo/logs/pct.log: No such file or directory
2026-10-15 escaped
2026-10-15 demo-tool ok 23:31:01
$ sudo cat /home/crondemo/job_wrapper.sh
#!/bin/bash
export PATH=/home/crondemo/.local/bin:/usr/local/bin:/usr/bin:/bin
exec 9>/tmp/crondemo_job.lock; flock -n 9 || exit 0
echo "$(date +%F) $(demo-tool)"
점검 캡처에는 esc.log와 wrap.log가 보이고, pct.log를 읽으려는 호출은 No such file or directory로 끝납니다. 이스케이프한 작업의 날짜는 2026-10-15, 래퍼의 성공 시각은 23:31:01입니다. 화면 아래쪽에 래퍼 내용도 그대로 있어서 PATH, 잠금, 날짜 처리를 출력과 맞대어 볼 수 있습니다. 파일을 못 만든 작업과 결과를 남긴 작업을 한자리에서 보면 퍼센트 기호가 어디서 해석되는지가 분명해집니다.

화면이 보여 주는 사실은 pct.log 파일이 생기지 않았다는 것 하나입니다. 이 줄에서는 퍼센트 기호 앞에서 명령이 잘려 셸 구문 오류가 나고, 뒤에 붙인 리다이렉트도 의도한 대로 넘어가지 않습니다. job.log 같은 파일에 에러가 남으리라는 기대가 빗나가는 이유입니다. 직접 보이는 증거는 파일이 없다는 점이고, 그 원인은 crontab이 퍼센트 기호를 처리하는 문법과 이어서 해석해야 합니다.
날짜 처리를 crontab에 유지하려면 퍼센트 기호 앞에 백슬래시를 붙입니다. 실행 기록의 이스케이프 작업은 2026-10-15 escaped를 남깁니다. 래퍼 방식은 날짜 명령을 스크립트 안에 두고, crontab에서는 파일 경로만 호출합니다. wrap.log에는 2026-10-15 demo-tool ok 23:31:01을 기록합니다.
날짜 명령을 작업 줄에 그대로 두고 싶다면 백슬래시 이스케이프로 그 줄만 고치면 되고, 간단한 날짜 출력이라면 바꿀 곳도 적습니다. 그러나 날짜 형식을 손볼 때마다 이스케이프 상태를 다시 확인해야 하므로, 처리 내용이 늘어나면 래퍼로 옮기는 쪽이 관리가 편해집니다.
성패를 가르는 것은 PATH와 완료 로그
환경을 업데이트할 때 cron 패키지 이름이 같다고 기존 실행 조건이 그대로일 거라 가정하지 않습니다. 운영 점검에서는 설치 경로, 실행 계정, PATH, 래퍼가 부르는 도구의 위치를 함께 맞춰 봅니다. 도구 위치가 바뀌었다면 crontab의 PATH, 절대 경로, 래퍼 내부 설정 가운데 실제로 쓰는 부분을 골라 고칩니다. 이번 기록에서 결정적이었던 것은 버전 번호의 차이가 아니라, 로그인 셸과 예약 작업이 서로 다른 환경을 받았다는 사실입니다.
헬스체크도 cron 프로세스가 떠 있는지만 볼 것이 아니라, 예정된 작업이 완료 로그를 갱신했는지까지 보게 짜 두어야 합니다. pct.log처럼 파일이 아예 안 생기는 경우가 있으니 로그의 존재와 마지막 성공 기록을 같이 봅니다. 래퍼의 잠금 때문에 실행을 건너뛴 경우와 작업이 끝난 경우도 갈라 두어야 감시 결과를 믿을 수 있습니다. 설정을 배포한 뒤 실제 예약 실행에서 환경과 완료 기록을 맞춰 보면, 경로 변경이 감시에 어떤 영향을 주는지까지 확인됩니다.
메일 대신 로그를 남기고 잠금 옵션을 함께 확인합니다
MTA가 없는 서버에서는 cron의 출력과 에러를 메일로도 받을 수 없으니, 작업 줄마다 >> 로그 2>&1을 붙여 남깁니다. 이번 실습도 그렇게 했습니다. 로그가 비어 있다면 작업 결과가 원래 없는 것인지, 리다이렉트까지 가지도 못한 것인지를 나눠 봐야 합니다. 날짜 명령을 붙인 뒤로 파일이 사라졌다면 PATH만 계속 바꿔 볼 일이 아니라 crontab의 특수문자 해석을 의심해야 합니다.
래퍼의 flock -n은 중복 실행을 막는 용도이고, 잠금 동작은 flock(1) 매뉴얼 (확인일 2026-10-15)에 설명되어 있습니다. PATH 문제를 푸는 설정과 중복 실행을 막는 설정은 목적이 다르니 따로따로 점검합니다. 성공 로그를 확인한 다음 아래 명령으로 실습용 crondemo의 crontab을 지웠습니다. 이 명령은 그 계정의 예약 작업을 전부 지우므로, 운영 계정에 쓰기 전에는 대상 계정과 작업 목록을 다시 보십시오.
sudo crontab -u crondemo -r
공용 도구는 PATH 한 줄, 감시·점검 작업은 래퍼 스크립트로
이번 장애의 출발점은 로그인 셸의 PATH와 cron이 넘긴 PATH의 차이였습니다. 명령 검색 경로를 명시한 뒤 성공 로그를 확인했고, 날짜 처리에서는 crontab의 퍼센트 기호 해석을 추가로 짚었습니다. 환경 덤프, 실제 에러, 수정 후 로그를 순서대로 놓고 보면 무엇을 왜 바꿨고 그 결과가 무엇인지가 한 줄로 이어집니다.
| 방법 | 적합한 상황 | 장점 | 관리할 부분 |
|---|---|---|---|
| crontab에 PATH 지정 | 같은 경로의 도구를 여러 작업에서 호출합니다. | 경로 설정을 모아서 관리합니다. | 설치 경로 변경을 반영합니다. |
| 절대 경로 호출 | 호출할 파일 위치가 명확합니다. | 실행 대상을 직접 표시합니다. | 파일 위치와 내부 명령 환경을 점검합니다. |
| 날짜 기호 이스케이프 | 간단한 날짜 처리를 작업 줄에 둡니다. | 해당 줄에서 문법을 수정합니다. | 날짜 형식 변경 때 이스케이프를 점검합니다. |
| 래퍼 스크립트 | 환경과 잠금, 날짜 처리를 함께 관리합니다. | 실행 절차를 파일에 모읍니다. | crontab과 래퍼 파일을 함께 관리합니다. |
사용자 도구를 여러 예약 작업에서 부른다면 crontab 첫 줄의 PATH 지정이 맞습니다. 실행 파일이 분명한 단순 호출은 절대 경로로 충분합니다. 사이트 감시나 예약 글 점검처럼 환경 준비, 날짜 처리, 중복 실행 방지가 한꺼번에 필요한 작업이라면 래퍼 스크립트를 권합니다. 어느 쪽을 고르든 터미널에서 성공한 것으로 끝내지 말고, cron이 남긴 실제 환경과 작업 로그로 결과를 확인합니다.
두 방식의 로그를 나란히 놓고 보면 사이트 감시와 예약 글 점검에는 경로 설정, 날짜 처리, 잠금을 한 파일에서 읽을 수 있는 래퍼 구성이 관리하기 편했습니다. 저는 crontab에는 래퍼의 절대 경로와 로그 위치만 두고, 환경 변경은 래퍼와 함께 검토하는 구성을 추천합니다. 배포한 뒤에는 다음 예약 실행에서 성공 기록이 갱신되는지 보고, 파일이 없다면 경로뿐 아니라 날짜 기호와 리다이렉트 위치도 의심합니다. 실시간 응답이 필요한 기능이라면 예약 시각을 기다리는 cron과 요청 처리를 떼어 상시 실행 서비스에서 처리하는 편을 검토하겠습니다. 이 테스트로 장비 사양을 정할 수는 없고, 실행 환경을 명시하고 작업 완료를 감시하는 절차를 운영 기준으로 삼는 것이 이번 결론입니다.






