This commit is contained in:
Ryahn 2025-03-23 14:44:38 -05:00
commit 136cfbb862
10 changed files with 466 additions and 0 deletions

7
.env Normal file
View File

@ -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

7
.env-example Normal file
View File

@ -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

0
.gitignore vendored Normal file
View File

35
Dockerfile Normal file
View File

@ -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"]

21
LICENSE Normal file
View File

@ -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.

25
README.md Normal file
View File

@ -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)

217
backup.sh Normal file
View File

@ -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 ""

36
create_admin.sh Normal file
View File

@ -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

44
docker-compose.yml Normal file
View File

@ -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

74
entrypoint.sh Normal file
View File

@ -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