Astro 사이트를 바로 덮어쓰지 않고 새 릴리스로 교체하는 이유
Astro 완성본을 새 릴리스에 업로드하고 검사한 뒤 current 심볼릭 링크를 교체해 안전하게 공개하는 실제 배포 순서입니다.

Astro 사이트의 배포 결과물은 HTML, CSS, JavaScript와 이미지로 이루어진 파일 묶음입니다. 그래서 가장 단순하게 생각하면 Nginx가 읽는 공개 폴더에 새 dist 파일을 그대로 복사하면 될 것 같았습니다.
하지만 파일은 한꺼번에 도착하지 않습니다. 수십 개의 HTML과 이미지가 차례로 복사되는 동안 홈페이지는 새 버전인데 연결된 파일은 아직 이전 버전이거나, 새 글의 HTML은 생겼지만 대표 이미지는 도착하지 않은 순간이 생길 수 있습니다. 업로드가 중간에 실패하면 그 혼합 상태가 더 오래 남습니다.
minml은 공개 폴더를 직접 덮어쓰는 대신 새 릴리스 디렉터리에 완성본을 전부 올리고 검사한 뒤, Nginx가 따라가는 current 심볼릭 링크만 새 릴리스로 교체합니다. 문제가 확인되면 같은 방식으로 직전 릴리스를 다시 가리킵니다.
이 글은 Astro 글을 작성하고 배포하는 전체 과정 가운데 서버에서 공개 대상이 바뀌는 순간만 자세히 다룹니다. 오래된 릴리스를 몇 개 남길지는 롤백용 릴리스 5개만 보관한 과정에서 별도로 설명했습니다.
공개 폴더를 직접 덮어쓰면 완성 시점이 모호합니다
정적 사이트라고 해서 파일 하나만 교체되는 것은 아닙니다. 글 한 편을 추가해도 해당 글의 HTML, 목록 페이지, 사이트맵, RSS와 대표 이미지 등이 함께 바뀔 수 있습니다. CSS나 JavaScript 파일 이름에 내용 해시가 들어간다면 새 HTML이 참조하는 새 파일도 같은 배포에 포함됩니다.
공개 폴더에 직접 복사할 때는 첫 파일이 도착한 순간부터 마지막 파일이 도착할 때까지 사이트 상태가 계속 달라집니다.
| 복사 중인 시점 | 방문자가 볼 수 있는 상태 |
|---|---|
| 새 HTML만 먼저 도착 | HTML이 아직 없는 새 이미지나 자산을 요청할 수 있음 |
| 새 자산만 먼저 도착 | 화면은 이전 HTML이지만 서버에는 두 버전의 파일이 섞여 있음 |
| 전송이 중간에 실패 | 어느 파일까지 바뀌었는지 따로 조사해야 함 |
| 일부 파일을 삭제하는 변경 | 이전 버전의 불필요한 파일이 공개 폴더에 남을 수 있음 |
파일 수가 적고 전송이 빠르면 이 구간이 짧을 수는 있습니다. 하지만 짧다는 것과 상태가 명확하다는 것은 다릅니다. 실패했을 때 이전 상태로 돌아가려면 어떤 파일이 덮어써졌고 어떤 파일이 남았는지 다시 계산해야 합니다.
반면 릴리스 방식은 공개 여부를 두 상태로 나눕니다. 새 파일 묶음을 준비하는 동안 방문자는 계속 기존 릴리스를 보고, 준비와 검사가 끝난 뒤에만 공개 대상이 새 릴리스로 바뀝니다.
새 릴리스는 공개 전 완성본을 두는 공간입니다
원리를 단순화하면 서버 구조는 다음과 같습니다.
<site-root>/
├─ releases/
│ ├─ <previous-release>/
│ └─ <new-release>/
└─ current -> releases/<previous-release>/
Nginx는 타임스탬프가 붙은 개별 릴리스 경로를 직접 바라보지 않고 current 아래의 파일을 제공합니다. 새 배포를 시작해도 current는 그대로이므로 새 릴리스에 파일을 복사하는 작업이 공개 사이트에 바로 드러나지 않습니다.
minml의 실제 배포 순서는 다음과 같습니다.
- Git 작업 상태가 깨끗한지 확인합니다.
- 로컬에서 Astro를 빌드하고 내부 링크와 이미지 경로를 검사합니다.
- 현재
current가 가리키는 직전 릴리스를 기록합니다. - 새 릴리스 디렉터리를 만들고
dist전체를 업로드합니다. - 새 릴리스의 필수 파일과 권한을 확인합니다.
- 임시 심볼릭 링크를 만든 뒤 그 링크를
current자리에 옮깁니다. - 공개 주소와 Nginx 상태를 검사합니다.
- 모두 정상이면 새 릴리스를 유지하고, 실패하면 직전 릴리스로 되돌립니다.
여기서 중요한 점은 업로드 완료와 공개 완료가 서로 다른 사건이라는 것입니다. 전송이 끝났더라도 검사를 통과하기 전까지는 새 릴리스를 공개하지 않습니다.
링크를 바꾸기 전에 파일과 권한을 먼저 확인합니다
새 디렉터리가 존재한다는 사실만으로 완성된 릴리스라고 판단하지 않습니다. 업로드 직후에는 공개 전 상태에서 다음 조건을 검사합니다.
| 확인 항목 | 중단하는 경우 |
|---|---|
| 홈페이지 파일 | index.html이 없음 |
| 오류 페이지 | 404.html이 없음 |
| 사이트맵 | sitemap-index.xml이 없음 |
| 디렉터리 권한 | 755가 아님 |
| 일반 파일 권한 | 644가 아님 |
| 쓰기 권한 | 그룹 또는 기타 사용자가 쓸 수 있는 경로가 하나라도 있음 |
Windows에서 전송한 디렉터리 권한을 그대로 믿지 않고 새 릴리스 안에서만 디렉터리는 755, 일반 파일은 644로 정규화합니다. 그 뒤 그룹이나 기타 사용자에게 쓰기 권한이 남은 경로가 없는지 별도로 검사합니다. 이 문제를 처음 발견하고 자동화한 과정은 Astro 배포 파일 권한을 755·644로 고친 기록에 정리했습니다.
이 단계에서 실패하면 current는 아직 이전 릴리스를 가리키므로 공개 사이트를 되돌릴 작업도 없습니다. 불완전한 새 릴리스가 서버에 생겼을 뿐, 방문자가 보는 경로에는 연결되지 않은 상태입니다.
current를 지웠다가 다시 만드는 방식은 사용하지 않았습니다
새 릴리스가 준비되면 임시 심볼릭 링크를 먼저 만들고, 그 링크를 기존 current 자리에 옮깁니다. 아래 코드는 실제 주소를 제거한 공개용 예시이며, RELEASE_ID는 배포 과정에서 생성된 값이라고 가정합니다.
base="/srv/example-site"
release="$base/releases/$RELEASE_ID"
temporary="$base/.current-$RELEASE_ID"
ln -s "$release" "$temporary"
mv -Tf "$temporary" "$base/current"
current를 먼저 삭제하고 새 링크를 만들면 두 명령 사이에 경로가 존재하지 않는 순간이 생깁니다. 대신 완성된 임시 링크를 같은 디렉터리에 만든 뒤 mv로 기존 링크를 교체하면, 같은 파일시스템에서 일반적인 이름 변경으로 처리되는 조건에서는 공개 링크가 비어 있는 중간 단계를 피할 수 있습니다.
GNU Coreutils 문서에서 ln -s는 대상 이름을 가리키는 심볼릭 링크를 만들고, 대부분의 파일 접근은 그 링크가 가리키는 대상으로 전달된다고 설명합니다. mv는 보통 이름 변경을 사용하지만 대상이 다른 파일시스템이면 복사와 삭제로 동작할 수 있습니다. 그래서 minml은 임시 링크와 current를 같은 배포 루트에 만들며, 이 글의 교체 방식도 같은 파일시스템이라는 조건을 전제로 합니다.
-T는 목적지를 디렉터리로 취급하지 않게 하고, -f는 기존 current를 교체할 수 있게 합니다. 특히 목적지가 디렉터리를 가리키는 심볼릭 링크일 때 해석이 달라질 수 있으므로, GNU 환경에서 mv -Tf로 링크 자체를 목적지로 명확히 지정했습니다.
최근 배포에서는 공개 전후를 따로 확인했습니다
이 구조는 설명용으로만 만들어둔 것이 아니라 현재 minml 배포에 사용하고 있습니다. 최근 새 글을 배포했을 때 Astro는 28개 페이지를 만들었고, 별도 검증에서는 발행 글 22개, 내부 경로 32개와 로컬 이미지 23개를 확인했습니다.
그다음 새 릴리스에 파일을 올려 권한 검사를 통과한 뒤 current를 전환했습니다. 공개 후 결과는 다음과 같았습니다.
| 공개 후 확인 항목 | 결과 |
|---|---|
| Nginx 서비스 | 실행 중 |
| 홈페이지 | 200 |
| 새 글 | 200 |
| 새 대표 이미지 | 200 |
| 사이트맵 | 200 |
| 존재하지 않는 시험 주소 | 404 |
| 통합된 과거 글 주소 | 301 |
| 같은 서버의 별도 사이트 | 200 |
홈페이지가 열린다는 사실만으로 새 배포가 완전하다고 판단하지 않았습니다. 새 글 HTML과 이미지를 각각 요청했고, 사이트맵과 오류 페이지, 기존 리디렉션도 함께 확인했습니다. 같은 서버의 별도 사이트는 변경하지 않고 응답만 확인해 배포 범위가 다른 서비스에 영향을 주지 않았는지 살폈습니다.
깨진 링크를 넣어도 Astro 빌드가 성공한 통제 시험에서 확인했듯이 빌드 성공은 모든 공개 경로의 정상 응답을 보장하지 않습니다. 그래서 로컬 산출물 검사와 실제 공개 주소 검사를 서로 대체하지 않고 순서대로 실행합니다.
공개 검사에 실패하면 직전 릴리스를 다시 가리킵니다
배포를 시작할 때 기존 current의 실제 대상을 먼저 기록하는 이유가 여기에 있습니다. 새 릴리스로 교체한 뒤 하나라도 공개 검사가 실패하면 다음 순서로 복구합니다.
- 기록해둔 직전 릴리스가 실제 디렉터리인지 다시 확인합니다.
- 직전 릴리스를 가리키는 임시 롤백 링크를 만듭니다.
- 그 링크를
current자리에 같은 방식으로 옮깁니다. current의 실제 대상이 직전 릴리스와 일치하는지 확인합니다.- 배포 명령을 실패로 끝내 원인을 숨기지 않습니다.
새 릴리스는 즉시 삭제하지 않습니다. 공개 대상에서 분리된 상태로 남기면 어떤 파일이나 검사에서 실패했는지 조사할 수 있습니다. 공개 검사를 통과하지 못했으므로 오래된 릴리스를 정리하는 단계도 실행하지 않습니다.
롤백은 문제가 생겼다는 사실을 없애는 기능이 아닙니다. 방문자에게 직전의 검증된 결과를 다시 제공하면서, 운영자가 실패 원인을 확인할 시간을 확보하는 장치에 가깝습니다.
원자적인 링크 교체도 모든 문제를 해결하지는 않습니다
current 교체는 업로드 중인 디렉터리가 그대로 노출되는 문제와 공개 경로가 비는 구간을 줄여줍니다. 그렇다고 배포 전체가 무조건 무중단이 되거나 새 콘텐츠가 올바르다는 뜻은 아닙니다.
- 교체 전에 이미 시작된 요청은 이전 파일을 읽을 수 있습니다.
- HTML을 받은 뒤 자산을 요청하는 사이에 링크가 바뀔 수 있으므로, 자산 파일명 충돌을 피하는 빌드 방식도 필요합니다.
- 브라우저나 CDN 캐시는 교체 후에도 이전 응답을 잠시 보여줄 수 있습니다.
- 잘못된 문장이나 노출되면 안 되는 정보도 정상적인 HTML이라면 그대로 배포될 수 있습니다.
- 서버, 저장장치 또는 Nginx 자체의 장애는 릴리스 링크만으로 복구되지 않습니다.
minml에서는 Astro 빌드가 만든 해시 이름의 CSS 자산을 사용하고, 배포 전 링크 검사와 기술 글의 개인정보·서버 정보 검토를 별도로 수행합니다. 릴리스 교체는 이 여러 안전장치 가운데 하나이지, 다른 검사를 생략할 수 있게 해주는 만능 장치는 아닙니다.
개인 사이트에는 공개 전과 공개 후를 나누는 것부터 유용했습니다
대규모 서비스의 배포 시스템을 그대로 흉내 내는 것이 목표는 아니었습니다. 한 사람이 운영하는 정적 블로그에서 필요했던 것은 다음 질문에 명확히 답할 수 있는 구조였습니다.
- 지금 방문자가 보고 있는 완성본은 어느 릴리스인가
- 새 파일은 모두 도착하고 검사됐는가
- 공개 대상을 바꾸는 동안 경로가 비지 않는가
- 새 글과 이미지가 실제로 응답하는가
- 실패하면 어느 완성본으로 돌아갈 것인가
새 릴리스 디렉터리와 current 링크는 이 질문들을 비교적 단순한 파일 구조로 해결했습니다. 업로드, 공개 전 검사, 링크 교체, 공개 후 검사와 롤백의 경계가 분명해졌고, 오래된 배포본도 최근 다섯 개라는 기준으로 관리할 수 있게 됐습니다.
앞으로도 minml은 공개 폴더에 파일을 하나씩 덮어쓰지 않습니다. 새 결과물을 별도 릴리스로 완성하고 권한과 필수 파일을 확인한 뒤, 공개 대상을 한 번에 교체합니다. 정적 사이트 배포에서 가장 도움이 된 변화는 복잡한 도구를 추가한 것이 아니라 완성하기 전에는 공개하지 않는 순서를 만든 것이었습니다.