BlueByte
GH001Fixed

GitHub: push 거부 — GH001 Large files detected (100 MB 초과)

작성 Haneul Seo2026년 9월 5일 업데이트8 min

안녕하세요, BlueByte입니다. push를 했더니 GitHub이 거부합니다: remote: error: GH001: Large files detected. 로컬 history는 멀쩡한데, 그 안 어딘가에 GitHub이 허용하는 한도보다 큰 파일이 있어서 서버가 push 전체를 거부한 것입니다. 증상은 push가 (pre-receive hook declined)로 끝나면서 GitHub 크기 한도를 넘는 파일을 지목하는 것입니다. 오늘은 한도가 얼마인지, 파일만 지우면 왜 안 되는지, 정확한 객체를 찾는 법, 파일이 어디 있느냐에 따라 두 가지로 고치는 법, 그리고 다시 안 생기게 막는 법까지 짚어보겠습니다.

"GH001: Large files detected"가 뜻하는 것

GitHub은 파일 크기 한도를 서버에서 강제하므로, 아무것도 저장되기 전에 push가 거부됩니다:

remote: error: GH001: Large files detected. You may want to try Git Large File Storage - https://git-lfs.github.com.
remote: error: Trace: 9f8c1a2b3c4d5e6f
remote: error: See https://gh.io/lfs for more information.
remote: error: File models/checkpoint.bin is 210.44 MB; this exceeds GitHub's file size limit of 100.00 MB
To github.com:acme/ml.git
 ! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs to 'github.com:acme/ml.git'

한도는 두 가지가 중요합니다. GitHub은 50 MiB를 넘는 파일에 경고를, 100 MiB를 넘는 파일은 완전히 차단합니다 — 지금 걸린 게 이 차단입니다. (웹 업로드 UI는 별도로 25 MiB에서 막힙니다.) GitHub은 또한 리포지토리를 1 GB 미만, 되도록 5 GB 미만으로 유지하길 권장합니다. 해결책은 파일을 history 안에서 100 MiB 아래로 낮추거나 Git LFS로 옮기는 것입니다.

파일을 지우고 다시 커밋해도 왜 안 되나

파일을 git rm하고 커밋한 뒤 다시 push하고 싶은 게 본능입니다. 하지만 안 됩니다. Git은 전체 history에 걸쳐 모든 파일의 모든 버전을 저장하고, push는 서버에 아직 없는 커밋을 전부 보냅니다. 큰 blob은 앞선 커밋에 여전히 있으므로 pre-receive hook이 계속 그것을 보고 계속 거부합니다. 고치려면 그 파일을 도입한 커밋에서 제거해야 합니다 — tip에서만 지우는 게 아니라요.

어느 커밋의 어떤 파일이 큰지 찾기

오류가 파일을 지목해 주지만, 여러 개라면 history에서 가장 큰 blob들을 나열합니다:

git rev-list --objects --all \
  | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
  | awk '/^blob/ {print $3, $4}' | sort -n | tail -5
54182391 src/data/sample.csv
220612334 models/checkpoint.bin

크기 단위는 바이트라서 220612334는 약 210 MB — 이게 범인입니다. 경로를 적어 두면 다음 단계에서 겨냥합니다.

최신 커밋에만 있다면: 빼고 다시 커밋

방금 파일을 커밋했고 브랜치를 아직 공유하지 않았다면, 커밋 하나로 끝납니다:

git rm --cached models/checkpoint.bin
echo "models/checkpoint.bin" >> .gitignore
git commit --amend -C HEAD
git push

--cached는 커밋에서 파일을 빼되 디스크에는 남깁니다; amend는 tip 커밋 하나를 blob 없이 다시 씁니다. 파일이 최근 여러 커밋에 걸쳐 추가됐다면 각 커밋을 손보는 것보다 아래의 migrate 방식이 안전합니다.

history 깊이 묻혀 있다면: git lfs migrate로 재작성

blob이 여러 커밋 전에 있다면, history를 다시 써서 한 번에 Git LFS로 옮깁니다:

git lfs migrate import --include="*.bin" --everything
git push --force-with-lease

git lfs migrate import는 history 전반에서 해당 파일들을 작은 LFS pointer 파일로 바꾸고 그 패턴을 .gitattributes에 기록합니다. 그래서 실제 바이트는 Git 객체가 아니라 LFS에 놓입니다. --everything은 모든 브랜치와 태그를 포함합니다. 이건 history를 다시 씁니다 — 변경 이후 모든 커밋의 SHA가 새로 바뀝니다 — 그래서 파괴적입니다: force-push해야 하고, 그 clone을 가진 다른 사람은 다시 clone하거나 hard-reset해야 합니다. 공유 브랜치에서 돌리기 전에 협의하세요. 파일을 보관하는 대신 아예 지우고 싶다면 git filter-repo --path models/checkpoint.bin --invert-paths로 제거합니다.

