Nginx | Apache | Caddy | Traefik
If you want to expose paperless to the internet, you should hide it behind a reverse proxy with SSL enabled. The officially supported method is Nginx.
If you get a CSRF verification failed
error upon login, your compose or .env file is missing a valid PAPERLESS_URL value.
Nginx
In addition to the usual configuration for SSL, the following configuration is required for paperless to operate:
http {
# Adjust as required. This is the maximum size for file uploads.
# The default value 1M might be a little too small.
client_max_body_size 10M;
server {
location / {
# Adjust host and port as required.
proxy_pass http://localhost:8000/;
# These configuration options are required for WebSockets to work.
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Referrer-Policy "strict-origin-when-cross-origin";
}
}
}
The PAPERLESS_URL
configuration variable is also required when using a
reverse proxy; consider setting USE_X_FORWARD_HOST=true
, USE_X_FORWARD_PORT=true
and PAPERLESS_PROXY_SSL_HEADER='["HTTP_X_FORWARDED_PROTO", "https"]'
. Please refer to the hosting and security docs.
When using a domain subpath (e.g. /paperless
), you need to set PAPERLESS_FORCE_SCRIPT_NAME=/paperless
and adjust proxy_pass
as well:
...
location /paperless {
# Adjust host and port as required.
proxy_pass http://localhost:8000/paperless;
...
Also read this, towards the end of the section.
Some have found adding the P3P header (add_header P3P 'CP=""';
see #817) works; only IE and Edge support it.
Apache
Below is an example of an apache2 conf file that you may customize to fit your environment and needs.
DEFINE local_url 127.0.0.1
DEFINE local_port 8000
DEFINE url_prefix paperless
DEFINE public_url ${url_prefix}.my.domain
DEFINE email ${url_prefix}@my.domain
ServerTokens Prod
SSLStaplingCache "shmcb:${APACHE_LOG_DIR}/stapling-cache(150000)"
SSLSessionCache "shmcb:${APACHE_LOG_DIR}/ssl_scache(512000)"
SSLSessionCacheTimeout 300
### If you have Google's Mod PageSpeed, disable it ###
# ModPagespeed Off
<VirtualHost *:80>
ServerName ${public_url}
DocumentRoot /var/www/html
ServerAdmin ${email}
ErrorLog ${APACHE_LOG_DIR}/${url_prefix}.error.log
CustomLog ${APACHE_LOG_DIR}/${url_prefix}.access.log combined
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/\.well\-known/acme\-challenge/
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>
<VirtualHost *:443>
ServerName ${public_url}
DocumentRoot /var/www/html
ServerAdmin ${email}
ErrorLog ${APACHE_LOG_DIR}/${url_prefix}.error.log
CustomLog ${APACHE_LOG_DIR}/${url_prefix}.access.log combined
SSLEngine On
SSLCertificateFile /etc/letsencrypt/live/my.domain/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/my.domain/privkey.pem
### Forbid the http1.0 protocol ###
Protocols h2 http/1.1
Timeout 360
ProxyRequests Off
ProxyPreserveHost On
ProxyTimeout 600
ProxyReceiveBufferSize 4096
SSLProxyEngine On
RequestHeader set Front-End-Https "On"
ServerSignature Off
SSLCompression Off
SSLUseStapling On
SSLStaplingResponderTimeout 5
SSLStaplingReturnResponderErrors Off
SSLSessionTickets Off
RequestHeader set X-Forwarded-Proto 'https' env=HTTPS
Header always set Strict-Transport-Security "max-age=15552000; preload"
Header always set X-Content-Type-Options nosniff
Header always set X-Robots-Tag none
Header always set X-XSS-Protection "1; mode=block"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
RequestHeader set X-Real-IP %{REMOTE_ADDR}s
### Lax CSP and will not score the best on Mozilla Observatory or other platforms alike, but won't need to be updated with version changes ###
Header always set Content-Security-Policy "default-src 'none'; base-uri 'self'; font-src 'self' data: ${public_url}; media-src 'self' blob: data: https: ${public_url}; script-src 'self' 'unsafe-inline' ${public_url}; style-src 'self' 'unsafe-inline' ${public_url}; img-src 'self' data: blob: https: ${public_url}; worker-src * blob:; frame-src 'self' https://${public_url}; connect-src 'self' wss: https: ${public_url}; form-action 'self'; frame-ancestors 'self' https://${public_url} https://my.domain https://*.my.domain; manifest-src 'self'; object-src 'self' https://${public_url}"
Header always set Permissions-Policy 'geolocation=(self "https://${public_url}"), midi=(self "https://${public_url}"), sync-xhr=(self "https://${public_url}"), microphone=(self "https://${public_url}"), camera=(self "https://${public_url}"), magnetometer=(self "https://${public_url}"), gyroscope=(self "https://${public_url}"), fullscreen=(self "https://${public_url}"), payment=(self "https://${public_url}")'
SSLHonorCipherOrder Off
### Use next two for very secure connections ###
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
SSLProtocol All -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
### Use next two for secure connections and support more endpoints ###
#SSLCipherSuite EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:ECDHE-RSA-AES128-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA128:DHE-RSA-AES128-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA128:ECDHE-RSA-AES128-SHA384:ECDHE-RSA-AES128-SHA128:ECDHE-RSA-AES128-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA128:DHE-RSA-AES128-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA384:AES128-GCM-SHA128:AES128-SHA128:AES128-SHA128:AES128-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4
#SSLProtocol All -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
### Actually proxy the traffic and really the only important part ###
AllowEncodedSlashes On
RewriteEngine On
SetEnvIf Cookie "(^|;\ *)csrftoken=([^;\ ]+)" csrftoken=$2
RequestHeader set X-CSRFToken "%{csrftoken}e"
### Proxy Websockets Section 1 (works for me) ###
RewriteCond %{HTTP:UPGRADE} ^WebSocket$ [NC]
RewriteCond %{HTTP:CONNECTION} Upgrade$ [NC]
RewriteRule ^/?(.*) "ws://${local_url}:${local_port}/$1" [P,L]
### Proxy Websockets Section 2 (untested) ###
#RewriteCond %{HTTP:UPGRADE} =websocket [NC]
#RewriteRule ^/ws/(.*) ws://${local_url}:${local_port}/ws/$1 [P,L]
### Proxy everything else ###
ProxyPass / http://${local_url}:${local_port}/ connectiontimeout=6 timeout=60
ProxyPassReverse / http://${local_url}:${local_port}/
### If Docker and/or Paperless-NGX server is down but webserver is up, show error page ###
ErrorDocument 503 '<!DOCTYPE html>\n<html xml:lang="en" lang="en" dir="ltr" prefix="og: http://ogp.me/ns#">\n<meta http-equiv="refresh" content="15" />\n<head id="head">\n<meta http-equiv="X-UA-Compatible" content="IE=edge"/>\n<title>Offline</title>\n<style>html{width:100%}body{background-color:#a6a6a6;text-align:center;font-family:Helvetica,Tahoma}</style>\n</head>\n<body>\n<h1>${public_url}</h1>\n<p>Appears to be offline... will try again every 15 seconds.<br><br>Nothing happening? Contact the <a href="mailto:${email}" target="_blank">admin</a>.</p>\n</body>\n</html>'
</VirtualHost>
Caddy
Below is an example Caddy configuration
:80 {
reverse_proxy http://localhost:8000 {
header_down Referrer-Policy "strict-origin-when-cross-origin"
}
}
Traefik
Below is an example Traefik configuration you would add to the webserver container.
- Make sure you replace
traefik_proxy
with the name of your own reverse proxy network. - Change the hostname to your own.
- Adjust the entrypoints if needed. Usually
websecure
, some people have it setup ashttps
.
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik_proxy"
- "traefik.http.routers.paperless.rule=Host(`paperless.example.com`)"
- "traefik.http.routers.paperless.entrypoints=websecure"
- "traefik.http.routers.paperless.tls=true"
- "traefik.http.routers.paperless.tls.certresolver=letsencrypt"
- "traefik.http.services.paperless.loadbalancer.server.port=8000"
Feel free to contribute to the wiki pages - enhance and extend the content!
Also browse Discussions & connect in Matrix chat.