서버를 조금이라도 오래 굴리다 보면 “이 작업, 매일 새벽에 알아서 돌면 좋겠는데” 하는 순간이 옵니다. DB 덤프를 떠서 백업 디렉터리에 넣는 일, 쌓이는 로그를 지우는 일, 인증서 갱신을 확인하는 일 같은 것들입니다. 사람이 손으로 하면 반드시 까먹습니다. 그래서 리눅스에는 오래전부터 cron이라는 스케줄러가 붙어 있고, 지금도 서버 자동화의 첫 번째 선택지입니다.
요즘은 systemd timer도 있고 컨테이너 환경의 잡 스케줄러도 있습니다. 그런데 웹호스팅이나 회사 공용 서버처럼 내가 root가 아닌 환경에서는 여전히 crontab 하나만 열어 주는 경우가 많습니다. 그리고 꼭 알아둬야 할 게 하나 있습니다. cron은 정해진 시각에 명령을 대신 쳐 주는 알람 시계일 뿐, 그 명령이 제대로 돌아가는지는 전혀 책임지지 않습니다. 등록은 됐는데 아무 일도 안 일어나는 사고의 절반은 여기서 나옵니다.
한눈에 · crontab 위치와 -l/-e 확인 → 다섯 자리 시간 필드 작성 → 절대 경로와 로그 리다이렉션 붙이기 → 안 돌면 crontab -l → cron 로그 → 최소 환경 재현 순으로 좁히기
시작 전에: crontab 파일은 어디에 있고 누가 실행하나
알람 시계라고 했는데, 그 알람표가 사용자마다 따로 있다는 게 첫 번째 갈림길입니다. 내 계정으로 등록한 crontab은 내 권한으로 돌고, root가 등록한 건 root 권한으로 돕니다. “분명 등록했는데 안 돈다”는 문의 중 상당수가 sudo 붙여서 등록해 놓고 일반 계정에서 crontab -l을 확인하는 경우입니다.
그 전에 cron 데몬이 살아 있는지부터 봅니다. 서비스 이름은 배포판마다 다릅니다. 데비안·우분투 계열은 cron, RHEL·로키·CentOS 계열은 crond입니다.
systemctl status cron # Debian / Ubuntu
systemctl status crond # RHEL / Rocky / CentOS
Active: active (running) 이 보이면 정상입니다. 컨테이너 안에서는 아예 cron이 설치조차 안 된 경우도 흔하니 command not found가 뜨면 설치부터 해야 합니다.
crontab -l / -e / -r 기본 3종
일단 이 세 개만 기억하면 됩니다.
| 명령 | 하는 일 | 주의 |
|---|---|---|
crontab -l | 현재 계정의 등록 목록 출력 | 아무것도 없으면 no crontab for 계정명 |
crontab -e | 편집기로 열어서 수정 | 저장 후 installing new crontab 메시지 확인 |
crontab -r | 등록 내용 전체 삭제 | 확인 절차 없이 즉시 지웁니다 |
-r은 정말 조심해야 합니다. -e를 치려다 손이 미끄러져 -r을 누르면 그동안 쌓아 둔 스케줄이 한 번에 사라집니다. 되돌리는 명령도 없습니다. 그래서 뒤에서 백업 습관을 따로 이야기합니다.
crontab -e를 처음 실행하면 편집기를 고르라고 물어봅니다. 익숙하지 않은 에디터가 떠서 당황했다면 환경변수로 지정하면 됩니다.
export EDITOR=vim
crontab -e
계속 쓰려면 ~/.bashrc에 같은 줄을 넣어 두면 됩니다.
사용자 crontab과 /etc/cron.d의 차이
crontab이 저장되는 자리는 한 곳이 아닙니다. 차이는 사용자 필드가 있느냐 없느냐입니다.
| 위치 | 편집 방법 | 사용자 필드 |
|---|---|---|
| 사용자 crontab | crontab -e | 없음 (등록한 계정으로 실행) |
| root crontab | sudo crontab -e | 없음 (root로 실행) |
/etc/crontab | 직접 편집 | 있음 (시간 뒤에 실행 계정) |
/etc/cron.d/파일명 | 파일 추가 | 있음 |
/etc/cron.d에 파일을 넣을 때 사용자 필드를 빼먹으면, cron이 계정 이름 자리에 있는 걸 명령으로 해석해서 조용히 실패합니다. 형식 차이를 보면 이렇습니다.
# 사용자 crontab (crontab -e)
0 3 * * * /usr/local/bin/backup.sh
# /etc/cron.d/backup — 실행 계정이 한 칸 더 들어갑니다
0 3 * * * www-data /usr/local/bin/backup.sh
다섯 자리 시간 필드 읽고 쓰는 법
파일을 여는 법을 알았으니 이제 한 줄을 어떻게 채우는지 볼 차례입니다. cron 한 줄은 앞의 다섯 칸이 시간, 그 뒤가 실행할 명령입니다.
필드 순서와 범위
| 순서 | 필드 | 범위 | 비고 |
|---|---|---|---|
| 1 | 분 | 0-59 | |
| 2 | 시 | 0-23 | 24시는 없습니다 |
| 3 | 일 | 1-31 | |
| 4 | 월 | 1-12 | jan, feb 같은 이름도 대부분 지원 |
| 5 | 요일 | 0-7 | 0과 7이 모두 일요일 |
요일에서 0과 7이 둘 다 일요일인 건 역사적인 호환 때문입니다. 일요일을 0으로 쓰는 관습과 7로 쓰는 관습이 둘 다 있어서 양쪽을 받아 줍니다. 헷갈리면 sun, mon처럼 이름으로 써도 됩니다.
* , – / 네 가지 기호
기호는 사실상 네 개면 끝입니다.
*: 그 필드의 모든 값,: 나열 (0,30)-: 범위 (9-18)/: 간격 (*/5는 5마다)
시간 표기 예시 비교표
말로 설명하는 것보다 예시를 나란히 보는 게 빠릅니다.
| 표기 | 언제 실행되나 |
|---|---|
0 3 * | 매일 새벽 3시 정각 |
/5 * | 5분마다 |
0 9-18 1-5 | 평일 9시부터 18시까지 매 정시 |
0 0 1 | 매월 1일 자정 |
30 4 0 | 매주 일요일 새벽 4시 30분 |
0,30 | 매시 0분과 30분 |
여기서 /30과 0,30은 결과가 같습니다. /30은 0부터 59까지 중 30 간격, 즉 0분과 30분이라서 그렇습니다. 다만 */7처럼 60으로 나누어떨어지지 않는 값을 쓰면 0, 7, 14 … 56 다음에 다시 0으로 돌아가면서 마지막 간격만 짧아집니다. 이건 의도한 동작이 아닐 때가 많으니 주의하세요.

