by: Helge, published: Feb 26, 2025, updated: Feb 27, 2025, in

Encrypted Offsite Backup With Ransomware Protection for WordPress

This article explains how to set up restic (with the resticprofile wrapper) for automated scheduled backups of your (dockerized) WordPress server. The backups are protected from ransomware through temporary immutability, which makes it much harder for attackers to delete your data.

Prerequisites

This post uses the server setup described in great detail in my Guide: WordPress on Dockerized Apache on Hetzner Cloud.

The backup configuration is based on my detailed article about restic: Encrypted Offsite Backup With Ransomware Protection for Your Homeserver.

All that’s left to do here is bring the two together.

Backup Strategy

We’re using restic (with the resticprofile wrapper) on the Docker host to back up both the host configuration as well as the Docker container data.

Backing Up the Database From a Running Docker Container

The simplest way to back up a running docker container is to stop it and copy all the files. I employed that strategy in my article about restic on my home server. In that scenario, it’s OK to stop Docker for the duration of the backup. It saves us the trouble of figuring out how to perform a hot backup for each container individually.

Obviously, things are different with a webserver. You want it to run 24/7. But there’s only one database involved and we know exactly how to perform a hot backup: we dump its contents into a .sql file.

Ransomware Protection Through Temporary Immutability

If you back up WordPress by way of a plugin, like I did for a long time, the application keys to read from and, critically, write to the storage location need to be available to the plugin. If an attacker manages to compromise your WordPress instance they’ll have access to the backup storage – and they can wipe it or do other creative things with your backup data.

By moving the storage keys from the WordPress instance to the Docker host we make it a lot harder for attackers to gain access to them. They’d have to break out of the WordPress Docker container first. But that is not the end.

Due to the way restic is architected, we can even create an application key without delete capabilities. Thus, even if an attacker managed to get hold of all the data on your WordPress Docker host, they wouldn’t be able to wipe your backup data. See my restic article for more details.

Benefits Compared to WordPress Backup Plugins

The solution presented here has numerous benefits compared to a WordPress backup plugin:

  • Better security
  • Faster backups
  • Higher performance
  • Less backup storage

Backup Implementation

Follow the steps in my restic article to set up the (Backblaze or S3) storage and the restic/resticprofile tool combo.

As changes from the original setup, I placed the resticprofile configuration in /data/resticprofile/profiles.yaml and modified the configuration slightly. Note that I’m also running a command to dump the WordPress database before the backup.

The following lists the full configuration:

default:
  lock: "/tmp/resticprofile-profile-default.lock"
  force-inactive-lock: true
  initialize: true
  repository: "s3:BUCKET_ENDPOINT_URL/YOUR_BUCKET_NAME"     # Example: s3:s3.eu-central-003.backblazeb2.com/YOUR_BUCKET_NAME 
  password-file: "YOUR_BUCKET_NAME.key"
  status-file: "backup-status.json"

  env:
    AWS_ACCESS_KEY_ID: "YOUR_KEY_ID"
    AWS_SECRET_ACCESS_KEY: "YOUR_KEY"

  # Backup command
  backup:
    run-before:
      - 'docker exec mariadb /usr/bin/mariadb-dump --user=root --password=YOUR_DB_PASSWORD --lock-tables --all-databases  > /data/docker/lamp/data/mariadb-backup/backup.sql'
    run-finally:
      - 'rm /data/docker/lamp/data/mariadb-backup/backup.sql'
    one-file-system: true                                   # Don't leave the file system via mount points
    source:
      - "/data/resticprofile"
      - "/etc/ssh/sshd_config"
      - "/etc/docker/daemon.json"
      - "/etc/sysctl.conf"
      - "/data/docker/caddy/Caddyfile"
      - "/data/docker/caddy/caddy_security.conf"
      - "/data/docker/caddy/container-vars.env"
      - "/data/docker/caddy/docker-compose.yml"
      - "/data/docker/lamp/config/apache-misc"
      - "/data/docker/lamp/config/php"
      - "/data/docker/lamp/config/vhosts"
      - "/data/docker/lamp/data/mariadb-backup"
      - "/data/docker/lamp/data/www"
      - "/data/docker/lamp/dockerfile-apache"
      - "/data/docker/lamp/container-vars-mariadb.env"
      - "/data/docker/lamp/docker-compose.yml"
      - "/etc/postfix/main.cf"
      - "/etc/postfix/mynetworks"
      - "/etc/postfix/sasl_passwd"
      - "/etc/postfix/generic"
      - "/etc/cron.weekly/00logwatch"
      - "/etc/logwatch"
    schedule: "04:00"
    schedule-permission: system
    schedule-lock-wait: 10m
    schedule-log: '/data/resticprofile/backup.log'
    verbose: 2                                              # Write details about each processed file to the log

  # Retention policy command
  forget:
    keep-daily: 7
    keep-weekly: 8
    keep-monthly: 12
    keep-yearly: 10
    prune: true
    schedule: "05:00"
    schedule-permission: system
    schedule-lock-wait: 1h

  # Verify command
  check:
    schedule: "06:00"
    schedule-permission: system
    schedule-lock-wait: 1h

Before putting the above to use, create the directory /data/docker/lamp/data/mariadb-backup and replace the following variables with values suitable for your environment:

  • BUCKET_ENDPOINT_URL
  • YOUR_BUCKET_NAME
  • YOUR_KEY_ID
  • YOUR_KEY
  • YOUR_DB_PASSWORD

Previous Article Firmware-Update der Enertex Meta² KNX-Raumcontroller