.env
· 2.8 KiB · Bash
Raw
# The UID and GID of the user used to run paperless in the container. Set this
# to your UID and GID on the host so that you have write access to the
# consumption directory.
USERMAP_UID=1001
USERMAP_GID=1001
# Database credentials
POSTGRES_DB=paperless
POSTGRES_USER=paperless
POSTGRES_PASSWORD=paperless
PAPERLESS_DBHOST=paperless-ngx-db
PAPERLESS_DBNAME=$POSTGRES_DB
PAPERLESS_DBUSER=$POSTGRES_USER
PAPERLESS_DBPASS=$POSTGRES_PASSWORD
# Additional languages to install for text recognition, separated by a
# whitespace. Note that this is
# different from PAPERLESS_OCR_LANGUAGE (default=eng), which defines the
# language used for OCR.
# The container installs English, German, Italian, Spanish and French by
# default.
# See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names&suite=buster
# for available languages.
PAPERLESS_OCR_LANGUAGES=deu fra eng nld
###############################################################################
# Paperless-specific settings #
###############################################################################
# All settings defined in the paperless.conf.example can be used here. The
# Docker setup does not use the configuration file.
# A few commonly adjusted settings are provided below.
# This is required if you will be exposing Paperless-ngx on a public domain
# (if doing so please consider security measures such as reverse proxy)
PAPERLESS_URL=https://paperless.domain.tld
# Adjust this key if you plan to make paperless available publicly. It should
# be a very long sequence of random characters. You don't need to remember it.
PAPERLESS_SECRET_KEY=GENERATEME
# Use this variable to set a timezone for the Paperless Docker containers. If not specified, defaults to UTC.
PAPERLESS_TIME_ZONE=Europe/Berlin
# The default language to use for OCR. Set this to the language most of your
# documents are written in.
PAPERLESS_OCR_LANGUAGE=deu
# Set if accessing paperless via a domain subpath e.g. https://domain.com/PATHPREFIX and using a reverse-proxy like traefik or nginx
#PAPERLESS_FORCE_SCRIPT_NAME=/PATHPREFIX
#PAPERLESS_STATIC_URL=/PATHPREFIX/static/ # trailing slash required
PAPERLESS_CONSUMER_ENABLE_BARCODES=true
PAPERLESS_OCR_USER_ARGS= '{"continue_on_soft_render_error": true, "invalidate_digital_signatures": true}'
PAPERLESS_USE_X_FORWARD_HOST=true
PAPERLESS_CONSUMER_POLLING=10
PAPERLESS_CONSUMER_POLLING_RETRY_COUNT=5
PAPERLESS_CONSUMER_POLLING_DELAY=20
PAPERLESS_ADMIN_USER=MyAmazingAdmin
PAPERLESS_ADMIN_MAIL=alias@domain.tld
PAPERLESS_ADMIN_PASSWORD="SuperSecure!"
PAPERLESS_EMAIL_HOST=mail.domain.tld
PAPERLESS_EMAIL_PORT=587
PAPERLESS_EMAIL_HOST_USER=paperless@domain.tld
PAPERLESS_EMAIL_HOST_PASSWORD="SuperSecureToo!"
PAPERLESS_EMAIL_USE_TLS=false
PAPERLESS_EMAIL_USE_SSL=true
| 1 | # The UID and GID of the user used to run paperless in the container. Set this |
| 2 | # to your UID and GID on the host so that you have write access to the |
| 3 | # consumption directory. |
| 4 | USERMAP_UID=1001 |
| 5 | USERMAP_GID=1001 |
| 6 | |
| 7 | # Database credentials |
| 8 | POSTGRES_DB=paperless |
| 9 | POSTGRES_USER=paperless |
| 10 | POSTGRES_PASSWORD=paperless |
| 11 | |
| 12 | PAPERLESS_DBHOST=paperless-ngx-db |
| 13 | PAPERLESS_DBNAME=$POSTGRES_DB |
| 14 | PAPERLESS_DBUSER=$POSTGRES_USER |
| 15 | PAPERLESS_DBPASS=$POSTGRES_PASSWORD |
| 16 | |
| 17 | # Additional languages to install for text recognition, separated by a |
| 18 | # whitespace. Note that this is |
| 19 | # different from PAPERLESS_OCR_LANGUAGE (default=eng), which defines the |
| 20 | # language used for OCR. |
| 21 | # The container installs English, German, Italian, Spanish and French by |
| 22 | # default. |
| 23 | # See https://packages.debian.org/search?keywords=tesseract-ocr-&searchon=names&suite=buster |
| 24 | # for available languages. |
| 25 | PAPERLESS_OCR_LANGUAGES=deu fra eng nld |
| 26 | |
| 27 | ############################################################################### |
| 28 | # Paperless-specific settings # |
| 29 | ############################################################################### |
| 30 | |
| 31 | # All settings defined in the paperless.conf.example can be used here. The |
| 32 | # Docker setup does not use the configuration file. |
| 33 | # A few commonly adjusted settings are provided below. |
| 34 | |
| 35 | # This is required if you will be exposing Paperless-ngx on a public domain |
| 36 | # (if doing so please consider security measures such as reverse proxy) |
| 37 | PAPERLESS_URL=https://paperless.domain.tld |
| 38 | # Adjust this key if you plan to make paperless available publicly. It should |
| 39 | # be a very long sequence of random characters. You don't need to remember it. |
| 40 | PAPERLESS_SECRET_KEY=GENERATEME |
| 41 | |
| 42 | # Use this variable to set a timezone for the Paperless Docker containers. If not specified, defaults to UTC. |
| 43 | PAPERLESS_TIME_ZONE=Europe/Berlin |
| 44 | |
| 45 | # The default language to use for OCR. Set this to the language most of your |
| 46 | # documents are written in. |
| 47 | PAPERLESS_OCR_LANGUAGE=deu |
| 48 | |
| 49 | # Set if accessing paperless via a domain subpath e.g. https://domain.com/PATHPREFIX and using a reverse-proxy like traefik or nginx |
| 50 | #PAPERLESS_FORCE_SCRIPT_NAME=/PATHPREFIX |
| 51 | #PAPERLESS_STATIC_URL=/PATHPREFIX/static/ # trailing slash required |
| 52 | |
| 53 | PAPERLESS_CONSUMER_ENABLE_BARCODES=true |
| 54 | |
| 55 | PAPERLESS_OCR_USER_ARGS= '{"continue_on_soft_render_error": true, "invalidate_digital_signatures": true}' |
| 56 | |
| 57 | PAPERLESS_USE_X_FORWARD_HOST=true |
| 58 | PAPERLESS_CONSUMER_POLLING=10 |
| 59 | PAPERLESS_CONSUMER_POLLING_RETRY_COUNT=5 |
| 60 | PAPERLESS_CONSUMER_POLLING_DELAY=20 |
| 61 | |
| 62 | PAPERLESS_ADMIN_USER=MyAmazingAdmin |
| 63 | PAPERLESS_ADMIN_MAIL=alias@domain.tld |
| 64 | PAPERLESS_ADMIN_PASSWORD="SuperSecure!" |
| 65 | |
| 66 | PAPERLESS_EMAIL_HOST=mail.domain.tld |
| 67 | PAPERLESS_EMAIL_PORT=587 |
| 68 | PAPERLESS_EMAIL_HOST_USER=paperless@domain.tld |
| 69 | PAPERLESS_EMAIL_HOST_PASSWORD="SuperSecureToo!" |
| 70 | PAPERLESS_EMAIL_USE_TLS=false |
| 71 | PAPERLESS_EMAIL_USE_SSL=true |
| 72 | |
| 73 |
README
· 962 B · Text
Raw
Last change: 2025-09-25 Marcel Herrguth
As shown on https://youtu.be/P-Pr7Dy6mP8
Attribution back to this GIST is required!
Parts of this script are oriented from https://github.com/zammad/zammad/tree/develop/contrib/backup
This script supports PostGreSQL (docker) stacks only and expects your media to be in a mounted folder.
Restoration only works on a pre-installed Paperless NGX installation of the same version (or higher).
During Restore, this script will remove the existing database and stop all relevant containers.
Containers will be re-started after the restoration finished.
COMMAND REFERENCE
- backup: creates a new snapshot and backs up database and storage.
- restore <snapshot>: Restore a given snapshot from your backup. If you omit the snapshot ID, latest will be used.
- list_snapshots: List all available snapshot IDs.
- verify_health <percentage>: Verify the health of your backup repository. If you omit percentage, 100% will be assumed.
| 1 | Last change: 2025-09-25 Marcel Herrguth |
| 2 | As shown on https://youtu.be/P-Pr7Dy6mP8 |
| 3 | Attribution back to this GIST is required! |
| 4 | |
| 5 | Parts of this script are oriented from https://github.com/zammad/zammad/tree/develop/contrib/backup |
| 6 | This script supports PostGreSQL (docker) stacks only and expects your media to be in a mounted folder. |
| 7 | |
| 8 | Restoration only works on a pre-installed Paperless NGX installation of the same version (or higher). |
| 9 | During Restore, this script will remove the existing database and stop all relevant containers. |
| 10 | Containers will be re-started after the restoration finished. |
| 11 | |
| 12 | COMMAND REFERENCE |
| 13 | - backup: creates a new snapshot and backs up database and storage. |
| 14 | - restore <snapshot>: Restore a given snapshot from your backup. If you omit the snapshot ID, latest will be used. |
| 15 | - list_snapshots: List all available snapshot IDs. |
| 16 | - verify_health <percentage>: Verify the health of your backup repository. If you omit percentage, 100% will be assumed. |
docker-compose.yml
· 3.0 KiB · YAML
Raw
services:
paperless-ngx-broker:
image: docker.io/library/redis:8.2-alpine
container_name: paperless-ngx-broker
hostname: paperless-ngx-broker
restart: always
networks:
- paperless-ngx
volumes:
- ./data/redis:/data
paperless-ngx-db:
image: docker.io/library/postgres:17-alpine
container_name: paperless-ngx-db
hostname: paperless-ngx-db
restart: always
networks:
- paperless-ngx
volumes:
- ./pg:/var/lib/postgresql/data
paperless-ngx-webserver:
image: ghcr.io/paperless-ngx/paperless-ngx:2.18.2
restart: always
container_name: paperless-ngx-webserver
hostname: paperless-ngx-webserver
depends_on:
- paperless-ngx-db
- paperless-ngx-broker
- paperless-ngx-gotenberg
- paperless-ngx-tika
networks:
- paperless-ngx
- external_network
volumes:
- ./data/paperless:/usr/src/paperless/data
- ./data/media:/usr/src/paperless/media
- ./volume/_consume:/usr/src/paperless/consume:z
- ./volume/_export:/usr/src/paperless/export:z
ports:
- "127.0.0.1:8000:8000"
env_file: .env
environment:
PAPERLESS_REDIS: redis://paperless-ngx-broker:6379
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://paperless-ngx-gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://paperless-ngx-tika:9998
paperless-ngx-gotenberg:
image: docker.io/gotenberg/gotenberg:8.22
restart: always
networks:
- paperless-ngx
container_name: paperless-ngx-gotenberg
hostname: paperless-ngx-gotenberg
# The gotenberg chromium route is used to convert .eml files. We do not
# want to allow external content like tracking pixels or even javascript.
# environment:
# CHROMIUM_DISABLE_ROUTES: 1
command:
- 'gotenberg'
- "--chromium-disable-javascript=true"
- "--chromium-allow-list=file:///tmp/.*"
# - "--chromium-disable-routes=true"
# - "--chromium-restart-after=5"
# - "--chromium-auto-start=true"
# - "--chromium-ignore-certificate-errors=true"
# - "--chromium-disable-web-security=true"
# - '--chromium-allow-insecure-localhost=true'
# - "--chromium-start-timeout=30s"
# - "--uno-listener-start-timeout=180s"
# - "--libreoffice-disable-routes=false"
# - "--libreoffice-auto-start=true"
# - "--libreoffice-restart-after=5"
# - '--libreoffice-start-timeout=30s'
# - '--api-timeout=3000s'
paperless-ngx-tika:
image: ghcr.io/paperless-ngx/tika:2.9.1-full
container_name: paperless-ngx-tika
hostname: paperless-ngx-tika
restart: always
networks:
- paperless-ngx
networks:
paperless-ngx:
name: paperless-ngx
internal: true
external_network:
enable_ipv6: true
ipam:
config:
- subnet: fd63:e614:1cf8:fb00::2:0/112
# volumes:
# consume:
# driver: local
# driver_opts:
# type: nfs
# o: addr=yamato.tha.vpn,nfsvers=4.1,nolock,soft,rw,async
# device: :/volume1/import/_consume
| 1 | services: |
| 2 | paperless-ngx-broker: |
| 3 | image: docker.io/library/redis:8.2-alpine |
| 4 | container_name: paperless-ngx-broker |
| 5 | hostname: paperless-ngx-broker |
| 6 | restart: always |
| 7 | networks: |
| 8 | - paperless-ngx |
| 9 | volumes: |
| 10 | - ./data/redis:/data |
| 11 | |
| 12 | paperless-ngx-db: |
| 13 | image: docker.io/library/postgres:17-alpine |
| 14 | container_name: paperless-ngx-db |
| 15 | hostname: paperless-ngx-db |
| 16 | restart: always |
| 17 | networks: |
| 18 | - paperless-ngx |
| 19 | volumes: |
| 20 | - ./pg:/var/lib/postgresql/data |
| 21 | |
| 22 | paperless-ngx-webserver: |
| 23 | image: ghcr.io/paperless-ngx/paperless-ngx:2.18.2 |
| 24 | restart: always |
| 25 | container_name: paperless-ngx-webserver |
| 26 | hostname: paperless-ngx-webserver |
| 27 | depends_on: |
| 28 | - paperless-ngx-db |
| 29 | - paperless-ngx-broker |
| 30 | - paperless-ngx-gotenberg |
| 31 | - paperless-ngx-tika |
| 32 | networks: |
| 33 | - paperless-ngx |
| 34 | - external_network |
| 35 | volumes: |
| 36 | - ./data/paperless:/usr/src/paperless/data |
| 37 | - ./data/media:/usr/src/paperless/media |
| 38 | - ./volume/_consume:/usr/src/paperless/consume:z |
| 39 | - ./volume/_export:/usr/src/paperless/export:z |
| 40 | ports: |
| 41 | - "127.0.0.1:8000:8000" |
| 42 | env_file: .env |
| 43 | environment: |
| 44 | PAPERLESS_REDIS: redis://paperless-ngx-broker:6379 |
| 45 | PAPERLESS_TIKA_ENABLED: 1 |
| 46 | PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://paperless-ngx-gotenberg:3000 |
| 47 | PAPERLESS_TIKA_ENDPOINT: http://paperless-ngx-tika:9998 |
| 48 | |
| 49 | paperless-ngx-gotenberg: |
| 50 | image: docker.io/gotenberg/gotenberg:8.22 |
| 51 | restart: always |
| 52 | networks: |
| 53 | - paperless-ngx |
| 54 | container_name: paperless-ngx-gotenberg |
| 55 | hostname: paperless-ngx-gotenberg |
| 56 | |
| 57 | # The gotenberg chromium route is used to convert .eml files. We do not |
| 58 | # want to allow external content like tracking pixels or even javascript. |
| 59 | # environment: |
| 60 | # CHROMIUM_DISABLE_ROUTES: 1 |
| 61 | command: |
| 62 | - 'gotenberg' |
| 63 | - "--chromium-disable-javascript=true" |
| 64 | - "--chromium-allow-list=file:///tmp/.*" |
| 65 | # - "--chromium-disable-routes=true" |
| 66 | # - "--chromium-restart-after=5" |
| 67 | # - "--chromium-auto-start=true" |
| 68 | # - "--chromium-ignore-certificate-errors=true" |
| 69 | # - "--chromium-disable-web-security=true" |
| 70 | # - '--chromium-allow-insecure-localhost=true' |
| 71 | # - "--chromium-start-timeout=30s" |
| 72 | # - "--uno-listener-start-timeout=180s" |
| 73 | # - "--libreoffice-disable-routes=false" |
| 74 | # - "--libreoffice-auto-start=true" |
| 75 | # - "--libreoffice-restart-after=5" |
| 76 | # - '--libreoffice-start-timeout=30s' |
| 77 | # - '--api-timeout=3000s' |
| 78 | |
| 79 | |
| 80 | paperless-ngx-tika: |
| 81 | image: ghcr.io/paperless-ngx/tika:2.9.1-full |
| 82 | container_name: paperless-ngx-tika |
| 83 | hostname: paperless-ngx-tika |
| 84 | restart: always |
| 85 | networks: |
| 86 | - paperless-ngx |
| 87 | |
| 88 | networks: |
| 89 | paperless-ngx: |
| 90 | name: paperless-ngx |
| 91 | internal: true |
| 92 | external_network: |
| 93 | enable_ipv6: true |
| 94 | ipam: |
| 95 | config: |
| 96 | - subnet: fd63:e614:1cf8:fb00::2:0/112 |
| 97 | |
| 98 | # volumes: |
| 99 | # consume: |
| 100 | # driver: local |
| 101 | # driver_opts: |
| 102 | # type: nfs |
| 103 | # o: addr=yamato.tha.vpn,nfsvers=4.1,nolock,soft,rw,async |
| 104 | # device: :/volume1/import/_consume |
restic_paperless.sh
· 8.9 KiB · Bash
Raw
#!/usr/bin/env bash
#
# 2025-09-25 Marcel Herrguth
# As shown on https://youtu.be/P-Pr7Dy6mP8
# Attribution back to this GIST is required!
#
# Parts of this script are oriented from https://github.com/zammad/zammad/tree/develop/contrib/backup
# This script supports PostGreSQL (docker) stacks only and expects your media to be in a mounted folder.
#
# Restoration only works on a pre-installed Paperless NGX installation of the same version (or higher).
# During Restore, this script will remove the existing database and stop all relevant containers.
# Containers will be re-started after the restoration finished.
#----- CONFIG -------------------------------------------------------------------------------#
# Use either a directory or a supported repository type (like s3:https//domain.tld/bucket)
BACKUP_REPOSITORY='s3:https://s3.domain.tld/resticsample'
# Data directory where your Paperless NGX stores your files
# If you're using volumes, use /var/lib/docker/volumes/paperless_*
# Alternatively, use several folder-paths seperated by space if needed
DATA_DIR='/opt/paperless-ngx/data/'
# Path and filename of the environment file of your stack
# This file will be evaluated for access credentials
ENV_FILE='/opt/paperless-ngx/.env'
# Paperless Stack name
# Used as prefix for being able to
STACK_NAME='paperless-ngx'
# If you're not using PostGreSQL but SQlite, set the following to 'no'
BACKUP_DATABASE='yes'
# How many daily and hourly snapshot should stay available?
HOLD_DAYS=14
HOLD_HOURLY=1
# Do you want to cause downtime during backup? (This is the safest backup way)
STOP_DURING_BACKUP='yes'
#----- CONFIG END ---------------------------------------------------------------------------#
# ---- Magic --------------------------------------------------------------------------------#
function pre_flight () {
# Verify that we have all we need.
if [[ ""$BACKUP_REPOSITORY"" == s3:* ]]; then
if [[ -z "${AWS_ACCESS_KEY_ID}" || -z "${AWS_SECRET_ACCESS_KEY}" ]]; then
echo "You have chosen to backup to S3 (it seems), but either AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY."
exit 1
fi
else
echo "This script has been tested with S3 only. This should be fine."
echo "Ensure that the required environment information for restic are available as per their documentation:"
echo "https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html"
fi
if [[ -z "${RESTIC_PASSWORD}" ]]; then
echo "You forgot to set RESTIC_PASSWORD which is mandatory for your backup repository!"
exit 1
fi
# Get all currently running NGX containers
DC_DB=$(docker ps| grep "${STACK_NAME}-db" |cut -d " " -f1)
DC_BROKER=$(docker ps| grep "${STACK_NAME}-broker" |cut -d " " -f1)
DC_GOTENBERG=$(docker ps| grep "${STACK_NAME}-gotenberg" |cut -d " " -f1)
DC_TIKA=$(docker ps| grep "${STACK_NAME}-tika" |cut -d " " -f1)
DC_WEB=$(docker ps| grep "${STACK_NAME}-webserver" |cut -d " " -f1)
}
function start_paperless () {
echo "# Starting Paperless"
docker start $DC_WEB $DC_TIKA $DC_GOTENBERG $DC_BROKER
}
function stop_paperless () {
echo "# Stopping Paperless"
docker stop $DC_WEB $DC_TIKA $DC_GOTENBERG $DC_BROKER
for i in {15..1}; do
echo -ne "... Waiting $i seconds for the stack to stop.\r"; sleep 1
done
}
function ensure_variable_set () {
if [ -z $1 ]; then
echo "ERROR: environment variable ${1} not set!"
exit 1
fi
}
function get_db_credentials () {
if [ ! -f $ENV_FILE ]; then
echo "ERROR: Could not find the configured environment file!"
exit 1
fi
eval $(grep -E '^(POSTGRES_DB|POSTGRES_USER|POSTGRES_PASSWORD|USERMAP_UID|USERMAP_GID)=' "$ENV_FILE")
}
function kind_exit () {
# We're nice to our admin and bring Zammad back up before exiting
start_paperless
exit 1
}
function delete_old_backups () {
echo "# Invoking cleanup as per snapshot rules"
restic -r "$BACKUP_REPOSITORY" forget --group-by '' --keep-hourly $HOLD_HOURLY --keep-daily $HOLD_DAYS --prune
}
function write_backup () {
stop_paperless if "${STOP_DURING_BACKUP}x" == 'yesx'
echo "# Creating postgresql backup..."
docker exec $DC_DB pg_dump --dbname "${POSTGRES_DB}" \
--username "${POSTGRES_USER}" \
--no-privileges --no-owner > /tmp/paperless_db.psql
state=$?
if [ "${state}" == "1" ]; then
echo -e "\n\n # ERROR(${state}) - Database credentials are wrong or database server configuration is invalid."
echo -e " #-> BACKUP WAS NOT SUCCESSFUL"
kind_exit
exit 2
fi
restic -r "$BACKUP_REPOSITORY" migrate
restic --no-scan --read-concurrency=25 --pack-size=128 --compression off -r "$BACKUP_REPOSITORY" backup \
${DATA_DIR} /tmp/paperless_db.psql
state=$?
# clean up temporary database dump
rm -f /tmp/paperless_db.psql
start_paperless if "${STOP_DURING_BACKUP}x" == 'yesx'
if [ $state == '1' ]; then
echo "# FATAL - Restic could not create the snapshot."
elif [ $state == '3' ]; then
echo "# WARNING - Files changed during snapshot creation; Snapshot potentially incomplete!"
fi
if [ $state -gt 0 ]; then
echo "# BACKUP WAS NOT SUCCESSFUL"
exit $state
fi
}
function list_available_snapshots () {
pre_flight
echo "# Here's your currently available snapshots in your backup repository:"
restic -r "$BACKUP_REPOSITORY" snapshots
}
function verify_backup_repository_health () {
pre_flight
VERIFY_PERCENTAGE="${1:-100}"
echo "# Checking ${VERIFY_PERCENTAGE} of your repository ..."
restic -r "$BACKUP_REPOSITORY" check --read-data-subset="${VERIFY_PERCENTAGE}%"
}
function get_snapshot_id () {
if [ -n "${1}" ]; then
RESTORE_SNAPSHOT_ID="${1}"
else
# User did not provide snapshot so we'll hard guess 'latest'
RESTORE_SNAPSHOT_ID='latest'
echo "... No Snapshot provided, guessing you want to restore 'latest'!"
for i in {15..1}; do
echo -ne "... You have $i seconds to abort.\r"; sleep 1
done
fi
}
function restore_backup () {
stop_paperless
echo "# ... Dropping current database ${POSTGRES_DB}"
docker exec $DC_DB psql -U ${POSTGRES_USER} -c "\c postgres; DROP DATABASE IF EXISTS ${POSTGRES_DB}; CREATE DATABASE ${POSTGRES_DB} OWNED BY ${POSTGRES_USER};"
echo "# Restoring PostgreSQL DB"
# We're removing uncritical dump information that caused "ugly" error
# messages on older script versions. These could safely be ignored.
restic -r "$BACKUP_REPOSITORY" dump latest '/tmp/paperless_db.psql' | \
sed '/^CREATE EXTENSION IF NOT EXISTS plpgsql/d'| \
sed '/^COMMENT ON EXTENSION plpgsql/d'| \
docker exec $DC_DB psql -U ${POSTGRES_USER} ${POSTGRES_DB}
state=$?
if [[ ("${state}" == "1") || ( "${state}" == "2") || ( "${state}" == "3") ]]; then
# We're checking for critical restoration errors
# It may not cover all possible errors which is out of scope of this script
echo -e "\n\n # ERROR(${state}) - Database credentials are wrong or database server configuration is invalid."
echo -e " #-> RESTORE WAS NOT SUCCESSFUL"
kind_exit
exit 2
fi
echo "# Restoring Files"
restic -r "$BACKUP_REPOSITORY" restore $RESTORE_SNAPSHOT_ID --include "$DATA_DIR" --target /
state=$?
if [[ ($state == '1') || ($state == '2') ]]; then
echo "# ERROR(${state}) - File restore reported an error."
echo "- Check file permissions, and ensure Zammad IS NOT running, and try again."
echo -e " \n# RESTORE WAS NOT SUCCESSFUL"
exit 1
fi
echo "# Ensuring correct file permissions ..."
chown -R ${USERMAP_UID}:${USERMAP_GID} ${DATA_DIR}
start_paperless
}
function start_backup_message () {
echo -e "\n# Backup script started - $(date)!\n"
}
function start_restore_message () {
echo -e "\n# Restore script started - $(date)!\n"
}
function finished_backup_message () {
echo -e "\n# Backup script finished; Check output! - $(date)!\n"
}
function finished_restore_message () {
echo -e "\n# Restore script finished; Check output! - $(date)!\n"
}
function execute_backup () {
pre_flight
get_db_credentials
start_backup_message
write_backup
delete_old_backups
finished_backup_message
}
function execute_restoration () {
pre_flight
get_db_credentials
start_restore_message
get_snapshot_id $1
restore_backup
finished_restore_message
}
function command_reference () {
echo "COMMAND REFERENCE"
echo "- backup: creates a new snapshot and backs up database and storage."
echo "- restore <snapshot>: Restore a given snapshot from your backup. If you omit the snapshot ID, latest will be used."
echo "- list_snapshots: List all available snapshot IDs."
echo "- verify_health <percentage>: Verify the health of your backup repository. If you omit percentage, 100% will be assumed."
}
# ---- Option part and control --------------------------------------------------------------#
case "$1" in
backup)
execute_backup
;;
restore)
execute_restoration $2
;;
list_snapshots)
list_available_snapshots
;;
verify_health)
verify_backup_repository_health "$2"
;;
*)
command_reference
;;
esac
| 1 | #!/usr/bin/env bash |
| 2 | # |
| 3 | # 2025-09-25 Marcel Herrguth |
| 4 | # As shown on https://youtu.be/P-Pr7Dy6mP8 |
| 5 | # Attribution back to this GIST is required! |
| 6 | # |
| 7 | # Parts of this script are oriented from https://github.com/zammad/zammad/tree/develop/contrib/backup |
| 8 | # This script supports PostGreSQL (docker) stacks only and expects your media to be in a mounted folder. |
| 9 | # |
| 10 | # Restoration only works on a pre-installed Paperless NGX installation of the same version (or higher). |
| 11 | # During Restore, this script will remove the existing database and stop all relevant containers. |
| 12 | # Containers will be re-started after the restoration finished. |
| 13 | |
| 14 | #----- CONFIG -------------------------------------------------------------------------------# |
| 15 | # Use either a directory or a supported repository type (like s3:https//domain.tld/bucket) |
| 16 | BACKUP_REPOSITORY='s3:https://s3.domain.tld/resticsample' |
| 17 | |
| 18 | # Data directory where your Paperless NGX stores your files |
| 19 | # If you're using volumes, use /var/lib/docker/volumes/paperless_* |
| 20 | # Alternatively, use several folder-paths seperated by space if needed |
| 21 | DATA_DIR='/opt/paperless-ngx/data/' |
| 22 | # Path and filename of the environment file of your stack |
| 23 | # This file will be evaluated for access credentials |
| 24 | ENV_FILE='/opt/paperless-ngx/.env' |
| 25 | # Paperless Stack name |
| 26 | # Used as prefix for being able to |
| 27 | STACK_NAME='paperless-ngx' |
| 28 | # If you're not using PostGreSQL but SQlite, set the following to 'no' |
| 29 | BACKUP_DATABASE='yes' |
| 30 | |
| 31 | # How many daily and hourly snapshot should stay available? |
| 32 | HOLD_DAYS=14 |
| 33 | HOLD_HOURLY=1 |
| 34 | |
| 35 | # Do you want to cause downtime during backup? (This is the safest backup way) |
| 36 | STOP_DURING_BACKUP='yes' |
| 37 | #----- CONFIG END ---------------------------------------------------------------------------# |
| 38 | |
| 39 | # ---- Magic --------------------------------------------------------------------------------# |
| 40 | function pre_flight () { |
| 41 | # Verify that we have all we need. |
| 42 | if [[ ""$BACKUP_REPOSITORY"" == s3:* ]]; then |
| 43 | if [[ -z "${AWS_ACCESS_KEY_ID}" || -z "${AWS_SECRET_ACCESS_KEY}" ]]; then |
| 44 | echo "You have chosen to backup to S3 (it seems), but either AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY." |
| 45 | exit 1 |
| 46 | fi |
| 47 | else |
| 48 | echo "This script has been tested with S3 only. This should be fine." |
| 49 | echo "Ensure that the required environment information for restic are available as per their documentation:" |
| 50 | echo "https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html" |
| 51 | fi |
| 52 | |
| 53 | if [[ -z "${RESTIC_PASSWORD}" ]]; then |
| 54 | echo "You forgot to set RESTIC_PASSWORD which is mandatory for your backup repository!" |
| 55 | exit 1 |
| 56 | fi |
| 57 | |
| 58 | # Get all currently running NGX containers |
| 59 | DC_DB=$(docker ps| grep "${STACK_NAME}-db" |cut -d " " -f1) |
| 60 | DC_BROKER=$(docker ps| grep "${STACK_NAME}-broker" |cut -d " " -f1) |
| 61 | DC_GOTENBERG=$(docker ps| grep "${STACK_NAME}-gotenberg" |cut -d " " -f1) |
| 62 | DC_TIKA=$(docker ps| grep "${STACK_NAME}-tika" |cut -d " " -f1) |
| 63 | DC_WEB=$(docker ps| grep "${STACK_NAME}-webserver" |cut -d " " -f1) |
| 64 | } |
| 65 | |
| 66 | function start_paperless () { |
| 67 | echo "# Starting Paperless" |
| 68 | docker start $DC_WEB $DC_TIKA $DC_GOTENBERG $DC_BROKER |
| 69 | } |
| 70 | |
| 71 | function stop_paperless () { |
| 72 | echo "# Stopping Paperless" |
| 73 | docker stop $DC_WEB $DC_TIKA $DC_GOTENBERG $DC_BROKER |
| 74 | |
| 75 | for i in {15..1}; do |
| 76 | echo -ne "... Waiting $i seconds for the stack to stop.\r"; sleep 1 |
| 77 | done |
| 78 | } |
| 79 | |
| 80 | function ensure_variable_set () { |
| 81 | if [ -z $1 ]; then |
| 82 | echo "ERROR: environment variable ${1} not set!" |
| 83 | exit 1 |
| 84 | fi |
| 85 | } |
| 86 | |
| 87 | function get_db_credentials () { |
| 88 | if [ ! -f $ENV_FILE ]; then |
| 89 | echo "ERROR: Could not find the configured environment file!" |
| 90 | exit 1 |
| 91 | fi |
| 92 | |
| 93 | eval $(grep -E '^(POSTGRES_DB|POSTGRES_USER|POSTGRES_PASSWORD|USERMAP_UID|USERMAP_GID)=' "$ENV_FILE") |
| 94 | } |
| 95 | |
| 96 | function kind_exit () { |
| 97 | # We're nice to our admin and bring Zammad back up before exiting |
| 98 | start_paperless |
| 99 | exit 1 |
| 100 | } |
| 101 | |
| 102 | function delete_old_backups () { |
| 103 | echo "# Invoking cleanup as per snapshot rules" |
| 104 | restic -r "$BACKUP_REPOSITORY" forget --group-by '' --keep-hourly $HOLD_HOURLY --keep-daily $HOLD_DAYS --prune |
| 105 | } |
| 106 | |
| 107 | function write_backup () { |
| 108 | stop_paperless if "${STOP_DURING_BACKUP}x" == 'yesx' |
| 109 | echo "# Creating postgresql backup..." |
| 110 | |
| 111 | docker exec $DC_DB pg_dump --dbname "${POSTGRES_DB}" \ |
| 112 | --username "${POSTGRES_USER}" \ |
| 113 | --no-privileges --no-owner > /tmp/paperless_db.psql |
| 114 | |
| 115 | state=$? |
| 116 | |
| 117 | if [ "${state}" == "1" ]; then |
| 118 | echo -e "\n\n # ERROR(${state}) - Database credentials are wrong or database server configuration is invalid." |
| 119 | echo -e " #-> BACKUP WAS NOT SUCCESSFUL" |
| 120 | |
| 121 | kind_exit |
| 122 | exit 2 |
| 123 | fi |
| 124 | |
| 125 | restic -r "$BACKUP_REPOSITORY" migrate |
| 126 | restic --no-scan --read-concurrency=25 --pack-size=128 --compression off -r "$BACKUP_REPOSITORY" backup \ |
| 127 | ${DATA_DIR} /tmp/paperless_db.psql |
| 128 | |
| 129 | state=$? |
| 130 | |
| 131 | # clean up temporary database dump |
| 132 | rm -f /tmp/paperless_db.psql |
| 133 | |
| 134 | start_paperless if "${STOP_DURING_BACKUP}x" == 'yesx' |
| 135 | |
| 136 | if [ $state == '1' ]; then |
| 137 | echo "# FATAL - Restic could not create the snapshot." |
| 138 | elif [ $state == '3' ]; then |
| 139 | echo "# WARNING - Files changed during snapshot creation; Snapshot potentially incomplete!" |
| 140 | fi |
| 141 | |
| 142 | if [ $state -gt 0 ]; then |
| 143 | echo "# BACKUP WAS NOT SUCCESSFUL" |
| 144 | exit $state |
| 145 | fi |
| 146 | } |
| 147 | |
| 148 | function list_available_snapshots () { |
| 149 | pre_flight |
| 150 | echo "# Here's your currently available snapshots in your backup repository:" |
| 151 | restic -r "$BACKUP_REPOSITORY" snapshots |
| 152 | } |
| 153 | |
| 154 | function verify_backup_repository_health () { |
| 155 | pre_flight |
| 156 | |
| 157 | VERIFY_PERCENTAGE="${1:-100}" |
| 158 | |
| 159 | echo "# Checking ${VERIFY_PERCENTAGE} of your repository ..." |
| 160 | restic -r "$BACKUP_REPOSITORY" check --read-data-subset="${VERIFY_PERCENTAGE}%" |
| 161 | } |
| 162 | |
| 163 | function get_snapshot_id () { |
| 164 | if [ -n "${1}" ]; then |
| 165 | RESTORE_SNAPSHOT_ID="${1}" |
| 166 | else |
| 167 | # User did not provide snapshot so we'll hard guess 'latest' |
| 168 | RESTORE_SNAPSHOT_ID='latest' |
| 169 | |
| 170 | echo "... No Snapshot provided, guessing you want to restore 'latest'!" |
| 171 | for i in {15..1}; do |
| 172 | echo -ne "... You have $i seconds to abort.\r"; sleep 1 |
| 173 | done |
| 174 | fi |
| 175 | } |
| 176 | |
| 177 | function restore_backup () { |
| 178 | stop_paperless |
| 179 | |
| 180 | echo "# ... Dropping current database ${POSTGRES_DB}" |
| 181 | |
| 182 | docker exec $DC_DB psql -U ${POSTGRES_USER} -c "\c postgres; DROP DATABASE IF EXISTS ${POSTGRES_DB}; CREATE DATABASE ${POSTGRES_DB} OWNED BY ${POSTGRES_USER};" |
| 183 | |
| 184 | echo "# Restoring PostgreSQL DB" |
| 185 | |
| 186 | # We're removing uncritical dump information that caused "ugly" error |
| 187 | # messages on older script versions. These could safely be ignored. |
| 188 | restic -r "$BACKUP_REPOSITORY" dump latest '/tmp/paperless_db.psql' | \ |
| 189 | sed '/^CREATE EXTENSION IF NOT EXISTS plpgsql/d'| \ |
| 190 | sed '/^COMMENT ON EXTENSION plpgsql/d'| \ |
| 191 | docker exec $DC_DB psql -U ${POSTGRES_USER} ${POSTGRES_DB} |
| 192 | |
| 193 | state=$? |
| 194 | |
| 195 | if [[ ("${state}" == "1") || ( "${state}" == "2") || ( "${state}" == "3") ]]; then |
| 196 | # We're checking for critical restoration errors |
| 197 | # It may not cover all possible errors which is out of scope of this script |
| 198 | echo -e "\n\n # ERROR(${state}) - Database credentials are wrong or database server configuration is invalid." |
| 199 | echo -e " #-> RESTORE WAS NOT SUCCESSFUL" |
| 200 | |
| 201 | kind_exit |
| 202 | exit 2 |
| 203 | fi |
| 204 | |
| 205 | echo "# Restoring Files" |
| 206 | restic -r "$BACKUP_REPOSITORY" restore $RESTORE_SNAPSHOT_ID --include "$DATA_DIR" --target / |
| 207 | |
| 208 | state=$? |
| 209 | |
| 210 | if [[ ($state == '1') || ($state == '2') ]]; then |
| 211 | echo "# ERROR(${state}) - File restore reported an error." |
| 212 | echo "- Check file permissions, and ensure Zammad IS NOT running, and try again." |
| 213 | echo -e " \n# RESTORE WAS NOT SUCCESSFUL" |
| 214 | exit 1 |
| 215 | fi |
| 216 | |
| 217 | echo "# Ensuring correct file permissions ..." |
| 218 | chown -R ${USERMAP_UID}:${USERMAP_GID} ${DATA_DIR} |
| 219 | |
| 220 | start_paperless |
| 221 | } |
| 222 | |
| 223 | function start_backup_message () { |
| 224 | echo -e "\n# Backup script started - $(date)!\n" |
| 225 | } |
| 226 | |
| 227 | function start_restore_message () { |
| 228 | echo -e "\n# Restore script started - $(date)!\n" |
| 229 | } |
| 230 | |
| 231 | function finished_backup_message () { |
| 232 | echo -e "\n# Backup script finished; Check output! - $(date)!\n" |
| 233 | } |
| 234 | |
| 235 | function finished_restore_message () { |
| 236 | echo -e "\n# Restore script finished; Check output! - $(date)!\n" |
| 237 | } |
| 238 | |
| 239 | function execute_backup () { |
| 240 | pre_flight |
| 241 | get_db_credentials |
| 242 | start_backup_message |
| 243 | write_backup |
| 244 | delete_old_backups |
| 245 | finished_backup_message |
| 246 | } |
| 247 | |
| 248 | function execute_restoration () { |
| 249 | pre_flight |
| 250 | get_db_credentials |
| 251 | start_restore_message |
| 252 | get_snapshot_id $1 |
| 253 | restore_backup |
| 254 | finished_restore_message |
| 255 | } |
| 256 | |
| 257 | function command_reference () { |
| 258 | echo "COMMAND REFERENCE" |
| 259 | echo "- backup: creates a new snapshot and backs up database and storage." |
| 260 | echo "- restore <snapshot>: Restore a given snapshot from your backup. If you omit the snapshot ID, latest will be used." |
| 261 | echo "- list_snapshots: List all available snapshot IDs." |
| 262 | echo "- verify_health <percentage>: Verify the health of your backup repository. If you omit percentage, 100% will be assumed." |
| 263 | } |
| 264 | |
| 265 | # ---- Option part and control --------------------------------------------------------------# |
| 266 | |
| 267 | case "$1" in |
| 268 | backup) |
| 269 | execute_backup |
| 270 | ;; |
| 271 | |
| 272 | restore) |
| 273 | execute_restoration $2 |
| 274 | ;; |
| 275 | |
| 276 | list_snapshots) |
| 277 | list_available_snapshots |
| 278 | ;; |
| 279 | |
| 280 | verify_health) |
| 281 | verify_backup_repository_health "$2" |
| 282 | ;; |
| 283 | |
| 284 | *) |
| 285 | command_reference |
| 286 | ;; |
| 287 | esac |