일·요일 동시 지정의 OR 동작
여기가 진짜 함정입니다. 일 필드와 요일 필드를 둘 다 *가 아닌 값으로 지정하면, cron은 두 조건을 AND가 아니라 OR로 봅니다.
0 0 1 * 1
“매월 1일이면서 월요일”이라고 읽기 쉽지만 실제로는 “매월 1일 또는 매주 월요일”입니다. 한 달에 다섯 번 넘게 돌 수도 있다는 뜻입니다. 둘 중 하나만 쓰고 나머지는 *로 두는 게 안전합니다. 굳이 AND가 필요하면 시간 필드는 넓게 잡고 스크립트 안에서 date +%u 같은 걸로 걸러내는 편이 낫습니다.
마지막으로 @daily, @hourly, @reboot 같은 특수 문자열도 있습니다. 편하긴 한데 구현체에 따라 지원 여부가 갈리고, 특히 @reboot은 호스팅 환경에서 막혀 있는 경우가 있습니다. 확실한 곳이 아니면 다섯 자리로 쓰는 걸 권합니다.
실행할 명령을 쓰는 요령, 여기서 대부분 깨진다
시간 표기를 맞게 썼는데도 안 돌아간다면 십중팔구 이 섹션의 문제입니다. cron은 우리가 SSH로 접속했을 때와 전혀 다른 환경에서 명령을 실행합니다.
PATH와 절대 경로
터미널에서 로그인하면 ~/.bashrc, ~/.profile 같은 파일이 읽히면서 PATH가 길게 채워집니다. cron은 그걸 읽지 않습니다. 보통 /usr/bin:/bin 정도의 아주 짧은 PATH만 가지고 시작합니다. 그래서 터미널에서는 잘 되던 php, python3, node가 cron에서는 command not found가 됩니다.
해결은 단순합니다. 실행 파일은 전부 절대 경로로 씁니다. 경로는 which로 확인합니다.
which php
# /usr/bin/php
which python3
# /usr/local/bin/python3
나온 값을 그대로 crontab에 붙여 넣습니다.
0 3 * * * /usr/local/bin/python3 /home/user/scripts/report.py
스크립트 안에서 상대 경로로 파일을 읽는 경우도 같은 이유로 깨집니다. cron이 스크립트를 실행할 때 현재 디렉터리는 보통 그 사용자의 홈입니다. 스크립트가 있는 폴더가 아닙니다. 그래서 이 패턴을 씁니다.
0 3 * * * cd /var/www/html && ./deploy.sh
그리고 스크립트 파일에 실행 권한이 없으면 아무 소리 없이 실패합니다. chmod +x /path/to/script.sh로 실행 비트를 켜 두세요. 권한 숫자가 헷갈린다면 chmod 755와 644의 실제 차이, 왜 폴더는 755여야 하나 쪽을 같이 보면 정리가 됩니다.
로그를 남기는 리다이렉션
cron 작업의 출력은 기본적으로 메일로 갑니다. 메일 서버가 없으면 그냥 버려집니다. 그래서 무슨 일이 있었는지 알 방법이 사라집니다. 등록할 때부터 로그를 남겨 두는 게 낫습니다.
0 3 * * * /usr/local/bin/backup.sh >> /var/log/mycron.log 2>&1
뒤쪽을 한 조각씩 뜯어보면 이렇습니다.
| 조각 | 의미 |
|---|---|
>> | 파일에 이어쓰기 (>는 매번 덮어씀) |
/var/log/mycron.log | 기록할 파일 경로 |
2>&1 | 표준에러(2)를 표준출력(1)이 가는 곳으로 합침 |
2>&1이 없으면 정상 출력만 저장되고 정작 필요한 에러 메시지는 사라집니다. 순서도 중요합니다. 2>&1 >> 파일처럼 앞에 쓰면 의도대로 동작하지 않으니 항상 리다이렉션 뒤에 붙이세요. 쌓인 로그에서 특정 줄만 뽑아 보는 요령은 grep으로 로그에서 원하는 줄만 뽑기, 실전 옵션 정리에 정리해 뒀습니다.
% 이스케이프 문제
cron 파일 안에서 %는 특별한 의미를 가집니다. 명령줄에서 첫 번째 %는 거기서 명령이 끝났다는 표시이고, 그 뒤는 표준입력으로 넘어갑니다. 사실상 줄바꿈처럼 취급됩니다.
그래서 날짜를 파일명에 넣는 흔한 패턴이 그대로는 깨집니다.
# 이렇게 쓰면 date 뒤가 잘려 나갑니다
0 3 * * * /usr/bin/mysqldump db > /backup/db_$(date +%Y%m%d).sql
# 백슬래시로 이스케이프해야 정상 동작합니다
0 3 * * * /usr/bin/mysqldump db > /backup/db_$(date +\%Y\%m\%d).sql
%가 여러 개면 전부 이스케이프해야 합니다. 매번 신경 쓰기 번거로우면 아예 스크립트 파일로 빼는 게 답입니다. 스크립트 안에서는 이스케이프가 필요 없습니다.
마지막 함정 하나. crontab 파일의 마지막 줄에 개행이 없으면 그 줄이 통째로 무시되는 구현이 있습니다. crontab -e로 편집하면 대개 알아서 붙여 주지만, 파일을 만들어 crontab 파일명으로 등록할 때는 끝에 빈 줄을 하나 남기세요.
등록했는데 안 돌아갈 때 확인 순서
위 원칙을 다 지켰는데도 안 돌면, 추측하지 말고 아래 순서대로 좁혀 나가는 게 빠릅니다. 순서를 지키는 게 핵심입니다. 로그부터 뒤지다가 사실은 등록조차 안 돼 있던 경우를 여러 번 봤습니다.

