From 136cfbb86292e3c6c5c52da3fcc960f24d77d0fa Mon Sep 17 00:00:00 2001 From: Ryahn Date: Sun, 23 Mar 2025 14:44:38 -0500 Subject: [PATCH] init --- .env | 7 ++ .env-example | 7 ++ .gitignore | 0 Dockerfile | 35 ++++++++ LICENSE | 21 +++++ README.md | 25 ++++++ backup.sh | 217 +++++++++++++++++++++++++++++++++++++++++++++ create_admin.sh | 36 ++++++++ docker-compose.yml | 44 +++++++++ entrypoint.sh | 74 ++++++++++++++++ 10 files changed, 466 insertions(+) create mode 100644 .env create mode 100644 .env-example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 backup.sh create mode 100644 create_admin.sh create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh diff --git a/.env b/.env new file mode 100644 index 0000000..079c948 --- /dev/null +++ b/.env @@ -0,0 +1,7 @@ +MYSQL_ROOT_PASSWORD=p8CQ8KSypTWFIfWfvsnFYpUq +MYSQL_PASSWORD=5niR56SNrcoO7YEy/zz1qPdm +MYSQL_USER=fileserver +MYSQL_DATABASE=fileserver +MYSQL_CHARSET=utf8mb4 +MYSQL_COLLATION=utf8mb4_unicode_ci +NETWORK=app-network \ No newline at end of file diff --git a/.env-example b/.env-example new file mode 100644 index 0000000..56a5e24 --- /dev/null +++ b/.env-example @@ -0,0 +1,7 @@ +MYSQL_ROOT_PASSWORD=CHANGEME +MYSQL_PASSWORD=CHANGEME +MYSQL_USER=CHANGEME +MYSQL_DATABASE=CHANGEME +MYSQL_CHARSET=utf8mb4 +MYSQL_COLLATION=utf8mb4_unicode_ci +NETWORK=CHANGEME \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b3e6453 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +FROM mysql:8.0.41-bookworm + +# Install cron and other required tools +RUN apt-get update && apt-get install -y \ + mysql-client \ + cron \ + openssl \ + zip \ + rsync \ + unzip \ + && rm -rf /var/lib/apt/lists/* + +# Create necessary directories +RUN mkdir -p /opt/backups /opt/backups/tmp /var/log/backup /etc/cron.d + +# Copy scripts +COPY backup.sh /usr/local/bin/backup.sh +COPY create_admin.sh /usr/local/bin/create_admin.sh +COPY entrypoint.sh /entrypoint.sh + +# Make scripts executable +RUN chmod +x /usr/local/bin/backup.sh \ + && chmod +x /usr/local/bin/create_admin.sh \ + && chmod +x /entrypoint.sh + +# Setup cron jobs +RUN echo "0 * * * * root /usr/local/bin/backup.sh --hourly >> /var/log/backup/hourly.log 2>&1" > /etc/cron.d/backup-cron && \ + echo "0 0 * * * root /usr/local/bin/backup.sh --daily >> /var/log/backup/daily.log 2>&1" >> /etc/cron.d/backup-cron && \ + echo "0 0 * * 0 root /usr/local/bin/backup.sh --weekly >> /var/log/backup/weekly.log 2>&1" >> /etc/cron.d/backup-cron && \ + chmod 0644 /etc/cron.d/backup-cron + +EXPOSE 3306 + +# Use our custom entrypoint +ENTRYPOINT ["/entrypoint.sh"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..63b4b68 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [year] [fullname] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..345b1cb --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Custom MySQL Docker + +This is a custom docker container that is used in my home lab and in my production servers. Feel free to use it. + +## Prerequisites +1. docker +2. docker compose +3. zip unzip +4. (OPTIONAL) rsync and remote server for storing backups offsite + +### 1. Rename ENV +Rename `.env-example` to `.env` +### 2. EDIT ENV +Edit and change all the `CHANGEME` to values you are using +### 3. Create Docker Network +Run `docker network create your_network_name` to create network and be sure to change `your_network_name` to someting you will use +### 4. Modify `backup.sh` if needed +`backup.sh` can be modified to store locally, mounted directories, s3, etc. Its currently configured to use SSH +### 5. Install +Run `docker compose up -d --build` + +> A separate admin user is created with full permissions and grant options. +> run docker logs mysql to see password + +[LICENSE](/LICENSE) \ No newline at end of file diff --git a/backup.sh b/backup.sh new file mode 100644 index 0000000..3e52e97 --- /dev/null +++ b/backup.sh @@ -0,0 +1,217 @@ +#!/bin/bash + +# Load environment variables from .env file +if [ -f .env ]; then + # Read each line from .env and export variables + while IFS= read -r line || [[ -n "$line" ]]; do + # Skip comments and empty lines + [[ $line =~ ^#.*$ ]] && continue + [[ -z "$line" ]] && continue + + # Export the variable + export "$line" + done < .env +else + echo "Error: .env file not found" + exit 1 +fi + +# Variables (now using environment variables with defaults) +MYSQL_USER="admin" +MYSQL_PASSWORD=${MYSQL_ADMIN_PASSWORD} +BACKUP_DIR="$(pwd)/backups" +REMOTE_HOST="${BACKUP_SERVER_HOST}" # Remote server hostname/IP +REMOTE_USER="${BACKUP_SERVER_USER}" # Remote server username +REMOTE_BACKUP_DIR="${BACKUP_SERVER_PATH}" # Remote server backup path +TEMP_DIR="$BACKUP_DIR/tmp" +TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") +MODE="" +HOURLY_RETENTION=4 +DAILY_RETENTION=3 +WEEKLY_RETENTION=1 +MIN_SPACE=20 # Minimum space in GB +DB_CONTAINER="mailserver_db" + +# Function to display usage +function usage() { + echo "Usage: $0 --hourly | --daily | --weekly" + exit 1 +} + +# Function to check available space and cleanup if necessary +function check_and_cleanup_space() { + local available_space=$(df -BG "$REMOTE_BACKUP_DIR" | awk 'NR==2 {gsub("G","",$4); print $4}') + + if [ "$available_space" -le "$MIN_SPACE" ]; then + echo "Available space ($available_space GB) is less than minimum required ($MIN_SPACE GB)" + echo "Starting cleanup..." + + while [ "$available_space" -le "$MIN_SPACE" ]; do + # Find oldest backup file + oldest_file=$(find "$REMOTE_BACKUP_DIR" -type f -name "*.zip" -printf '%T+ %p\n' | sort | head -n 1 | awk '{print $2}') + + if [ -z "$oldest_file" ]; then + echo "No more files to delete!" + break + fi + + # Get file size before deletion for logging + file_size=$(du -h "$oldest_file" | cut -f1) + + # Delete the file + rm -f "$oldest_file" + echo "Deleted old backup: $oldest_file (Size: $file_size)" + + # Recalculate available space + available_space=$(df -BG "$REMOTE_BACKUP_DIR" | awk 'NR==2 {gsub("G","",$4); print $4}') + done + + echo "Cleanup complete. Available space: $available_space GB" + fi +} + +echo "##########################" +echo "Starting backup..." +echo "##########################" +echo "" + +# Check if the correct parameter is passed +if [[ "$1" == "--hourly" ]]; then + MODE="hourly" +elif [[ "$1" == "--daily" ]]; then + MODE="daily" +elif [[ "$1" == "--weekly" ]]; then + MODE="weekly" +else + usage +fi +echo "##########################" +echo "Mode: $MODE" +echo "##########################" +echo "" + +echo "##########################" +echo "Creating backup directories..." +echo "##########################" +echo "" + +# Create backup and temp directories if they don't exist +mkdir -p "$BACKUP_DIR" +mkdir -p "$TEMP_DIR" +mkdir -p "/var/log/backup" + +# Backup all MySQL databases +echo "##########################" +echo "Backing up MySQL databases..." +echo "##########################" +echo "" + +# Test database connection first +if ! docker exec -i $DB_CONTAINER mariadb -u $MYSQL_USER -p${MYSQL_PASSWORD} -e "SELECT 1;" >/dev/null 2>&1; then + echo "Error: Cannot connect to MySQL database. Please check credentials." + echo "Container: $DB_CONTAINER" + echo "User: $MYSQL_USER" + echo "Password being used: ${MYSQL_PASSWORD}" + exit 1 +fi + +# Get databases list without using -it flag (which requires terminal) +databases=$(docker exec -i $DB_CONTAINER mariadb -u $MYSQL_USER -p${MYSQL_PASSWORD} -e "SHOW DATABASES;" | grep -Ev "(Database|information_schema|performance_schema|mysql|sys)") + +for db in $databases; do + echo "Backing up database: $db" + docker exec -i $DB_CONTAINER mariadb-dump -u $MYSQL_USER -p${MYSQL_PASSWORD} --databases "$db" > "$TEMP_DIR/${db}.sql" + if [ $? -eq 0 ]; then + zip -j "$TEMP_DIR/${db}.sql.zip" "$TEMP_DIR/${db}.sql" + rm "$TEMP_DIR/${db}.sql" + else + echo "Error backing up database: $db" + fi +done + +DIRECTORIES_TO_BACKUP=( + "/root/isekai" +) + +for dir in "${DIRECTORIES_TO_BACKUP[@]}"; do + DIR_NAME=$(basename "$dir") + zip -r "$TEMP_DIR/${DIR_NAME}_${MODE}_$TIMESTAMP.zip" "$dir" -x "*/node_modules/*" "*/backups/*" +done + +# Compress all SQL and directories into a single zip file +echo "##########################" +echo "Compressing backup files..." +echo "##########################" +echo "" + +FINAL_BACKUP_FILE="$BACKUP_DIR/${MODE}_backup_$TIMESTAMP.zip" +find "$TEMP_DIR" -name "*.zip" | while read file; do + zip -ur "$FINAL_BACKUP_FILE" "$file" +done + +# Clean up temporary files +rm -rf "$TEMP_DIR" + +echo "##########################" +echo "Backup complete: $FINAL_BACKUP_FILE" +echo "##########################" +echo "" + +function apply_retention() { + backup_type=$1 + retention_count=$2 + + # Find all backup files of the specified type, sort them by modification time, and keep the newest + backups=($(ls -t $BACKUP_DIR/${backup_type}_backup_*.zip)) + + # If the number of backups exceeds the retention limit, delete the older ones + if [ ${#backups[@]} -gt $retention_count ]; then + delete_count=$((${#backups[@]} - $retention_count)) + for (( i=$retention_count; i<${#backups[@]}; i++ )); do + rm -f "${backups[$i]}" + echo "Deleted old $backup_type backup: ${backups[$i]}" + done + fi +} + +echo "##########################" +echo "Applying retention policy..." +echo "##########################" +echo "" + +# Apply retention policy +if [[ "$MODE" == "hourly" ]]; then + apply_retention "hourly" $HOURLY_RETENTION +elif [[ "$MODE" == "daily" ]]; then + apply_retention "daily" $DAILY_RETENTION +elif [[ "$MODE" == "weekly" ]]; then + apply_retention "weekly" $WEEKLY_RETENTION +fi + +echo "Retention policy applied: $MODE backups cleaned." +echo "##########################" +echo "Backup cleanup complete." +echo "##########################" +echo "" + +echo "##########################" +echo "Checking remote backup space..." +echo "##########################" +echo "" + +# Check and cleanup space before copying new backup +check_and_cleanup_space + +# Use rsync to copy the backup +if ! ssh -p "$BACKUP_SERVER_PORT" "${REMOTE_USER}@${REMOTE_HOST}" exit 2>/dev/null; then + echo "Error: Cannot connect to remote backup server" + echo "Backup file is saved locally at: $FINAL_BACKUP_FILE" + exit 1 +fi + +rsync -av --progress -e "ssh -p $BACKUP_SERVER_PORT" "$FINAL_BACKUP_FILE" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_BACKUP_DIR}/" + +echo "##########################" +echo "Remote backup complete" +echo "##########################" +echo "" \ No newline at end of file diff --git a/create_admin.sh b/create_admin.sh new file mode 100644 index 0000000..8adbf70 --- /dev/null +++ b/create_admin.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -e + +# Variables - replace these with your desired values or use environment variables +ADMIN_USER="admin" +ADMIN_PASSWORD=$(openssl rand -base64 32) + +echo "Creating MySQL admin user with root-like privileges..." + +# Execute MySQL commands inside the container +mysql -uroot -p"${MYSQL_ROOT_PASSWORD}" -e " +DROP USER IF EXISTS '$ADMIN_USER'@'%'; +CREATE USER '$ADMIN_USER'@'%' IDENTIFIED BY '$ADMIN_PASSWORD'; +GRANT ALL PRIVILEGES ON *.* TO '$ADMIN_USER'@'%' WITH GRANT OPTION; +FLUSH PRIVILEGES; +SELECT User, Host FROM mysql.user WHERE User = '$ADMIN_USER'; +" 2>/dev/null + +# Check if the command was successful +if [ $? -eq 0 ]; then + echo "✅ MySQL admin user '$ADMIN_USER' created successfully!" + echo "Connection details:" + echo " - Username: $ADMIN_USER" + echo " - Password: $ADMIN_PASSWORD" + echo " - Host: localhost" + echo " - Port: 3806 (mapped port)" + + # Optionally save credentials to a file + echo "Saving credentials to /opt/admin_credentials.txt" + echo "Username: $ADMIN_USER" > /opt/admin_credentials.txt + echo "Password: $ADMIN_PASSWORD" >> /opt/admin_credentials.txt + chmod 600 /opt/admin_credentials.txt +else + echo "❌ Failed to create MySQL admin user." + echo "Please check your container status and credentials." +fi \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e413b3c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +services: + mysql: + build: + context: . + dockerfile: Dockerfile + container_name: mysql + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ADMIN_PASSWORD} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_CHARSET: utf8mb4 + MYSQL_COLLATION: utf8mb4_unicode_ci + volumes: + - mysql_data:/var/lib/mysql + - ./config/my.cnf:/etc/mysql/my.cnf + command: > + --character-set-server=utf8mb4 + --collation-server=utf8mb4_unicode_ci + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - ${NETWORK} + deploy: + resources: + limits: + cpus: '2' + memory: 2G + reservations: + cpus: '1' + memory: 1G + +volumes: + mysql_data: + +networks: + ${NETWORK}: + name: ${NETWORK} + external: true \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..d99846d --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,74 @@ +#!/bin/bash +set -e + +# Create required MySQL directories +echo "Creating required MySQL directories..." +mkdir -p /var/lib/mysql /var/lib/mysql-files /var/run/mysqld +chown -R mysql:mysql /var/lib/mysql /var/lib/mysql-files /var/run/mysqld +chmod 750 /var/lib/mysql /var/lib/mysql-files +chmod 755 /var/run/mysqld + +# Check if MySQL has been initialized +if [ ! "$(ls -A /var/lib/mysql/mysql)" ]; then + echo "Initializing MySQL data directory..." + # Initialize without root password + mysqld --initialize-insecure --user=mysql + + # Start MySQL temporarily with --skip-networking to set root password + echo "Starting MySQL temporarily to set root password..." + mysqld --user=mysql --skip-networking & + pid="$!" + + # Wait for MySQL to start up + echo "Waiting for temporary MySQL instance to start..." + for i in {30..0}; do + if mysqladmin ping --socket=/var/run/mysqld/mysqld.sock &> /dev/null; then + break + fi + echo "MySQL not ready yet... waiting" + sleep 1 + done + + if [ "$i" = 0 ]; then + echo >&2 "MySQL initialization process failed." + exit 1 + fi + + # Set root password + echo "Setting root password..." + mysql --socket=/var/run/mysqld/mysqld.sock -u root <<-EOSQL + ALTER USER 'root'@'localhost' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}'; + FLUSH PRIVILEGES; +EOSQL + + # Stop temporary MySQL server + echo "Stopping temporary MySQL instance..." + if ! mysqladmin shutdown --socket=/var/run/mysqld/mysqld.sock; then + kill "$pid" + wait "$pid" + fi +fi + +# Start MySQL in the background +echo "Starting MySQL server..." +mysqld --user=mysql & + +# Wait for MySQL to be ready +echo "Waiting for MySQL to be ready..." +until mysqladmin ping -h"localhost" -u"root" -p"${MYSQL_ROOT_PASSWORD}" --silent; do + echo "MySQL not ready yet... waiting" + sleep 2 +done +echo "MySQL is ready!" + +# Run the admin user creation script +echo "Creating admin user..." +/usr/local/bin/create_admin.sh + +# Start cron service +echo "Starting cron service..." +service cron start + +# Keep the container running by waiting on the MySQL process +echo "MySQL server is running. Container will stay alive until MySQL exits." +wait %1 \ No newline at end of file