Astro 배포 릴리스가 24개 쌓였습니다, 롤백용 5개만 남긴 과정
현재 릴리스와 최근 롤백 지점을 보호하면서 오래된 Astro 배포 폴더를 드라이런 후 정리하고 자동화한 실제 과정입니다.

개인 사이트 유지보수 점검을 하면서 서버에 배포 릴리스가 23개 남아 있다는 사실을 확인했습니다. 당장 디스크가 부족한 상태는 아니어서 그 자리에서 지우지는 않았습니다. 현재 사이트가 어느 폴더를 가리키는지, 실패했을 때 돌아갈 릴리스는 무엇인지 확인하지 않고 오래된 순서대로 삭제하는 편이 더 위험했기 때문입니다.
그 뒤 한 차례 배포가 추가되어 실제 정리를 시작할 때는 릴리스가 24개였습니다. 전체 크기는 약 355MB였고 서버 루트 디스크 사용률은 53%였습니다. 용량만 보면 긴급한 문제는 아니었습니다. 하지만 배포할 때마다 폴더가 하나씩 늘어나는데 보존 기준이 없다는 점은 분명했습니다.
그래서 이번에는 현재 공개 중인 릴리스와 최근 롤백 지점을 포함해 최신 5개만 남긴다는 기준을 정했습니다. 삭제 전에는 같은 계산을 드라이런으로 확인하고, 경로와 심볼릭 링크 조건을 모두 통과한 폴더만 정리했습니다. 결과적으로 오래된 릴리스 19개, 약 281MB를 제거했고 최신 5개는 그대로 남았습니다.
이 글은 서버에서 오래된 폴더를 찾는 일반적인 명령 모음이 아닙니다. minml의 실제 릴리스 구조에서 삭제 범위를 어떻게 제한했고, 다음 배포에서도 같은 기준이 자동으로 적용되도록 무엇을 바꿨는지 기록한 글입니다.
릴리스 폴더를 백업처럼 생각하고 있었습니다
minml은 새 글을 배포할 때 기존 공개 폴더를 덮어쓰지 않습니다. 완성된 정적 파일을 새로운 릴리스 디렉터리에 올린 뒤, current라는 심볼릭 링크가 새 릴리스를 가리키도록 전환합니다. 새 배포에 문제가 생기면 링크를 직전 릴리스로 돌려 빠르게 복구할 수 있습니다.
이 구조에서는 과거 릴리스가 남아 있는 것이 정상입니다. 문제는 몇 개를 남길지 정하지 않은 채 모든 배포본을 계속 보관한 것이었습니다.
처음에는 “많이 남겨두면 백업도 많아지는 것 아닌가?”라고 생각했습니다. 하지만 배포 릴리스에는 이미 빌드된 HTML, CSS, JavaScript와 이미지가 있을 뿐입니다. Markdown 원본, 설정 파일, 배포 스크립트의 변경 기록까지 온전히 복구하려면 Git 저장소와 별도로 보관한 이미지가 필요합니다.
즉 역할이 달랐습니다.
| 구분 | 주된 목적 | 보존 기준 |
|---|---|---|
| 최근 배포 릴리스 | 문제가 생겼을 때 빠른 서버 롤백 | 최근의 검증된 몇 개 |
| Git 저장소 | 글·설정·코드의 변경 기록 | 전체 이력과 별도 원격 저장소 |
| 별도 백업 | 서버·저장소 손상 때 복구 | 복구 시험을 포함한 보관 정책 |
Astro 블로그 백업과 복구 방법에서 정리했듯이, 같은 서버에 남아 있는 과거 배포본은 서버 자체가 손상되면 함께 사라질 수 있습니다. 릴리스가 24개라는 사실을 백업이 24개라고 해석해서는 안 됐습니다.
최신 5개를 남기기로 한 이유
5개가 모든 사이트에 맞는 정답은 아닙니다. 배포 빈도, 변경 규모와 장애를 발견하는 데 걸리는 시간에 따라 더 많이 또는 적게 남길 수 있습니다. minml은 한 사람이 운영하는 정적 블로그이고, 원본 변경은 Git에 남으며 배포 직후 주요 주소를 자동으로 확인합니다.
이 조건에서는 현재 릴리스 하나와 최근 네 번의 배포 결과를 남기면 일상적인 롤백에는 충분하다고 판단했습니다. 반대로 수십 개의 빌드 결과를 같은 서버에 계속 쌓아두는 것은 장기 백업 역할을 하지 못하면서 정리할 대상만 늘렸습니다.
보존 기준을 숫자만으로 적용하지는 않았습니다. 정리 전에 다음 조건을 확인했습니다.
- 릴리스 루트 바로 아래에 일반 디렉터리만 있는가
- 24개 디렉터리의 이름이 모두 정해진 타임스탬프 형식인가
- 각 릴리스에 홈페이지, 404와 사이트맵 파일이 있는가
- 그룹이나 기타 사용자가 쓸 수 있는 릴리스가 없는가
current가 릴리스 루트 안의 실제 디렉터리를 가리키는가- 현재 릴리스가 정렬상 가장 최신인가
검사 결과 24개 모두 필요한 파일과 권한 기준을 통과했고 현재 릴리스가 가장 최신이었습니다. 이 결과를 확인한 뒤에야 6번째 이후 릴리스를 삭제 후보로 계산했습니다.
현재 릴리스가 최신이 아니면 정리를 멈춥니다
현재 릴리스를 삭제하지 않는 조건만으로는 부족했습니다. 예를 들어 장애 때문에 관리자가 의도적으로 두세 단계 전 릴리스로 돌아간 상태라면, 단순히 이름이 최신인 폴더부터 5개를 남기는 방식이 현재 운영 상황과 어긋납니다.
그래서 정리 스크립트는 다음 조건을 모두 요구합니다.
base="/var/www/example"
release_root="$base/releases"
current="$(readlink -f "$base/current")"
case "$current" in
"$release_root"/*) ;;
*) exit 1 ;;
esac
위 경로는 공개용 예시입니다. 실제 스크립트는 프로젝트의 정해진 배포 루트만 사용합니다. current의 실제 경로가 릴리스 루트 밖이면 즉시 중단하고, 현재 릴리스의 이름이 가장 최신 릴리스와 다를 때도 삭제하지 않습니다.
이 조건 덕분에 수동 롤백 상태를 단순한 오래된 릴리스로 오해하지 않습니다. 현재 상태가 예상과 다르면 자동화가 판단을 계속하는 대신 사람이 먼저 이유를 확인하도록 했습니다.
GNU 문서에 따르면 심볼릭 링크는 대상 파일의 이름을 참조하며, 일반적인 파일 접근에서는 링크의 대상이 사용됩니다. 따라서 목록에 보이는 current 링크 자체보다 readlink -f로 확인한 실제 대상 디렉터리를 보호 기준으로 삼았습니다.
삭제 명령보다 앞에 드라이런을 기본값으로 뒀습니다
새로 만든 명령은 옵션 없이 실행하면 아무것도 삭제하지 않습니다.
npm run releases:prune
드라이런은 실제 릴리스 이름이나 서버 주소를 출력하지 않고 다음 집계만 보여줍니다.
mode=dry-run
before_count=24
retained_count=5
pruned_count=19
pruned_bytes=280751828
after_count=24
after_count가 여전히 24인 이유는 이 단계가 계산만 했기 때문입니다. 예상한 19개와 약 281MB가 맞는지 확인한 뒤에만 --apply 옵션을 붙였습니다.
npm run releases:prune -- --apply
적용 모드에서도 이름순으로 여섯 번째 이후라고 바로 지우지 않습니다. 각 후보의 실제 경로를 다시 계산하고, 부모가 정확히 릴리스 루트인지, 현재 릴리스와 다른지 확인합니다. 이름도 숫자와 UTC 표시로 구성된 정해진 형식만 허용합니다.
실제 제거에는 재귀 삭제가 필요하지만 다음 방어 옵션을 함께 사용했습니다.
rm -rf --one-file-system --preserve-root=all -- "$verified_release"
GNU rm 문서에서 --one-file-system은 재귀 삭제가 다른 파일시스템으로 넘어가는 일을 막고, --preserve-root=all은 명령 인자로 받은 경로 자체가 부모와 다른 파일시스템인 경우도 거부합니다. -- 뒤에는 옵션이 아니라 이미 검증한 하나의 대상 경로만 전달합니다.
이 옵션도 잘못된 경로를 자동으로 올바르게 만들어주지는 않습니다. 그래서 먼저 고정된 배포 루트, 한 단계 아래의 디렉터리, 이름 형식, 실제 부모 경로와 현재 릴리스 제외 조건을 확인하는 것이 핵심입니다.
24개에서 5개로 줄인 실제 결과
드라이런과 같은 결과를 확인한 뒤 적용 모드로 실행했습니다.
| 확인 항목 | 정리 전 | 정리 후 |
|---|---|---|
| 릴리스 개수 | 24개 | 5개 |
| 릴리스 전체 크기 | 약 355MB | 약 75MB |
| 제거한 릴리스 | - | 19개 |
| 확보한 크기 | - | 약 281MB |
| 루트 디스크 사용률 | 53% | 52% |
| 쓰기 가능한 경로 | 0개 | 0개 |
디스크 사용률은 1%포인트만 줄었습니다. 이번 작업의 의미를 “서버 용량 문제를 해결했다”고 과장할 수 없는 이유입니다. 아직 공간이 부족하지 않았고, 확보한 절대 용량도 약 281MB였습니다.
더 중요한 변화는 24개가 다시 40개, 50개로 늘어나기 전에 보존 기준과 실패 조건을 만들었다는 것입니다. 개인 사이트 유지보수 체크리스트에서 관찰 대상으로 남겼던 항목을 실제 정책으로 바꿨습니다.
삭제 후에는 폴더 개수보다 공개 사이트를 먼저 확인했습니다
오래된 릴리스를 잘 지웠더라도 현재 링크나 웹 서버가 깨졌다면 성공한 정리가 아닙니다. 적용 직후 다음 항목을 다시 확인했습니다.
| 확인 항목 | 결과 |
|---|---|
| 남은 릴리스 | 5개 |
current 대상의 필수 파일 |
정상 |
| 그룹·기타 사용자 쓰기 가능 경로 | 0개 |
| Nginx | 실행 중 |
| 홈페이지 | 200 |
| 사이트맵 | 200 |
| 존재하지 않는 시험 주소 | 404 |
| 통합된 과거 글 주소 | 301 |
| 같은 서버의 별도 사이트 | 200 |
다른 사이트는 정리 대상이 아니며 응답만 확인했습니다. 릴리스 정리 범위가 minml 전용 디렉터리를 벗어나지 않았는지 간접적으로 확인하기 위한 검사입니다.
배포 파일 권한을 755·644로 고친 과정에서 추가한 권한 검사도 그대로 유지됐습니다. 오래된 디렉터리를 정리했다고 새 릴리스의 권한 검증이나 공개 응답 검사를 줄이지 않았습니다.
다음 배포부터는 성공 확인 뒤에만 정리합니다
한 번 19개를 지워도 다음 배포마다 다시 쌓이면 같은 점검을 반복해야 합니다. 그래서 자동 배포 마지막에 릴리스 정리 스크립트를 연결했습니다.
현재 순서는 다음과 같습니다.
- 깨끗한 Git 작업 상태와 Astro 빌드 확인
- 새 릴리스 업로드
- 디렉터리
755, 일반 파일644적용과 쓰기 권한 검사 current를 새 릴리스로 전환- Nginx, 홈페이지, 새 페이지, 이미지, 사이트맵, 404와 301 확인
- 공개 검사가 모두 성공한 뒤 최신 릴리스 5개만 보존
정리 단계가 실패했다고 정상적으로 공개된 새 릴리스를 다시 이전 버전으로 돌리지는 않습니다. 사이트 배포와 오래된 파일 정리는 실패 영향이 다르기 때문입니다. 배포 결과는 유지하고 정리 실패를 경고로 남겨 별도로 확인합니다.
반대로 새 페이지나 공개 응답 검사에서 실패하면 릴리스 정리까지 도달하지 않습니다. 이때는 기존 릴리스로 복구하고 과거 롤백 지점도 그대로 남습니다. 전체 발행 흐름은 Astro 블로그를 작성하고 배포하는 과정과 연결됩니다.
릴리스 정리 전에 확인할 체크리스트
삭제 대상을 계산할 때
- 현재 사이트가 가리키는 실제 릴리스를 확인했는가
- 릴리스 루트가 예상한 절대 경로와 일치하는가
- 한 단계 아래의 일반 디렉터리만 대상으로 삼았는가
- 이름 형식이 예상과 다른 항목이 있으면 중단하는가
- 현재 릴리스가 최신이 아닐 때 자동 삭제를 거부하는가
- 보존 개수와 예상 확보 용량을 드라이런으로 확인했는가
실제 정리할 때
- 각 후보의 실제 부모 경로를 다시 확인하는가
- 현재 릴리스를 후보에서 명시적으로 제외하는가
- 다른 파일시스템으로 넘어가지 않는 옵션을 사용하는가
- 검증되지 않은 경로나 와일드카드를 삭제 명령에 넘기지 않는가
- 정리 도중 하나라도 실패하면 즉시 멈추는가
정리한 뒤
- 남은 릴리스 개수와 크기를 다시 측정했는가
-
current링크와 필수 HTML이 그대로 있는가 - 파일 권한 검사 결과가 정상인가
- 홈페이지, 사이트맵, 404와 리디렉션을 확인했는가
- 같은 서버의 다른 서비스가 영향을 받지 않았는가
많이 남기는 것보다 돌아갈 지점을 설명할 수 있어야 했습니다
이번 작업 전에도 사이트는 정상적으로 열렸고 디스크도 당장 부족하지 않았습니다. 그래서 오래된 릴리스 19개를 지운 것만 놓고 보면 작은 정리처럼 보일 수 있습니다.
하지만 보존 기준이 없으면 “혹시 필요할까 봐” 계속 쌓이게 되고, 정작 장애가 생겼을 때 어느 릴리스가 안전한지 알기 어렵습니다. 빠른 롤백용 릴리스와 장기 백업의 역할을 구분하고, 현재 상태가 예상과 다르면 삭제를 멈추도록 만든 것이 이번 작업에서 더 중요했습니다.
앞으로 minml은 공개 검증을 통과한 최신 5개 릴리스만 서버에 남깁니다. 더 오래된 상태가 필요하면 배포 폴더를 뒤지는 대신 Git과 별도 백업에서 소스와 이미지를 복원합니다. 많이 보관하는 것보다 무엇을 왜 남겼고 어떻게 돌아갈지 설명할 수 있는 상태가 운영에는 더 유용했습니다.