VPS에 Paperless-ngx 설치 및 Docker Compose 설정
VPS 환경에서 Paperless-ngx를 Docker Compose로 구축하는 방법을 설명합니다. 공식 Postgres 스택 구성, PAPERLESS_URL 설정, OCR 언어 적용 및 데이터 백업 전략을 포함하여 문서 관리 시스템을 안정적으로 운영하기 위한 핵심 가이드를 제공합니다.
구축할 시스템
VPS에서 구동하는 Paperless-ngx는 스캔한 문서 폴더를 검색 가능한 아카이브로 변환합니다. 감시 대상 디렉터리에 PDF를 넣으면 서버가 OCR(광학 문자 인식)을 실행하여 텍스트를 추출하고, 날짜와 발신자를 추론하여 파일을 분류합니다. 설치는 4개의 서비스로 구성된 하나의 Docker Compose 파일로 이루어집니다. 그 이후의 모든 과정은 설정이며, 이 가이드의 대부분을 설정에 할애하는 이유는 설치 오류가 주로 그 지점에서 발생하기 때문입니다. 이 시스템은 사진 라이브러리가 아닙니다. OCR과 발신자 추론 기능은 휴가철 JPEG 사진 폴더에는 아무런 도움이 되지 않으므로, 사진은 사진 전용 서버에 저장하고 Paperless는 문서용으로만 사용하십시오.
Paperless-ngx는 기존 Paperless 프로젝트에서 분기되어 커뮤니티가 유지 관리하는 프로젝트다. 무료로 사용할 수 있고 직접 호스팅할 수 있으며, 문서를 디스크에 일반 파일로 저장하므로 자체 문서 보관소에 접근하지 못하는 상황이 발생하지 않는다. 가정용 장비 대신 VPS에서 실행하면 홈 라우터의 포트를 열지 않고도 어디서든 스캔 문서에 접근할 수 있다. 또한 종이가 아닌 파일을 보관하는 비공개 Nextcloud 인스턴스와 함께 사용하기 좋다. 스캐너가 연결된 데스크톱에도 같은 원리를 적용할 수 있다. 해당 VPS에 직접 운영하는 RustDesk 릴레이를 두면 라우터에 포트를 열지 않고도 다른 곳에서 그 컴퓨터를 제어할 수 있다.
스택의 실제 실행 구성
공식 compose 파일은 4개의 컨테이너를 시작하며, 각 컨테이너의 역할을 이해하면 로그를 쉽게 파악할 수 있습니다.
webserver: paperless-ngx 이미지 자체입니다. 웹 인터페이스, API, 입력 폴더를 감시하는 컨슈머, 그리고 OCR을 수행하는 Celery 작업 워커를 실행합니다.db: PostgreSQL입니다. 메타데이터, 태그, 발신자 정보 및 전문 검색 인덱스 테이블을 보관합니다. PDF 파일은 여기에 저장되지 않습니다.broker: Valkey이며, Redis와 호환되는 키-값 저장소입니다. 웹 프로세스와 워커 사이의 작업 큐 역할을 합니다.gotenberg및tika: 선택 사항이며,-tikacompose 변형에서만 사용됩니다. Office 문서(.docx,.xlsx,.odt)를 PDF로 변환하여 paperless가 인덱싱할 수 있도록 합니다.
2026년 7월 기준으로 postgres compose 파일은 docker.io/library/postgres:18 및 docker.io/valkey/valkey:9-alpine 버전을 고정하며, 애플리케이션은 ghcr.io/paperless-ngx/paperless-ngx:latest에서 가져옵니다.
사전 요구 사항
- sudo 권한이 있는 Ubuntu 24.04 KVM VPS와 Docker Compose 플러그인이 설치되어 있어야 합니다. 이 과정이 처음이라면 VPS를 위한 Docker Compose 기초를 먼저 확인한 뒤 돌아오십시오.
- VPS를 가리키는 A 레코드가 설정된 도메인 이름이 필요합니다. Paperless는 설정되지 않은 호스트 이름으로 요청이 들어오면 응답을 거부하므로, 예상보다 일찍 이 설정을 마쳐야 합니다.
- 메모리가 가장 중요한 제약 사항입니다. PostgreSQL, Valkey, gunicorn, Tesseract OCR 워커가 동시에 실행되려면 가벼운 사용 환경에서도 2 GB의 메모리가 필요합니다. 수백 건의 스캔 문서를 한꺼번에 가져올 계획이라면 4 GB를 할당하십시오. 대용량 다중 페이지 PDF를 OCR 처리할 때 발생하는 메모리 급증으로 인해 커널의 OOM(out-of-memory) 킬러가 워커 프로세스를 종료시킬 수 있기 때문입니다.
- 디스크: 원본 파일과 OCR 처리된 아카이브 PDF가 모두 저장되므로, 스캔 데이터 용량의 약 2배를 할당하십시오.
공식 compose 파일 가져오기
대화형 설치 프로그램이 제공됩니다:
bash -c "$(curl --location --silent --show-error https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/install-paperless-ngx.sh)"이 프로그램은 질문을 통해 사용자를 대신하여 파일을 작성합니다. 하지만 직접 수동으로 설정하면 4개의 명령어를 실행하는 과정에서 모든 파일의 위치를 파악할 수 있으며, 이는 서버를 유지보수해야 하는 관리자에게 권장되는 방식입니다.
mkdir -p ~/paperless && cd ~/paperless
curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.postgres.yml
curl -fsSL -o docker-compose.env https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/docker-compose.env
curl -fsSL -o .env https://raw.githubusercontent.com/paperless-ngx/paperless-ngx/main/docker/compose/.env변형 파일들은 같은 디렉터리에 존재합니다: docker-compose.sqlite.yml, docker-compose.mariadb.yml, 그리고 각 파일의 -tika 버전이 있습니다. 신규 설치라면 postgres를 선택하십시오. SQLite는 수백 개의 문서를 다루기에는 적합하지만, 전체 텍스트 검색 인덱스 성능이 PostgreSQL보다 훨씬 빠르게 저하됩니다.
.env 파일에는 COMPOSE_PROJECT_NAME=paperless라는 한 줄의 내용이 들어 있습니다. 이 이름은 모든 컨테이너와 볼륨의 접두사(prefix)로 사용되므로, 파일을 삭제한 뒤 docker compose down -v에서 왜 데이터를 찾을 수 없는지 의문을 갖는 일이 없도록 주의하십시오.
첫 실행 전 docker-compose.env 설정하기
두 가지 설정은 선택 사항이 아닙니다. 프로젝트 문서에 명시된 명령어로 보안 키를 생성하십시오:
python3 -c "import secrets; print(secrets.token_urlsafe(64))"그런 다음 docker-compose.env 파일을 편집하십시오:
PAPERLESS_SECRET_KEY=<the long string you just generated>
PAPERLESS_URL=https://paperless.example.com
PAPERLESS_TIME_ZONE=Europe/Berlin
PAPERLESS_OCR_LANGUAGE=deu+eng
USERMAP_UID=1000
USERMAP_GID=1000PAPERLESS_SECRET_KEY은 기본값으로 change-me가 설정되어 있습니다. 이 값은 세션 쿠키를 서명하는 데 사용되므로, 기본값을 그대로 두면 해당 값을 아는 누구나 세션을 위조할 수 있습니다. 나중에 변경하면 모든 사용자가 로그아웃되므로 첫 실행 전에 반드시 설정하십시오.
PAPERLESS_URL은 시간을 절약해 주는 중요한 설정입니다. Paperless는 Django 애플리케이션이며, Django는 모든 요청의 Host 헤더를 검증합니다. PAPERLESS_URL를 설정하면 ALLOWED_HOSTS, CORS_ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS가 자동으로 채워집니다. 이 값을 비워둔 채 도메인을 서버에 연결하면 모든 페이지에서 Bad Request (400) 오류가 발생하며 컨테이너 로그에는 DisallowedHost 메시지가 기록됩니다. 설정 시 마지막에 슬래시(/)를 붙이지 말고 경로도 포함하지 마십시오.
USERMAP_UID 및 USERMAP_GID는 컨테이너가 실행될 사용자 계정을 지정합니다. id -u 및 id -g 명령어로 확인한 본인의 계정 정보와 일치시켜야 합니다. 일치하지 않으면 consume 폴더에 복사한 파일을 컨테이너가 읽을 수 없으며, 로그에는 가져오기 대신 권한 오류가 표시됩니다.
스택 시작 및 첫 번째 사용자 생성
docker compose pull
docker compose up -d
docker compose run --rm webserver createsuperuser
docker compose logs -f webservercreatesuperuser는 사용자 이름, 이메일, 비밀번호를 요구합니다. 기본 로그인 계정이 없으므로 이 단계를 건너뛰면 로그인 페이지에서 어떤 정보로도 접속할 수 없습니다. 브라우저로 접속하기 전에 서버가 8000 포트에서 대기 중이라는 로그 메시지가 출력될 때까지 기다리십시오. 최초 실행 시에는 데이터베이스 마이그레이션이 수행되며, 이는 1분에서 2분 정도 소요됩니다.
도메인을 연결하기 전에 로컬에서 확인하십시오:
curl -I http://127.0.0.1:8000302가 /accounts/login/로 리다이렉트되면 스택이 정상적으로 작동하는 것입니다.
앞단에 HTTPS 배치하기
기본 compose 파일은 8000:8000를 노출하며, 이는 모든 인터페이스에 바인딩됩니다. 주소를 알아낸 누구에게나 평문 HTTP로 전체 문서 아카이브를 제공하는 공용 VPS에서는 위험합니다. 포트 설정을 루프백에만 바인딩하도록 변경하십시오.
ports:
- "127.0.0.1:8000:8000"그다음 리버스 프록시에서 TLS(transport layer security)를 종료하고 127.0.0.1:8000으로 전달하십시오. 서버에 이 애플리케이션만 있다면 ACME(automatic certificate management environment) 클라이언트를 지원하는 어떤 프록시든 충분합니다. 하나의 인증서 설정 뒤에서 여러 컨테이너를 실행 중이라면 여러 Docker Compose 앱을 위한 Traefik 리버스 프록시 패턴을 따르고, webserver 서비스를 포트 노출 없이 프록시 네트워크에 연결하십시오.
어떤 프록시를 사용하든 X-Forwarded-Proto: https 헤더를 전달해야 합니다. 이 헤더가 없으면 Django는 요청이 HTTP로 들어왔다고 판단하여 로그인 폼의 origin 검증이 실패하고, 페이지는 정상적으로 보이지만 CSRF verification failed. Request aborted. 오류가 발생합니다. 이 문제를 해결하는 나머지 절반은 PAPERLESS_URL을 브라우저에 입력하는 정확한 https:// 주소로 설정하는 것입니다.
또한 프록시의 업로드 크기 제한을 상향 조정하십시오. 프록시가 본문 크기를 1 MB로 제한하면 40 MB 스캔 파일은 paperless에 도달하기도 전에 거부되며, 브라우저는 일반적인 업로드 실패 메시지를 표시합니다.
consume 디렉터리 작동 방식
compose 파일은 compose 디렉터리 내의 ./consume를 컨테이너 내부로 바인드 마운트합니다. 해당 폴더에 넣은 모든 파일은 가져오기 처리된 후 폴더에서 삭제됩니다. 파일이 이미 paperless 관리 하의 미디어 볼륨으로 이동했기 때문입니다.
cp ~/scan-2026-07-14.pdf ~/paperless/consume/
docker compose logs -f webserverconsumer가 파일명을 인식하고 OCR을 실행한 뒤, 문서가 추가되었다는 메시지를 출력하는 것을 확인할 수 있습니다. 한 페이지 분량의 스캔은 전체 과정이 수 초 내에 완료되지만, 긴 문서는 1분 이상 소요될 수 있습니다.
파일 탐색 방식은 두 가지 설정으로 변경할 수 있습니다. PAPERLESS_CONSUMER_RECURSIVE=true을 사용하면 paperless가 하위 폴더까지 탐색하며, PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=true를 활성화하면 각 하위 폴더명을 태그로 변환합니다. 예를 들어 consume/invoices/2026/에 파일을 넣으면 invoices 및 2026 태그가 자동으로 붙습니다. 이는 가장 효율적인 문서 분류 시스템이 될 것입니다.
나머지 절반은 감지 방식입니다. 기본적으로 PAPERLESS_CONSUMER_POLLING_INTERVAL은 0로 설정되어 있으며, 이는 paperless가 커널 파일 시스템 알림을 사용함을 의미합니다. 이 알림은 즉시 발생하지만 네트워크 파일 시스템을 통과하지는 못합니다. 만약 네트워크 스캐너가 파일을 저장할 수 있도록 consume 폴더를 NFS나 SMB 공유로 설정했다면 파일이 감지되지 않습니다. 이 경우 PAPERLESS_CONSUMER_POLLING_INTERVAL을 양수(초 단위)로 설정하여 paperless가 주기적으로 폴더를 스캔하도록 변경해야 합니다.
OCR 언어 및 비용
PAPERLESS_OCR_LANGUAGE은 3자리 Tesseract 코드를 사용하며, 기본값은 eng입니다. deu+eng와 같이 더하기 기호를 사용하여 언어를 조합할 수 있습니다. Tesseract는 각 언어를 시도한 뒤 최상의 결과를 유지하므로, 언어를 추가할 때마다 페이지당 CPU 소요 시간이 배로 늘어납니다. vCPU를 공유하는 VPS 환경에서는 스캔 완료 시간이 10초에서 1분으로 늘어나는 차이가 발생할 수 있습니다. 문서에 실제로 사용된 언어만 나열하십시오.
이미지에는 영어, 독일어, 이탈리아어, 스페인어, 프랑스어가 포함되어 있습니다. 다른 언어가 필요한 경우 PAPERLESS_OCR_LANGUAGES에 공백으로 구분된 목록 형태로 언어를 추가하고(예: PAPERLESS_OCR_LANGUAGES=tur ces) 재시작하십시오. 컨테이너가 시작될 때 Tesseract 데이터 팩을 다운로드하므로, 변경 후 첫 부팅은 평소보다 느립니다.
데이터베이스 및 미디어 백업
PostgreSQL이 실행 중인 상태에서 Docker 볼륨을 복사하면 복구가 불가능한 백업이 생성될 수 있습니다. Paperless는 자체 내장된 익스포터를 제공하며, 이 도구는 문서와 모든 메타데이터가 포함된 JSON 매니페스트를 ./export 바인드 마운트 경로에 기록합니다:
docker compose exec webserver document_exporter ../export --delete --no-progress-bar--delete 플래그는 현재 문서와 일치하지 않는 내보낸 파일을 삭제하므로, 폴더가 무한정 커지지 않고 항상 최신 상태를 유지합니다. --no-progress-bar 플래그는 cron에서 실행할 때 출력 내용을 깔끔하게 유지합니다.
복구는 새로운 스택의 동일한 폴더를 대상으로 document_importer 명령을 실행하여 수행하며, 따라서 내보내기 디렉터리만 안전하게 보관하면 됩니다. VPS에서 암호화 및 중복 제거된 restic 백업을 사용하여 정기적으로 외부로 전송하십시오. 이때 restic이 작성 중인 아카이브를 캡처하지 않도록 반드시 익스포트를 먼저 실행해야 합니다.
export/manifest.json이 존재하고 파일 수가 인터페이스에 표시되는 문서 수와 일치하는지 확인하여 백업을 검증합니다. 한 번도 목록을 확인하지 않은 백업은 백업이라고 할 수 없습니다. 야간 내보내기가 조용히 실패하기 시작하는 경우는 더 심각합니다. 따라서 cron 작업이 종료 상태를 자체 ntfy 서버로 전송하도록 설정해야 합니다. 그러면 복원이 필요한 날이 아니라 작업이 실패한 주에 문제를 알 수 있습니다.
FAQ
도메인을 연결한 후 모든 페이지에서 "Bad Request (400)" 오류가 발생하는 이유는 무엇입니까?
도메인이 ALLOWED_HOSTS에 포함되어 있지 않아 Django가 Host 헤더를 거부했기 때문입니다. docker-compose.env에 PAPERLESS_URL=https://paperless.example.com를 설정하되 끝에 슬래시를 붙이지 마십시오. 그 후 docker compose up -d를 실행하여 컨테이너를 다시 생성해야 합니다. 실행 중인 컨테이너는 시작 시점의 환경 변수를 유지하므로, env 파일만 수정해서는 아무런 변화가 없습니다.
consume 폴더에 PDF를 넣었는데 아무 일도 일어나지 않습니다. 무엇이 문제입니까?
먼저 docker compose logs webserver를 확인하십시오. 권한 오류가 발생했다면 USERMAP_UID과 USERMAP_GID이 파일을 소유한 계정과 일치하지 않는 것이므로, 이를 수정하고 컨테이너를 다시 생성하십시오. 로그에 아무런 기록이 없다면 파일 이벤트가 전달되지 않은 것입니다. 이는 커널 알림이 네트워크 공유를 통과하지 못하기 때문에 발생합니다. PAPERLESS_CONSUMER_POLLING_INTERVAL을 30와 같은 값으로 설정하면 paperless가 30초마다 폴더를 스캔하게 됩니다.
PostgreSQL 대신 SQLite로 paperless-ngx를 실행할 수 있습니까?
네, docker-compose.sqlite.yml이 지원되며 메모리 사용량이 적어 소규모 VPS에 적합합니다. 다만 문서가 수천 건 이상으로 늘어나면 전체 텍스트 검색과 대량 태그 편집 속도가 눈에 띄게 느려지는 단점이 있습니다. 나중에 마이그레이션하려면 내보내기와 가져오기 과정이 필요하므로, 아카이브가 계속 커질 것으로 예상된다면 지금 PostgreSQL을 선택하십시오.
스캔 아카이브에는 실제로 어느 정도의 디스크 공간이 필요합니까?
원본 파일 크기의 약 2배가 필요합니다. Paperless는 원본을 그대로 유지하면서 검색 가능한 텍스트 레이어가 포함된 OCR 처리된 PDF와 작은 썸네일을 추가로 저장합니다. 200 KB 크기의 텍스트 전용 스캔본은 용량을 적게 차지하지만, 30 MB 크기의 컬러 계약서 스캔본은 약 60 MB를 차지합니다. 같은 디스크에 내보내기(export) 디렉터리까지 유지한다면 동일한 아카이브가 디스크에 3배로 저장되는 셈입니다.
Tika와 Gotenberg 컨테이너가 꼭 필요합니까?
PDF와 함께 Word, Excel 또는 OpenDocument 파일을 인덱싱하려는 경우에만 필요합니다. 이 컨테이너들은 해당 형식들을 PDF로 변환하여 paperless가 OCR을 수행하고 검색할 수 있게 합니다. 또한 두 개의 컨테이너가 추가로 실행되고 수백 MB의 메모리를 점유하므로, 저장하는 모든 파일이 이미 PDF나 이미지라면 소규모 서버에서는 제외해도 됩니다.