Update backup script

This commit is contained in:
2026-01-21 19:14:59 +00:00
parent 0024c5e1d7
commit 6b3eec8303
10 changed files with 383 additions and 80 deletions

View File

@@ -7,43 +7,43 @@ The configuration files for the Illegal Crime Minecraft server.
Follow these steps to set up and configure the project:
1. **Clone the repository**:
```
git clone https://gitea.leaf.home.kappeh.org/Homelab/minecraft_server.git
cd minecraft_server
```
2. **Create required directories**: See [Directory Setup](#directory-setup) for details.
3. **Set up environment variables**: See [Environment Variables](#environment-variables) for details.
4. **Import world files**:
The world for the Fabric server is located at `fabric/data/world`. If you want to use an existing world, copy it to this location before running the container:
```
cp path/to/world fabric/data/world
```
5. **Adjust permissions (recommended)**: Run the following commands in the root of the repository, replacing `PUID` and `PGID` with the corresponding values from your `.env` file:
```
sudo chown -R PUID:PGID .
sudo chmod -R 770 .
```
6. **Start the Docker Compose stack**: To start the services in detached mode, run:
```
docker compose up -d
```
7. **Stop the Docker Compose stack**: To stop and remove the running containers, use:
```
docker compose down
```
## Directory Setup
Make a directory called `schematics` in the root of the repository. This directory is used for storing [WorldEdit](https://worldedit.org/) schematic files. To create it, run:
@@ -73,12 +73,11 @@ Then, open `.env` in a text editor and set appropriate values for your setup.
|`DCLINK_CHANNEL`|The Discord channel ID where the bot will operate. See [dclink](#dclink) for more information.|
|`DCLINK_ROLE`|The ID of the role that the bot will give to Discord members when they link their accounts. See [dclink](#dclink) for more information.|
|`DCLINK_TOKEN`|The bot token used for authentication with Discord's API. Ensure this is kept secret and secure.|
|`LUCKPERMS_DB_PASSWORD`|The password for the LuckPerms database. This should be a secure password.|
|`LUCKPERMS_PASSWORD`|The password for the LuckPerms database. This should be a secure password.|
Instead of using environment variables, the secrets section of `docker-compose.yml` can be modified to read files instead. This method will likely be used by default in future.
## dclink
This configuration uses [dclink](https://github.com/Kalimero2Team/dclink) to link users' Minecraft accounts and Discord accounts. This is so that a user can gain access to the Minecraft server if they are a member of the configured Discord server.
This configuration uses [dclink](https://github.com/Kalimero2Team/dclink) to link users' Minecraft accounts and Discord accounts. This is so that a user can gain access to the Minecraft server if they are a member of the configured Discord server.
For this to work you must first [setup a discord bot](https://github.com/Kalimero2Team/dclink/wiki/Setup-Discord-Bot). Then set the relevant [Environment Variables](#environment-variables).

View File

@@ -5,9 +5,10 @@ services:
user: 2015:2015 # minecraft_server:minecraft_server
restart: unless-stopped
depends_on:
init_volumes:
init:
condition: service_completed_successfully
luckperms_db:
restart: true
luckperms:
condition: service_healthy
restart: true
fabric:
@@ -20,7 +21,7 @@ services:
- rcon_password
- forwarding_secret
- dclink_token
- luckperms_db_password
- luckperms_password
networks:
minecraft_server_network:
ipv4_address: "10.100.1.3"
@@ -48,7 +49,7 @@ services:
CFG_DCLINK_CHANNEL: ${DCLINK_CHANNEL}
CFG_DCLINK_ROLE: ${DCLINK_ROLE}
CFG_DCLINK_TOKEN_FILE: /run/secrets/dclink_token
CFG_LUCKPERMS_DB_PASSWORD_FILE: /run/secrets/luckperms_db_password
CFG_LUCKPERMS_PASSWORD_FILE: /run/secrets/luckperms_password
PLUGINS: |
https://github.com/dbkynd-minecraft/VelocityPlayerList/releases/download/v1.0/PlayerList-1.0.jar
@@ -65,8 +66,9 @@ services:
user: 2015:2015 # minecraft_server:minecraft_server
restart: unless-stopped
depends_on:
init_volumes:
init:
condition: service_completed_successfully
restart: true
fabric:
condition: service_healthy
restart: true
@@ -90,9 +92,10 @@ services:
container_name: minecraft_server_paper
restart: unless-stopped
depends_on:
init_volumes:
init:
condition: service_completed_successfully
luckperms_db:
restart: true
luckperms:
condition: service_healthy
restart: true
healthcheck:
@@ -103,7 +106,7 @@ services:
secrets:
- rcon_password
- forwarding_secret
- luckperms_db_password
- luckperms_password
networks:
minecraft_server_network:
ipv4_address: "10.100.1.4"
@@ -166,7 +169,7 @@ services:
CFG_RCON_PASSWORD_FILE: /run/secrets/rcon_password
CFG_FORWARDING_SECRET_FILE: /run/secrets/forwarding_secret
CFG_LUCKPERMS_DB_PASSWORD_FILE: /run/secrets/luckperms_db_password
CFG_LUCKPERMS_PASSWORD_FILE: /run/secrets/luckperms_password
PLUGINS: |
https://github.com/EssentialsX/Essentials/releases/download/2.20.1/EssentialsX-2.20.1.jar
@@ -189,9 +192,10 @@ services:
container_name: minecraft_server_fabric
restart: unless-stopped
depends_on:
init_volumes:
init:
condition: service_completed_successfully
luckperms_db:
restart: true
luckperms:
condition: service_healthy
restart: true
healthcheck:
@@ -202,7 +206,7 @@ services:
secrets:
- rcon_password
- forwarding_secret
- luckperms_db_password
- luckperms_password
networks:
minecraft_server_network:
ipv4_address: "10.100.1.5"
@@ -260,7 +264,7 @@ services:
CFG_RCON_PASSWORD_FILE: /run/secrets/rcon_password
CFG_FORWARDING_SECRET_FILE: /run/secrets/forwarding_secret
CFG_LUCKPERMS_DB_PASSWORD_FILE: /run/secrets/luckperms_db_password
CFG_LUCKPERMS_PASSWORD_FILE: /run/secrets/luckperms_password
MODRINTH_PROJECTS: |
badpackets:hjhT2sMz
@@ -298,14 +302,15 @@ services:
vanilla-permissions:7awQNHzw
worldedit:3TQ8W0Ar
luckperms_db:
luckperms:
image: postgres:17.4
container_name: minecraft_server_luckperms_db
container_name: minecraft_server_luckperms
user: 2015:2015 # minecraft_server:minecraft_server
restart: unless-stopped
depends_on:
init_volumes:
init:
condition: service_completed_successfully
restart: true
healthcheck:
test: ["CMD-SHELL", "pg_isready -U luckperms -d luckperms"]
interval: 10s
@@ -313,7 +318,7 @@ services:
start_period: 30s
timeout: 10s
secrets:
- luckperms_db_password
- luckperms_password
networks:
minecraft_server_network:
ipv4_address: "10.100.1.2"
@@ -321,23 +326,35 @@ services:
- ${LUCKPERMS_PORT}:5432
volumes:
- luckperms_data:/var/lib/postgresql/data:rw
- backups:/backups:rw
- /etc/passwd:/etc/passwd:ro
# - ./backups:/backups:rw
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/luckperms_db_password
POSTGRES_PASSWORD_FILE: /run/secrets/luckperms_password
POSTGRES_USER: luckperms
POSTGRES_DB: luckperms
PGDATA: /var/lib/postgresql/data/pgdata
init_volumes:
container_name: minecraft_server_init_volumes
image: busybox:1.37.0
user: root:root
command: /init_volumes.sh
# Helper for dumping sqlite databases during backups
sqlite_helper:
container_name: minecraft_server_sqlite_helper
image: grepular/sqlite3:3.51.2
user: 0:0
restart: no
network_mode: none
volumes:
- ./scripts/init_volumes.sh:/init_volumes.sh:ro
- backups:/backups:rw
- velocity_data:/velocity_data:ro
init:
container_name: minecraft_server_init
image: busybox:1.37.0
user: root:root
command: /init.sh
restart: no
network_mode: none
volumes:
- ./scripts/init.sh:/init.sh:ro
- backups:/backups:rw
# Used for resolving user and group names within the init script
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro
@@ -368,8 +385,8 @@ secrets:
environment: FORWARDING_SECRET
dclink_token:
environment: DCLINK_TOKEN
luckperms_db_password:
environment: LUCKPERMS_DB_PASSWORD
luckperms_password:
environment: LUCKPERMS_PASSWORD
volumes:
velocity_data:
@@ -380,3 +397,4 @@ volumes:
paper_data:
fabric_data:
luckperms_data:
backups:

View File

@@ -100,7 +100,7 @@ data {
# Credentials for the database.
username = "luckperms"
password = "${CFG_LUCKPERMS_DB_PASSWORD}"
password = "${CFG_LUCKPERMS_PASSWORD}"
# These settings apply to the MySQL connection pool.
# - The default values will be suitable for the majority of users.

View File

@@ -100,7 +100,7 @@ data:
# Credentials for the database.
username: luckperms
password: '${CFG_LUCKPERMS_DB_PASSWORD}'
password: '${CFG_LUCKPERMS_PASSWORD}'
# These settings apply to the MySQL connection pool.
# - The default values will be suitable for the majority of users.

273
scripts/backup.sh Executable file
View File

@@ -0,0 +1,273 @@
#!/usr/bin/env sh
# Adjustable parameters
## The number of backups to keep
BACKUPS_KEEP=24
# Global variables (Do NOT change)
CLEANUP_FABRIC=false
CLEANUP_LUCKPERMS=false
CLEANUP_PAPER=false
main() {
log_info "=== Backup run started at $(date -u '+%Y-%m-%dT%H:%M:%SZ') ==="
log_info "Keeping last $BACKUPS_KEEP backups"
check_root_permissions
init_backup
broadcast_status started
backup_bluemap || cleanup_failure
backup_fabric || cleanup_failure
backup_luckperms || cleanup_failure
backup_paper || cleanup_failure
backup_schematics || cleanup_failure
backup_velocity || cleanup_failure
finalize_backup || cleanup_failure
prune_backups
cleanup_success
broadcast_status finished
log_info "=== Backup run completed successfully at $(date -u '+%Y-%m-%dT%H:%M:%SZ') ==="
}
check_root_permissions() {
if [ "$EUID" -ne 0 ]; then
log_error "This script must be run by the root user"
exit 1
fi
log_info "Running as root (EUID=$EUID)"
}
init_backup() {
BACKUP_ID="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
BACKUP_DIR="/backups/$BACKUP_ID"
log_info "Creating backup directory: $BACKUP_DIR"
docker compose run --rm init sh -c '
mkdir -p "$1" &&
chown minecraft_server:minecraft_server "$1"
' -- "$BACKUP_DIR" || {
log_error "Failed to create backup directory $BACKUP_DIR"
exit 1
}
log_info "Backup directory created and ownership set"
}
broadcast_status() {
STATUS="$1"
log_info "Broadcasting backup status: $STATUS"
if docker compose exec -T fabric true > /dev/null 2>&1; then
docker compose exec -T fabric rcon-cli "tellraw @a [{\"text\":\"Server\",\"color\":\"light_purple\"},{\"text\":\": Backup $STATUS.\",\"color\":\"white\"}]" > /dev/null 2>&1
fi
if docker compose exec -T paper true > /dev/null 2>&1; then
docker compose exec -T paper rcon-cli "tellraw @a [{\"text\":\"Server\",\"color\":\"light_purple\"},{\"text\":\": Backup $STATUS.\",\"color\":\"white\"}]" > /dev/null 2>&1
fi
}
backup_bluemap() {
log_info "Starting Bluemap backup..."
docker compose run --rm init sh -c '
cp -a --reflink=auto /bluemap_data "$1" &&
cp -a --reflink=auto /bluemap_maps "$1" &&
cp -a --reflink=auto /bluemap_web "$1"
' -- "$BACKUP_DIR" || return 1
log_info "Finished Bluemap backup"
}
backup_fabric() {
log_info "Starting Fabric backup..."
CLEANUP_FABRIC=false
if docker compose exec -T fabric true > /dev/null 2>&1; then
CLEANUP_FABRIC=true
log_info "Fabric server detected, disabling saves"
docker compose exec -T fabric rcon-cli save-off > /dev/null 2>&1
docker compose exec -T fabric rcon-cli save-all > /dev/null 2>&1
fi
docker compose run --rm init sh -c '
mkdir -p "$1"/fabric_data &&
cp -a --reflink=auto /fabric_data/g4mespeed "$1"/fabric_data &&
cp -a --reflink=auto /fabric_data/fabric-essentials.json "$1"/fabric_data &&
cp -a --reflink=auto /fabric_data/world "$1"/fabric_data
' -- "$BACKUP_DIR" || return 1
log_info "Finished Fabric backup"
}
backup_luckperms() {
log_info "Starting LuckPerms backup..."
CLEANUP_LUCKPERMS=false
if ! docker compose exec -T luckperms true > /dev/null 2>&1; then
CLEANUP_LUCKPERMS=true
log_info "LuckPerms not running, starting temporary container"
docker compose up --wait luckperms > /dev/null 2>&1
fi
docker compose exec luckperms sh -c '
pg_dump -U luckperms luckperms > "$1"/luckperms.sql
' -- "$BACKUP_DIR" || return 1
log_info "Finished LuckPerms backup"
}
backup_paper() {
log_info "Starting Paper backup..."
CLEANUP_PAPER=false
if docker compose exec -T paper true > /dev/null 2>&1; then
CLEANUP_PAPER=true
log_info "Paper server detected, disabling saves"
docker compose exec -T paper rcon-cli save-off > /dev/null 2>&1
docker compose exec -T paper rcon-cli save-all > /dev/null 2>&1
fi
docker compose run --rm init sh -c '
mkdir -p "$1"/paper_data/plugins &&
cp -a --reflink=auto /paper_data/plugins/Multiverse-Inventories "$1"/paper_data/plugins &&
cp -a --reflink=auto /paper_data/plugins/Essentials "$1"/paper_data/plugins &&
cp -a --reflink=auto /paper_data/plugins/PlotSquared "$1"/paper_data/plugins &&
cp -a --reflink=auto /paper_data/creative "$1"/paper_data &&
cp -a --reflink=auto /paper_data/creative_nether "$1"/paper_data &&
cp -a --reflink=auto /paper_data/survival "$1"/paper_data &&
cp -a --reflink=auto /paper_data/survival_nether "$1"/paper_data &&
cp -a --reflink=auto /paper_data/survival_the_end "$1"/paper_data
' -- "$BACKUP_DIR" || return 1
log_info "Finished Paper backup"
}
backup_schematics() {
log_info "Starting schematics backup..."
docker compose run --rm init sh -c '
cp -a --reflink=auto /schematics "$1"
' -- "$BACKUP_DIR" || return 1
log_info "Finished schematics backup"
}
backup_velocity() {
log_info "Starting Velocity backup..."
docker compose run --rm init sh -c 'mkdir -p "$1"/velocity_data/plugins/dclink-velocity' -- "$BACKUP_DIR" || return 1
docker compose run --rm sqlite_helper \
/velocity_data/plugins/dclink-velocity/dclink.db \
".backup $BACKUP_DIR/velocity_data/plugins/dclink-velocity/dclink.db" || return 1
log_info "Finished Velocity backup"
}
finalize_backup() {
log_info "Finalizing backup $BACKUP_ID"
docker compose run --rm init chown -R minecraft_server:minecraft_server "$BACKUP_DIR" || {
log_error "Failed to update ownership of backup files"
return 1
}
log_info "Backup ownership updated"
docker compose run --rm init sh -c '
ln -sfn "$1" /backups/.latest_tmp &&
mv -Tf /backups/.latest_tmp /backups/latest
' -- "$BACKUP_DIR" || {
log_error "Failed to update /backups/latest symlink"
return 1
}
log_info "Updated /backups/latest symlink"
}
prune_backups() {
BACKUPS_ALL=$(docker compose run --rm init sh -c '
find /backups -mindepth 1 -maxdepth 1 -type d \
-name "????-??-??T??-??-??Z" | sort
')
BACKUPS_TOTAL=$(echo "$BACKUPS_ALL" | wc -l)
BACKUPS_PRUNE=$((BACKUPS_TOTAL - BACKUPS_KEEP))
[ "$BACKUPS_PRUNE" -le 0 ] && BACKUPS_PRUNE=0
log_info "Total backups found: $BACKUPS_TOTAL"
log_info "Backups to prune: $BACKUPS_PRUNE"
echo "$BACKUPS_ALL" | head -n "$BACKUPS_PRUNE" | while read -r OLD_BACKUP_DIR; do
log_info "Removing old backup: $OLD_BACKUP_DIR"
docker compose run --rm init rm -rf "$OLD_BACKUP_DIR"
done
}
trap cleanup_trap HUP INT QUIT ABRT TERM
cleanup_trap() {
log_info "Backup cancelled by signal"
broadcast_status cancelled
cleanup
delete_backup
exit 0
}
cleanup_failure() {
log_info "Running cleanup due to failure"
broadcast_status failed
cleanup
delete_backup
exit 1
}
cleanup_success() {
log_info "Running cleanup"
cleanup
}
cleanup() {
# Ignore specified conditions to ensure cleanup runs to completion uninterrupted
trap '' HUP INT QUIT ABRT TERM
if [ "$CLEANUP_FABRIC" = true ]; then
log_info "Re-enabling Fabric saves"
docker compose exec -T fabric rcon-cli save-on > /dev/null 2>&1
fi
if [ "$CLEANUP_LUCKPERMS" = true ]; then
log_info "Bringing down temporary LuckPerms container"
docker compose stop luckperms > /dev/null 2>&1
fi
if [ "$CLEANUP_PAPER" = true ]; then
log_info "Re-enabling Paper saves"
docker compose exec -T paper rcon-cli save-on > /dev/null 2>&1
fi
}
delete_backup() {
if ! docker compose run --rm init rm -rf "$BACKUP_DIR"; then
log_error "Failed to remove $BACKUP_DIR"
fi
}
log_info() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] INFO: $*"
}
log_error() {
echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] ERROR: $*" >&2
}
main

45
scripts/init.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env sh
# Define a helper function that runs a command
# If the command fails, the script prints an error message
# and exits immediately.
run() {
# "$@" expands to all arguments passed to this function
# and preserves proper word splitting and quoting.
"$@" || {
echo "Error: command failed: $*" >&2
exit 1
}
}
# Make sure volumes have correct permissions
run chown minecraft_server:minecraft_server /bluemap_data
run chown minecraft_server:minecraft_server /bluemap_web
run chown minecraft_server:minecraft_server /bluemap_maps
run chown minecraft_server:minecraft_server /fabric_data
run chown minecraft_server:minecraft_server /luckperms_data
run chown minecraft_server:minecraft_server /paper_data
run chown minecraft_server:minecraft_server /schematics
run chown minecraft_server:minecraft_server /velocity_data
# Make sure nested volume mount points exist
run mkdir -p /fabric_data/bluemap/web/maps
run chown minecraft_server:minecraft_server /fabric_data/bluemap
run chown minecraft_server:minecraft_server /fabric_data/bluemap/web
run chown minecraft_server:minecraft_server /fabric_data/bluemap/web/maps
run mkdir -p /fabric_data/config/worldedit/schematics
run chown minecraft_server:minecraft_server /fabric_data/config
run chown minecraft_server:minecraft_server /fabric_data/config/worldedit
run chown minecraft_server:minecraft_server /fabric_data/config/worldedit/schematics
run mkdir -p /paper_data/bluemap/web/maps
run chown minecraft_server:minecraft_server /paper_data/bluemap
run chown minecraft_server:minecraft_server /paper_data/bluemap/web
run chown minecraft_server:minecraft_server /paper_data/bluemap/web/maps
run mkdir -p /paper_data/plugins/WorldEdit/schematics
run chown minecraft_server:minecraft_server /paper_data/plugins
run chown minecraft_server:minecraft_server /paper_data/plugins/WorldEdit
run chown minecraft_server:minecraft_server /paper_data/plugins/WorldEdit/schematics

View File

@@ -1,30 +0,0 @@
#!/usr/bin/env sh
# Define a helper function that runs a command
# If the command fails, the script prints an error message
# and exits immediately.
run() {
# "$@" expands to all arguments passed to this function
# and preserves proper word splitting and quoting.
"$@" || {
echo "Error: command failed: $*" >&2
exit 1
}
}
# Ensure required directories exist
run mkdir -p /fabric_data/bluemap/web/maps
run mkdir -p /fabric_data/config/worldedit/schematics
run mkdir -p /paper_data/bluemap/web/maps
run mkdir -p /paper_data/plugins/WorldEdit/schematics
# Ensure correct permissions of docker volumes
run chown -R minecraft_server:minecraft_server /bluemap_data
run chown -R minecraft_server:minecraft_server /bluemap_web
run chown -R minecraft_server:minecraft_server /bluemap_maps
run chown -R minecraft_server:minecraft_server /fabric_data
run chown -R minecraft_server:minecraft_server /luckperms_data
run chown -R minecraft_server:minecraft_server /paper_data
run chown -R minecraft_server:minecraft_server /schematics
run chown -R minecraft_server:minecraft_server /velocity_data

View File

@@ -43,7 +43,7 @@ docker exec illegal_crime_paper rcon-cli save-all
PG_USER="luckperms"
PG_DB="luckperms"
BACKUP_DIR_CONTAINER="/backups/$DATE"
docker exec illegal_crime_luckperms_db pg_dump -U "$PG_USER" -F c -b -v -f "$BACKUP_DIR_CONTAINER/luckperms.sql" "$PG_DB"
docker exec illegal_crime_luckperms pg_dump -U "$PG_USER" -F c -b -v -f "$BACKUP_DIR_CONTAINER/luckperms.sql" "$PG_DB"
# Copy WorldEdit schematics
cp -r "$PROJECT_DIR/schematics" "$BACKUP_DIR_HOST/"
@@ -109,4 +109,3 @@ fi
# Update permissions of backed up files
chown -R "$PUID:$PGID" "$BACKUPS_DIR_HOST"
chmod -R 770 "$BACKUPS_DIR_HOST"

View File

@@ -6,6 +6,5 @@ DCLINK_GUILD=${CFG_DCLINK_GUILD}
DCLINK_CHANNEL=${CFG_DCLINK_CHANNEL}
DCLINK_ROLE=${CFG_DCLINK_ROLE}
DCLINK_TOKEN=${CFG_DCLINK_TOKEN}
LUCKPERMS_DB_PASSWORD=${CFG_LUCKPERMS_DB_PASSWORD}
LUCKPERMS_PASSWORD=${CFG_LUCKPERMS_PASSWORD}
LUCKPERMS_PORT=${CFG_LUCKPERMS_PORT}

View File

@@ -90,7 +90,7 @@ data:
# Credentials for the database.
username: luckperms
password: '${CFG_LUCKPERMS_DB_PASSWORD}'
password: '${CFG_LUCKPERMS_PASSWORD}'
# These settings apply to the MySQL connection pool.
# - The default values will be suitable for the majority of users.