1단계: 정말 등록됐는지
crontab -l
crontab -e에서 편집하고 저장했는데 문법 오류가 있으면 cron이 거부합니다. 저장할 때 errors in crontab file, can't install 같은 메시지가 뜹니다. 그냥 지나쳤다면 등록이 안 된 상태입니다. no crontab for 계정명이 나오면 애초에 아무것도 없는 겁니다. sudo를 붙여 등록했다면 sudo crontab -l로도 확인하세요.
2단계: 로그 위치 찾기
cron은 작업을 실행할 때마다 시스템 로그에 흔적을 남깁니다. 위치가 배포판마다 다릅니다.
# Debian / Ubuntu
grep CRON /var/log/syslog
# RHEL / Rocky / CentOS
sudo tail -f /var/log/cron
# systemd 저널을 쓰는 환경
journalctl -u cron -n 50
journalctl -u crond -n 50
여기서 자주 보이는 게 이 메시지입니다.
(CRON) info (No MTA installed, discarding output)
에러가 아닙니다. “출력을 메일로 보내려 했는데 메일 전송 프로그램(MTA)이 없어서 버렸다”는 안내입니다. 즉 작업은 실행됐고 뭔가 출력했다는 뜻입니다. 그 내용을 보려면 앞에서 말한 >> 로그파일 2>&1을 붙여야 합니다.
3단계: 실행 기록은 있는데 결과가 없다
로그에 CMD (/usr/local/bin/backup.sh) 같은 줄이 찍혀 있다면 cron은 제 할 일을 한 겁니다. 문제는 명령 쪽입니다. 리다이렉션으로 남긴 로그 파일을 열어 보세요. command not found, Permission denied, No such file or directory 중 하나가 거의 확실히 들어 있습니다. 각각 절대 경로, 실행 권한, 작업 디렉터리 문제로 이어집니다.
4단계: 최소 환경에서 재현하기
터미널에서는 되는데 cron에서만 안 된다면, 내 셸 환경 덕을 보고 있던 겁니다. env -i로 환경변수를 전부 비우고 돌려 보면 cron과 비슷한 조건이 됩니다.
env -i /bin/sh -c '/usr/local/bin/backup.sh'
여기서 똑같이 실패하면 원인을 눈앞에서 확인할 수 있습니다. PATH가 필요하다면 스크립트 첫 줄 근처에 직접 선언해 주면 됩니다.
5단계: 타임존 확인
새벽 3시에 돌라고 했는데 엉뚱한 시각에 돈다면 서버 시간대를 봅니다.
date
timedatectl
Time zone: UTC 로 되어 있으면 한국 시각보다 9시간 느립니다. cron은 시스템 시간을 그대로 따르므로, 시간대를 바꾸거나 아예 UTC 기준으로 시각을 계산해서 넣어야 합니다. 참고로 타임존을 변경하면 이미 등록된 작업들의 실제 실행 시각이 전부 바뀝니다.
여기까지 봐도 원인이 안 잡히고, 특히 공유 호스팅이라면 그다음은 정책 문제일 수 있습니다. SELinux가 막고 있거나 호스팅사가 cron 실행 주기를 제한하는 경우입니다. 이건 사용자 쪽에서 판단하기 어려우니 로그 내용을 첨부해서 문의하는 게 빠릅니다.
다음에 할 일 체크리스트
원인 찾는 법까지 익혔다면, 이제 애초에 안 깨지게 쓰는 쪽으로 습관을 옮길 차례입니다. 아래 항목을 하나씩 적용해 보세요.
- [ ] 긴 명령은 crontab에 직접 쓰지 말고 스크립트 파일로 빼기. cron에는
0 3 * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1한 줄만 둡니다.%이스케이프 걱정도 사라집니다. - [ ] 스크립트 맨 위에 필요한 환경을 직접 선언하기.
#!/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
export LANG=ko_KR.UTF-8
- [ ] 지금 바로 백업 한 번 떠 두기.
-r사고를 되돌릴 유일한 방법입니다.
crontab -l > ~/crontab.bak
# 복구할 때
crontab ~/crontab.bak
- [ ] 오래 걸리는 작업이라면 중복 실행 방지 붙이기. 5분마다 도는 작업이 6분 걸리면 두 개가 겹칩니다.
flock을 쓰면 앞 작업이 끝나기 전에는 다음 실행이 그냥 넘어갑니다.
*/5 * * * * /usr/bin/flock -n /tmp/sync.lock /usr/local/bin/sync.sh >> /var/log/sync.log 2>&1
-n은 잠금을 못 잡으면 기다리지 않고 바로 종료하라는 옵션입니다.
- [ ] 백업 스크립트를 만들 계획이라면 압축 옵션과 디스크 여유를 같이 챙기기. tar 압축과 해제 옵션 정리, czvf xzvf 순서까지 헷갈리지 않게와 디스크 용량 부족, df와 du로 범인 찾아내는 순서가 바로 이어지는 내용입니다.
- [ ] 작업이 늘어나고 “A가 끝나야 B” 같은 의존관계가 생기면 systemd timer 검토하기. 실행 이력과 실패 상태를
systemctl status로 볼 수 있어서 관리가 편해지는 시점이 옵니다.
새 cron을 등록했다면, 진짜로 도는지 확인할 때까지는 끝난 게 아닙니다. 시간 필드를 잠깐 /1 *로 바꿔서 1분마다 돌려 보고 로그가 쌓이는 걸 눈으로 확인한 다음, 원래 시각으로 되돌리는 습관을 들이면 새벽에 안 돌아서 당황하는 일이 크게 줄어듭니다.

[…] 환경변수가 상속되지 않아 실패하는 경우가 있습니다. 이 부분은 crontab 작성법 정리, 등록해도 안 돌아가는 이유까지에 정리해 두었습니다. 그리고 도구가 여럿이면 도구마다 따로 발급하세요. […]