Резервное копирование PostgreSQL для 1С: BASH, pg_dump, cron, ротация и почта на Ubuntu
База данных - Архивирование (backup)
Резервное копирование нескольких баз PostgreSQL для 1С:Предприятие 8.3 на Ubuntu я оформляю одним басн-файлом: pg_dump со сжатием, ротация локальных файлов, журнал backupres.log и письмо по cron. Дальше - пошаговая настройка регламента, чтобы не запускать pg_dump вручную на каждой базе.
Регламент логического бэкапа: границы и результат для администратора 1С
На сервере 1С 8.3 с PostgreSQL от вас обычно ждут дампы всех рабочих баз по расписанию, контроль диска и короткий отчёт, что цикл отработал. Это логическое резервное копирование через pg_dump при работающей платформе. Физическая репликация, архив WAL и pg_basebackup - другой уровень: они нужны, когда важна точка восстановления «между транзакциями» или полная гибель кластера. Для выгрузки содержимого баз на тестовый стенд, переноса или архива «на вчера» хватает потока pg_dump в сжатие и файлов в PATHCORE.
cron гоняет один скрипт; для каждой строки LISTBASE появляется каталог PATHCORE/<имя_базы>/, внутри до четырёх свежих файлов.dump.gz и отдельная чистка по возрасту 30 дней. В backupres.log пишутся время старта и размеры, на почту уходит тот же список - администратор видит состав дампов без входа по SSH. Так и задуман регламент после каждого цикла.
Окружение в материале - Ubuntu, клиент PostgreSQL с pg_dump, pigz и отправка почты (sendEmail по классической схеме или msmtp, если пакета sendEmail уже нет). Файловую базу 1С и выгрузку.dt через конфигуратор здесь не разбираем.
PATHCORE, LISTBASE и пакеты postgresql-client, pigz, почта
Задайте PATHCORE и LISTBASE, затем установите postgresql-client и pigz - без каталога и списка баз cron некуда складывать результат.
Каталоги PATHCORE и файлы журнала для каждой базы LISTBASE
pg_dump входит в postgresql-client; версию клиента разумно держать не ниже серверной PostgreSQL на этом хосте или рядом.
sudo apt update
sudo apt install -y postgresql-client pigz bc
sudo apt install -y sendemail libio-socket-ssl-perl libnet-ssleay-perl
sendEmail давно помечен как unmaintained: на свежих Ubuntu пакета может не быть. Я чаще ставлю msmtp с ~/.msmtprc (chmod 600) или mailx с SMTP и TLS. Тело письма по-прежнему собираем в файл, тему и UTF-8 задаём явно - меняется только вызов отправки в конце скрипта.
Имена баз - как в pg_database, регистр важен. В контуре 1С это отдельные информационные базы на одном инстансе PostgreSQL.
Один PATHCORE, подкаталог на каждую базу из LISTBASE, общий backupres.log и временный backupres_log.txt для письма. Пользователь cron должен писать в PATHCORE и читать ~/.pgpass и файл SMTP; обычно завожу пользователя backup, не root.
sudo mkdir -p /var/backups/postgres-onec
sudo chown backup:backup /var/backups/postgres-onec
Список баз в скрипте - многострочная переменная:
LISTBASE=$'prod_buh
prod_zup
test_erp'
PATHCORE=/var/backups/postgres-onec
Опечатка в имени базы - частая поломка регламента: pg_dump упадёт, а без очистки FORBODY и проверки ERR письмо может выглядеть «успешным» хвостом прошлого цикла. В рабочем скрипте это закрыто флагом ERR и обнулением FORBODY в начале run.
Задание cron: запуск backup-onec.sh каждые 12 часов
Расписание - два раза в сутки, в ноль минут каждого двенадцатого часа.
0 */12 * * * /bin/bash /var/backups/postgres-onec/backup-onec.sh >> /var/backups/postgres-onec/cron.log 2>&1
crontab редактируем у того же пользователя ОС, что выполняет pg_dump (backup). Проверка - ручной запуск тем же путём. Если cron «не видит» pigz, надёжнее полные пути /usr/bin/pg_dump, как в скрипте ниже.
Для 1С 8.3 окно обслуживания согласуйте с нагрузкой: pg_dump читает данные, pigz пишет на том PATHCORE, на больших базах растёт длительность цикла. Платформа при этом работает; pg_dump для одной базы держит одну длинную транзакцию дампа, изR09;за этого растёт I/O и отдельные операции DDL или VACUUM могут подождать. Смотрю df на PATHCORE и длительность последних строк в backupres.log, прежде чем ставить cron на часы пиковой проводки.
Роль backup_dump должна проходить pg_hba.conf (часто local или scram для хоста 127.0.0.1) - иначе cron будет падать с «password authentication failed», хотя вручную под тем же пользователем всё ок.
backup-onec.sh: цикл pg_dump, pigz и имена файлов с датой
Один проход - от строки LISTBASE до файла с датой в имени; на схеме видно, где cron и почта.
Контекст: backup-onec.sh: цикл pg_dump, pigz и имена файлов с датой

