
# 서버를 재부팅해도 PM2가 살아남게 하기

서버가 재부팅됐는데 사이트가 돌아오지 않았습니다. 한 시간 뒤 누군가 배포를 돌리자 그제야 살아났습니다. 배포 문제처럼 보이지만 아닙니다. 그 배포가 한 일은 `pm2 start`를 손으로 한 번 더 실행한 것뿐입니다.

앱이 알아서 뜨게 하려면 서로 독립된 두 가지가 필요하고, 하나만 있으면 아무 소용이 없습니다.

## 두 가지

**`pm2 save`** 는 현재 프로세스 목록을 `~/.pm2/dump.pm2`에 씁니다. PM2가 되살리는 것은 그 파일에 든 것뿐입니다. `pm2 delete`와 `pm2 start`는 하면서 저장은 하지 않는 배포 스크립트는 오래된 덤프를 남깁니다. 한 번도 저장한 적이 없다면 아예 비어 있습니다.

**`pm2 startup`** 은 부팅할 때 `pm2 resurrect`를 실행하는 systemd 유닛을 설치합니다. 그 덤프를 읽는 쪽이 이 유닛입니다.

하나만 있으면 아무것도 띄우지 않는 부팅이 되거나, 아무도 읽지 않는 저장 목록이 됩니다.

## 함정

권한 없는 사용자로 `pm2 startup`을 실행하면 **아무것도 설치되지 않습니다**. systemd를 감지하고, 유닛을 설치할 명령을 만들어 화면에 출력한 다음 끝납니다. 출력은 길고 초록색이며, 마지막에 이 명령을 복사해 붙여넣으라는 줄이 붙습니다. 성공한 것처럼 읽힙니다. 실제로는 아무 일도 없었습니다.

그러니 root로 실행하고, 확인은 PM2가 출력한 내용이 아니라 systemd로 하세요.

## 올바른 순서

저장하기 전에 앱이 떠 있어야 합니다. 그렇지 않으면 빈 목록을 저장하게 됩니다.

```bash
# <user> 로 — 앱이 돌고 있는지 확인
pm2 status
```

```bash
# root 로 — 그 사용자용 유닛 설치
env PATH=$PATH:/usr/bin pm2 startup systemd -u <user> --hp /home/<user>
```

앞에 붙는 `env PATH=$PATH:/usr/bin`은 장식이 아닙니다. root의 `PATH`에는 보통 Node가 없고, 유닛은 생성될 때의 경로를 그대로 기록합니다. 이게 없으면 유닛은 존재하지만 부팅 때 `node`를 찾지 못합니다.

```bash
# 다시 <user> 로 — 덤프 쓰기
pm2 save
```

## 각각 따로 확인하기

따로 망가지니 따로 확인해야 합니다.

```bash
systemctl is-enabled pm2-<user>
```

`enabled`가 나와야 합니다. `disabled`든 `Failed to get unit file state`든, 다른 결과는 유닛이 설치된 적이 없다는 뜻입니다. PM2가 뭐라고 했는지는 상관없습니다.

```bash
grep -o '"name":"[^"]*"' /home/<user>/.pm2/dump.pm2
```

여기에 `<app>`이 나와야 합니다. 파일이 없거나 grep이 아무것도 출력하지 않으면, 앱이 떠 있는 상태에서 `pm2 save`를 돌린 적이 한 번도 없는 것입니다.

## 재부팅 없이 시험하기

이게 되는지 알아보자고 운영 서버를 재부팅할 필요는 없습니다.

```bash
# <user> 로
pm2 kill
```

```bash
# root 로
systemctl start pm2-<user>
```

```bash
# <user> 로
pm2 status
curl -sf http://127.0.0.1:3000/health
```

`<app>`이 다시 온라인이면 두 가지 다 제대로 걸린 것입니다. 부팅할 때 일어나는 일이 정확히 이겁니다.

**`pm2 kill` 뒤에는 절대 `pm2 save`를 실행하지 마세요.** 데몬을 죽인 상태에서는 프로세스 목록이 비어 있고, 저장하면 그 빈 상태가 덤프에 그대로 들어가서 방금 시험하던 것을 스스로 지워버립니다. 습관적으로 쳐버렸다면 앱을 다시 띄우고 `pm2 save`를 다시 실행하세요.

## 배포 스크립트에 `pm2 save` 넣기

배포가 프로세스를 건드리는 순간부터 덤프는 어긋나기 시작합니다. 스크립트가 지우고 다시 띄우는 방식이라면 그다음에 저장해야 합니다.

```bash
pm2 delete <app> || true
pm2 start ecosystem.config.cjs --only <app>
pm2 save
```

`|| true`는 첫 실행에서 의미가 있습니다. 지울 것이 없으면 `pm2 delete`가 0이 아닌 코드로 끝나고, `set -e` 아래에서는 스크립트 전체가 거기서 멈춥니다.

마지막 줄이 없어도 위의 모든 게 계속 잘 돌아갑니다. 앱 이름을 바꾸거나 인스턴스 수를 바꾸거나 진입점을 갈아끼우는 배포가 오기 전까지는요. 그 뒤로 덤프는 더 이상 존재하지 않는 프로세스를 가리키고, 다음 재부팅은 그걸 되살립니다.

## 부팅 순서에 대해 한 가지

생성되는 유닛은 `After=network.target`입니다. 데이터베이스 뒤에 놓여 있지 않습니다. 콜드 부팅에서는 MariaDB나 PostgreSQL이 연결을 받기 전에 앱이 먼저 떠서, 첫 쿼리에서 실패하고 종료될 수 있습니다.

보통은 저절로 해결됩니다. PM2가 프로세스를 다시 띄우고, 두세 번째 시도쯤에는 데이터베이스가 준비돼 사이트가 올라옵니다. 알아보는 표시는 그 밖에는 멀쩡한 프로세스의 재시작 카운터가 0보다 큰 것으로, `pm2 status`의 `↺` 열에 나옵니다.

정말로 복구되지 않는다면 그때 순서를 명시하세요.

```bash
sudo systemctl edit pm2-<user>
```

```ini
[Unit]
After=network.target mariadb.service
```

직접 실패하는 것을 본 경우에만 넣으세요. 느린 서비스나 다른 장비에 있는 서비스 뒤에 유닛을 세우면, 재시도해서 살아나던 부팅이 그냥 멈춰 서 있는 부팅으로 바뀝니다.

[사이트 확인하기](/ko) · [다른 글 보기](/ko/blog) · [AgentReady 소개](/ko/about)
