Всем привет.
Несколько недель назад захостил сервис для хранения документов Paperless по видеоинстрeкции от Romnero (установка в docker). В процессе эксплуатации столкнулся с рядом проблем - утчечка RAM и высокая утилизация CPU.
На просторах сети нашёл описание похожих проблем, так же автор канала Romnero в переписке подтвердил это.
- High CPU (& RAM?) idle usage on Cloudron · paperless-ngx/paperless-ngx · Discussion #7440 · GitHub
- Reddit — Сердце сети
- High cpu utilization since update 1.25.1 | Cloudron Forum
- https://www.youtube.com/watch?v=ygtp8Z5Mslk
Так как сервис мне понравился, то решил попробовать разобраться в проблеме и найти решение. Всё описанное ниже есть творчество человека, который является новичком в администрировании linux и не претендует на истину.
Поехали.
Архитектура
- Гипервизор Proxmox 8.4.1
- Установка в Unprivileged LXC containers (2CPU, 2Gb RAM, 0,5Gb swap, 8GB HDD)
- Сервисы Paperless-ngx v2.18.4 развёрнуты в виде docker образов.
3.1 Добавлен образ Tika (извлечения текста и поддержка доп. форматов - docx, xlsx, ppt и др.)
3.2 Добавлен образ Gotenberg (конвертация различных документов в PDF для дальнейшей обработки)
Описание проблемы
-
Сервис после перезагрузки LXC потребляет в простое: 0,6% CPU, 0,88GB (43%) RAM, 0% swap.
-
Сервис после загрузки документов (по 4 шт. каждого типа: docx, xlsx, txt, pdf, jpg) потребляет: 0,9% CPU, 0,76GB (38%) RAM, 0,46Gb 90% swap.
Изменние потребления RAM: (0,76+0,46)-0,88=0,34Gb
Считаю это утечкой по причине того, что после завершения обработки документов в Paperless RAM не возвращается к исходным значениям и по истечении времени может протечь ещё больше.
В случае с отключением swap в LXC ситуация с RAM немного лучше, после загрузки того же тестового набора файлов RAM фиксируется на отметке 1,16Gb (утечка RAM 1,16-0,88=0,28Gb)
Поиск решения
1. Альтернативная архитектура
Установка сборки для LXC через helper-scripts.
В этой сборке отсутствуют компоненты Tika и Gotenberg, а так же используется предыдущая версия БД postgres v.16, в отличие от официальной сборки для docker с postgres v.17 (это некритично, но сам факт).
У меня не получилось установить Tika и Gotenberg непосредственнов LXC и связать с Paperless, поэтому добавил их в виде docker образов.
Результаты тестирования с swap и без него оказались идентичными.
Из плюсов:
- Восстановление сервиса после перезагрузки LXC происходит в 2-3 раза быстрее (около 10 секунд) в сравнении с официальной сборкой.
- Субъективно сервис работает чуть стабильнее. Не сталкивался с 100% утилизацией CPU из-за циклической ошибки в web-server при загрузкке тяжёлых pdf.
Из минусов:
- Обновления зависят от разработчиков helper-scripts
- Использование docker образов - может быть причина утечки в них.
- Перенос БД между разными экземплярами Paperless нельзя выполнить простым копированием файлов на уровне директорий.
- Без скрипта процесс настройки усложняется.
Для удобства сделал скрипт, который устанавливает Paperless через helper-scripts, добавляет Tika и Gotenberg docker образы, обновляет сервисы и переменные окружения, отдаёт работоспособный сервис в LXC под ключ:
Спойлер
## 🛠 Script: `setup_paperless_to_lxc.sh`
## Description
## The script installs Paperless in an LXC container using
## PVE helper-scripts.
## It also installs Docker Engine and pulls two images:
## Tika and Gotenberg — to enable support for working with format
##such as DOCX, XLS, and PPT.
#!/bin/bash
# ===================== SETTINGS =====================
START_TIME=$(date +%s)
TOTAL_STEPS=14
COMPLETED_STEPS=0
ERROR_STEPS=()
# For calculating file size
TOTAL_SIZE=0
# Colors
GREEN="\033[0;32m"
RED="\033[0;31m"
YELLOW="\033[1;33m"
NC="\033[0m" # no color
CHECK_MARK="\xE2\x9C\x85" # ✅
CROSS_MARK="\xE2\x9D\x8C" # ❌
# ===================== UTILITIES =====================
function show_progress() {
local step=$1
local message=$2
echo -e "[${GREEN} Step $step/${TOTAL_STEPS}${NC}] $message"
}
function check_success() {
if [ $? -eq 0 ]; then
((COMPLETED_STEPS++))
echo -e "${GREEN}${CHECK_MARK} Success: $1${NC}"
else
ERROR_STEPS+=("$1")
echo -e "${RED}${CROSS_MARK} Error during: $1${NC}"
fi
}
# ===================== Step 1: Get container list before installation =====================
echo "📦 Getting list of containers before installation......"
mapfile -t existing_cts < <(pct list | awk 'NR>1 {print $1}' | sort -n)
# ===================== Step 1.2: Start Paperless installation =====================
echo
echo "🚀 Launching paperless-ngx installation (interactive mode)..."
echo "Please complete all installation steps interactively."
echo
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/paperless-ngx.sh)"
sleep 5
# ===================== Step 1.3: Get containers after installation =====================
show_progress 1 "📦 Getting list of containers after installation..."
mapfile -t new_cts < <(pct list | awk 'NR>1 {print $1}' | sort -n)
check_success "✅ Container list after installation received"
# ===================== Step 2: Determine CTID =====================
show_progress 2 "🆔 ОDetermining ID of new container..."
CTID=$(comm -13 <(printf "%s\n" "${existing_cts[@]}") <(printf "%s\n" "${new_cts[@]}") | head -n1 | tr -d '[:space:]')
if [ -z "$CTID" ]; then
echo -e "\n❌ No new container found."
echo "ℹ️ Make sure you've created a new LXC container before proceeding."
echo "📌 Current containers:"
printf "%s\n" "${new_cts[@]}"
ERROR_STEPS+=("New container not found")
exit 1
fi
check_success "✅ New container created with ID: $CTID"
# ===================== Step 3: Install bc in LXC =====================
# Installing 'bc' utility (Basic Calculator) in LXC...
show_progress 3 "🖩 Installing 'bc' utility (Basic Calculator)..."
pct exec $CTID -- bash -c "apt-get update -qq && apt-get install -y bc > /dev/null 2>&1"
# Saving the size of directories BEFORE installation
INITIAL_SIZE=$(pct exec "$CTID" -- bash -c \
"du -sk /opt/paperless /var/lib/docker /var/lib/systemd /var/log 2>/dev/null | awk '{sum += \$1} END {print int(sum)}'")
# ===================== Step 4: Install Docker Engine =====================
show_progress 4 "🐳 Installing Docker Engine in container $CTID..."
pct exec $CTID -- bash -c "curl -fsSL https://get.docker.com -o get-docker.sh"
pct exec $CTID -- bash -c "sh get-docker.sh"
echo "🔍 Verifying Docker installation..."
if ! pct exec $CTID -- bash -c "docker --version"; then
echo "❌ Error: Docker not installed or not working."
ERROR_STEPS+=("Docker Engine not installed or not working.")
exit 1
fi
check_success "✅ Docker Engine successfully installed in container $CTID"
# ===================== Step 5: Add root to docker group =====================
show_progress 5 "➕ Adding root user to docker group..."
if ! pct exec $CTID -- bash -c "usermod -aG docker root"; then
echo "⚠️ Warning: Failed to add root to docker group (can be ignored)."
ERROR_STEPS+=("Failed to add root to docker group")
fi
echo "🔍 Checking root user groups..."
if ! pct exec $CTID -- bash -c "id root"; then
echo "⚠️ ОWarning: Failed to verify root's groups (can be ignored)."
ERROR_STEPS+=("Failed to verify root group")
fi
check_success "✅ Root user added to docker group"
# ===================== Step 6: Start Tika =====================
show_progress 6 "🐳 Starting Tika container..."
pct exec $CTID -- bash -c "docker run -it -d --restart=always --name tika -p 9998:9998 apache/tika:latest"
check_success "✅ Tika container started"
# ===================== Step 7: Start Gotenberg =====================
show_progress 7 "📄 Starting Gotenberg container..."
pct exec $CTID -- bash -c "docker run -it -d --restart=always --name gotenberg -p 3000:3000 thecodingmachine/gotenberg:latest"
check_success "✅ Gotenberg container started"
# ===================== Step 8: Check containers =====================
show_progress 8 "🔍 Checking Docker containers..."
docker_output=$(pct exec $CTID -- bash -c "docker ps --format '{{.Names}}'" || echo "error")
if [[ "$docker_output" == "error" ]]; then
echo "❌ Failed to get list of Docker containers"
ERROR_STEPS+=("Docker container list check failed")
exit 1
fi
if echo "$docker_output" | grep -q tika && echo "$docker_output" | grep -q gotenberg; then
echo "✅ Tika and Gotenberg containers are running."
else
echo "❌ One or both containers are not running"
echo "Active containers: $docker_output"
ERROR_STEPS+=("Tika or Gotenberg containers not running")
exit 1
fi
check_success "✅ Tika and Gotenberg containers are running"
# ===================== Step 9: Configure environment variables =====================
show_progress 9 "⚙️ Configuring environment variables in paperless.conf..."
CONFIG_FILE="/opt/paperless/paperless.conf"
# Key=value pairs
declare -A settings=(
["PAPERLESS_OCR_LANGUAGE"]="rus"
["PAPERLESS_OCR_SKIP_ARCHIVE_FILE"]="always"
["PAPERLESS_TIME_ZONE"]="Europe/Moscow"
["PAPERLESS_ENABLE_UPDATE_CHECK"]="true"
["PAPERLESS_TIKA_ENABLED"]="true"
["PAPERLESS_GOTENBERG_ENABLED"]="true"
["PAPERLESS_TIKA_ENDPOINT"]="http://localhost:9998"
["PAPERLESS_TIKA_GOTENBERG_ENDPOINT"]="http://localhost:3000"
)
# Processing each setting
for key in "${!settings[@]}"; do
value="${settings[$key]}"
# Check: whether the line exists (commented or not)
pct exec "$CTID" -- bash -c "
if grep -qE '^\s*#?\s*${key}=' /opt/paperless/paperless.conf 2>/dev/null; then
# Update the value and remove the comment
sed -i -E 's|^\s*#?\s*${key}=.*|${key}=${value}|' /opt/paperless/paperless.conf
else
# Add the line to the end if it doesn't exist
echo '${key}=${value}' >> /opt/paperless/paperless.conf
fi
"
done
check_success "✅ Environment variables configured"
# ===================== Step 10: Install Russian OCR =====================
show_progress 10 "🈷️ Installing tesseract-ocr-rus..."
pct exec $CTID -- apt install -y tesseract-ocr-rus
check_success "✅ Tesseract OCR RUS installed"
# ===================== Step 11: Restart systemd =====================
show_progress 11 "🔄 Restarting systemd..."
pct exec $CTID -- systemctl daemon-reexec
pct exec $CTID -- systemctl daemon-reload
check_success "✅ systemd restarted"
# ===================== Step 12: Restart services =====================
show_progress 12 "♻️ Restarting Paperless services..."
pct exec $CTID -- systemctl restart paperless-consumer.service paperless-scheduler.service paperless-task-queue.service paperless-webserver.service
check_success "✅ Paperless services restarted"
# ===================== Step 13: Check service status =====================
show_progress 13 "🔍 Checking status of Paperless services..."
service_status=$(pct exec $CTID -- bash -c "systemctl list-units --type=service | grep paperless")
echo "$service_status"
errors=0
while IFS= read -r line; do
service_name=$(echo "$line" | awk '{print $1}')
status=$(echo "$line" | awk '{print $4}')
if [[ "$status" != "running" ]]; then
echo "⚠️ Warning: service $service_name has status $status"
errors=$((errors+1))
fi
done <<< "$service_status"
if [[ $errors -eq 0 ]]; then
echo "✅ All Paperless services are running correctly."
else
echo "⚠️ Some services are not in 'running' state."
fi
check_success "✅ Paperless service statuses checked"
# ===================== ШStep 14: Retrieve credentials =====================
show_progress 14 "📄 Retrieving credentials from paperless-ngx.creds..."
# Get only IPv4 address (filter by eth)
IP=$(pct exec "$CTID" -- bash -c \
"ip -4 -o addr show | grep 'eth' | awk '{print \$4}' | cut -d/ -f1 | head -n1")
# Check if an IP was obtained
if [ -z "$IP" ]; then
IP="⚠️Container_$CTID_has_no_IPv4_address"
else
:
fi
# Retrieve and display credentials
pct exec $CTID -- bash -c "
if [ -f ~/paperless-ngx.creds ]; then
LOGIN=\$(grep -i 'Paperless-ngx WebUI User:' ~/paperless-ngx.creds | cut -d ':' -f2- | xargs)
PASSWORD=\$(grep -i 'Paperless-ngx WebUI Password:' ~/paperless-ngx.creds | cut -d ':' -f2- | xargs)
echo -e '########################################################'
echo -e 'Paperless-ngx service is available:\n'
echo ' 🌐 URL: ${IP}:8000'
echo ' 👱 Login: '\$LOGIN
echo ' 🔑 Password: '\$PASSWORD
echo ' ℹ️ For information:'
echo ' ⚙️ ~/paperless-ngx.creds'
echo ' ⚙️ /opt/paperless/data'
echo ' ⚙️ /opt/paperless/media'
echo ' ⚙️ /opt/paperless/static'
echo ' ⚙️ /opt/paperless/consume'
echo -e '########################################################\n'
else
echo -e '########################################################'
echo -e 'Paperless-ngx service is available:\n'
echo ' 🌐 URL: ${IP}:8000'
echo ' 👱 Login: '⚠️ ~/paperless-ngx.creds file not found'
echo ' 🔑 Password: '⚠️ ~/paperless-ngx.creds file not found'
echo ' ℹ️ For information:'
echo ' ⚙️ ~/paperless-ngx.creds'
echo ' ⚙️ /opt/paperless/data'
echo ' ⚙️ /opt/paperless/media'
echo ' ⚙️ /opt/paperless/static'
echo ' ⚙️ /opt/paperless/consume'
echo -e '########################################################\n'
exit 1
fi
"
check_success "✅ Credentials retrieved from paperless-ngx.creds"
# ===================== FINAL =====================
END_TIME=$(date +%s)
EXECUTION_TIME=$((END_TIME - START_TIME))
# Total size of related files
FINAL_SIZE=$(pct exec "$CTID" -- bash -c "du -sk /opt/paperless /var/lib/docker /var/lib/systemd /var/log 2>/dev/null | awk '{sum += \$1} END {print int(sum)}'")
DIFF_SIZE=$((FINAL_SIZE - INITIAL_SIZE))
echo -e "\n${YELLOW}===== SCRIPT EXECUTION SUMMARY =====${NC}"
echo -e "${GREEN}Steps completed successfully: $COMPLETED_STEPS out of $TOTAL_STEPS${NC}"
if [ ${#ERROR_STEPS[@]} -gt 0 ]; then
echo -e "${RED}Errors in steps:${NC}"
for step in "${ERROR_STEPS[@]}"; do
echo -e " - $step"
done
else
echo -e "${GREEN}No errors.${NC}"
fi
echo -e "⏱️ Script execution time: ${EXECUTION_TIME} seconds"
echo -e "📦 Approximate size of created files and services: ${DIFF_SIZE} KB"
Как запустить скрипт:
- Создать в Proxmox файл и скопировать в него код, например script.sh
- Добавить право на выполнение файла: chmod +x script.sh
- Выполнить скрипт с хоста Proxmox: ./script.sh
2. Приделываем костыли
В ходе всех изысканий пришёл к тому, чтобы реализовать скрипт для официальной сборки на основне docker образов, который будет перезагружать docker контейнер с web-server (как самого тяжёловесного) следущим образом:
- При достижении CPU 85% в течение 10 минут с интервалом проверки 1 минута
- При достижении RAM 70% в течение 10 минут с интервалом проверки 1 минута
- Опционально настройка ежедневной перезагрузки через планировщик crontab.
Спойлер
## 🛠 Script: `setup_monitor.sh`
## Description
## This script is a workaround for addressing the issue of
## RAM and CPU leaks in the Paperless service deployed in docker.
## The script monitors the CPU and RAM utilization of the webserver Docker container.
## If threshold values are exceeded for a specified period of time,
## it automatically restarts the webserver docker container.
## The host operating system and other docker containers of the Paperless service
## continue running, which minimizes downtime.
## All variable values are configurable through an interactive dialog.
## Optionally, you can use a script to set up a crontab schedule for daily
## restarting of the Paperless web server Docker container.
## This script was written with the help of AI.
#!/bin/bash
# ===================== SETTINGS =====================
START_TIME=$(date +%s)
TOTAL_STEPS=16
COMPLETED_STEPS=0
ERROR_STEPS=()
# For calculating file size
TOTAL_SIZE=0
# Colors
GREEN="\033[0;32m"
RED="\033[0;31m"
YELLOW="\033[1;33m"
NC="\033[0m" # no color
CHECK_MARK="\xE2\x9C\x85" # ✅
CROSS_MARK="\xE2\x9D\x8C" # ❌
# ===================== UTILITIES =====================
function show_progress() {
local step=$1
local message=$2
echo -e "[${GREEN} Step $step/${TOTAL_STEPS}${NC}] $message"
}
function check_success() {
if [ $? -eq 0 ]; then
((COMPLETED_STEPS++))
echo -e "${GREEN}${CHECK_MARK} Success: $1${NC}"
else
ERROR_STEPS+=("$1")
echo -e "${RED}${CROSS_MARK} Error during: $1${NC}"
fi
}
# ===================== Step 1: Install bc =====================
show_progress 1 "Installing 'bc' utility (Basic Calculator)..."
apt-get update -qq && apt-get install -y bc > /dev/null 2>&1
check_success "bc utility installed"
# ===================== Step 2: Dialog - Choose script path =====================
show_progress 2 "Choosing path for the script monitor_CPU_RAM_docker.sh..."
read -p "Use default path (/home/scripts)? [Y/n]: " default_path
if [[ "$default_path" =~ ^[Nn]$ ]]; then
read -p "Enter the directory path: " custom_path
read -p "Create 'scripts' directory inside it? [Y/n]: " create_dir
if [[ "$create_dir" =~ ^[Nn]$ ]]; then
TARGET_SCRIPT_DIR="$custom_path"
else
TARGET_SCRIPT_DIR="$custom_path/scripts"
fi
else
TARGET_SCRIPT_DIR="/home/scripts"
fi
mkdir -p "$TARGET_SCRIPT_DIR"
chmod 755 "$TARGET_SCRIPT_DIR"
check_success "Script directory created: $TARGET_SCRIPT_DIR"
# ===================== Step 3: Create monitor_CPU_RAM_docker.sh =====================
show_progress 3 "Creating monitor_CPU_RAM_docker.sh file..."
SCRIPT_PATH="$TARGET_SCRIPT_DIR/monitor_CPU_RAM_docker.sh"
touch "$SCRIPT_PATH"
chmod 755 "$SCRIPT_PATH"
# ===================== Step 3.1: Write monitoring script code =====================
cat << 'EOF' > "$SCRIPT_PATH"
#!/bin/bash
# Find the Docker container name
CONTAINER_NAME=$(docker ps --format '{{.Names}}' | grep -F 'webserver' | head -n 1)
# CPU and RAM threshold values (in percent)
CPU_THRESHOLD=170
RAM_THRESHOLD=70
# Time for resource usage evaluation (in seconds)
## CPU/RAM usage check interval: 1 minute
CHECK_INTERVAL=60
## Allowed time for exceeding CPU/RAM usage thresholds: 5 minutes
MONITOR_TIME=600
# Log file path
LOG_FILE="/home/log/docker_resource_monitor.txt"
# Get current CPU usage
get_cpu_usage() {
docker stats --no-stream --format "{{.CPUPerc}}" "$CONTAINER_NAME" | tr -d '%'
}
# Get current RAM usage
get_ram_usage() {
docker stats --no-stream --format "{{.MemPerc}}" "$CONTAINER_NAME" | tr -d '%'
}
# Log message function
log_message() {
echo "$(date '+%d-%m-%Y %H:%M:%S') - $1" >> "$LOG_FILE"
tail -n 10 "$LOG_FILE" > "${LOG_FILE}.tmp" && mv "${LOG_FILE}.tmp" "$LOG_FILE"
}
# Main monitoring loop
start_time=$(date +%s)
while true; do
# Get current CPU and RAM values
cpu_usage=$(get_cpu_usage)
ram_usage=$(get_ram_usage)
# Convert strings to numbers
cpu_usage=${cpu_usage//[^0-9.]/}
ram_usage=${ram_usage//[^0-9.]/}
# Check if CPU or RAM usage exceeds the threshold
if (( $(echo "$cpu_usage > $CPU_THRESHOLD" | bc -l) )); then
# If the CPU threshold is exceeded
current_time=$(date +%s)
elapsed_time=$((current_time - start_time))
if ((elapsed_time >= MONITOR_TIME)); then
# If MONITOR_TIME seconds have passed, log the event and restart the container
log_message "RAM threshold ($CPU_THRESHOLD%) exceeded. Current: $cpu_usage%"
docker restart "$CONTAINER_NAME"
start_time=$current_time # Update the start time
fi
elif (( $(echo "$ram_usage > $RAM_THRESHOLD" | bc -l) )); then
# If the RAM threshold is exceeded
current_time=$(date +%s)
elapsed_time=$((current_time - start_time))
if ((elapsed_time >= MONITOR_TIME)); then
# If 10 seconds have passed, log the event and restart the container
log_message "RAM threshold ($RAM_THRESHOLD%) exceeded. Current: $ram_usage%"
docker restart "$CONTAINER_NAME"
start_time=$current_time # Update the start time
fi
else
# If resources do not exceed the threshold, reset the timer
start_time=$(date +%s)
fi
# Wait before the next check cycle
sleep $CHECK_INTERVAL
done
EOF
check_success "Resource monitoring script created"
# ===================== Step 4: Dialog - set variable values =====================
show_progress 4 "Configuring variables..."
read -p "Enter CPU threshold (e.g., 85 for 1 CPU = 85, 2 CPUs = 170, etc.): " CPU_THRESHOLD
read -p "Enter RAM threshold (e.g., 70 for 70%): " RAM_THRESHOLD
read -p "Enter check interval in seconds (e.g., 60): " CHECK_INTERVAL
read -p "Enter max allowed threshold time in seconds (e.g., 300): " MONITOR_TIME
sed -i "s/^CPU_THRESHOLD=.*/CPU_THRESHOLD=$CPU_THRESHOLD/" "$SCRIPT_PATH"
sed -i "s/^RAM_THRESHOLD=.*/RAM_THRESHOLD=$RAM_THRESHOLD/" "$SCRIPT_PATH"
sed -i "s/^CHECK_INTERVAL=.*/CHECK_INTERVAL=$CHECK_INTERVAL/" "$SCRIPT_PATH"
sed -i "s/^MONITOR_TIME=.*/MONITOR_TIME=$MONITOR_TIME/" "$SCRIPT_PATH"
check_success "Script variables updated"
# ===================== Step 5: Create log directory =====================
show_progress 5 "Creating log directory..."
read -p "Use /home/log for logs? [Y/n]: " use_default_log
if [[ "$use_default_log" =~ ^[Nn]$ ]]; then
read -p "Enter custom log directory: " custom_log_dir
read -p "Create 'log' subdirectory inside? [Y/n]: " create_log_subdir
if [[ "$create_log_subdir" =~ ^[Nn]$ ]]; then
LOG_DIR="$custom_log_dir"
else
LOG_DIR="$custom_log_dir/log"
fi
else
LOG_DIR="/home/log"
fi
mkdir -p "$LOG_DIR"
chmod 755 "$LOG_DIR"
check_success "Log directory created: $LOG_DIR"
# ===================== Step 6: Create log file =====================
show_progress 6 "Creating log file..."
LOG_FILE="$LOG_DIR/log_docker_resources.txt"
touch "$LOG_FILE"
chmod 766 "$LOG_FILE"
check_success "Log file created: $LOG_FILE"
# Update log file path in the script
sed -i "s|^LOG_FILE=.*|LOG_FILE=\"$LOG_FILE\"|" "$SCRIPT_PATH"
# ===================== Step 7: Create systemd unit =====================
show_progress 7 "Creating systemd service..."
SERVICE_PATH="/etc/systemd/system/monitor_CPU_RAM_docker.service"
cat << EOF > "$SERVICE_PATH"
[Unit]
Description=Script 'monitor_CPU_RAM_docker.sh' starts when container starts
[Service]
Type=simple
User=root
ExecStart=$SCRIPT_PATH
[Install]
WantedBy=multi-user.target
EOF
chmod 644 "$SERVICE_PATH"
check_success "systemd unit file created"
# ===================== Steps 8-10: Enable and start systemd =====================
show_progress 8 "Reloading systemd..."
systemctl daemon-reload
check_success "systemd reloaded"
show_progress 9 "Enabling service to start at boot..."
systemctl enable monitor_CPU_RAM_docker.service --now
check_success "Service autostart enabled"
show_progress 10 "Starting the systemd service..."
systemctl start monitor_CPU_RAM_docker.service
check_success "systemd service started"
# ===================== Step 11: Create cron job =====================
show_progress 11 "Creating cron job to restart paperless webserver..."
read -p "Create cron job to restart paperless webserver? [Y/n]: " setup_cron
if [[ "$setup_cron" =~ ^[Nn]$ ]]; then
echo -e "${YELLOW}Skipping cron job creation.${NC}"
else
# === Step 11.1: Timezone setup ===
echo -e "${YELLOW}Set timezone?${NC}"
echo "1. Default: Europe/Moscow"
echo "2. Enter custom timezone"
read -p "Choose option (1/2): " tz_choice
if [[ "$tz_choice" == "2" ]]; then
read -p "Enter your timezone (e.g., Europe/Berlin): " user_timezone
if timedatectl list-timezones | grep -q "^$user_timezone$"; then
timedatectl set-timezone "$user_timezone"
check_success "Timezone set: $user_timezone"
else
echo -e "${RED}Invalid timezone. Keeping current.${NC}"
fi
else
timedatectl set-timezone "Europe/Moscow"
check_success "Timezone set: Europe/Moscow"
fi
# === Step 11.2: Choose path for log_reboot_docker.txt ===
echo -e "${YELLOW}Select path for the log file log_reboot_docker.txt:${NC}"
echo "1. Default: /home/log"
echo "2. Specify path manually and create 'log' subdirectory"
echo "3. Specify path manually without subdirectory"
read -p "Enter choice (1/2/3): " log_choice
case $log_choice in
2)
read -p "Enter directory path: " custom_log_path
CRON_LOG_DIR="$custom_log_path/log"
;;
3)
read -p "Enter directory path: " custom_log_path
CRON_LOG_DIR="$custom_log_path"
;;
*)
CRON_LOG_DIR="/home/log"
;;
esac
mkdir -p "$CRON_LOG_DIR"
chmod 755 "$CRON_LOG_DIR"
check_success "Log directory created: $CRON_LOG_DIR"
CRON_LOG_FILE="$CRON_LOG_DIR/log_reboot_docker.txt"
touch "$CRON_LOG_FILE"
chmod 766 "$CRON_LOG_FILE"
check_success "Log file created: $CRON_LOG_FILE"
# === Step 11.3: Choose path for crontab_reboot_docker.sh ===
echo -e "${YELLOW}Select path for the script crontab_reboot_docker.sh:${NC}"
echo "1. Default: /home/scripts"
echo "2. Specify path manually and create 'scripts' subdirectory"
echo "3. Specify path manually without subdirectory"
read -p "Enter choice (1/2/3): " script_choice
case $script_choice in
2)
read -p "Enter directory path: " custom_script_path
CRON_SCRIPT_DIR="$custom_script_path/scripts"
;;
3)
read -p "Enter directory path: " custom_script_path
CRON_SCRIPT_DIR="$custom_script_path"
;;
*)
CRON_SCRIPT_DIR="/home/scripts"
;;
esac
mkdir -p "$CRON_SCRIPT_DIR"
chmod 755 "$CRON_SCRIPT_DIR"
check_success "Script directory created: $CRON_SCRIPT_DIR"
# === Step 11.4: Create the script crontab_reboot_docker.sh ===
CRON_SCRIPT_PATH="$CRON_SCRIPT_DIR/crontab_reboot_docker.sh"
cat << EOF > "$CRON_SCRIPT_PATH"
#!/bin/bash
# Description of actions
# The Docker web server container for the Paperless service is restarted
# A message is written to the log file: log_reboot_docker.txt
# Containing the following information:
## Docker container name: paperless-webserver-1
## Restart date in the format: DD.MM.YYYY
## Restart time in the format: hh.mm.ss
# The expression "2>&1" is used to redirect the error stream (stderr)
# to the output stream (stdout)
# Searching for the docker container name 'webserver'
CONTAINER_NAME=$(docker ps --format '{{.Names}}' | grep -F 'webserver' | head -n 1)
# Restarting the docker container and writing to the log file
docker restart \$CONTAINER_NAME && echo "last reboot paperless-webserver-1: \$(date +%d.%m.%Y' '%H:%M:%S)" > "$CRON_LOG_FILE" 2>&1
EOF
chmod 755 "$CRON_SCRIPT_PATH"
check_success "The script created: $CRON_SCRIPT_PATH"
# === Step 11.5: Add to crontab ===
echo -e "${YELLOW}Set schedule for crontab:${NC}"
echo "1. Daily at 00:00 (default)"
echo "2. Specify custom schedule"
read -p "Enter choice (1/2): " cron_schedule_choice
if [[ "$cron_schedule_choice" == "2" ]]; then
read -p "Enter schedule in crontab format (e.g., '30 3 * * *'): " custom_cron_schedule
CRON_SCHEDULE="$custom_cron_schedule"
else
CRON_SCHEDULE="0 0 * * *"
fi
(crontab -l 2>/dev/null; echo "$CRON_SCHEDULE $CRON_SCRIPT_PATH") | crontab -
check_success "Task added to crontab: $CRON_SCHEDULE $CRON_SCRIPT_PATH"
fi
# ===================== FINAL =====================
END_TIME=$(date +%s)
EXECUTION_TIME=$((END_TIME - START_TIME))
# Total size of related files
FINAL_SIZE=$(pct exec $CTID -- bash -c "du -sk /opt/paperless /var/lib/docker /var/lib/systemd /var/log 2>/dev/null | awk '{sum += \$1} END {print sum}'")
DIFF_SIZE=$((FINAL_SIZE - INITIAL_SIZE))
echo -e "\n${YELLOW}===== SCRIPT EXECUTION SUMMARY =====${NC}"
echo -e "${GREEN}Steps completed successfully: $COMPLETED_STEPS из $TOTAL_STEPS${NC}"
if [ ${#ERROR_STEPS[@]} -gt 0 ]; then
echo -e "${RED}Errors in steps:${NC}"
for step in "${ERROR_STEPS[@]}"; do
echo -e " - $step"
done
else
echo -e "${GREEN}No errors.${NC}"
fi
echo -e "Script execution time: ${EXECUTION_TIME} seconds"
echo -e "📦 Approximate size of created files and services: ${DIFF_SIZE} KB"
Как запустить скрипт:
- Создать в LXC файл и скопировать в него код, например script.sh
- Добавить право на выполнение файла: chmod +x script.sh
- Выполнить скрипт в LXC: ./script.sh
В скрипте не реализован мониторинг активных процессов обработки дкументов в Paperless, поэтому если актуален сценарий с ситематической загрузкой больших пакетов документов, то можно настроить предельное пороговое время больше 10 минут, например 60 минут.
Назначение порогового значения для CPU имеет особенность - это значение необходимо умножать на количество CPU выделенных для LXC. Например, чтобы выставить пороговое значение для CPU в 85% для LXC:
- с 1 CPU, то указываем: 85
- с 2 CPU, то указываем: 170
- с 3 CPU, то указываем: 255
и т.д.
Из плюсов:
- Наличие официальных обновлений
- Всё работает из “коробки” при минимальных затратах на настройки
- Простое конфигурирование сервиса через docker-compose
- Простой перенос БД между разными экземплярами Paperless через копирование директорий data и pgdata
Из минусов:
- Субъективно менее стабильный (несколько раз сталкивался с 100% утилизацией CPU из-за циклической ошибки в web-server при загрузкке тяжёлых pdf).
- Сложнее диагностировать и анализировать утилизацию HDD (не всегда удаляются временные файлы из директории /tmp/paperless, а в случае с docker для этого нужно получать доступ в контейнер с web-server).
Ещё выявил одну особенность сервиса связанную с очисткой временных файлов в директории /tmp/paperless (для официальной версии на базе docker образов эта директория находится внутри docker контейнера web-server). Если посмотреть на эту директорию через утилиту ncdu, то можно обраружить в ней файлы, в т.ч. после загрузок, которые завершились с ошибкой. Таким образом эта директория потенциально может подъедать хранилище в LXC.
Например вы прокинули все каталоги с media и data в NAS и рассчитываете на то, что LXC в части хранилища расти не будет, но в один прекрасный день сервис может упасть, поэтому нужно настраивать мониторинг и создавать расписание в crontab на очистку этой директории.
Итоги
Для себя остановлися на официальной сборке на базе образов docker, отключил swap для LXC и внедрил скрипт для пониторинга и перезагрузки сервисов по расписанию.
Надеюсь, разработчики стабилизируют сервис или светлые головы из комьюнити найдут более изящное решение.
P.s.: Cкрипты писались с помощью ChatGPT и отлаживались на двух версиях Proxmox: 8.4.1 и 9.0.10.






