> ## Content Index
> Fetch the complete content index at: https://mlog.me/llms.txt
> Use this file to discover other available public pages before exploring further.

# Linux에서 파일을 지웠는데 용량이 그대로일 때: df·du 차이와 lsof +L1 진단
- URL: https://mlog.me/linux-df-du-disk-usage-deleted-open-files-lsof/
- Published: 2026-09-04T05:22:27.000Z
- Updated: 2026-09-04T05:22:27.000Z
- Description: 파일을 삭제했는데도 df 용량이 줄지 않을 때, lsof +L1로 열린 삭제 파일을 찾고 서비스 단위로 안전하게 공간을 회수하는 방법입니다.
- Author: mLog
- Tags: 서버·인프라, Ubuntu, Linux, Docker, #디스크-진단

Linux 서버에서 큰 로그 파일을 삭제했는데도 `df -h`의 사용량이 줄지 않을 때가 있다. 반면 `du`로 디렉터리를 더해 보면 문제의 크기만큼 파일이 보이지 않는다. 이 상황에서 파일을 더 지우거나 서버를 바로 재부팅하면 원인은 사라질 수 있지만, 어떤 프로세스가 공간을 붙잡고 있었는지 확인할 기회도 함께 사라진다.

핵심은 `df`와 `du`가 같은 질문에 답하는 명령이 아니라는 점이다.

- `df`는 **파일시스템 전체의 사용·가용 블록**을 보여준다.
- `du`는 현재 디렉터리 트리에서 **찾아갈 수 있는 파일과 디렉터리의 사용량을 합산**한다.
- 파일 이름이 디렉터리에서 사라져도 프로세스가 파일 디스크립터를 열고 있으면, 그 공간은 프로세스가 닫을 때까지 해제되지 않을 수 있다.

GNU `df` 설명은 지정한 파일이 속한 파일시스템의 공간을 보고한다고 정의한다. `lsof` 매뉴얼에서 `+L1`은 링크 수가 1보다 작은, 즉 연결이 끊겼지만 여전히 열린 파일을 선택한다. Linux의 `/proc/<PID>/fd/`에는 프로세스가 열고 있는 파일 디스크립터가 링크 형태로 나타난다.