실제 사례: 지난주 커밋된 210 MB 체크포인트

동료가 다섯 커밋 전에 models/checkpoint.bin(210 MB)을 커밋해 feature 브랜치에 push했다가 GH001로 실패했습니다. 브랜치가 아직 공유 전임을 확인하고, git lfs install로 Git LFS를 설치한 뒤 git lfs migrate import --include="*.bin" --everything을 돌립니다. 명령이 그 다섯 커밋을 다시 쓰고, .gitattributes에 *.bin filter=lfs diff=lfs merge=lfs -text를 추가하며, blob 자리에 pointer를 남깁니다. 이제 git push --force-with-lease가 성공합니다 — GitHub은 pointer(수백 바이트)를 받고 실제 파일은 LFS에 저장합니다.

push가 성공하고 파일이 LFS로 추적되는지 확인

Git이 이제 그 파일을 LFS 객체로 다루는지 확인합니다:

git lfs ls-files
a1b2c3d4e5 * models/checkpoint.bin

여기 한 줄이 나오면 그 파일은 raw blob이 아니라 LFS가 뒷받침하는 pointer입니다. git cat-file -s HEAD:models/checkpoint.bin이 작은 크기 — pointer는 약 130 바이트입니다 — 를 돌려주면 history가 더 이상 전체 파일을 지고 있지 않다는 확인입니다.

큰 파일을 애초에 Git에 넣지 않기

무엇이 LFS에 속하는지 미리 정하고 첫 커밋 전에 추적하세요: git lfs track "*.bin"이 .gitattributes 규칙을 써 두어 새 큰 파일이 raw blob으로 Git history에 들어오지 않게 합니다. 빌드 산출물, 데이터셋, 모델 파일은 .gitignore에 넣어 실수로 커밋되지 않게 합니다. 버전 관리가 필요 없는 릴리스 바이너리는 커밋하지 말고 GitHub Release에 첨부하세요. 약 50 MB를 넘는 파일을 막는 pre-commit hook은 이 실수를 push 거부가 되기 전에 로컬에서 잡아 줍니다.

"pack exceeds maximum allowed size"와는 어떻게 다른가

GH001은 100 MiB를 넘는 단일 파일에 대한 것입니다. remote: fatal: pack exceeds maximum allowed size 같은 다른 거부는 한 파일이 아니라 push 전체의 크기에 대한 것입니다. 메시지가 파일과 파일당 한도를 지목하면 GH001이고 LFS나 history 재작성이 해결책입니다; pack 크기를 지목하면 한 번에 커밋을 더 적게 push하거나 push를 작은 덩어리로 나누세요.

관련 질문

파일을 지우고 커밋했는데도 push가 거부됩니다.

blob이 그 파일을 추가한 앞선 커밋에 여전히 있습니다. push는 history 전체를 보내므로 큰 객체가 그대로 남아 있습니다. 그것을 도입한 커밋에서 제거하세요 — tip이면 amend, 더 깊으면 git lfs migrate나 git filter-repo를 씁니다.

git lfs track과 git lfs migrate는 뭐가 다른가요?

git lfs track은 지금 이후에 커밋되는 파일에만 영향을 줍니다 — .gitattributes 규칙을 씁니다. git lfs migrate는 이미 커밋된 파일을 바꾸려고 기존 history를 다시 씁니다. 이미 커밋에 들어간 파일을 고치려면 migrate가 필요합니다.

git lfs migrate 후에는 반드시 force-push해야 하나요?

네. migrate는 history를 다시 써서 영향받은 커밋마다 SHA가 바뀌므로 일반 push는 non-fast-forward로 거부됩니다. git push --force-with-lease를 쓰고, 협업자에게 먼저 알리세요.

100 MiB 한도를 올릴 수 있나요?

아니요 — 일반 Git 객체에 대한 하드 서버 한도라 올릴 수 없습니다. 대신 큰 파일은 Git LFS로 버전 관리하거나, 릴리스 바이너리는 GitHub Release에 첨부하세요.

migrate하고 force-push하면 동료들 clone이 깨지나요?

동료의 기존 clone은 다시 쓴 history와 갈라집니다. force-push한 뒤 그들은 다시 clone하거나 git fetch 후 git reset --hard origin/<branch>를 해야 합니다. 진행 중인 작업이 사라지지 않게 타이밍을 협의하세요.

참고 자료

Haneul Seo

Infrastructure engineer · 10+ years running Linux fleets

같은 카테고리 다른 글

detected dubious ownershipFixed

Git: fatal: detected dubious ownership in repository

