VPS에 paperless-ngx 설치하기: Docker Compose 설정
VPS에서 paperless-ngx를 Docker Compose로 운영하는 방법입니다. 공식 Postgres 스택, PAPERLESS_URL, consume 폴더, OCR 언어, HTTPS와 백업 설정을 다룹니다.
구축하는 항목
VPS에서 Paperless-ngx를 실행하면 스캔한 문서를 검색 가능한 아카이브로 관리할 수 있습니다. 감시 디렉터리에 PDF를 넣으면 서버가 OCR(광학 문자 인식)을 실행하고, 텍스트를 추출하며, 날짜와 발신자를 추정한 후 문서를 분류합니다. 설치는 4개의 서비스로 구성된 Docker Compose 파일 하나로 진행합니다. 그 이후에는 구성이 필요합니다. 설치가 실패하는 대부분의 원인이 구성에 있으므로 이 가이드에서는 구성 방법을 주로 설명합니다.
Paperless-ngx는 기존 Paperless 프로젝트에서 파생되어 현재 유지 관리되는 커뮤니티 포크입니다. 무료로 사용할 수 있는 자체 호스팅 소프트웨어이며, 문서를 디스크의 일반 파일로 저장하므로 자신의 아카이브에 접근할 수 없게 되는 일이 없습니다. 가정용 장비 대신 VPS에서 실행하면 가정용 라우터의 포트를 외부에 열지 않고도 어디서나 스캔 문서에 접근할 수 있습니다. 또한 종이가 아닌 파일을 보관할 수 있는 비공개 Nextcloud 인스턴스와 함께 사용하기 좋습니다.
스택이 실제로 실행하는 구성
공식 compose 파일은 4개의 컨테이너를 시작합니다. 각 컨테이너의 역할을 알면 로그를 쉽게 이해할 수 있습니다.
webserver: paperless-ngx 이미지 자체입니다. 웹 인터페이스, API, 입력 폴더를 모니터링하는 consumer, OCR을 수행하는 Celery task worker를 실행합니다.db: PostgreSQL입니다. 메타데이터, 태그, correspondents, 전체 텍스트 검색 인덱스 테이블을 저장합니다. PDF는 저장하지 않습니다.broker: Redis와 호환되는 key-value 저장소인 Valkey입니다. 웹 프로세스와 worker 사이의 task queue입니다.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 액세스 권한이 있고 Docker와 Compose 플러그인이 이미 설치된 Ubuntu 24.04 KVM VPS가 필요합니다. 이 부분이 익숙하지 않다면 VPS용 Docker Compose 기본 사항부터 확인한 후 돌아옵니다.
- VPS를 가리키는 A 레코드가 있는 도메인 이름이 필요합니다. Paperless는 자신에게 알려지지 않은 호스트 이름으로 서비스를 제공하지 않으므로, 이 설정은 예상보다 먼저 필요합니다.
- 실제 제약은 메모리입니다. PostgreSQL, Valkey, gunicorn 및 Tesseract OCR worker가 동시에 상주하는 환경은 가벼운 사용량에서 2 GB로 작동합니다. 수백 개의 스캔 파일을 한꺼번에 가져올 계획이라면 4 GB를 할당합니다. 대용량 여러 페이지 PDF의 OCR 처리에서 메모리 사용량이 급증해 kernel의 out-of-memory killer가 worker를 종료할 수 있기 때문입니다.
- 디스크: 보관 파일은 원본 파일과 OCR 처리된 archive PDF로 2번 저장됩니다. 따라서 스캔 파일 크기의 대략 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라는 한 줄이 있습니다. 이 이름은 모든 컨테이너와 볼륨의 접두사가 됩니다. 따라서 이 파일을 삭제한 후 docker compose down -v이 데이터를 찾지 못한다고 당황하지 않도록 합니다.
첫 시작 전에 docker-compose.env 구성
2개의 설정은 선택 사항이 아닙니다. 프로젝트에서 문서화한 명령으로 비밀 키를 생성합니다.
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은 1시간을 절약해 주는 설정입니다. 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 폴더에 복사한 파일을 consumer가 읽을 수 없습니다. 가져오기 대신 로그에 권한 오류가 표시됩니다.
스택을 시작하고 첫 번째 사용자 생성
docker compose pull
docker compose up -d
docker compose run --rm webserver createsuperuser
docker compose logs -f webservercreatesuperuser에서 사용자 이름, 이메일 및 비밀번호를 입력하라는 메시지가 표시됩니다. 기본 로그인 계정이 없으므로 이 단계를 건너뛰면 아무 정보도 허용되지 않는 로그인 페이지에 도달합니다. 브라우저에서 접속하기 전에 서버가 port 8000에서 수신 대기 중이라는 로그 행이 표시될 때까지 기다립니다. 최초 시작 시 데이터베이스 마이그레이션도 실행되며, 1~2분 정도 걸립니다.
도메인을 사용하기 전에 로컬에서 확인합니다.
curl -I http://127.0.0.1:8000302에서 /accounts/login/로 redirect되면 스택이 정상적으로 작동하는 것입니다.
HTTPS 적용
기본 compose 파일은 8000:8000를 공개하며, 모든 인터페이스에 바인딩합니다. 공용 VPS에서 이 설정을 사용하면 주소를 찾은 누구에게나 전체 문서 보관소가 일반 HTTP로 제공됩니다. 포트 행을 변경하여 loopback에만 바인딩합니다.
ports:
- "127.0.0.1:8000:8000"그런 다음 reverse proxy에서 TLS(transport layer security)를 종료하고 127.0.0.1:8000로 전달합니다. 이 서버에서 실행하는 앱이 하나뿐이라면 ACME(automatic certificate management environment) client가 있는 proxy를 사용하면 됩니다. 하나의 certificate 설정 뒤에서 여러 container를 실행하는 경우 여러 Docker Compose 앱에 Traefik reverse proxy를 사용하는 방식을 따르고, webserver service를 proxy network에 연결하되 공개된 port는 전혀 지정하지 않습니다.
사용하는 proxy와 관계없이 X-Forwarded-Proto: https를 전송해야 합니다. 이 값이 없으면 Django는 요청이 HTTP를 통해 도착했다고 판단합니다. 그러면 login form의 origin 확인이 실패하고, 정상적으로 보이는 페이지에서 CSRF verification failed. Request aborted.가 발생합니다. 이 문제를 해결하려면 PAPERLESS_URL도 browser에서 입력하는 정확한 https:// address로 설정해야 합니다.
또한 proxy의 upload size limit를 높입니다. body 크기를 1 MB로 제한하는 proxy를 통해 40 MB scan을 업로드하면 paperless가 요청을 처리하기 전에 거부됩니다. 이때 browser에는 일반적인 upload failure가 표시됩니다.
consume 디렉터리 작동 방식
compose 파일은 compose 디렉터리의 ./consume를 컨테이너에 bind mount합니다. 이 위치에 넣은 파일은 가져온 후 해당 폴더에서 삭제됩니다. 이제 파일이 paperless에서 관리하는 media 볼륨에 저장되기 때문입니다.
cp ~/scan-2026-07-14.pdf ~/paperless/consume/
docker compose logs -f webserverconsumer가 파일 이름을 확인하고 OCR을 실행한 다음, 문서가 추가되었다고 보고하는 줄과 함께 처리를 완료하는 것을 확인할 수 있습니다. 1페이지 스캔은 전체 과정에 몇 초가 걸리며, 긴 문서는 1분 이상 걸릴 수 있습니다.
파일을 찾는 방식을 변경하는 설정은 2가지입니다. PAPERLESS_CONSUMER_RECURSIVE=true는 paperless가 하위 폴더도 검색하도록 하며, PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=true는 각 하위 폴더 이름을 tag로 사용하도록 합니다. 따라서 파일을 consume/invoices/2026/에 넣으면 invoices 및 2026 tag가 지정됩니다. 이보다 저렴하게 구축할 수 있는 filing system은 없습니다.
탐지는 또 다른 부분입니다. 기본적으로 PAPERLESS_CONSUMER_POLLING_INTERVAL는 0입니다. 즉, paperless는 즉시 발생하는 kernel filesystem 알림을 사용합니다. 이러한 알림은 network filesystem을 통과하지 않습니다. consume 폴더가 네트워크 스캐너에서 파일을 쓸 수 있는 NFS 또는 SMB 공유라면 아무것도 탐지되지 않습니다. 이 경우 interval을 양의 초 단위 값으로 설정하여 paperless가 대신 폴더를 스캔하도록 해야 합니다.
OCR 언어와 비용
PAPERLESS_OCR_LANGUAGE은 기본적으로 3글자 Tesseract 코드를 eng로 사용합니다. deu+eng와 같이 더하기 기호로 언어를 결합할 수 있습니다. 그러면 Tesseract는 각 언어를 시도하고 가장 나은 결과를 유지합니다. 따라서 언어를 하나 추가할 때마다 모든 페이지를 처리하는 데 필요한 CPU 시간이 늘어납니다. vCPU를 공유하는 VPS에서는 스캔이 10초 안에 끝나는지 1분이 걸리는지를 좌우할 수 있습니다. 문서가 실제로 작성된 언어만 나열합니다.
이미지에는 English, German, Italian, Spanish, French가 포함되어 있습니다. 그 밖의 언어를 사용하려면 공백으로 구분한 목록으로 PAPERLESS_OCR_LANGUAGES에 해당 언어를 추가합니다. 예를 들면 PAPERLESS_OCR_LANGUAGES=tur ces와 같습니다. 그런 다음 다시 시작합니다. 컨테이너는 시작할 때 Tesseract 데이터 팩을 다운로드하므로, 변경 후 첫 번째 부팅은 더 오래 걸립니다.
데이터베이스와 미디어 백업
PostgreSQL이 실행 중일 때 Docker 볼륨을 복사하면 복원되지 않을 수 있는 백업이 생성됩니다. Paperless에는 자체 exporter가 포함되어 있으며, 모든 메타데이터가 포함된 문서와 JSON manifest를 ./export bind mount에 기록합니다.
docker compose exec webserver document_exporter ../export --delete --no-progress-bar--delete는 현재 문서와 더 이상 일치하지 않는 export 파일을 제거합니다. 따라서 폴더가 계속 커지지 않고 미러 상태를 유지합니다. --no-progress-bar는 cron에서 실행할 때 출력 내용을 정리합니다.
새로운 stack에서 동일한 폴더를 기준으로 복원하려면 document_importer를 실행합니다. 따라서 안전하게 보관해야 하는 것은 export directory뿐입니다. VPS에서 암호화되고 중복 제거되는 restic 백업을 사용하여 일정에 따라 오프사이트에 전송합니다. 먼저 export를 실행해야 restic이 작성 중인 archive를 캡처하지 않습니다.
export/manifest.json가 존재하고 파일 수가 인터페이스에 표시되는 문서 수와 일치하는지 확인하여 백업을 검증합니다. 목록을 한 번도 확인하지 않은 백업은 백업이 아닙니다.
FAQ
도메인을 연결한 후 모든 페이지에서 "Bad Request (400)"이 반환되는 이유는 무엇입니까?
Django가 Host 헤더를 거부했습니다. 도메인이 ALLOWED_HOSTS에 포함되어 있지 않기 때문입니다. 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 컨테이너가 필요합니까?
Word, Excel 또는 OpenDocument 파일도 PDF와 함께 색인하려는 경우에만 필요합니다. 이 컨테이너는 해당 형식을 PDF로 변환하므로 paperless가 파일을 OCR 처리하고 검색할 수 있습니다. 또한 실행 중인 컨테이너 2개와 수백 MB의 메모리가 추가로 필요합니다. 보관하는 모든 파일이 이미 PDF 또는 이미지라면 소형 서버에서는 사용하지 않는 것이 좋습니다.