개요
사무직으로 일하다보면 PDF로부터 특정 페이지만을 추출하거나, 페이지의 순서를 바꾸거나, 다른 PDF와 병합하는 등, PDF자체를 다룰 일이 빈번하게 발생합니다. 워드나 HWP등의 문서 파일의 원본이 있다면 간단하게 해결할 수 있지만, 스캔파일이거나, 문서 원본 없이 PDF만 갖고 있을 때는 그렇게 간단하게 해결할 수 없습니다.
구글에 검색하면 iLovePDF같은, 웹 호스팅을 기반으로 약간의 무료 할당량을 제공해주는 서비스도 있고, PDFSam Basic같은 부분무료 소프트웨어를 사용할 수도 있습니다.
저는 홈서버의 남는 자원을 활용해 위에 언급한 것들보다 조금 더 많은 기능을 제공하는(OCR가능!!!) Stirling-PDF을 셀프 호스팅으로 구현해보겠습니다.
해당 프로그램은 도커 이미지를 제공하고 있고 ARM 이미지도 제공하므로, 오라클 A1이나 라즈베리파이에도 올릴 수 있습니다 +_+
도커로 설치하기
Stirling-PDF Github 레포지토리에서 docker compose를 제공합니다.
version: '3.3'
services:
stirling-pdf:
image: frooodle/s-pdf:latest
ports:
- '8080:8080'
volumes:
- ./trainingData:/usr/share/tessdata #Required for extra OCR languages
- ./extraConfigs:/configs
# - ./customFiles:/customFiles/
# - ./logs:/logs/
environment:
- DOCKER_ENABLE_SECURITY=false
- INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
- LANGS=en_GB
- 이미지 태그는 latest-fat, latest, latest-ultra-lite 총 3가지를 사용할 수 있으며, 버전 별 차이는 이 곳에서 확인할 수 있습니다.
적어도, OCR기능이나 PDF압축(용량 줄이기)등의 기능을 사용하기 위해선 fat버전을 사용해야 합니다. - DOCKER_ENABLE_SECURITY는 아래에서 설명할 OAuth를 위해서는 true가 되어야 합니다.
- volumes에서 ./trainingData은 OCR을 위한 데이터 볼륨으로 해당 데이터는 이 곳에서 다운로드 받을 수 있습니다.
- 그 외의 추가적인 설정은 ./extraconfigs에 settings.yml를 통해 구성할 수 있습니다.
이를 통해 로그인 실패 시 일정 시간 차단, OAuth를 통한 로그인 구성 등을 설정할 수 있습니다.
security:
enableLogin: false # set to 'true' to enable login
csrfDisabled: true # Set to 'true' to disable CSRF protection (not recommended for production)
loginAttemptCount: 5 # lock user account after 5 tries
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
loginMethod: all # 'all' (Login Username/Password and OAuth2[must be enabled and configured]), 'normal'(only Login with Username/Password) or 'oauth2'(only Login with OAuth2)
initialLogin:
username: '' # Initial username for the first login
password: '' # Initial password for the first login
oauth2:
enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
client:
keycloak:
issuer: '' # URL of the Keycloak realm's OpenID Connect Discovery endpoint
clientId: '' # Client ID for Keycloak OAuth2
clientSecret: '' # Client Secret for Keycloak OAuth2
scopes: openid, profile, email # Scopes for Keycloak OAuth2
useAsUsername: preferred_username # Field to use as the username for Keycloak OAuth2
google:
clientId: '' # Client ID for Google OAuth2
clientSecret: '' # Client Secret for Google OAuth2
scopes: https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile # Scopes for Google OAuth2
useAsUsername: email # Field to use as the username for Google OAuth2
github:
clientId: '' # Client ID for GitHub OAuth2
clientSecret: '' # Client Secret for GitHub OAuth2
scopes: read:user # Scope for GitHub OAuth2
useAsUsername: login # Field to use as the username for GitHub OAuth2
issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
clientId: '' # Client ID from your provider
clientSecret: '' # Client Secret from your provider
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
useAsUsername: email # Default is 'email'; custom fields can be used as the username
scopes: openid, profile, email # Specify the scopes for which the application will request permissions
provider: google # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
system:
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
enableAlphaFunctionality: false # Set to enable functionality which might need more testing before it fully goes live (This feature might make no changes)
showUpdate: true # see when a new update is available
showUpdateOnlyAdmin: false # Only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files
ui:
appName: '' # Application's visible name
homeDescription: '' # Short description or tagline shown on homepage.
appNameNavbar: '' # Name displayed on the navigation bar
endpoints:
toRemove: [] # List endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
groupsToRemove: [] # List groups to disable (e.g. ['LibreOffice'])
metrics:
enabled: true # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable
쿠버네티스(K8S)로 설치하기
기본 구성으로 실행하기
Stirling-PDF는 별도의 Helm Chart는 없습니다. 따라서 manifest를 직접 작성해야 합니다.
TrueNAS에서 OCR데이터와 log저장 경로를 미리 생성하고 CSI-NFS를 이용해 PV와 PVC로 사용하겠습니다.
먼저 OCR데이터를 다운받아 TrueNAS 저장소(NFS로 마운트하려는 폴더)에 옮겨준 뒤, SSH로 접속해서 압축을 풀어줍니다.
unzip tessdata-4.1.0.zip
그러면 아래처럼 자동적으로 폴더를 생성하고 압축이 해제됩니다.
...
inflating: tessdata-4.1.0/tel.traineddata
creating: tessdata-4.1.0/tessconfigs/
inflating: tessdata-4.1.0/tgk.traineddata
inflating: tessdata-4.1.0/tgl.traineddata
inflating: tessdata-4.1.0/tha.traineddata
inflating: tessdata-4.1.0/tir.traineddata
inflating: tessdata-4.1.0/ton.traineddata
inflating: tessdata-4.1.0/tur.traineddata
inflating: tessdata-4.1.0/uig.traineddata
inflating: tessdata-4.1.0/ukr.traineddata
inflating: tessdata-4.1.0/urd.traineddata
inflating: tessdata-4.1.0/uzb.traineddata
inflating: tessdata-4.1.0/uzb_cyrl.traineddata
inflating: tessdata-4.1.0/vie.traineddata
inflating: tessdata-4.1.0/yid.traineddata
inflating: tessdata-4.1.0/yor.traineddata
finishing deferred symbolic links:
tessdata-4.1.0/configs -> tessconfigs/configs
tessdata-4.1.0/pdf.ttf -> tessconfigs/pdf.ttf
심볼릭 링크를 생성하기 때문에, 윈도우에서 압축해제하고 붙여넣으려는 것보단, 저장소에서 직접 해제하는 것이 낫습니다.
이제 PV와 PVC를 마운트할 yaml을 작성합니다.
#! stirlingpdf-PVC.yaml
#OCR data PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-truenas-stirlingpdf-ocr
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
path: # NFS 공유 폴더 경로
server: # NFS 서버의 IP 주소
mountOptions: # 필요한 마운트 옵션
- rw
- vers=4.2
---
#OCR data PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-truenas-stirlingpdf-ocr
namespace: stirlingpdf
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi
volumeName: nfs-truenas-stirlingpdf-ocr
---
#logs PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-truenas-stirlingpdf-logs
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
path: # NFS 공유 폴더 경로
server: # NFS 서버의 IP 주소
mountOptions: # 필요한 마운트 옵션
- rw
- vers=4.2
---
#logs PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-truenas-stirlingpdf-logs
namespace: stirlingpdf
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi
volumeName: nfs-truenas-stirlingpdf-logs
컨테이너를 작성합니다.
#! stirlingpdf-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: stirlingpdf
namespace: stirlingpdf
spec:
replicas: 1
strategy:
type: RollingUpdate
selector:
matchLabels:
app: stirlingpdf
template:
metadata:
labels:
app: stirlingpdf
spec:
containers:
- name: stirlingpdf
image: frooodle/s-pdf:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: DOCKER_ENABLE_SECURITY
value: "false"
- name: INSTALL_BOOK_AND_ADVANCED_HTML_OPS
value: "true"
- name: LANGS
value: "ko_KR"
volumeMounts:
- name: nfs-truenas-stirlingpdf-ocr
mountPath: /usr/share/tessdata
- name: nfs-truenas-stirlingpdf-logs
mountPath: /logs
# resources:
# requests:
# memory: "256Mi"
# cpu: "250m"
# limits:
# memory: "256Mi"
# cpu: "250m"
volumes:
- name: nfs-truenas-stirlingpdf-ocr
persistentVolumeClaim:
claimName: nfs-truenas-stirlingpdf-ocr
- name: nfs-truenas-stirlingpdf-logs
persistentVolumeClaim:
claimName: nfs-truenas-stirlingpdf-logs
그리고 노출할 서비스와 ingress까지 작성합니다.
#! stirlingpdf-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: stirlingpdf
namespace: stirlingpdf
spec:
selector:
app: stirlingpdf
ports:
- protocol: TCP
port: 8080
targetPort: 8080
name: web
type: LoadBalancer
externalTrafficPolicy: Local
사설망 안에서 MetalLB를 활용해서 사용하려면 여기까지만 진행하시고 워커노드에서 8080포트를 방화벽에서 열어주거나, NodePort를 사용하면 됩니다.
저는 최종적으로 Authentik과 연결하여 외부 서비스로 노출할 계획이기 때문에 Ingress도 작성했습니다.
#! stirlingpdf-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: stirlingpdf-ingress
namespace: stirlingpdf
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/proxy-body-size: "0"
spec:
ingressClassName: nginx
rules:
- host: #사용할 외부 도메인
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: stirlingpdf
port:
number: 8080
여기까지만 진행하고 파드를 띄워주면 로그인 절차 없이 바로 영어로 된 메인화면을 만날 수 있습니다.
상단의 지구본을 클릭해 언어를 ‘한국어’로 변경하면 대부분의 항목이 한글로 번역되어 보입니다.
서비스를 외부에 노출하지 않고 내부에서만 사용한다면 이 상태만으로도 충분히 사용할 수 있습니다.
그렇지만, 저는 이 기능을 외부에서 사용하는 것이 목적이므로, 로그인 기능을 구현하고 Authentik과 연동하는 것까지 진행해 보겠습니다.
추가환경 구성하기
custom_settings 작성
Stirling-PDF에서 env로 지정할 수 있는 것은 딱 5개 뿐입니다. 그 외의 값은 yaml파일에 지정하여 사용합니다.
여기에는 2가지 방법이 있는데, settings.yml 파일을 작성하여 사용하는 방법과 custom_settings.yml을 작성하는 방법이 있습니다.
전자의 경우, 모든 설정을 사용자가 작성한 파일 값으로 사용하기 때문에, 빈 설정값이 있으면 안됩니다.
후자의 경우, 사용자가 설정한 값은 override하고, 없는 값은 기본 값을 사용합니다.
이 글에선 후자를 선택하겠습니다.
먼저 custom_settings.yml을 생성하고 필요한 부분을 가져다가 정의합니다. 아래의 내용은 제가 사용하는 예시입니다.
security:
enableLogin: 'true' # set to 'true' to enable login
csrfDisabled: 'false' # Set to 'true' to disable CSRF protection (not recommended for production)
loginMethod: 'oauth2' # 'all' (Login Username/Password and OAuth2[must be enabled and configured]), 'normal'(only Login with Username/Password) or 'oauth2'(only Login with OAuth2
initialLogin:
username: 'username' # Initial username for the first login
password: 'password' # Initial password for the first login
oauth2:
enabled: 'true' # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
clientId: '' # Client ID from your provider
clientSecret: '' # Client Secret from your provider
autoCreateUser: 'true' # set to 'true' to allow auto-creation of non-existing users
useAsUsername: 'name' # Default is 'email'; custom fields can be used as the username
scopes: openid, profile, email, name # Specify the scopes for which the application will request permissions
provider: 'One-Click Login' # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
system:
defaultLocale: 'ko-KR' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
ui:
appName: 'e-PDF' # Application's visible name
homeDescription: 'Stirling-PDF hosted by Fentanest' # Short description or tagline shown on homepage.
appNameNavbar: 'e-PDF' # Name displayed on the navigation bar
metrics:
enabled: 'false' # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable
- enableLogin : 활성화하지 않으면 로그인 기능을 사용할 수 없으므로 true
- csrfDisabled : 환경 상 일부러 비활성화할 이유가 없어 false
- loginMethod : ID/PW를 이용한 로그인만 사용할 경우 normal, OAuth만 사용할 경우 oauth2, 모두 사용할 경우 all
- initialLogin : 이 부분을 정의하지 않으면 파드를 재시작할 때마다 admin/stirling 이라는 관리자 계정이 새로 생성되고 로그인을 할 수 있게 됩니다. 해당 계정은 로그인할 때마다 아래처럼 새로운 비밀번호를 설정하고 관리자를 부여받기 때문에, 필수적으로 설정해야 합니다
- oauth2 : Authentik을 이용하여 OAuth/OpenID를 구성하는 방법을 참조하여 구성하면 됩니다.
(생성 시 리디렉션 URI는 “http://{외부 도메인 주소}/login/oauth2/code/oidc” 사용)
issuer경로는 Authentik에서 공급자를 생성한 뒤 ‘OpenID 구성 발급자’를 기입하면 됩니다.
autoCreateUser는 true로 해서 Authentik을 통해 접근한 유저가 막힘 없이 서비스에 접근할 수 있도록 하고
useAsUsername은 계정이름으로 사용될 scope를 지정하는 것으로, 저는 name을 사용하고 있습니다.
이 경우 해당 scope값을 가져올 수 있도록 그 아래의 scopes: 에다가 name을 추가해야 합니다.
provider의 경우 아래처럼 SSO로그인 시 나타날 이름을 정하는 곳입니다. - appname, homeDescription 등은 브라우저의 탭, 메인 페이지의 아이콘 옆에 표시될 앱 이름, 짧은 한 줄 내용 등을 변경하는 것으로 아래처럼 변경할 수 있습니다.
작성이 끝났다면, 파일로부터 configmap을 생성합니다.
kubectl create configmap custom-settings --from-file=custom_settings.yml --namespace=stirlingpdf
manifest 수정
여러 가지 설정이 변경되었으니 deploy에서 이를 반영해 주어야 합니다.
우선, 제일 먼저 configmap으로 생성된 custom_settings.yml을 마운트해야 합니다.
...
...
volumeMounts:
...
- name: custom-settings
mountPath: /configs/custom_settings.yml
subPath: custom_settings.yml
volumes:
...
- name: custom-settings
configMap :
name: custom-settings
또한, 로그인 기능을 구현했기 때문에 env값의 DOCKER_ENABLE_SECURITY를 true로 변경해야 할 필요가 있습니다.
그리고 SYSTEM_CONNECTIONTIMEOUTMINUTES값을 정의해 너무 자주 로그아웃되지 않도록 변경하겠습니다.
env:
- name: DOCKER_ENABLE_SECURITY
value: "true"
- name: SYSTEM_CONNECTIONTIMEOUTMINUTES
value: "10"
변경된 부분을 모두 반영하고 하나의 yaml로 합쳐 manifest를 아래와 같이 만들었습니다.
#! stirlingpdf-manifest.yaml
#OCR data PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-truenas-stirlingpdf-ocr
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
path: # NFS 공유 폴더 경로
server: # NFS 서버의 IP 주소
mountOptions:
- rw
- vers=4.2
---
#OCR data PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-truenas-stirlingpdf-ocr
namespace: stirlingpdf
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi
volumeName: nfs-truenas-stirlingpdf-ocr
---
#logs PV
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-truenas-stirlingpdf-logs
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
path: # NFS 공유 폴더 경로
server: # NFS 서버의 IP 주소
mountOptions:
- rw
- vers=4.2
---
#logs PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-truenas-stirlingpdf-logs
namespace: stirlingpdf
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi
volumeName: nfs-truenas-stirlingpdf-logs
---
#deploy
apiVersion: apps/v1
kind: Deployment
metadata:
name: stirlingpdf
namespace: stirlingpdf
spec:
replicas: 1
strategy:
type: RollingUpdate
selector:
matchLabels:
app: stirlingpdf
template:
metadata:
labels:
app: stirlingpdf
spec:
containers:
- name: stirlingpdf
image: frooodle/s-pdf:latest
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: DOCKER_ENABLE_SECURITY
value: "true"
- name: INSTALL_BOOK_AND_ADVANCED_HTML_OPS
value: "true"
- name: LANGS
value: "ko_KR"
- name: SYSTEM_CONNECTIONTIMEOUTMINUTES
value: "10"
volumeMounts:
- name: nfs-truenas-stirlingpdf-ocr
mountPath: /usr/share/tessdata
- name: nfs-truenas-stirlingpdf-logs
mountPath: /logs
- name: custom-settings
mountPath: /configs/custom_settings.yml
subPath: custom_settings.yml
# resources:
# requests:
# memory: "256Mi"
# cpu: "250m"
# limits:
# memory: "256Mi"
# cpu: "250m"
volumes:
- name: nfs-truenas-stirlingpdf-ocr
persistentVolumeClaim:
claimName: nfs-truenas-stirlingpdf-ocr
- name: nfs-truenas-stirlingpdf-logs
persistentVolumeClaim:
claimName: nfs-truenas-stirlingpdf-logs
- name: custom-settings
configMap :
name: custom-settings
---
#stirlingpdf-service
apiVersion: v1
kind: Service
metadata:
name: stirlingpdf
namespace: stirlingpdf
spec:
selector:
app: stirlingpdf
ports:
- protocol: TCP
port: 8080
targetPort: 8080
name: web
type: LoadBalancer
externalTrafficPolicy: Local
---
#stirlingpdf-ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: stirlingpdf-ingress
namespace: stirlingpdf
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/proxy-body-size: "0"
spec:
ingressClassName: nginx
rules:
- host: #사용할 외부 도메인명
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: stirlingpdf
port:
number: 8080
이제 해당 manifest를 통해 파드를 띄우면 아래처럼 로그인 기능이 구현된 첫 페이지를 볼 수 있고,
custom_settings.yml에 정의된 initialLogin값을 통해 로그인하거나 싱글사인온으로 Authentik을 경유해 로그인할 수 있게 되었습니다.
알려진 문제점(?)
맨 처음 Stirling-PDF에 접근하는 Authentik계정은 아래처럼 오류가 발생합니다.
로그를 살펴보면, 사전에 정의되어 있던 provider(keycloak, google, github) 외의 custom provider와의 호환 문제가 있는 것 같습니다.
이 상태에서 다시 한 번 싱글사인온을 통한 로그인을 진행하면 문제없이 로그인 되긴 합니다…=ㅅ=
관련 글
2025.01.23 - [Apps] - Authentik으로 홈서버 SSO 구현하기 (3) – OAuth/OpenID
Authentik으로 홈서버 SSO 구현하기 (3) – OAuth/OpenID
개요LDAP는 있으면 좋지만, 이것만으로는 번거로운 로그인 과정을 전부 해결할 수는 없습니다.이번 글에서는 OAuth / OpenID Provider를 설정하는 방법을 적어 보겠습니다.LDAP가 조직 내부 네트워크 인
worklazy.net
출처
1. https://hub.docker.com/r/frooodle/s-pdf
https://hub.docker.com/r/frooodle/s-pdf
hub.docker.com
2. https://github.com/Stirling-Tools/Stirling-PDF?tab=readme-ov-file
GitHub - Stirling-Tools/Stirling-PDF: #1 Locally hosted web application that allows you to perform various operations on PDF fil
#1 Locally hosted web application that allows you to perform various operations on PDF files - Stirling-Tools/Stirling-PDF
github.com
3. https://github.com/tesseract-ocr/tessdata
GitHub - tesseract-ocr/tessdata: Trained models with fast variant of the "best" LSTM models + legacy models
Trained models with fast variant of the "best" LSTM models + legacy models - tesseract-ocr/tessdata
github.com