mirror of
https://github.com/jkaninda/mysql-bkup.git
synced 2026-11-13 08:37:15 +00:00
feat: mysql-bkup v2 on bkupkit
MysQL backup and restore built on the shared bkupkit core: streaming end to end, with no dump held in memory or written to local disk.
This commit is contained in:
1 parent
ccb3fcdf26
commit
28de0d7b52
65 files changed
+3453
-3871
No files matched your search
@@ -1,14 +1,6 @@
|
||||
# MYSQL-BKUP
|
||||
|
||||
**MYSQL-BKUP** is a Docker container image designed to **backup, restore, and migrate MySQL databases**.
|
||||
It supports a variety of storage options and ensures data security through GPG encryption.
|
||||
|
||||
MYSQL-BKUP is designed for seamless deployment on **Docker** and **Kubernetes**, simplifying MySQL backup, restoration, and migration across environments.
|
||||
It is a lightweight, multi-architecture solution compatible with **Docker**, **Docker Swarm**, **Kubernetes**, and other container orchestration platforms.
|
||||
|
||||
|
||||
|
||||
|
||||
**MYSQL-BKUP** backs up, restores and migrates **MySQL and MariaDB** databases. It runs as a Docker image (one-shot, as a scheduled daemon, or as a Kubernetes Job/CronJob) and streams every backup end to end: dump, compression, encryption and upload happen in one pipe, without temporary dump files and with a fixed memory footprint, whatever the database size.
|
||||
|
||||
[](https://github.com/jkaninda/mysql-bkup/actions/workflows/tests.yml)
|
||||
[](https://github.com/jkaninda/mysql-bkup/actions/workflows/release.yml)
|
||||
@@ -17,323 +9,439 @@ It is a lightweight, multi-architecture solution compatible with **Docker**, **D
|
||||

|
||||
<a href="https://ko-fi.com/jkaninda"><img src="https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/5cbed8a4ae2b88347c06c923_BuyMeACoffee_blue.png" height="20" alt="buy ma a coffee"></a>
|
||||
|
||||
Documentation: <https://jkaninda.github.io/mysql-bkup>
|
||||
|
||||
## Features
|
||||
|
||||
- **Flexible Storage Backends:**
|
||||
- Local filesystem
|
||||
- Amazon S3 & S3-compatible storage (e.g., MinIO, Wasabi)
|
||||
- FTP
|
||||
- SSH-compatible storage
|
||||
- Azure Blob storage
|
||||
- **MySQL and MariaDB, each with its own tools.** The server flavour is detected (`SELECT VERSION()`): MariaDB is dumped with `mariadb-dump` and restored with `mariadb`, MySQL 8+ with Oracle's `mysqldump` and `mysql` (8.4 LTS client). Tested against MySQL 8.0, MySQL 8.4 and MariaDB 11.8.
|
||||
- **Storage:** local, Amazon S3 and S3-compatible (MinIO, Wasabi, R2, ...), Google Cloud Storage, Azure Blob, SFTP, FTP/FTPS. Extra copies to more storages from the config file.
|
||||
- **Encryption:** GPG (passphrase or public key, the default and compatible with 1.x) or [age](https://age-encryption.org) (recipients or passphrase). Encryption fails closed: a configured key that cannot be loaded is an error, never a silent plaintext upload.
|
||||
- **Compression:** gzip (default, parallel) or zstd; restore detects the format.
|
||||
- **Backup index and safe retention:** every storage folder has an index (`mysql-bkup-metadata.json`) and a `.meta.json` sidecar per backup with its sha256. Retention by age and/or count only deletes backups the index knows, and always keeps the newest one.
|
||||
- **Commands:** `backup`, `restore`, `migrate`, `verify`, `list`, `delete`, `prune`, `reconcile`, `config`, `health`.
|
||||
- **Scheduling:** cron expressions (5 or 6 fields, `@daily`, `@every 6h`), no overlapping runs, clean shutdown on SIGTERM, Prometheus metrics, Docker `HEALTHCHECK`.
|
||||
- **Notifications:** email, Telegram, Slack, Discord, ntfy, generic webhooks (HMAC-signed), healthchecks.io-style pings.
|
||||
- **Security:** no shell anywhere; database passwords go to the client tools through a 0600 option file in a private directory (never on the command line), and every secret can be read from `<NAME>_FILE` (Docker/Kubernetes secrets).
|
||||
- **Multi-arch image:** `amd64`, `arm64`, `arm/v7` (see [Client tools](#client-tools)).
|
||||
|
||||
- **Data Security:**
|
||||
- Backups can be encrypted using **GPG** to ensure confidentiality.
|
||||
## Quick start
|
||||
|
||||
- **Deployment Flexibility:**
|
||||
- Available as the [jkaninda/mysql-bkup](https://hub.docker.com/r/jkaninda/mysql-bkup) Docker image.
|
||||
- Deployable on **Docker**, **Docker Swarm**, and **Kubernetes**.
|
||||
- Supports recurring backups of MySQL databases when deployed:
|
||||
- On Docker for automated backup schedules.
|
||||
- As a **Job** or **CronJob** on Kubernetes.
|
||||
|
||||
- **Notifications:**
|
||||
- Get real-time updates on backup success or failure via:
|
||||
- **Telegram**
|
||||
- **Email**
|
||||
|
||||
## 💡Use Cases
|
||||
|
||||
- **Scheduled Backups**: Automate recurring backups using Docker or Kubernetes.
|
||||
- **Disaster Recovery:** Quickly restore backups to a clean MySQL instance.
|
||||
- **Database Migration**: Seamlessly move data across environments using the built-in `migrate` feature.
|
||||
- **Secure Archiving:** Keep backups encrypted and safely stored in the cloud or remote servers.
|
||||
|
||||
|
||||
## ✅ Verified Platforms:
|
||||
MYSQL-BKUP has been tested and runs successfully on:
|
||||
|
||||
- Docker
|
||||
- Docker Swarm
|
||||
- Kubernetes
|
||||
- OpenShift
|
||||
|
||||
## Documentation is found at <https://jkaninda.github.io/mysql-bkup>
|
||||
|
||||
|
||||
## Links:
|
||||
|
||||
- [Docker Hub](https://hub.docker.com/r/jkaninda/mysql-bkup)
|
||||
- [Github](https://github.com/jkaninda/mysql-bkup)
|
||||
|
||||
## PostgreSQL solution :
|
||||
|
||||
- [PostgreSQL](https://github.com/jkaninda/pg-bkup)
|
||||
|
||||
|
||||
## Quickstart
|
||||
|
||||
### Simple Backup Using Docker CLI
|
||||
|
||||
To perform a one-time backup, bind your local volume to `/backup` in the container and run the `backup` command:
|
||||
Back up the database `shop` into `./backup`:
|
||||
|
||||
```shell
|
||||
docker run --rm --network your_network_name \
|
||||
-v $PWD/backup:/backup/ \
|
||||
-e "DB_HOST=dbhost" \
|
||||
-e "DB_PORT=3306" \
|
||||
-e "DB_USERNAME=username" \
|
||||
-e "DB_PASSWORD=password" \
|
||||
jkaninda/mysql-bkup backup -d database_name
|
||||
docker run --rm --network your_network \
|
||||
-v $PWD/backup:/backup \
|
||||
-e DB_HOST=mysql -e DB_USER=root -e DB_PASSWORD=password \
|
||||
jkaninda/mysql-bkup backup -d shop
|
||||
```
|
||||
|
||||
Alternatively, use an environment file (`--env-file`) for configuration:
|
||||
The last line of the output names the backup:
|
||||
|
||||
```
|
||||
level=INFO msg="Backup completed" database=shop file=shop_20261027_031500.sql.gz size="1.2 MiB" duration=2.1s storage=local location=/backup
|
||||
```
|
||||
|
||||
Restore it:
|
||||
|
||||
```shell
|
||||
docker run --rm --network your_network_name \
|
||||
--env-file your-env-file \
|
||||
-v $PWD/backup:/backup/ \
|
||||
jkaninda/mysql-bkup backup -d database_name
|
||||
docker run --rm --network your_network \
|
||||
-v $PWD/backup:/backup \
|
||||
-e DB_HOST=mysql -e DB_USER=root -e DB_PASSWORD=password \
|
||||
jkaninda/mysql-bkup restore -d shop -f shop_20261027_031500.sql.gz
|
||||
```
|
||||
|
||||
### Backup All Databases
|
||||
Restore accepts mysql-bkup backups and any plain `.sql` or gzip `.sql.gz` dump (encrypted or not). A missing database is created first.
|
||||
|
||||
To back up all databases on the server, use the `--all-databases` or `-a` flag. By default, this creates individual backup files for each database.
|
||||
|
||||
```shell
|
||||
docker run --rm --network your_network_name \
|
||||
-v $PWD/backup:/backup/ \
|
||||
-e "DB_HOST=dbhost" \
|
||||
-e "DB_PORT=3306" \
|
||||
-e "DB_USERNAME=username" \
|
||||
-e "DB_PASSWORD=password" \
|
||||
jkaninda/mysql-bkup backup --all-databases --disable-compression
|
||||
```
|
||||
|
||||
> **Note:** Use the `--all-in-one` or `-A` flag to combine backups into a single file.
|
||||
|
||||
---
|
||||
|
||||
### Simple Restore Using Docker CLI
|
||||
|
||||
To restore a database, bind your local volume to `/backup` and run the `restore` command:
|
||||
|
||||
```shell
|
||||
docker run --rm --network your_network_name \
|
||||
-v $PWD/backup:/backup/ \
|
||||
-e "DB_HOST=dbhost" \
|
||||
-e "DB_PORT=3306" \
|
||||
-e "DB_USERNAME=username" \
|
||||
-e "DB_PASSWORD=password" \
|
||||
jkaninda/mysql-bkup restore -d database_name -f backup_file.sql.gz
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Backup with Docker Compose
|
||||
|
||||
Below is an example of a `docker-compose.yml` file for running a one-time backup:
|
||||
### Docker Compose, scheduled
|
||||
|
||||
```yaml
|
||||
services:
|
||||
pg-bkup:
|
||||
# In production, pin your image tag to a specific release version instead of `latest`.
|
||||
# See available releases: https://github.com/jkaninda/mysql-bkup/releases
|
||||
mysql-bkup:
|
||||
image: jkaninda/mysql-bkup
|
||||
container_name: mysql-bkup
|
||||
command: backup
|
||||
command: backup -d shop --cron-expression "@daily"
|
||||
volumes:
|
||||
- ./backup:/backup
|
||||
environment:
|
||||
- DB_PORT=3306
|
||||
- DB_HOST=mysql
|
||||
- DB_NAME=foo
|
||||
- DB_USERNAME=bar
|
||||
- DB_PASSWORD=password
|
||||
- TZ=Europe/Paris
|
||||
networks:
|
||||
- web
|
||||
|
||||
networks:
|
||||
web:
|
||||
- DB_USER=backup
|
||||
- DB_PASSWORD_FILE=/run/secrets/db_password
|
||||
- BACKUP_RETENTION_DAYS=7
|
||||
- GPG_PASSPHRASE=change-me
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Recurring Backups with Docker
|
||||
|
||||
You can schedule recurring backups using the `--cron-expression` or `-e` flag:
|
||||
|
||||
```shell
|
||||
docker run --rm --network network_name \
|
||||
-v $PWD/backup:/backup/ \
|
||||
-e "DB_HOST=hostname" \
|
||||
-e "DB_USERNAME=user" \
|
||||
-e "DB_PASSWORD=password" \
|
||||
jkaninda/mysql-bkup backup -d dbName --cron-expression "@every 15m"
|
||||
```
|
||||
|
||||
For predefined schedules, refer to the [documentation](https://jkaninda.github.io/mysql-bkup/reference/#predefined-schedules).
|
||||
|
||||
---
|
||||
|
||||
## Deploy on Kubernetes
|
||||
|
||||
For Kubernetes, you can deploy `mysql-bkup` as a Job or CronJob. Below are examples for both.
|
||||
|
||||
### Kubernetes Backup Job
|
||||
|
||||
This example defines a one-time backup job:
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: backup-job
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 100
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: mysql-bkup
|
||||
# Pin the image tag to a specific release version in production.
|
||||
# See available releases: https://github.com/jkaninda/mysql-bkup/releases
|
||||
image: jkaninda/mysql-bkup
|
||||
command: ["backup", "-d", "dbname"]
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
env:
|
||||
- name: DB_HOST
|
||||
value: "mysql"
|
||||
- name: DB_USERNAME
|
||||
value: "user"
|
||||
- name: DB_PASSWORD
|
||||
value: "password"
|
||||
volumeMounts:
|
||||
- mountPath: /backup
|
||||
name: backup
|
||||
volumes:
|
||||
- name: backup
|
||||
hostPath:
|
||||
path: /home/toto/backup # Directory location on the host
|
||||
type: Directory # Optional field
|
||||
restartPolicy: Never
|
||||
```
|
||||
|
||||
### Kubernetes CronJob for Scheduled Backups
|
||||
|
||||
For scheduled backups, use a `CronJob`:
|
||||
### Kubernetes CronJob
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: pg-bkup-cronjob
|
||||
name: mysql-bkup
|
||||
spec:
|
||||
schedule: "0 2 * * *" # Runs daily at 2 AM
|
||||
schedule: "0 1 * * *"
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: pg-bkup
|
||||
- name: mysql-bkup
|
||||
image: jkaninda/mysql-bkup
|
||||
command: ["backup", "-d", "dbname"]
|
||||
env:
|
||||
- name: DB_HOST
|
||||
value: "mysql"
|
||||
- name: DB_USERNAME
|
||||
value: "user"
|
||||
- name: DB_PASSWORD
|
||||
value: "password"
|
||||
volumeMounts:
|
||||
- mountPath: /backup
|
||||
name: backup
|
||||
volumes:
|
||||
- name: backup
|
||||
hostPath:
|
||||
path: /home/toto/backup
|
||||
type: Directory
|
||||
restartPolicy: OnFailure
|
||||
args: ["backup", "-d", "shop", "--storage", "s3", "--path", "/mysql"]
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: mysql-bkup # DB_HOST, DB_USER, DB_PASSWORD, AWS_* ...
|
||||
```
|
||||
|
||||
---
|
||||
## Commands
|
||||
|
||||
## 🚀 Why Use MYSQL-BKUP?
|
||||
| Command | Purpose |
|
||||
|---|---|
|
||||
| `backup` | Back up now, or as a daemon with `--cron-expression` / `BACKUP_CRON_EXPRESSION`. `-d DB`, `-a` (every database, one file each), `-A` (every database in one file), `--exclude-db`, `--custom-name`, `--disable-compression`, `-c config.yaml` |
|
||||
| `restore` | Stream a backup back: `-f FILE [-d DB]`. Without `-d`, the dump's own `USE` statements decide (all-in-one backups) |
|
||||
| `migrate` | Copy `DB_NAME` (or every database with `-a`) straight into `TARGET_DB_*`: `dump \| restore` through a pipe, no file |
|
||||
| `verify` | Download, decrypt and decompress a backup and check its sha256, without restoring |
|
||||
| `list` | Backup history from the index (`--json`, `--storage-scan`) |
|
||||
| `delete` | Delete a backup, its sidecar and its index entry |
|
||||
| `prune` | Apply the retention policy (`--dry-run`) |
|
||||
| `reconcile` | Rebuild the index from the sidecars (`--from-index OLD` after renaming it) |
|
||||
| `config` | Show the resolved configuration with sources, secrets redacted; `--env-docs` prints the reference below |
|
||||
| `health` | Docker `HEALTHCHECK` for the daemon |
|
||||
| `version` | Print the version |
|
||||
|
||||
**MYSQL-BKUP** isn't just another MySQL backup tool, it's a robust, production-ready solution purpose-built for modern DevOps workflows.
|
||||
The image also has the `backup`, `restore` and `migrate` shortcuts (`docker run ... --entrypoint backup`, or in Kubernetes `command: [backup]`) and the `bkup` alias.
|
||||
|
||||
Here’s why developers, sysadmins, and DevOps choose **MYSQL-BKUP**:
|
||||
### Exit codes
|
||||
|
||||
### ✅ All-in-One Backup, Restore & Migration
|
||||
`0` success, `1` failure, `2` configuration error, `3` database connection or authentication, `4` storage, `5` encryption/decryption.
|
||||
|
||||
Whether you're backing up a single database, restoring critical data, or migrating across environments, MYSQL-BKUP handles it all with a **single, unified CLI** no scripting gymnastics required.
|
||||
## Storage
|
||||
|
||||
```shell
|
||||
# S3 / MinIO
|
||||
-e AWS_S3_ENDPOINT=https://s3.amazonaws.com -e AWS_S3_BUCKET_NAME=backups -e AWS_REGION=us-east-1 \
|
||||
-e AWS_ACCESS_KEY=... -e AWS_SECRET_KEY=... \
|
||||
jkaninda/mysql-bkup backup -d shop --storage s3 --path /mysql
|
||||
# MinIO and other S3 alternatives: add AWS_FORCE_PATH_STYLE=true (and AWS_DISABLE_SSL=true for plain http).
|
||||
# Without AWS_ACCESS_KEY, the AWS default credential chain is used (IAM role, IRSA, profile).
|
||||
|
||||
# SFTP (host keys are verified: mount a known_hosts file)
|
||||
-e SFTP_HOST=backup.example.com -e SFTP_USER=backup -e SFTP_IDENTITY_FILE=/config/id_ed25519 \
|
||||
-e SFTP_KNOWN_HOSTS=/config/known_hosts \
|
||||
jkaninda/mysql-bkup backup -d shop --storage sftp --path /home/backup/mysql
|
||||
|
||||
# FTP / FTPS
|
||||
-e FTP_HOST=ftp.example.com -e FTP_USER=backup -e FTP_PASSWORD=... -e FTP_TLS=true \
|
||||
jkaninda/mysql-bkup backup -d shop --storage ftp --path /mysql
|
||||
|
||||
# Azure Blob
|
||||
-e AZURE_STORAGE_ACCOUNT_NAME=... -e AZURE_STORAGE_ACCOUNT_KEY=... -e AZURE_STORAGE_CONTAINER_NAME=backups \
|
||||
jkaninda/mysql-bkup backup -d shop --storage azure --path mysql
|
||||
|
||||
# Google Cloud Storage
|
||||
-e GCS_BUCKET=backups -e GCS_CREDENTIALS_FILE=/config/gcs.json \
|
||||
jkaninda/mysql-bkup backup -d shop --storage gcs --path mysql
|
||||
```
|
||||
|
||||
`STORAGE` (env) and `--storage` are equivalent; `ssh` and `remote` are accepted for `sftp`. The object key is exactly `<path>/<file>`.
|
||||
|
||||
## Encryption
|
||||
|
||||
| Mode | Backup | Restore | File |
|
||||
|---|---|---|---|
|
||||
| GPG passphrase | `GPG_PASSPHRASE` | `GPG_PASSPHRASE` | `.sql.gz.gpg` |
|
||||
| GPG public key | `GPG_PUBLIC_KEY` (path or inline), or `/config/gnupg/public_key.asc` | `GPG_PRIVATE_KEY` (+ `GPG_PRIVATE_KEY_PASSPHRASE`) | `.sql.gz.gpg` |
|
||||
| age recipients | `AGE_RECIPIENTS` (`age1...`, `ssh-ed25519 ...`) or `AGE_RECIPIENTS_FILE` | `AGE_IDENTITY` / `AGE_IDENTITY_FILE` (default `/config/age/identity.txt`) | `.sql.gz.age` |
|
||||
| age passphrase | `AGE_PASSPHRASE` | `AGE_PASSPHRASE` | `.sql.gz.age` |
|
||||
|
||||
`ENCRYPTION=auto` (default) picks the mode from the keys that are set; GPG wins when both GPG and age keys are set. Restore detects the format from the content.
|
||||
|
||||
## Backup index and retention
|
||||
|
||||
Each storage folder holds an index file (`BACKUP_INDEX_NAME`, default `{tool}-metadata.json`; placeholders `{tool}`, `{engine}` (always `mysql`), `{name}`, `{db}`) and one `<backup>.meta.json` sidecar per backup. `list`, `delete`, `prune` and retention work from the index, so mysql-bkup only ever deletes files it created itself, and several instances can share a folder with different index names.
|
||||
|
||||
- `BACKUP_RETENTION_DAYS=N` deletes backups older than N days, `BACKUP_RETENTION_COUNT=N` keeps the newest N. The newest successful backup is always kept.
|
||||
- Backups made before the index existed (1.x) are never deleted, unless `BACKUP_RETENTION_ADOPT_LEGACY=true` imports them.
|
||||
- `BACKUP_INDEX=false` turns the index off; retention then only matches this tool's file names directly in the folder.
|
||||
|
||||
## Multiple databases
|
||||
|
||||
```shell
|
||||
backup -a # every database, one file each (system databases are skipped)
|
||||
backup -a --exclude-db test,tmp
|
||||
backup -A # everything in one file: all_databases_<timestamp>.sql.gz
|
||||
```
|
||||
|
||||
Or a YAML file (`-c /config/config.yaml`, also picked up automatically at `/config/config.yaml`); the 1.x format still works:
|
||||
|
||||
```yaml
|
||||
cronExpression: "@daily"
|
||||
databases:
|
||||
- name: shop
|
||||
host: mysql1
|
||||
user: backup
|
||||
password: ${SHOP_PASSWORD}
|
||||
path: /shop
|
||||
- name: blog # credentials from DB_USER_BLOG / BLOG_DB_USER ... or the global DB_*
|
||||
```
|
||||
|
||||
## Client tools
|
||||
|
||||
| Server | Dump | Restore |
|
||||
|---|---|---|
|
||||
| MariaDB | `mariadb-dump` (MariaDB 11.8 LTS client) | `mariadb` |
|
||||
| MySQL 8.0 / 8.4 / 9.x | Oracle `mysqldump` 8.4 LTS | Oracle `mysql` 8.4 LTS |
|
||||
|
||||
Dumps use `--single-transaction --routines --triggers --events --hex-blob --quick`, plus `--no-tablespaces --set-gtid-purged=OFF` on MySQL. A single-database dump has no `CREATE DATABASE`/`USE`, so it can be restored under another name.
|
||||
|
||||
The image is based on Debian 13 (trixie) slim. Oracle publishes its client for `amd64` and `arm64` only: on `arm/v7` the image has the MariaDB tools only, and MySQL servers are dumped with `mariadb-dump` (a warning is logged). `MYSQL_CLIENT=mysql|mariadb` forces one family.
|
||||
|
||||
TLS: `DB_SSL_MODE=disabled` (default, as in 1.x) `| preferred | required | verify_ca | verify_identity`, with `DB_SSL_CA` (used for the verify modes), `DB_SSL_CERT`, `DB_SSL_KEY`. 1.x values `0` and `1` still mean disabled and required. With the MariaDB client, `verify_ca` also checks the host name.
|
||||
|
||||
Restores are not atomic: MySQL cannot roll back DDL, so a restore that fails halfway leaves the statements before the failure applied (the exit code is non-zero). Restore into a new database, or keep a backup of the target, when that matters.
|
||||
|
||||
## Use with Miabi
|
||||
|
||||
Miabi runs `backup -d <db>`, `backup --storage s3 -d <db> --path <path>` and `restore -d <db> -f <file> [--storage s3 --path <path>]` with `DB_*`, `AWS_*` and `GPG_PASSPHRASE`, and reads the artifact name from the last `file=` in the output. That contract is unchanged and covered by the integration tests.
|
||||
|
||||
## Upgrading from 1.x
|
||||
|
||||
Everything that worked in 1.x keeps working; a few behaviours changed on purpose.
|
||||
|
||||
- **Image:** Debian 13 slim instead of Alpine, with Oracle's `mysqldump` for MySQL servers (correct MySQL 8 dumps) and `mariadb-dump` for MariaDB. `/bin/sh`, `/backup`, `/config`, the `backup`/`restore`/`migrate` shortcuts and the `bkup` alias are still there; `tini` is now PID 1.
|
||||
- **Renamed variables** are still read, with a deprecation warning: `DB_USER` → `DB_USER`, `S3_ENDPOINT` → `AWS_S3_ENDPOINT`, `BUCKET_NAME`/`AWS_BUCKET_NAME` → `AWS_S3_BUCKET_NAME`, `ACCESS_KEY`/`SECRET_KEY` → `AWS_ACCESS_KEY`/`AWS_SECRET_KEY`, `AWS_S3_PATH`/`S3_PATH`/`REMOTE_PATH` → `REMOTE_PATH`, `SFTP_HOST` → `SFTP_HOST`, `SFTP_IDENTITY_FILE` → `SFTP_IDENTITY_FILE`, `FTP_HOST_NAME` → `FTP_HOST`, `BACKUP_TMP_DIR` → `TMP_DIR`.
|
||||
- **`DB_SSL_MODE`** takes MySQL's mode names; `0`/`1` still work.
|
||||
- **`STORAGE` and `STORAGE_PATH` env are honoured** (1.x always used the `--storage` default and `/backup`). Unknown storage names are an error instead of a silent local backup.
|
||||
- **SFTP variables are renamed, and the old names are no longer read:** `SSH_HOST`/`SSH_HOST_NAME` → `SFTP_HOST`, `SSH_PORT` → `SFTP_PORT`, `SSH_USER` → `SFTP_USER`, `SSH_PASSWORD` → `SFTP_PASSWORD`, `SSH_IDENTIFY_FILE`/`SSH_IDENTITY_FILE` → `SFTP_IDENTITY_FILE`, `SSH_REMOTE_PATH` → `REMOTE_PATH`.
|
||||
- **SFTP host keys are verified by default.** Set `SFTP_KNOWN_HOSTS=/path/known_hosts`, or `SFTP_INSECURE_IGNORE_HOST_KEY=true` to keep the 1.x behaviour.
|
||||
- **`MAIL_SKIP_TLS` is read with its documented meaning** (`true` skips certificate verification). 1.x had it inverted, so `MAIL_SKIP_TLS=false` (the value in `.env.example`) turned verification *off*. Use `MAIL_INSECURE_SKIP_VERIFY` from now on.
|
||||
- **Retention only deletes indexed backups** (see above). 1.x deleted every file older than N days in the folder. Set `BACKUP_RETENTION_ADOPT_LEGACY=true` once to let retention handle 1.x backups, or `BACKUP_INDEX=false` for name-based retention.
|
||||
- **Encryption fails closed**: a `GPG_PUBLIC_KEY` that cannot be read is an error (1.x uploaded plaintext). The env var now wins over the auto-detected `/config/gnupg` key.
|
||||
- **Errors no longer kill the daemon**: a failed run is logged, notified and retried at the next schedule; other databases of the same run continue (`BACKUP_RESCUE_MODE` is no longer needed).
|
||||
- **Restore** works from every storage (1.x had no SFTP restore), is case-insensitive about the storage name, and no longer goes through `sh -c`.
|
||||
- **`migrate`** streams directly into the target (no intermediate file), creates the target database if needed, and `TARGET_DB_NAME` defaults to the source name; `-a` migrates every database.
|
||||
- **Notification templates** are built in; custom ones still go in `/config/templates` (`TEMPLATES_DIR`).
|
||||
- **Version** is injected with `-ldflags "-X main.Version=..."` (was `utils.Version`).
|
||||
|
||||
## Building
|
||||
|
||||
```shell
|
||||
make build # bin/mysql-bkup
|
||||
make test # unit tests
|
||||
make test-integration # Docker: MySQL 8.4/8.0, MariaDB 11.8, MinIO
|
||||
make docker-build VERSION=v2.0.0
|
||||
make env-docs # regenerate the reference below
|
||||
```
|
||||
|
||||
## Environment variables
|
||||
|
||||
Generated with `mysql-bkup config --env-docs`. Every secret also accepts `<NAME>_FILE`. Resolution order: flag, then env, then deprecated names, then the YAML `env:` section, then the default.
|
||||
|
||||
### storage
|
||||
|
||||
| Variable | Flag | Default | Fallbacks | Description |
|
||||
|---|---|---|---|---|
|
||||
| `STORAGE` | `--storage` `-s` | `local` | | Storage backend: local, s3, gcs, azure, sftp (ssh, remote), ftp (local, s3, gcs, azure, sftp, ssh, ftp, remote) |
|
||||
| `STORAGE_PATH` | | `/backup` | | Root directory of the local storage backend |
|
||||
| `REMOTE_PATH` | `--path` `-P` | | `AWS_S3_PATH` (deprecated), `S3_PATH` (deprecated) | Folder inside the storage backend (without file name) |
|
||||
|
||||
### general
|
||||
|
||||
| Variable | Flag | Default | Fallbacks | Description |
|
||||
|---|---|---|---|---|
|
||||
| `BACKUP_NAME` | `--name` `-n` | | | Base name of the artifact; a timestamp is appended (default: the database or volume name) |
|
||||
| `BACKUP_CUSTOM_NAME` | `--custom-name` | | | Fixed artifact name without timestamp (overwrites the previous one) |
|
||||
| `FILE_NAME` | `--file` `-f` | | | Backup file to restore, verify or delete |
|
||||
| `BACKUP_CONFIG_FILE` | `--config` `-c` | | | YAML configuration file (default /config/config.yaml if present) |
|
||||
| `BACKUP_TIMEOUT` | | | | Abort a backup after this duration (0 = none) |
|
||||
| `RESTORE_TIMEOUT` | | | | Abort a restore after this duration (0 = none) |
|
||||
| `TMP_DIR` | | `/tmp/backup` | `BACKUP_TMP_DIR` (deprecated) | Scratch directory (only for secret files and disk-based modes) |
|
||||
| `COMPRESSION` | `--compression` | `gzip` | | Compression: gzip, zstd or none (gzip, zstd, none) |
|
||||
| `COMPRESSION_LEVEL` | | | | Compression level (0 = default) |
|
||||
| `DISABLE_COMPRESSION` | `--disable-compression` | | | Same as COMPRESSION=none |
|
||||
| `RESTORE_VERIFY_FIRST` | `--verify-first` | `auto` | | Verify the whole artifact before restoring (auto: on for GPG-encrypted volumes) (auto, true, false) |
|
||||
| `LOG_LEVEL` | | `info` | | Log level (debug, info, warn, error) |
|
||||
| `LOG_FORMAT` | | `text` | | Log format (text, json) |
|
||||
| `TIME_FORMAT` | | `2006-01-02 15:04:05` | | Time layout used in notifications |
|
||||
| `RESULT_FILE` | `--result-file` | | | Write the run result as JSON to this path |
|
||||
|
||||
### scheduling
|
||||
|
||||
| Variable | Flag | Default | Fallbacks | Description |
|
||||
|---|---|---|---|---|
|
||||
| `BACKUP_CRON_EXPRESSION` | `--cron-expression` `-e` | | | Run as a daemon on this cron schedule (5 or 6 fields, @daily, @every 6h) |
|
||||
| `BACKUP_RUN_ON_START` | | | | In daemon mode, run one backup immediately at start |
|
||||
| `BACKUP_RESCUE_MODE` | | | | Deprecated: runs always continue with the remaining targets after a failure |
|
||||
| `METRICS_ADDR` | | | | Serve Prometheus /metrics and /healthz on this address in daemon mode (e.g. :9090) |
|
||||
| `HEALTH_FILE` | | `/tmp/bkup-health.json` | | State file read by the health command |
|
||||
| `HEALTH_MAX_AGE` | | | | health fails when the last success is older than this (default: 2x the schedule interval) |
|
||||
|
||||
### retention
|
||||
|
||||
| Variable | Flag | Default | Fallbacks | Description |
|
||||
|---|---|---|---|---|
|
||||
| `BACKUP_RETENTION_DAYS` | | `0` | | Delete backups older than N days (0 = keep) |
|
||||
| `BACKUP_RETENTION_COUNT` | | `0` | | Keep only the newest N backups (0 = no limit) |
|
||||
| `BACKUP_RETENTION_ALLOW_ROOT` | | | | Allow pattern-based retention at the storage root (index disabled) |
|
||||
| `BACKUP_RETENTION_ADOPT_LEGACY` | | | | Import pre-index backups matching this tool's naming into the index |
|
||||
| `BACKUP_INDEX` | | `true` | | Keep the backup index file and per-backup .meta.json sidecars |
|
||||
| `BACKUP_INDEX_NAME` | `--index-name` | `{tool}-metadata.json` | | Index file name; placeholders {tool} {engine} {name} {db} |
|
||||
|
||||
### storage-s3
|
||||
|
||||
| Variable | Flag | Default | Fallbacks | Description |
|
||||
|---|---|---|---|---|
|
||||
| `AWS_S3_ENDPOINT` | | | `S3_ENDPOINT` (deprecated), `AWS_ENDPOINT_URL_S3`, `AWS_ENDPOINT_URL` | S3 endpoint (empty = AWS) |
|
||||
| `AWS_S3_BUCKET_NAME` | | | `BUCKET_NAME` (deprecated), `AWS_BUCKET_NAME` (deprecated) | S3 bucket |
|
||||
| `AWS_REGION` | | `us-east-1` | `AWS_DEFAULT_REGION` | S3 region |
|
||||
| `AWS_ACCESS_KEY` | | | `ACCESS_KEY` (deprecated), `AWS_ACCESS_KEY_ID` | S3 access key (empty = default AWS credential chain) |
|
||||
| `AWS_SECRET_KEY` | | | `SECRET_KEY` (deprecated), `AWS_SECRET_ACCESS_KEY`, `AWS_SECRET_KEY_FILE` | S3 secret key |
|
||||
| `AWS_SESSION_TOKEN` | | | `AWS_SESSION_TOKEN_FILE` | S3 session token |
|
||||
| `AWS_PROFILE` | | | | AWS shared config profile |
|
||||
| `AWS_DISABLE_SSL` | | | | Use http for the S3 endpoint |
|
||||
| `AWS_FORCE_PATH_STYLE` | | | | Path-style S3 addressing (MinIO) |
|
||||
| `S3_STORAGE_CLASS` | | | | S3 storage class |
|
||||
| `S3_SSE` | | | | Server-side encryption: AES256 or aws:kms (aes256, aws:kms) |
|
||||
| `S3_SSE_KMS_KEY_ID` | | | | KMS key id for aws:kms |
|
||||
| `S3_PART_SIZE` | | `64MiB` | | Multipart part size; max object size is 10000 x this |
|
||||
| `S3_UPLOAD_CONCURRENCY` | | `2` | | Parallel part uploads (memory = part size x this) |
|
||||
|
||||
### storage-sftp
|
||||
|
||||
| Variable | Flag | Default | Fallbacks | Description |
|
||||
|---|---|---|---|---|
|
||||
| `SFTP_HOST` | | | | SFTP host |
|
||||
| `SFTP_PORT` | | `22` | | SFTP port |
|
||||
| `SFTP_USER` | | | | SFTP user |
|
||||
| `SFTP_PASSWORD` | | | `SFTP_PASSWORD_FILE` | SFTP password |
|
||||
| `SFTP_IDENTITY_FILE` | | | | SFTP private key file (the SSH agent is also used when SSH_AUTH_SOCK is set) |
|
||||
| `SFTP_IDENTITY_PASSPHRASE` | | | `SFTP_IDENTITY_PASSPHRASE_FILE` | Passphrase of the SFTP private key |
|
||||
| `SFTP_KNOWN_HOSTS` | | | | known_hosts file used to verify the SFTP host key (default ~/.ssh/known_hosts) |
|
||||
| `SFTP_INSECURE_IGNORE_HOST_KEY` | | | | Do not verify the SFTP host key (insecure) |
|
||||
|
||||
### storage-ftp
|
||||
|
||||
| Variable | Flag | Default | Fallbacks | Description |
|
||||
|---|---|---|---|---|
|
||||
| `FTP_HOST` | | | `FTP_HOST_NAME` (deprecated) | FTP host |
|
||||
| `FTP_PORT` | | `21` | | FTP port |
|
||||
| `FTP_USER` | | | | FTP user |
|
||||
| `FTP_PASSWORD` | | | `FTP_PASSWORD_FILE` | FTP password |
|
||||
| `FTP_TLS` | | | | Use explicit FTPS |
|
||||
|
||||
### storage-azure
|
||||
|
||||
| Variable | Flag | Default | Fallbacks | Description |
|
||||
|---|---|---|---|---|
|
||||
| `AZURE_STORAGE_ACCOUNT_NAME` | | | | Azure storage account |
|
||||
| `AZURE_STORAGE_ACCOUNT_KEY` | | | `AZURE_STORAGE_ACCOUNT_KEY_FILE` | Azure storage account key |
|
||||
| `AZURE_STORAGE_CONTAINER_NAME` | | | | Azure blob container |
|
||||
| `AZURE_STORAGE_ENDPOINT` | | | | Azure blob endpoint (Azurite, sovereign clouds) |
|
||||
| `AZURE_STORAGE_SAS_TOKEN` | | | `AZURE_STORAGE_SAS_TOKEN_FILE` | Azure SAS token (instead of the account key) |
|
||||
|
||||
### storage-gcs
|
||||
|
||||
| Variable | Flag | Default | Fallbacks | Description |
|
||||
|---|---|---|---|---|
|
||||
| `GCS_BUCKET` | | | | GCS bucket |
|
||||
| `GCS_CREDENTIALS_FILE` | | | `GOOGLE_APPLICATION_CREDENTIALS` | GCS service account JSON file |
|
||||
| `GCS_CREDENTIALS_JSON` | | | `GCS_CREDENTIALS_JSON_FILE` | GCS service account JSON content |
|
||||
| `GCS_ENDPOINT` | | | | GCS endpoint (emulators) |
|
||||
|
||||
### encryption
|
||||
|
||||
| Variable | Flag | Default | Fallbacks | Description |
|
||||
|---|---|---|---|---|
|
||||
| `ENCRYPTION` | `--encryption` | `auto` | | auto (from the keys set), none, gpg or age (auto, none, gpg, age) |
|
||||
| `ENCRYPTION_ARMOR` | | | | ASCII-armor encrypted output |
|
||||
| `GPG_PASSPHRASE` | | | `GPG_PASSPHRASE_FILE` | GPG symmetric passphrase |
|
||||
| `GPG_PUBLIC_KEY` | | | | GPG public key(s): file path or inline key (default /config/gnupg/public_key.{asc,gpg}) |
|
||||
| `GPG_PRIVATE_KEY` | | | `GPG_PRIVATE_KEY_FILE` | GPG private key for restore: file path or inline key (default /config/gnupg/private_key.{asc,gpg}) |
|
||||
| `GPG_PRIVATE_KEY_PASSPHRASE` | | | `GPG_PRIVATE_KEY_PASSPHRASE_FILE` | Passphrase of the GPG private key (default GPG_PASSPHRASE) |
|
||||
| `AGE_RECIPIENTS` | | | | age recipients (age1..., ssh-ed25519 ...), comma or newline separated |
|
||||
| `AGE_RECIPIENTS_FILE` | | | | File with age recipients |
|
||||
| `AGE_PASSPHRASE` | | | `AGE_PASSPHRASE_FILE` | age passphrase (scrypt) |
|
||||
| `AGE_IDENTITY` | | | `AGE_IDENTITY_FILE` | age identity (AGE-SECRET-KEY-...) for restore |
|
||||
| `AGE_IDENTITY_FILE` | | | | age identity file for restore (default /config/age/identity.txt) |
|
||||
|
||||
### notifications
|
||||
|
||||
| Variable | Flag | Default | Fallbacks | Description |
|
||||
|---|---|---|---|---|
|
||||
| `MAIL_HOST` | | | | SMTP host |
|
||||
| `MAIL_PORT` | | `587` | | SMTP port |
|
||||
| `MAIL_USERNAME` | | | | SMTP user |
|
||||
| `MAIL_PASSWORD` | | | `MAIL_PASSWORD_FILE` | SMTP password |
|
||||
| `MAIL_FROM` | | | | Sender address |
|
||||
| `MAIL_TO` | | | | Recipients, comma separated |
|
||||
| `MAIL_SKIP_TLS` | | | | Deprecated: true skips certificate verification (use MAIL_INSECURE_SKIP_VERIFY) |
|
||||
| `MAIL_INSECURE_SKIP_VERIFY` | | | | Skip SMTP certificate verification |
|
||||
| `MAIL_TLS` | | `starttls` | | SMTP TLS mode (starttls, tls, none) |
|
||||
| `TG_TOKEN` | | | `TELEGRAM_BOT_TOKEN`, `TG_TOKEN_FILE` | Telegram bot token |
|
||||
| `TG_CHAT_ID` | | | `TELEGRAM_CHAT_ID` | Telegram chat id |
|
||||
| `POSTA_URL` | | | | Posta base URL (https://posta.example.com) for email notifications through Posta |
|
||||
| `POSTA_API_KEY` | | | `POSTA_API_KEY_FILE` | Posta API key with the send scope |
|
||||
| `POSTA_WORKSPACE_ID` | | | | Posta workspace id (only for account-wide API keys) |
|
||||
| `POSTA_FROM` | | | | Posta sender address (default MAIL_FROM) |
|
||||
| `POSTA_TO` | | | | Posta recipients, comma separated (default MAIL_TO) |
|
||||
| `POSTA_TEMPLATE` | | | | Posta template name for notifications; the event fields are passed as template data (default: built-in HTML) |
|
||||
| `POSTA_TEMPLATE_ERROR` | | | | Posta template name for failures (default POSTA_TEMPLATE) |
|
||||
| `POSTA_LANGUAGE` | | | | Posta template language |
|
||||
| `WEBHOOK_URL` | | | | Generic JSON webhook URL |
|
||||
| `WEBHOOK_SECRET` | | | `WEBHOOK_SECRET_FILE` | HMAC-SHA256 secret for the X-Bkup-Signature header |
|
||||
| `SLACK_WEBHOOK_URL` | | | `SLACK_WEBHOOK_URL_FILE` | Slack incoming webhook URL |
|
||||
| `DISCORD_WEBHOOK_URL` | | | `DISCORD_WEBHOOK_URL_FILE` | Discord webhook URL |
|
||||
| `NTFY_URL` | | | | ntfy topic URL |
|
||||
| `NTFY_TOKEN` | | | `NTFY_TOKEN_FILE` | ntfy access token |
|
||||
| `NOTIFY_ON` | | `always` | | When to notify (always, failure, success, never) |
|
||||
| `HEALTHCHECK_PING_URL` | | | | healthchecks.io-style URL pinged on start (/start), success and failure (/fail) |
|
||||
| `BACKUP_REFERENCE` | | | | Free text included in notifications |
|
||||
| `TEMPLATES_DIR` | | `/config/templates` | | Directory with custom notification templates |
|
||||
|
||||
### database
|
||||
|
||||
| Variable | Flag | Default | Fallbacks | Description |
|
||||
|---|---|---|---|---|
|
||||
| `DB_URL` | | | `DB_URL_FILE` | Connection URL mysql://user:password@host:port/database?ssl-mode=required (overrides host, port, user, password) |
|
||||
| `DB_HOST` | | `localhost` | | Database host (a path starting with / is a Unix socket) |
|
||||
| `DB_PORT` | | `3306` | | Database port |
|
||||
| `DB_NAME` | `--dbname` `-d` | | | Database name |
|
||||
| `DB_USER` | | | `DB_USERNAME` | Database user |
|
||||
| `DB_PASSWORD` | | | `DB_PASSWORD_FILE` | Database password |
|
||||
| `DB_SSL_MODE` | | `disabled` | | TLS mode: disabled, preferred, required, verify_ca, verify_identity (1.x values 0 and 1 mean disabled and required) |
|
||||
| `DB_SSL_CA` | | `/etc/ssl/certs/ca-certificates.crt` | | CA certificate file, used with DB_SSL_MODE=verify_ca or verify_identity |
|
||||
| `DB_SSL_CERT` | | | | Client certificate file |
|
||||
| `DB_SSL_KEY` | | | | Client key file |
|
||||
| `BACKUP_ALL_DATABASES` | `--all-databases` `-a` | | | Back up every database, one file each |
|
||||
| `BACKUP_ALL_IN_ONE` | `--all-in-one` `-A` | | | Back up every database into a single file |
|
||||
| `BACKUP_EXCLUDE_DATABASES` | `--exclude-db` | | | Databases to skip with --all-databases, comma separated |
|
||||
| `TARGET_DB_URL` | | | `TARGET_DB_URL_FILE` | migrate: target connection URL |
|
||||
| `TARGET_DB_HOST` | | | | migrate: target host |
|
||||
| `TARGET_DB_PORT` | | `3306` | | migrate: target port |
|
||||
| `TARGET_DB_NAME` | | | | migrate: target database (default: the source name) |
|
||||
| `TARGET_DB_USER` | | | `TARGET_DB_USERNAME` | migrate: target user |
|
||||
| `TARGET_DB_PASSWORD` | | | `TARGET_DB_PASSWORD_FILE` | migrate: target password |
|
||||
| `TARGET_DB_SSL_MODE` | | `disabled` | | migrate: target TLS mode (same values as DB_SSL_MODE) |
|
||||
| `TARGET_DB_SSL_CA` | | `/etc/ssl/certs/ca-certificates.crt` | | migrate: target CA certificate file |
|
||||
| `MYSQL_CLIENT` | | `auto` | | Client tools: auto (mariadb-dump for MariaDB, Oracle mysqldump for MySQL), mysql or mariadb (auto, mysql, mariadb) |
|
||||
|
||||
|
||||
### 🔄 Works Everywhere You Deploy
|
||||
|
||||
Designed to be cloud-native:
|
||||
|
||||
* **Runs seamlessly on Docker, Docker Swarm, and Kubernetes**
|
||||
* Supports **CronJobs** for automated scheduled backups
|
||||
* Compatible with GitOps and CI/CD workflows
|
||||
|
||||
### ☁️ Flexible Storage Integrations
|
||||
|
||||
Store your backups **anywhere**:
|
||||
|
||||
* Local disks
|
||||
* Amazon S3, MinIO, Wasabi, Azure Blob, FTP, SSH
|
||||
|
||||
### 🔒 Enterprise-Grade Security
|
||||
|
||||
* **GPG Encryption**: Protect sensitive data with optional encryption before storing backups locally or in the cloud.
|
||||
* **Secure Storage** Options: Supports S3, Azure Blob, SFTP, and SSH with encrypted transfers, keeping backups safe from unauthorized access.
|
||||
|
||||
### 📬 Instant Notifications
|
||||
|
||||
Stay in the loop with real-time notifications via **Telegram** and **Email**. Know immediately when a backup succeeds—or fails.
|
||||
|
||||
### 🏃♂️ Lightweight and Fast
|
||||
|
||||
Written in **Go**, MYSQL-BKUP is fast, multi-arch compatible (`amd64`, `arm64`, `arm/v7`), and optimized for minimal memory and CPU usage. Ideal for both cloud and edge deployments.
|
||||
|
||||
### 🧪 Tested. Verified. Trusted.
|
||||
|
||||
Actively maintained with **automated testing**, **Docker image size optimizations**, and verified support across major container platforms.
|
||||
|
||||
---
|
||||
## Available image registries
|
||||
|
||||
This Docker image is published to both Docker Hub and the GitHub container registry.
|
||||
Depending on your preferences and needs, you can reference both `jkaninda/mysql-bkup` as well as `ghcr.io/jkaninda/mysql-bkup`:
|
||||
|
||||
```
|
||||
docker pull jkaninda/mysql-bkup
|
||||
docker pull ghcr.io/jkaninda/mysql-bkup
|
||||
```
|
||||
|
||||
Documentation references Docker Hub, but all examples will work using ghcr.io just as well.
|
||||
Tags: `latest`, and immutable release tags (`v2.0.0`, `2.0.0`, `2.0`, `2`). Pin a release tag in production.
|
||||
|
||||
## References
|
||||
|
||||
We created this image as a simpler and more lightweight alternative to existing solutions. Here’s why:
|
||||
|
||||
- **Lightweight:** Written in Go, the image is optimized for performance and minimal resource usage.
|
||||
- **Multi-Architecture Support:** Supports `arm64` and `arm/v7` architectures.
|
||||
- **Docker Swarm Support:** Fully compatible with Docker in Swarm mode.
|
||||
- **Kubernetes Support:** Designed to work seamlessly with Kubernetes.
|
||||
## PostgreSQL
|
||||
|
||||
Looking for PostgreSQL? See [pg-bkup](https://github.com/jkaninda/pg-bkup).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the LICENSE file for details.
|
||||
|
||||
## Authors
|
||||
|
||||
**Jonas Kaninda**
|
||||
- <https://github.com/jkaninda>
|
||||
|
||||
## Copyright
|
||||
|
||||
|
||||
Reference in new issue
Block a user