Цикл cron: дамп, сжатие, ротация, лог и уведомление
В исходных how-to дамп идёт от суперпользователя postgres; ниже подключение через роль backup_dump и.pgpass - отдельная учётная запись с минимальными правами вместо postgres «по умолчанию». Поток тот же: pg_dump с ключом -c, pipe в pigz, имя PATHCORE/<база>/<дата>-<база>.dump.gz. Ключ -c удобен при восстановлении на пустую базу через psql: в начале скрипта дампа идут DROP. На прод восстановление только после прогона на копии. mkdir -p создаёт каталог базы перед первым дампом.
Ниже цельный backup-onec.sh: дамп, проверка gzip -t, ротация, журнал, отправка почты. Паролей PostgreSQL и SMTP в файле нет.
#!/bin/bash
set -uo pipefail
DATA=$(date +"%Y-%m-%d_%H-%M")
PATHCORE=/var/backups/postgres-onec
BACKUPRES="${PATHCORE}/backupres.log"
FORBODY="${PATHCORE}/backupres_log.txt"
n=1048576
LISTBASE=$'prod_buh
prod_zup'
PGUSER=backup_dump
export PGUSER
source "${PATHCORE}/.backup_env" 2>/dev/null || true
ERR=0: > "$FORBODY"
echo "" >> "$BACKUPRES"
echo "Start backup from $(date +"%Y-%m-%d_%H-%M-%S")" | tee -a "$BACKUPRES" >> "$FORBODY"
while IFS= read -r line; do
[ -z "$line" ] && continue
path="${PATHCORE}/${line}"
mkdir -p "$path"
outfile="${path}/${DATA}-${line}.dump.gz"
if! /usr/bin/pg_dump -U "$PGUSER" -c "$line" | /usr/bin/pigz -c > "$outfile"; then
echo "ERROR pg_dump/pigz for ${line} at $(date +"%Y-%m-%d_%H-%M-%S")" >> "$BACKUPRES"
ERR=1
continue
fi
if! gzip -t "$outfile"; then
echo "ERROR gzip -t failed for ${outfile}" >> "$BACKUPRES"
ERR=1
rm -f "$outfile"
continue
fi
colfile=$(find "$path" -maxdepth 1 -type f -name '*.dump.gz' 2>/dev/null | wc -l)
if [ "$colfile" -gt 4 ]; then
oldfile=$(ls -1rt "$path"/*.dump.gz 2>/dev/null | head -1)
rm -f "$oldfile"
fi
find "$path" -maxdepth 1 -type f -name '*.dump.gz' -mtime +30 -delete
done <<< "$LISTBASE"
while IFS= read -r linee; do
[ -z "$linee" ] && continue
find "${PATHCORE}/${linee}/" -maxdepth 1 -type f -name '*.dump.gz' -print0 2>/dev/null | while IFS= read -r -d '' file; do
sum=$(stat --printf="%s" "$file")
result=$(LC_NUMERIC="en_US.UTF-8" printf "%.2fMb" "$(echo "$sum/$n" | bc -l)")
shortfile=$(basename "$file")
echo "$shortfile $result" >> "$BACKUPRES"
echo "$shortfile $result" >> "$FORBODY"
done
done <<< "$LISTBASE"
if [ "$ERR" -eq 0 ]; then
if command -v sendemail >/dev/null 2>&1; then
sendemail -f "$FROM" -t "$MAILTO" -u "PostgreSQL backup OK $(hostname -s)" \
-m "$(cat "$FORBODY")" -s "$SMTPSERVER" -o tls=yes \
-xu "$SMTPLOGIN" -xp "$SMTPPASS" -o message-charset=utf-8
elif command -v msmtp >/dev/null 2>&1; then
{
echo "To: $MAILTO"
echo "From: $FROM"
echo "Subject: PostgreSQL backup OK $(hostname -s)"
echo "Content-Type: text/plain; charset=utf-8"
echo ""
cat "$FORBODY"
} | msmtp -a default "$MAILTO"
fi: > "$FORBODY"
else
tail -n 50 "$BACKUPRES" > "${PATHCORE}/backup_fail_snippet.txt"
if command -v sendemail >/dev/null 2>&1; then
sendemail -f "$FROM" -t "$MAILTO" -u "BACKUP FAILED $(hostname -s)" \
-m "$(cat "${PATHCORE}/backup_fail_snippet.txt")" -s "$SMTPSERVER" -o tls=yes \
-xu "$SMTPLOGIN" -xp "$SMTPPASS" -o message-charset=utf-8
elif command -v msmtp >/dev/null 2>&1; then
{
echo "To: $MAILTO"
echo "From: $FROM"
echo "Subject: BACKUP FAILED $(hostname -s)"
echo "Content-Type: text/plain; charset=utf-8"
echo ""
cat "${PATHCORE}/backup_fail_snippet.txt"
} | msmtp -a default "$MAILTO"
fi: > "$FORBODY"
fi
Фрагмент ротации внутри того же цикла (после успешного gzip -t):
colfile=$(find "$path" -maxdepth 1 -type f -name '*.dump.gz' 2>/dev/null | wc -l)
if [ "$colfile" -gt 4 ]; then
oldfile=$(ls -1rt "$path"/*.dump.gz 2>/dev/null | head -1)
rm -f "$oldfile"
fi
find "$path" -maxdepth 1 -type f -name '*.dump.gz' -mtime +30 -delete
Ротация: четыре последние копии и find -mtime +30
В каталоге каждой базы считаем только файлы *.dump.gz - посторонние файлы в PATHCORE/<база>/ ротацию не ломают. Если дампов больше четырёх, удаляем один самый старый по времени модификации. find с -mtime +30 снимает архивы старше 30 календарных дней. Это два независимых ограничения.
При cron каждые 12 часов «четыре файла» - это примерно двое суток хвоста по четырём запускам, а не «четыре последние ночи». Четыре полных дампа крупной ERP на одном томе - заметный объём; df по PATHCORE обязателен. Обрыв pg_dump или pigz при нехватке места даёт битый файл - поэтому сразу после записи идёт gzip -t. Локальная ротация не заменяет вынос: после успешного цикла имеет смысл rsync или scp на другой хост поверх правил «четыре копии и тридцать дней».
colfile=$(find "$path" -maxdepth 1 -type f -name '*.dump.gz' 2>/dev/null | wc -l)
if [ "$colfile" -gt 4 ]; then
oldfile=$(ls -1rt "$path"/*.dump.gz 2>/dev/null | head -1)
rm -f "$oldfile"
fi
find "$path" -maxdepth 1 -type f -name '*.dump.gz' -mtime +30 -delete
backupres.log, backupres_log.txt и письмо по SMTP
После дампов администратору нужен отчёт на почту; на схеме - как тело уходит на SMTP с TLS.

СМТР-отчёт после цикла pg_dump: тело из backupres_log.txt
backupres.log копит историю; FORBODY (backupres_log.txt) - только текущий цикл. В начале run обнуляем FORBODY, затем tee пишет время старта. Список файлов с размерами в мегабайтах собираем вторым проходом по тем же строкам, что LISTBASE, а не по содержимому FORBODY:
: > "$FORBODY"
echo "" >> "$BACKUPRES"
echo "Start backup from $(date +"%Y-%m-%d_%H-%M-%S")" | tee -a "$BACKUPRES" >> "$FORBODY"
while IFS= read -r linee; do
[ -z "$linee" ] && continue
find "${PATHCORE}/${linee}/" -maxdepth 1 -type f -name '*.dump.gz' -print0 2>/dev/null | while IFS= read -r -d '' file; do
sum=$(stat --printf="%s" "$file")
result=$(LC_NUMERIC="en_US.UTF-8" printf "%.2fMb" "$(echo "$sum/$n" | bc -l)")
shortfile=$(basename "$file")
echo "$shortfile $result" >> "$BACKUPRES"
echo "$shortfile $result" >> "$FORBODY"
done
done <<< "$LISTBASE"
FROM, MAILTO, SMTPSERVER, логин и пароль - в.backup_env (chmod 600) или EnvironmentFile= у systemd timer. Кодировка письма - UTF-8. При ERR равном нулю тема вроде «PostgreSQL backup OK»; при ошибке - «BACKUP FAILED», тело из хвоста backupres.log. Успешное письмо при сбое дампа регламент обнуляет.
Полный цикл отправки уже в backup-onec.sh (sendemail или msmtp). Для msmtp заранее настройте аккаунт умолчанию в ~/.msmtprc у пользователя cron.
.pgpass, проверка дампа, формат -Fc и секреты для 1С 8.3
Пароль PostgreSQL - в ~/.pgpass (600), строка hostname:port:база:username:password. Роль backup_dump подключается к каждой базе из LISTBASE и читает схему для pg_dump. Одного GRANT SELECT ON ALL TABLES IN SCHEMA public на одну базу для типовой ERP часто мало: pg_dump может потребовать sequences, large объекты, иногда представления; в PG 10+ на практике проще выдать pg_read_all_data (если политика безопасности позволяет) или калибровать права по тексту ошибок pg_dump. Для первого внедрения иногда оставляют владельца БД или read-only реплику - но не путать с логическим дампом на бою.
CREATE ROLE backup_dump LOGIN PASSWORD 'задайте_сильный_пароль';
GRANT CONNECT ON DATABASE prod_buh TO backup_dump;
GRANT CONNECT ON DATABASE prod_zup TO backup_dump;
\c prod_buh
GRANT USAGE ON SCHEMA public TO backup_dump;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO backup_dump;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO backup_dump;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO backup_dump;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON SEQUENCES TO backup_dump;
\c prod_zup
GRANT USAGE ON SCHEMA public TO backup_dump;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO backup_dump;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO backup_dump;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO backup_dump;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON SEQUENCES TO backup_dump;
-- PostgreSQL 14+: при согласии политики безопасности
-- GRANT pg_read_all_data TO backup_dump;
Имена для сверки с LISTBASE:
SELECT datname
FROM pg_database
WHERE datistemplate = false
AND datname NOT IN ('postgres');
Лишняя строка в LISTBASE - ошибка pg_dump и ERR=1; пропущенная база - дыра в регламенте.
После.dump.gz проверяем gzip -t; коды pg_dump и pigz пишем в backupres.log. set -e на весь скрипт жёсткий: одна база с ошибкой оборвёт цикл, поэтому удобнее ERR и continue по базам.
Крупные базы 1С: pipe в pigz даёт привычный.sql.gz и gunzip | psql. Когда цикл не укладывается в окно, перехожу на формат -Fc (custom): pg_dump -Fc -f file.dump без pipe, восстановление pg_restore -j N. Пока места и времени хватает, оставляю.sql.gz; когда узкое место - однопоточный psql при восстановлении. Для архива -Fc проверка: pg_restore --list file.dump > /dev/null.
Секреты SMTP в.backup_env:
FROM=noreply@example.com
MAILTO=admin@example.com
SMTPSERVER=smtp.example.com
SMTPLOGIN=noreply@example.com
SMTPPASS=секрет
chmod 600. Пароли из примеров в прод без отдельного защищённого файла - риск утечки вместе с копией самого скрипта из бэкапа конфигов.
Пример.pgpass: localhost:5432:prod_buh:backup_dump:пароль; для всех баз пользователя - localhost:5432:*:backup_dump:пароль. PGPASSWORD в cron не нужен.
Восстановление 1С - не только pg_restore или psql: версия платформы, права на каталоги сервера, иногда блокировка пользователей на время развёртывания. Логический дамп не заменяет WAL и не спасает при уничтожении всего кластера без отдельного бэкапа ролей (pg_dumpall --globals-only периодически, если переносите на другой сервер).
Сеансы 1С продолжают работать, но I/O на томе данных PostgreSQL и на PATHCORE растёт. Если дампы лежат на том же диске, что data_directory, четыре полных копии нескольких баз упираются в лимит раньше, чем кажется по одной базе.
После успешного цикла и gzip -t для новых файлов можно добавить rsync -a path/ user@backup-host:/archive/postgres/ или scp только свежего outfile. Локальная ротация экономит место на сервере приложений; удалённая копия закрывает отказ диска или ВМ.
Кратко по рискам:
| Ситуация | Решение | Риск |
|---|---|---|
| Несколько баз 1С на одном PostgreSQL | LISTBASE и цикл while read | Опечатка в имени базы, пропуск в списке |
| Пароль в скрипте или в git | ~/.pgpass,.backup_env chmod 600 | Утечка учётных данных |
| Диск PATHCORE заполнен | df, ротация 4+30, вынос rsync/scp | Битый или неполный дамп |
| sendEmail недоступен | msmtp, mailx с SMTP | Нет отчёта при успешном бэкапе |
| Очень большая база | pg_dump -Fc, pg_restore -j | Долгий цикл, нагрузка в часы пика |
| Нужен PITR / физический бэкап | WAL, pg_basebackup (отдельная тема) | Логический дамп не покрывает сценарий |
- Опечатка или лишняя база в LISTBASE - сверка с pg_database; pg_dump падает, ERR=1, письмо о сбое.
- Недостаточно прав backup_dump - неполный дамп или ошибка pg_dump; pg_read_all_data или донастройка sequences и CONNECT на каждую базу.
- Том PATHCORE заполнен - мониторинг df; битый.dump.gz отлавливает gzip -t.
- Четыре копии при cron 0 */12 - около двух суток истории на диске; рост ERP умножает объём.
- Только локальные файлы - пожар диска; rsync/scp после успешного цикла.
- Логический дамп - не PITR; для DR нужны другие инструменты и бэкап globals.
- sendEmail отсутствует в репозитории - msmtp или mailx с тем же телом из FORBODY.
- pg_hba не пускает backup - правило для пользователя cron и метода подключения.
Перед включением cron прогоните backup-onec.sh вручную дважды: появились.dump.gz, gzip -t без ошибок, строки в backupres.log, одно тестовое письмо. Потом остаётся строка в crontab и контроль места на PATHCORE раз в неделю.
Вступайте в нашу телеграмм-группу Инфостарт
Вы можете заказать платную адаптацию этой статьи под ваши задачи на «Бирже заказов».
- 0% комиссии — оплата напрямую исполнителю;
- Исполнители любого масштаба — от отдельных специалистов до команд под проект;
- Прямой обмен контактами между заказчиком и исполнителем;
- Безопасная сделка — при необходимости;
- Рейтинги, кейсы и прозрачная система откликов.