Git 2.35.2의 CVE-2022-24765 수정 이후, Git은 작업 트리나 .git 디렉터리의 소유자가 명령을 실행한 사용자와 다르면 저장소를 읽지 않습니다. 컨테이너, CI 작업, sudo 세션, 공유 드라이브에서 주로 나타납니다. 저장소가 내 것이어야 한다면 소유권을 바로잡고, 아니라면 global 설정의 safe.directory에 정확한 경로를 추가하세요 — Git은 이 설정을 저장소 자신의 설정에서는 무시합니다.

Git
failed calling webhookFixed

Kubernetes: Internal error occurred: failed calling webhook

admission webhook이 쓰기 요청 앞에 서 있는데 API server가 응답을 받지 못했고, 기본값인 failurePolicy: Fail이 그 침묵을 거부로 바꾼 것입니다. 메시지 끝부분이 곧 진단입니다 — context deadline exceeded는 호출이 도달하지 못한 것, no endpoints available은 떠 있는 게 없는 것, x509 줄은 API server가 webhook 인증서를 신뢰하지 않는 것입니다. 원인마다 해결이 다르고, 어느 것도 매니페스트 문제가 아닙니다.

Kubernetes
1205Fixed

MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

다른 트랜잭션이 쥐고 있는 행 잠금을 innodb_lock_wait_timeout 만큼 기다리다 포기한 것입니다. sys.innodb_lock_waits 가 막고 있는 세션을 지목하고 KILL 문까지 만들어 주며, blocking_query 가 NULL 이면 블로커가 열린 트랜잭션 위에서 놀고 있다는 뜻입니다. 재시도 로직이 가장 자주 틀리는 지점은 따로 있습니다. 기본값에서는 타임아웃된 문장 하나만 롤백되므로, 트랜잭션은 그대로 열린 채 앞서 잡은 잠금을 전부 쥐고 있습니다.

MySQL
MISCONFFixed

Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk

읽기는 되는데 모든 쓰기가 거부됩니다. 마지막 백그라운드 저장이 실패했고 stop-writes-on-bgsave-error 기본값이 yes 이기 때문입니다. 진짜 원인은 로그에 적혀 있습니다 — 디스크 공간 부족, redis 사용자가 쓸 수 없는 dir, rename 시점의 read-only 마운트, 또는 fork 가 Cannot allocate memory 로 실패하는 경우입니다. 원인을 고치고 BGSAVE 한 번만 돌리면 재시작 없이 쓰기가 돌아옵니다. rdb_last_bgsave_status 가 err 에서 ok 로 바뀝니다. stop-writes-on-bgsave-error no 는 쓰기를 즉시 되살리지만 스냅샷은 여전히 실패한 상태로 두므로, 해결이 아니라 의도한 맞교환으로 다뤄야 합니다.

Redis
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memoryFixed

Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (종료 코드 134)

V8 힙에는 시스템 메모리와 Node 릴리스에서 유도되는 자기만의 천장이 있고, 가진 RAM 보다 훨씬 낮은 경우가 흔합니다. 빌드나 서버가 거기 닿으면 V8 은 FATAL ERROR: Reached heap limit 과 종료 코드 134 로 스스로 중단합니다. v8.getHeapStatistics().heap_size_limit 으로 실제 한도를 읽은 뒤, 큰 작업은 --max-old-space-size(MiB)나 NODE_OPTIONS 로 올리고, 컨테이너 안에서는 cgroup 한도보다 낮게 잡고, 오래 도는 프로세스의 누수는 --heapsnapshot-near-heap-limit 으로 잡으세요. FATAL ERROR 줄 없이 137 로 끝나면 컨테이너 kill 이지 이 오류가 아닙니다.

Node.js
exec /docker-entrypoint.sh: exec format errorFixed

Docker: 컨테이너 시작 직후 "exec format error" — 플랫폼이 다른 이미지, 에뮬레이터 없음, 셔뱅 없는 스크립트

컨테이너가 첫 명령에서 exec format error 로 끝납니다. 커널의 ENOEXEC, 즉 파일은 있지만 여기서는 실행할 수 없다는 뜻입니다. 실제로는 한 CPU 아키텍처에서 빌드한 이미지(Apple 실리콘 Mac은 linux/arm64 를 만듭니다)를 binfmt_misc 에 QEMU 핸들러가 없는 다른 아키텍처(x86_64 서버)에서 돌리거나, 엔트리포인트 스크립트의 첫 줄이 셔뱅이 아닌 경우입니다. uname -m, docker image inspect, ls /proc/sys/fs/binfmt_misc 로 원인을 가르고, docker buildx build --platform 을 명시(또는 두 플랫폼 매니페스트 목록 발행)하거나, 에뮬레이션이 목적이면 QEMU 등록과 --platform, 스크립트라면 #!/bin/sh 한 줄로 해결합니다.

Docker