이 글에서는 삭제를 더 진행하기 전에 범위를 고정하고, 열린 삭제 파일을 서비스나 컨테이너까지 추적한 뒤, 가장 작은 영향 범위로 공간을 회수하는 순서를 정리한다. 일반적인 `/var/log`, `/var/lib`, Docker 용량 분류부터 필요한 경우에는 [Ubuntu 서버 용량 부족을 안전하게 진단하는 순서](https://mlog.me/ubuntu-server-disk-usage-var-log-docker-cleanup/)를 먼저 참고한다.

> 아래 명령은 대부분 조회용이다. `systemctl restart`, `docker compose restart`는 서비스 중단을 일으킬 수 있으므로 실제 서비스명·이중화·점검창·백업 상태를 확인한 뒤 실행한다. 예시의 PID, 마운트 지점, 서비스명은 실제 환경 값으로 바꿔야 한다.

## 3줄 요약

- `df`는 파일시스템의 할당 상태를 보고하고, `du`는 이름으로 찾아갈 수 있는 파일을 순회해 합산하므로 두 값은 다를 수 있다.
- `sudo lsof -nP +L1`로 삭제됐지만 열린 파일을 찾고, PID가 어떤 systemd 서비스나 Docker 컨테이너에 속하는지 먼저 확인한다.
- 공간 회수는 무조건 재부팅하거나 파일 디스크립터를 직접 비우는 방식보다, 원인을 기록한 뒤 해당 서비스만 정상적으로 로그를 다시 열거나 점검창에서 재시작하는 방식이 안전하다.

## 먼저 확인할 것: 정말 같은 파일시스템을 비교했는가

`df /`와 `du /var`를 단순히 비교하면 범위부터 다르다. `/var`가 별도 마운트일 수도 있고, `/` 아래에 다른 파일시스템이 중첩돼 있을 수도 있다. 먼저 문제가 난 경로가 어느 파일시스템에 속하는지 확인한다.

```bash
findmnt -T /
findmnt -T /var
df -hT /
df -hT /var
df -i /

```

`df -hT`는 파일시스템 형식과 블록 사용량을 함께 보여준다. `df -i`는 공간이 아니라 inode 사용량을 확인한다. 작은 파일이 지나치게 많으면 바이트 여유가 있어도 inode가 고갈될 수 있으므로 `IUse%`도 따로 본다.

그다음 같은 파일시스템 안에서만 상위 디렉터리 사용량을 합산한다.

```bash
sudo du -xhd1 / 2>/dev/null | sort -h
sudo du -xhd1 /var 2>/dev/null | sort -h

```

여기서 `-x`는 다른 파일시스템으로 넘어가지 않게 한다. `-d1`은 한 단계 아래까지만 보여준다. `df`가 가리키는 마운트와 `du`가 순회하는 범위를 맞춰야 불필요한 오해를 줄일 수 있다.

`du` 결과에서 `.git`가 크게 보이는 문제라면 열린 삭제 파일과는 별개다. 이 경우에는 [.git 대용량 파일 진단과 안전한 정리](https://mlog.me/git-remote-remove-repository-size-cleanup/)처럼 Git 객체와 참조를 기준으로 확인한다.

## 왜 파일을 삭제했는데 공간이 남는가

일반 파일은 디렉터리 항목과 inode, 그리고 실제 데이터 블록으로 구성된다. 실행 중인 프로세스가 로그 파일을 열어 둔 상태에서 `rm`이나 잘못된 로그 정리 작업이 파일 이름을 제거하면 디렉터리에서 그 파일을 더는 찾을 수 없다. 그래서 `du`의 디렉터리 순회에는 나타나지 않는다.

그러나 프로세스가 기존 파일 디스크립터를 계속 들고 있으면 커널은 그 열린 파일을 즉시 폐기할 수 없다. 프로세스가 계속 쓰는 서비스라면 이름은 사라졌는데 사용량은 오히려 더 늘어날 수도 있다. 이때 파일시스템 블록은 아직 할당돼 있으므로 `df`에는 남는다.

이 현상은 `df`의 버그라고 단정할 일이 아니다. 서로 다른 관점의 결과다.

| 도구       | 주로 보는 대상                    | 열린 삭제 파일                   |
| -------- | --------------------------- | -------------------------- |
| df       | 파일시스템의 전체 블록 할당·가용 상태       | 공간이 해제되기 전까지 사용량에 포함될 수 있음 |
| du       | 현재 경로에서 이름으로 순회 가능한 파일      | 디렉터리 항목이 없어지면 보통 합산하지 못함   |
| lsof +L1 | 프로세스가 열고 있으며 링크 수가 1 미만인 파일 | 원인 PID와 파일 디스크립터를 찾는 데 사용  |

## 1단계: 변경하지 말고 현재 상태를 기록한다

디스크가 완전히 가득 차기 직전이라도 먼저 최소한의 증거를 남긴다. 출력 전체에 경로·사용자·프로세스 인수가 들어갈 수 있으므로 공개 게시판에 그대로 붙이지 않는다.

```bash
date -Is
hostname
df -hT
df -i
findmnt
sudo du -xhd1 / 2>/dev/null | sort -h

```

Docker를 운영한다면 Docker가 인식하는 사용량도 별도로 기록한다.

```bash
docker system df
docker system df -v
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'

```

Docker 공식 문서에서 `docker system df`는 Docker 데몬이 사용하는 공간을 표시하며, `-v`는 이미지의 공유·고유 크기와 컨테이너·볼륨 세부 정보를 보여준다. `du /var/lib/docker`와 숫자가 완전히 같아야 한다는 뜻은 아니다. Docker가 관리하는 논리 객체와 호스트 파일시스템 관점이 다르기 때문이다.

백업이나 복구 절차가 불명확한 상태에서 볼륨이나 데이터 경로를 먼저 건드리지 않는다. Ghost·MySQL처럼 상태를 가진 컨테이너라면 [백업 복원 검증과 Docker 실패 포렌식 절차](https://mlog.me/server-security-check-backup-restore-docker-forensics/)를 먼저 확인한다.

## 2단계: 삭제됐지만 열린 파일을 찾는다

가장 직접적인 조회는 다음과 같다.

```bash
sudo lsof -nP +L1

```

`-nP`는 호스트명과 포트명 변환을 생략해 조회 지연과 불필요한 이름 해석을 줄인다. `+L1`은 링크 수가 1보다 작은 열린 파일만 선택한다. 권한이 부족하면 다른 사용자의 프로세스나 파일이 빠질 수 있으므로 서버 전체를 볼 때는 `sudo`가 필요할 수 있다.

출력에서 주로 볼 열은 다음과 같다.

| 열        | 의미            | 확인 포인트                     |
| -------- | ------------- | -------------------------- |
| COMMAND  | 파일을 연 프로세스 이름 | 어떤 애플리케이션 계열인지             |
| PID      | 프로세스 ID       | 서비스·컨테이너 역추적 기준            |
| USER     | 프로세스 사용자      | 예상 계정인지                    |
| FD       | 파일 디스크립터      | 쓰기용인지, 파일 번호가 무엇인지         |
| DEVICE   | 파일시스템 장치      | 문제가 난 마운트와 같은 장치인지         |
| SIZE/OFF | 파일 크기 또는 오프셋  | 큰 항목을 우선 조사                |
| NLINK    | 링크 수          | 일반적인 열린 삭제 파일은 0으로 보일 수 있음 |
| NAME     | 기존 경로와 상태     | (deleted) 표시 확인            |

목록이 많으면 원본을 삭제하거나 수정하지 말고 우선 파일로 보존할 수 있다. 다만 이 출력에는 내부 경로와 프로세스 정보가 포함되므로 접근 권한을 제한한다.

```bash
sudo lsof -nP +L1 > "$HOME/lsof-unlinked-$(date +%Y%m%d-%H%M%S).txt"
chmod 600 "$HOME"/lsof-unlinked-*.txt

```

## 3단계: PID를 실제 서비스까지 역추적한다

`lsof`에 PID가 보였다고 곧바로 종료하지 않는다. 먼저 어떤 서비스가 소유하는지 확인한다.

```bash
PID='<ACTUAL_PID>'

ps -p "$PID" -o pid,ppid,user,lstart,comm,args
sudo readlink -f "/proc/$PID/exe"
sudo cat "/proc/$PID/cgroup"

```

Linux 커널 문서에 따르면 `/proc`에는 PID별 하위 디렉터리가 있고, `fd` 디렉터리는 그 프로세스가 열고 있는 파일 디스크립터를 담는다. 특정 PID의 링크를 조회하는 명령은 다음과 같다.

```bash
sudo ls -l "/proc/$PID/fd"

```

systemd가 관리하는 프로세스라면 해당 유닛을 찾는다.

```bash
systemctl status "$PID"

```

Docker 컨테이너 내부 프로세스로 의심되면 컨테이너의 호스트 PID와 대조한다.

```bash
docker inspect \
  --format '{{.State.Pid}} {{.Name}} {{.State.Status}}' \
  $(docker ps -q)

```

여기서 목적은 “PID를 죽이는 것”이 아니라 다음 질문에 답하는 것이다.

1. 어떤 서비스나 컨테이너가 파일을 열고 있는가?
2. 그 파일은 로그인가, 임시 파일인가, 데이터 파일인가?
3. 프로세스가 정상적으로 파일을 다시 열 수 있는 방법이 있는가?
4. 단일 인스턴스를 재시작하면 사용자 영향이 있는가?
5. 조치 전 백업이나 장애조치가 필요한 상태 저장 서비스인가?

## 4단계: 가장 작은 영향 범위로 파일을 닫는다

공간은 마지막 열린 참조가 닫혀야 실제로 해제된다. 가장 안전한 방법은 애플리케이션이 지원하는 공식 로그 재열기나 정상 종료 절차를 사용하는 것이다. 서비스마다 지원 신호와 명령이 다르므로 `kill -HUP`을 모든 프로세스에 공통 해법처럼 사용하면 안 된다.

systemd 서비스라면 설정과 영향 범위를 확인한 뒤 점검창에서 해당 유닛만 재시작한다.

```bash
sudo systemctl restart <ACTUAL_UNIT>
sudo systemctl --no-pager --full status <ACTUAL_UNIT>

```

Docker Compose 서비스라면 프로젝트 디렉터리와 서비스명을 확인하고 대상 하나만 재시작한다.

```bash
docker compose config --services
docker compose restart <ACTUAL_SERVICE>
docker compose ps

```

`docker compose restart`도 실제 서비스 중단을 만든다. 데이터베이스처럼 연결 복구와 준비 시간이 필요한 서비스는 애플리케이션 영향과 복제·백업 상태를 먼저 확인한다. 네트워크나 Compose 경계를 함께 점검해야 한다면 [NPM·Ghost·MySQL 네트워크 분리 구성](https://mlog.me/docker-compose-network-isolation-npm-ghost-mysql/)의 검증 순서를 참고한다.

## 5단계: 공간 회수와 서비스 정상 여부를 함께 검증한다

재시작 명령이 성공했다는 사실만으로 끝내지 않는다. 같은 열린 삭제 파일이 사라졌는지, `df`가 회복됐는지, 서비스가 실제 요청을 처리하는지 순서대로 본다.

```bash
sudo lsof -nP +L1
df -hT /
df -i /

```

그다음 서비스 종류에 맞게 검증한다.

```bash
sudo systemctl is-active <ACTUAL_UNIT>
sudo journalctl -u <ACTUAL_UNIT> --since '-10 min' --no-pager

```

Docker Compose라면 다음처럼 상태와 최근 로그 범위를 제한해서 본다.

```bash
docker compose ps
docker compose logs --since=10m <ACTUAL_SERVICE>

```

HTTP 서비스는 내부 헬스체크와 외부 HTTPS를 각각 확인한다. 데이터베이스는 단순 포트 연결뿐 아니라 실제 읽기·쓰기 또는 애플리케이션 쿼리가 정상인지 확인해야 한다.

검증 기준은 다음과 같다.

- 원인이던 PID 또는 파일 디스크립터가 더 이상 `lsof +L1`에 나타나지 않는다.
- 대상 파일시스템의 `df` 사용량이 예상 방향으로 감소한다.
- 서비스가 active/running 상태이고 재시작 반복이 없다.
- 최근 로그에 파일 권한, 로그 경로, DB 복구 관련 새 오류가 없다.
- 사용자 요청 경로와 백엔드 의존성이 정상이다.

## `lsof +L1`이 비어 있으면 확인할 다른 원인

열린 삭제 파일은 흔한 원인이지만 유일한 원인은 아니다. 결과가 비어 있으면 다음 분기로 이동한다.

### 1\. 다른 파일시스템을 섞어 비교했다

`findmnt -T <PATH>`로 경로별 마운트를 확인하고 `du -x`로 한 파일시스템에 한정한다. 바인드 마운트나 별도 데이터 볼륨 때문에 눈에 보이는 디렉터리 구조와 실제 파일시스템 경계가 다를 수 있다.

### 2\. inode가 고갈됐다

```bash
df -i
sudo find /var -xdev -type f -printf '%h\n' 2>/dev/null \
  | sort | uniq -c | sort -n | tail

```

바이트가 아니라 파일 수 문제라면 큰 파일을 찾는 접근만으로 해결되지 않는다. 캐시·세션·메일 큐·작은 로그 조각처럼 파일이 폭증한 디렉터리를 애플리케이션의 보존 정책으로 정리해야 한다.

### 3\. 스냅샷이 블록을 보존한다

Btrfs, ZFS, LVM 씬 프로비저닝 등은 스냅샷이 과거 블록을 계속 참조할 수 있다. 파일을 삭제해도 스냅샷 정책 때문에 기대만큼 줄지 않을 수 있다. 파일시스템을 먼저 식별하고 해당 기술의 공식 조회 명령으로 스냅샷을 확인한다. 일반적인 `rm`이나 Docker prune으로 해결하려 하지 않는다.

### 4\. Docker 이미지·빌드 캐시·컨테이너·볼륨이 실제로 크다

```bash
docker system df -v
docker ps -a --size
docker volume ls

```

이 경우에는 `df`·`du` 불일치라기보다 Docker가 관리하는 객체가 실제 공간을 사용하는 문제일 수 있다. Docker 문서상 `docker system prune`은 중지된 컨테이너, 사용하지 않는 네트워크, dangling 이미지와 빌드 캐시 등을 제거한다. `-a`와 `--volumes`를 붙이면 삭제 범위가 더 넓어진다. 운영 서버에서 다음 명령을 습관적으로 실행하지 않는다.

```bash
# 운영 서버에서 확인 없이 실행하지 말 것
docker system prune -af --volumes

```

먼저 `docker system df -v`와 객체 목록을 보고, 필요한 이미지·컨테이너·볼륨과 백업 상태를 식별한 뒤 종류별로 좁혀 처리한다.

## 하면 안 되는 응급조치

### `/var/lib/docker/overlay2`를 직접 지우기

Docker의 저장소 내부는 Docker 데몬이 관리한다. 디렉터리를 직접 삭제하면 이미지·컨테이너 메타데이터와 실제 레이어의 대응 관계를 깨뜨릴 수 있다. `du`에서 크게 보인다는 이유만으로 하위 파일을 직접 지우지 않는다.

### `/proc/<PID>/fd/<FD>`에 0바이트를 쓰기

인터넷에는 열린 파일 디스크립터를 `truncate`하거나 셸 리디렉션으로 비우는 방법이 나온다. 이것은 실행 중인 프로세스의 파일 오프셋과 애플리케이션 동작을 고려하지 않은 직접 변경이다. 데이터 파일이나 예상하지 못한 FD를 잘못 지정하면 손상 위험이 있다. 원인 식별과 서비스 정상 재열기·재시작을 우선한다.

### 원인을 확인하지 않고 `kill -9`

SIGKILL은 프로세스가 정리 작업을 수행할 기회를 주지 않는다. 연결 종료, 버퍼 flush, 잠금 해제, 데이터베이스 일관성에 영향을 줄 수 있다. 어떤 서비스인지 확인하고 그 서비스의 정상 종료 경로를 사용한다.

### 서버 전체를 바로 재부팅하기

재부팅하면 열린 파일 디스크립터가 닫혀 공간이 돌아올 수 있지만, 원인 로그와 프로세스 상태가 사라지고 전체 서비스가 중단된다. 디스크가 완전히 고갈돼 다른 조치가 불가능한 비상 상황이 아니라면 PID와 서비스부터 기록한다.

## 재발 방지: 로그 회전과 보존 한도를 명시한다

Docker 공식 문서에 따르면 기본 `json-file` 로깅 드라이버는 로그 회전이 기본 적용되지 않아 출력이 많은 컨테이너에서 디스크 고갈로 이어질 수 있다. Docker는 일반적인 환경에서 자동 회전을 제공하는 `local` 드라이버를 권장한다.

Compose 서비스별로 보존 한도를 명시하는 예시는 다음과 같다.

```yaml
services:
  app:
    image: your-image:<PINNED_TAG>
    logging:
      driver: local
      options:
        max-size: "20m"
        max-file: "5"

```

기존 `json-file`을 유지해야 한다면 회전 옵션을 명시한다.

```yaml
services:
  app:
    image: your-image:<PINNED_TAG>
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

```

Docker 문서에서 `log-opts` 값은 문자열로 지정해야 하며, 데몬 기본값 변경은 기존 컨테이너에 자동 적용되지 않는다. Compose 설정을 바꿀 때도 최종 모델을 확인하고 필요한 컨테이너를 계획적으로 재생성한 뒤 실제 드라이버를 검증한다.

```bash
docker compose config
docker inspect \
  --format '{{.Name}} {{.HostConfig.LogConfig.Type}} {{json .HostConfig.LogConfig.Config}}' \
  <ACTUAL_CONTAINER>

```

systemd journal도 사용량과 보존 정책을 확인한다.

```bash
journalctl --disk-usage
grep -R --no-filename -E '^(SystemMaxUse|SystemKeepFree|MaxRetentionSec)=' \
  /etc/systemd/journald.conf /etc/systemd/journald.conf.d/*.conf 2>/dev/null

```

`journalctl --vacuum-size=`와 `--vacuum-time=`은 오래된 archived journal을 대상으로 정리한다. 운영 보존 요구와 장애 분석 기간을 먼저 정한 뒤 사용하며, 명령을 매번 수동으로 실행하는 것보다 `journald.conf`의 한도를 명시적으로 설계하는 편이 재발 방지에 가깝다.

## 운영 체크리스트

- \[ \] `df -hT`와 `findmnt -T`로 문제 파일시스템을 특정했다.
- \[ \] `df -i`로 inode 고갈을 분리했다.
- \[ \] `du -x`로 다른 마운트를 제외하고 비교했다.
- \[ \] `lsof -nP +L1` 출력을 변경 전에 보존했다.
- \[ \] PID를 systemd 유닛 또는 Docker 컨테이너까지 역추적했다.
- \[ \] 열린 대상이 로그인지 데이터 파일인지 확인했다.
- \[ \] 점검창·백업·사용자 영향을 확인했다.
- \[ \] 애플리케이션 공식 로그 재열기 또는 정상 재시작 경로를 사용했다.
- \[ \] 조치 뒤 `lsof`, `df`, 서비스 상태, 최근 로그, 사용자 요청을 다시 확인했다.
- \[ \] Docker 또는 journald 로그 보존 한도를 설정했다.
- \[ \] `/var/lib/docker`, `/proc/<PID>/fd`, 볼륨 데이터를 직접 삭제하지 않았다.

## 마무리

파일을 삭제했는데 `df`가 줄지 않고 `du`에서는 파일이 보이지 않는다면, 더 많은 파일을 지우기 전에 **열린 삭제 파일**을 의심해야 한다. `df`는 파일시스템의 블록 상태를 보고하고, `du`는 현재 이름으로 찾아갈 수 있는 파일을 합산하기 때문에 두 결과가 달라질 수 있다.

안전한 해결 순서는 단순하다.

1. 같은 파일시스템을 비교한다.
2. `lsof +L1`로 열린 삭제 파일을 찾는다.
3. PID를 서비스나 컨테이너까지 역추적한다.
4. 원인과 영향 범위를 기록한다.
5. 해당 서비스만 정상 절차로 파일을 다시 열거나 재시작한다.
6. `df`, `lsof`, 서비스 상태와 실제 요청을 함께 재검증한다.

이 순서를 지키면 재부팅이나 광범위한 삭제로 증상을 가리는 대신, 왜 공간이 회수되지 않았는지 설명 가능한 상태로 복구할 수 있다.

## 공식 문서

- [GNU coreutils df 매뉴얼](https://manpages.debian.org/testing/coreutils/df.1.en.html?ref=mlog.me)
- [GNU coreutils du 매뉴얼](https://manpages.debian.org/testing/coreutils/du.1.en.html?ref=mlog.me)
- [lsof +L1 매뉴얼](https://man7.org/linux/man-pages/man8/lsof.8.html?ref=mlog.me)
- [Linux 커널 /proc 문서](https://docs.kernel.org/filesystems/proc.html?ref=mlog.me)
- [/proc/<PID>/fd 매뉴얼](https://man7.org/linux/man-pages/man5/proc%5Fpid%5Ffd.5.html?ref=mlog.me)
- [Docker docker system df](https://docs.docker.com/reference/cli/docker/system/df/?ref=mlog.me)
- [Docker 로깅 드라이버 구성](https://docs.docker.com/engine/logging/configure/?ref=mlog.me)
- [Docker json-file 로그 회전](https://docs.docker.com/engine/logging/drivers/json-file/?ref=mlog.me)
- [Docker local 로깅 드라이버](https://docs.docker.com/engine/logging/drivers/local/?ref=mlog.me)
- [systemd journalctl](https://www.freedesktop.org/software/systemd/man/latest/journalctl.html?ref=mlog.me)