Node 판 설치하기
Node 판은 상시 도는 서비스로 동작합니다. 프로세스 하나, 디스크에 놓인 SQLite 데이터베이스 하나, 그 밖에 설치할 것은 없습니다 — 외부 데이터베이스도, 캐시도, 큐도 필요 없습니다. 이 안내서는 빈 디렉터리에서 운영 중인 설치본까지를 다루며, 대부분의 안내서가 건너뛰는 부분도 담았습니다. 시스템 서비스, 역방향 프록시, TLS, 백업, 갱신.
무엇이 필요한가#
| 항목 | 판 | 왜 |
|---|---|---|
| Node.js | 22 이상 | 제품은 내장 테스트 실행기와 비교적 새로운 인터페이스에 기댑니다. |
| C 컴파일러 | build-essential | 사용하는 플랫폼에 미리 만들어진 바이너리가 없으면 better-sqlite3는 설치할 때 컴파일됩니다. |
| 로컬 디스크 | — | SQLite와 올라온 파일을 위해. 네트워크 공유는 절대 쓰지 마십시오. 아래를 보십시오. |
코어 하나에 512 MB 메모리면 수십 명은 감당합니다. 중요한 것은 성능이 아니라 디스크입니다. 로컬이어야 하고, 백업이 되어 있어야 합니다.
1. 받아서 설치하기#
git clone <당신의-저장소> /var/www/toutadmin
cd /var/www/toutadmin
npm ci --omit=dev
npm install이 아니라 npm ci를 쓰십시오. 잠금 파일이 적은 그대로
설치하며, 그 파일을 결코 고쳐 쓰지 않습니다. 서버에서 슬쩍 어긋난 버전은 아무도 이유를 모르는
장애가 됩니다.
C를 컴파일하는 의존은 better-sqlite3 하나뿐입니다.
sudo apt install -y build-essential python3이면 거의 모든 경우가 풀립니다.
2. 설정하기#
cp .env.example .env
모든 설정은 환경 변수로 합니다 — .env에서 읽거나, 서비스 관리자가 넣어 줍니다.
필수인 것은 하나도 없습니다. .env가 없으면 설치본은 3000 포트로 뜨고 당신을
설치 마법사로 보냅니다.
| 변수 | 기본값 | 무엇을 하는가 |
|---|---|---|
PORT | 3000 | 귀 기울이는 포트. |
NODE_ENV | — | 운영에서는 production. 쿠키가 엄격해지고 자세한 흔적은 나오지 않습니다. |
SESSION_SECRET | 자동 생성 | 세션을 봉인합니다. 비워 두면 data/session.key에 만들어집니다. |
INSTALL_TOKEN | — | 설치 마법사가 설치 전에 요구합니다. 외부에 노출된 서버라면 권합니다. |
TRUST_PROXY | — | 믿을 만한 프록시 뒤에서만 1. 그 밖에는 절대 켜지 마십시오. |
DB_PATH | data/app.sqlite | 데이터베이스. |
UPLOAD_DIR, CV_DIR, VAULT_DIR, SIGN_DIR, DOCS_DIR, BACKUP_DIR | data/ 아래 | 파일 폴더: 사진, 이력서, 금고, 결재함, 받은 문서, 보관 파일. |
LOGIN_RATE_LIMIT | 10 | 주소마다 십오 분당 로그인 시도. |
GLOBAL_RATE_LIMIT | 300 | 주소마다 분당 요청. |
API_RATE_LIMIT | — | 토큰마다 분당 API 호출. |
SESSION_IDLE_MINUTES | 60 | 이만큼 놀려 두면 세션이 끊깁니다. |
SESSION_MAX_HOURS | 12 | 어떤 활동으로도 늘어나지 않는 절대 시간. |
ADMIN_EMAIL, ADMIN_PASSWORD | — | 화면 없는 설치: 시작할 때 관리자를 만듭니다. |
SESSION_SECRET을 바꾸면 모두가 한꺼번에 쫓겨납니다. 한 번만 정하고, 무작위로
만들고
(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"),
나머지와 함께 보관하십시오. git 저장소에는 절대 남기지 마십시오.
설치 토큰 두기#
첫 구동부터 당신이 설치 마법사에 들어가기까지, 그 설치본은 먼저 찾은 사람의
것입니다. 먼저 온 사람이 관리자 계정을 만들어 버립니다. 그러니 포트를 열기 전에
INSTALL_TOKEN을 두십시오.
node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"
# 그다음 .env에:
INSTALL_TOKEN=c3f1…
마법사는 이를 일정한 시간으로 견줍니다 — 보통 비교라면 토큰을 한 글자씩 알아맞힐 수 있기 때문입니다 — 그리고 거절할 때마다 기록을 남깁니다. 설치가 끝나면 마법사는 스스로 닫힙니다.
화면 없이 설치하기#
자동 배포에서는 ADMIN_EMAIL과 ADMIN_PASSWORD가 마법사를 거치지 않고
시작할 때 관리자를 만듭니다. 그다음에는 치우십시오. 서비스 환경에 놓인 비밀번호는 그 서비스를
읽을 수 있는 사람 모두가 읽습니다.
3. 권한, 그리고 데이터가 놓이는 곳#
sudo useradd --system --home /var/www/toutadmin --shell /usr/sbin/nologin toutadmin
sudo chown -R toutadmin:toutadmin /var/www/toutadmin/data
sudo chmod 750 /var/www/toutadmin/data
sudo chmod 640 /var/www/toutadmin/.env
코드는 읽기 전용으로 두어도 됩니다. 쓸 수 있어야 하는 것은 data/뿐입니다. SQLite는
옆에 딸린 파일도 씁니다(-wal, -shm). 중요한 것은 폴더이지
데이터베이스 하나가 아닙니다.
NFS와 SMB는 파일 잠금에 대해 거짓말을 합니다. SQLite는 두 곳이 동시에 쓰는 것을 막으려고 그 잠금에 기댑니다. 공유 위에서는 데이터베이스가 아무 말 없이 망가집니다. 언제나 로컬 디스크로 — 밖으로 나가는 것은 백업이지 데이터베이스가 아닙니다.
4. 서비스로 만들기#
손으로 띄우면 터미널을 닫는 순간 멈춥니다. systemd에 맡기십시오.
# /etc/systemd/system/toutadmin.service
[Unit]
Description=Toutadmin
After=network.target
[Service]
Type=simple
User=toutadmin
WorkingDirectory=/var/www/toutadmin
EnvironmentFile=/var/www/toutadmin/.env
ExecStart=/usr/bin/node src/server.js
Restart=always
RestartSec=5
# 이 서비스가 써야 하는 곳은 data/ 뿐입니다.
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/www/toutadmin/data
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now toutadmin
sudo systemctl status toutadmin
journalctl -u toutadmin -f
단단히 조이는 다섯 줄은 장식이 아닙니다. ProtectSystem=strict가 파일 체계 전체를
쓸 수 없게 만들고, ReadWritePaths가 반드시 써야 하는 폴더 하나만 다시 엽니다.
임의로 쓸 수 있게 되는 결함이 생겨도 닿는 곳은 data/뿐입니다.
5. 역방향 프록시#
3000 포트를 인터넷에 그대로 내놓지 마십시오. TLS를 하지 않고, 배워야 할 이유도 없습니다.
server {
listen 443 ssl http2;
server_name intranet.yeje.kr;
ssl_certificate /etc/letsencrypt/live/intranet.yeje.kr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/intranet.yeje.kr/privkey.pem;
client_max_body_size 20M; # 금고에 올리는 파일, 받은 문서
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name intranet.yeje.kr;
return 301 https://$host$request_uri;
}
아울러 서비스가 되돌이 주소에서만 귀 기울이게 하십시오. 그러면 3000 포트에 닿는 것은 프록시뿐이 됩니다.
TLS#
sudo certbot --nginx -d intranet.yeje.kr
제품은 연결이 암호화되는 즉시 Strict-Transport-Security 헤더를 붙이고, 평문에서는
결코 붙이지 않습니다. 암호화되지 않은 쪽에서 알려 봐야 읽히지 않을뿐더러, 로컬 시험을 여섯 달
동안 https 안에 가둬 버리기 때문입니다.
TRUST_PROXY는 프록시와 한 쌍입니다 — 그 밖에는 쓰지 마십시오
이 변수는 헤더에 실린 출발지 주소를 서버가 곧이듣게 만듭니다. nginx 뒤에서는 그래야 합니다 —
그러지 않으면 모든 요청이 127.0.0.1에서 온 것처럼 보이고, 주소별 한도가 아무것도
지키지 못합니다. 앞에 프록시가 없으면 반대입니다. 누구나 원하는 주소를 내세워 한도를
빠져나갑니다.
6. 설치 마법사#
당신의 도메인을 여십시오. 계정이 하나도 없는 동안에는 모든 주소가 /installation으로
넘어갑니다. 다섯 단계입니다.
- 이 설치본의 언어. 16개 가운데에서.
- 준비물. 확인해서 화면에 보여 줍니다.
- 회사: 여기저기 나타날 이름.
- 프리랜서가 아닌 새 직원에게 주는 연차 일수.
- 관리자 계정: 주소와 열두 자 이상의 비밀번호.
계정이 하나 생기는 순간 /installation은 로그인 화면으로 넘깁니다. 마법사가 스스로
닫힌 것이라, 손으로 지울 파일은 없습니다.
7. 주기적인 훑기#
PHP 판과 달리 따로 걸어 둘 것이 없습니다. 서버가 자체 스케줄러를 지니고, 한 시간마다 깨어나 여덟 가지 일을 합니다.
| 훑기가 하는 일 | 보장 |
|---|---|
| 계약이 끝난 계정을 닫습니다 | 로그인할 때와 대시보드를 열 때도 확인합니다 |
| 기한이 된 구독의 청구서를 냅니다 | 고유 색인이 두 번 청구되는 것을 막습니다 |
| 기한을 알림으로 바꿉니다 | 중복을 거르는 키가 있어 한 기한은 한 번만 알립니다 |
| 읽은 알림과 감사 기록을 정리합니다 | 정한 보관 기간에 따라 |
| 경리 우편함을 가져옵니다(IMAP) | 가져오기가 설정되어 있으면 |
| 웹훅 대기열을 재시도까지 포함해 비웁니다 | 다섯 번 시도한 뒤 포기 |
| 자동 백업을 만들어 바깥으로 보냅니다 | 정한 주기가 지났을 때 |
모든 작업이 몇 번을 해도 같은 결과입니다. 훑기를 다시 돌려도 두 번 청구하지 않고 두 번 알리지 않습니다. 그래서 아무 때나 마음 놓고 서비스를 다시 띄울 수 있습니다.
8. 다 도는지 확인하기#
# 테스트 (당신의 데이터베이스에는 쓰지 않습니다)
npm test
# 둘러볼 수 있는 시연용 설치본
node scripts/seed-demo.js
그다음 화면에서: 구성원을 하나 만들고, 그 계정으로 로그인하고, 첨부를 하나 올리고, 백업을 손으로 돌려 백업 화면에서 확인하십시오. 이 네 가지가 데이터베이스와 파일과 권한과 보관 파일을 모두 건드립니다.
9. 갱신하기#
# 1. 백업 먼저, 언제나 — '백업' 화면에서
# 2. 코드
cd /var/www/toutadmin
git pull
npm ci --omit=dev
# 3. 다시 띄우기; 데이터베이스는 시작할 때 스스로 옮겨 갑니다
sudo systemctl restart toutadmin
journalctl -u toutadmin -n 30 --no-pager
구조는 몇 번을 돌려도 같은 결과인 이행으로 나아갑니다. 갱신을 다시 돌려도 아무것도 깨지지
않습니다. data/와 .env는 결코 건드리지 않습니다.
되돌리기#
코드를 앞 판으로 돌리고 다시 띄우십시오. 이행은 열을 지우지 않으므로, 옮겨 간 데이터베이스도 앞 판에서 읽힙니다. 변경 기록에 따로 적힌 경우만 예외입니다. 미심쩍으면 1단계에서 만든 보관 파일로 되돌리십시오.
10. 제대로 백업하기#
tar.gz 보관 파일에는 데이터베이스와 다섯 개 파일 폴더가 들어갑니다. 데이터베이스는
SQLite의 온라인 백업으로 복사하므로 쓰는 도중에도 앞뒤가 맞는 사본이 나옵니다 — PHP 판은
VACUUM INTO로 같은 결과를 얻습니다. 파일마다 SHA-256 지문을 지니며, 되돌릴 때
대조합니다.
지켜야 할 서버에 그대로 남은 보관 파일은 아무것도 지키지 못합니다. 백업 화면에서 원격
보관처(FTPS 또는 Google Drive)를 설정하고, 바깥으로 보내기가 실패했을 때 알도록
sauvegarde.echec에 웹훅을 걸어 두십시오. 자세한 내용은
백업과 복원 문서에 있습니다.
먼저 로컬에서 해 보기#
npm install
cp .env.example .env
npm run dev # 고칠 때마다 다시 뜹니다
http://localhost:3000을 여십시오. 로컬에서는 NODE_ENV를 비워 두십시오.
production에서는 쿠키가 '보안' 표시를 달아 평문 연결에서는 보관되지 않고, 로그인
화면에서 제자리를 맴돌게 됩니다.
PHP 판으로 옮기거나, 거기서 오거나#
두 판본은 같은 구조 — 테이블 142개, 열 1311개 — 와 같은 비밀번호
형식을 씁니다. 한쪽을 멈추고 app.sqlite와 파일 폴더를 복사한 뒤 다른 쪽을 켜면
됩니다. 변환은 없습니다. 두 판본을 보십시오.
Toutadmin 문서 —— 2026-09-13에 생성. 소프트웨어와 분리된 독립 사이트입니다.