commit d22b377f73e8cb5008dc4abd2e8235512da6fbe6 Author: Thuan Bui Date: Mon Dec 30 19:16:20 2024 +0900 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92fafc5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +caddy/* +wordpress/* \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..722777c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Thuan Bui + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad2f4f2 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# **CaddyWP** + +**CaddyWP** is an open-source tool designed to simplify the deployment and management of multiple WordPress sites behind a Caddy reverse proxy. It uses Docker and Bash scripts to automate configuration, allowing you to easily set up and scale your WordPress instances with minimal hassle. + +--- + +## **Features** + +- **Multiple WordPress Sites**: Manage as many WordPress sites as you want, all running on a single reverse proxy server. +- **Caddy Integration**: Seamlessly integrates Caddy as the reverse proxy for efficient traffic routing and SSL management. +- **Dockerized**: Fully containerized environment for easy setup and management. +- **Bash Automation**: Automate your WordPress site configurations using simple Bash scripts. +- **Scalable**: Easily add new sites and scale your infrastructure as needed. + +--- + +## **Getting Started** + +### **Prerequisites** + +Ensure you have the following installed: + +- [Docker](https://www.docker.com/) +- [Docker Compose](https://docs.docker.com/compose/) +- A Unix-based environment (Linux or macOS) or WSL on Windows + +### **Installation** + +1. **Clone the repository**: + ```bash + git clone https://github.com/your-username/CaddyWP.git + cd CaddyWP + ``` + +2. **Run the install script**: + ```bash + ./install.sh + ``` + +## How It Works + + Caddy: Serves as a reverse proxy, automatically obtaining SSL certificates and routing traffic to the correct WordPress container. + WordPress: Each WordPress site is hosted in its own Docker container, ensuring that each site runs in isolation with its own environment and database. + MariaDB: A single MariaDB container serves as the database for all WordPress sites. Each site uses a unique database user and password. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. +Acknowledgements + + Caddy: https://caddyserver.com/ + WordPress: https://wordpress.org/ + Docker: https://www.docker.com/ + MariaDB: https://mariadb.org/ + +

(back to top)

diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..7505f11 --- /dev/null +++ b/install.sh @@ -0,0 +1,117 @@ +#!/bin/bash + +# Get the directory where the script is located +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# Source library files in specific order +source "${SCRIPT_DIR}/lib/colors.sh" +source "${SCRIPT_DIR}/lib/config.sh" +source "${SCRIPT_DIR}/lib/utils.sh" +source "${SCRIPT_DIR}/lib/validation.sh" +source "${SCRIPT_DIR}/lib/docker.sh" +source "${SCRIPT_DIR}/lib/caddy.sh" +source "${SCRIPT_DIR}/lib/wordpress.sh" + +echo "WordPress Site Setup Script" +echo "==========================" + +setup_directories + +FIRST_TIME=false +if ! check_container_running "caddy"; then + FIRST_TIME=true +fi + +while true; do + read -p "Enter domain (e.g., example.com): " DOMAIN + if validate_domain "$DOMAIN"; then + break + fi +done + +while true; do + read -p "Enter admin email: " ADMIN_EMAIL + if validate_email "$ADMIN_EMAIL"; then + break + fi +done + +read -p "Enter admin username: " ADMIN_USER + +read -s -p "Enter password (press Enter for random password): " ADMIN_PASSWORD +echo + +if [ -z "$ADMIN_PASSWORD" ]; then + ADMIN_PASSWORD=$(generate_password) + echo "Generated password: $ADMIN_PASSWORD" +fi + +read -p "Enter site title: " SITE_TITLE + +MYSQL_ROOT_PASSWORD=$(openssl rand -base64 32) +MYSQL_PASSWORD=$(openssl rand -base64 32) + +WP_PROJECT_DIR="${WORDPRESS_DIR}/${DOMAIN}" +if [ -d "$WP_PROJECT_DIR" ]; then + echo -e "${RED}Directory ${WP_PROJECT_DIR} already exists!${NC}" + exit 1 +fi + +mkdir -p "$WP_PROJECT_DIR" +cd "$WP_PROJECT_DIR" + +create_docker_compose "$DOMAIN" "$MYSQL_ROOT_PASSWORD" "$MYSQL_PASSWORD" +create_caddy_config "$DOMAIN" +create_wp_setup "$DOMAIN" "$ADMIN_USER" "$ADMIN_PASSWORD" "$ADMIN_EMAIL" "$SITE_TITLE" +create_env_file "$DOMAIN" "$ADMIN_USER" "$ADMIN_PASSWORD" "$ADMIN_EMAIL" "$MYSQL_ROOT_PASSWORD" "$MYSQL_PASSWORD" + + +while true; do + read -p "Do you want to (1) start services and set up WordPress automatically or (2) do it manually later? [1/2]: " SETUP_CHOICE + case $SETUP_CHOICE in + 1) + if start_services "$FIRST_TIME" "$DOMAIN"; then + if run_wp_setup "$DOMAIN"; then + echo -e "${GREEN}Complete setup finished successfully!${NC}" + else + echo -e "${RED}WordPress setup failed. You may need to run setup manually later.${NC}" + fi + else + echo -e "${RED}Service startup failed. You may need to start services manually.${NC}" + fi + break + ;; + 2) + echo -e "\n${BLUE}Manual setup instructions:${NC}" + if [ "$FIRST_TIME" = true ]; then + echo "1. Start Caddy:" + echo " cd ${CADDY_DIR} && docker compose up -d" + fi + echo "2. Start WordPress:" + echo " cd ${WP_PROJECT_DIR} && docker compose up -d" + echo "3. Run the WordPress setup script:" + echo " ./wp-setup.sh" + break + ;; + *) + echo -e "${RED}Invalid choice. Please enter 1 or 2.${NC}" + ;; + esac +done + +echo -e "\n${BLUE}WordPress Site Information:${NC}" +echo "----------------------------------------" +echo "Domain: https://$DOMAIN" +echo "Admin URL: https://$DOMAIN/wp-admin" +echo "Username: $ADMIN_USER" +if [ "$PASSWORD_CHOICE" = "2" ]; then + echo "Password: $ADMIN_PASSWORD (SAVE THIS PASSWORD!)" +fi +echo "Email: $ADMIN_EMAIL" +echo "----------------------------------------" + +save_credentials "$WP_PROJECT_DIR" "$DOMAIN" "$ADMIN_USER" "$ADMIN_PASSWORD" "$ADMIN_EMAIL" + +echo -e "\nCredentials have been saved to: ${WP_PROJECT_DIR}/credentials.txt" + +exit 0 \ No newline at end of file diff --git a/lib/caddy.sh b/lib/caddy.sh new file mode 100644 index 0000000..2a4d58e --- /dev/null +++ b/lib/caddy.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +reload_caddy() { + echo "Reloading Caddy configuration..." + docker exec caddy caddy reload --config /etc/caddy/Caddyfile +} + +create_caddy_config() { + export DOMAIN=$1 + export CONFIG_FILE="${CADDY_DIR}/sites/${DOMAIN}.caddy" + + mkdir -p "${CADDY_DIR}/sites" + + envsubst < "${SCRIPT_DIR}/templates/caddy.template" > "$CONFIG_FILE" + + if ! grep -q "import sites/\*.caddy" "${CADDY_DIR}/Caddyfile"; then + echo 'import sites/*.caddy' >> "${CADDY_DIR}/Caddyfile" + fi +} + +create_caddy_docker_compose() { + mkdir -p "${CADDY_DIR}" + + # Create necessary directories + mkdir -p "${CADDY_DIR}/sites" + mkdir -p "${CADDY_DIR}/caddy_data" + mkdir -p "${CADDY_DIR}/caddy_config" + + # Create initial Caddyfile + cat > "${CADDY_DIR}/Caddyfile" < "${CADDY_DIR}/compose.yaml" < compose.yaml + #envsubst '$DOMAIN $MYSQL_ROOT_PASSWORD $MYSQL_PASSWORD' < "${SCRIPT_DIR}/templates/docker-compose.yml.template" > compose.yaml + +} + +start_services() { + local FIRST_TIME=$1 + local DOMAIN=$2 + + if [ "$FIRST_TIME" = true ]; then + echo "Starting Caddy server..." + cd "${CADDY_DIR}" + docker compose up -d + if [ $? -ne 0 ]; then + echo -e "${RED}Failed to start Caddy server${NC}" + return 1 + fi + else + # Check if Caddy is running and reload configuration + if check_container_running "caddy"; then + reload_caddy + else + echo -e "${RED}Caddy is not running. Please start it first${NC}" + return 1 + fi + fi + + echo "Starting WordPress for ${DOMAIN}..." + cd "${WORDPRESS_DIR}/${DOMAIN}" + docker compose up -d + if [ $? -ne 0 ]; then + echo -e "${RED}Failed to start WordPress services${NC}" + return 1 + fi + + return 0 +} \ No newline at end of file diff --git a/lib/utils.sh b/lib/utils.sh new file mode 100644 index 0000000..0426532 --- /dev/null +++ b/lib/utils.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +function generate_password() { + openssl rand -base64 12 +} +export -f generate_password + +function save_credentials() { + local WP_PROJECT_DIR=$1 + local DOMAIN=$2 + local ADMIN_USER=$3 + local ADMIN_PASSWORD=$4 + local ADMIN_EMAIL=$5 + + cat > "${WP_PROJECT_DIR}/credentials.txt" < /dev/null 2>&1; then + if docker compose run --rm wpcli core is-installed > /dev/null 2>&1; then + return 0 + fi + echo "Attempt $ATTEMPT of $MAX_ATTEMPTS..." + sleep 10 + ATTEMPT=$((ATTEMPT + 1)) + done + return 1 +} + +create_env_file() { + export DOMAIN=$1 + export ADMIN_USER=$2 + export ADMIN_PASSWORD=$3 + export ADMIN_EMAIL=$4 + export MYSQL_ROOT_PASSWORD=$5 + export MYSQL_PASSWORD=$6 + + # Output file + ENV_FILE=".env" + # Generate .env file + echo "Generating $ENV_FILE..." + + cat < "${WP_PROJECT_DIR}/$ENV_FILE" + DOMAIN_NAME = ${DOMAIN} + + ## Wordpress ## + WORDPRESS_DB_USER=wordpress + WORDPRESS_DB_PASSWORD=${DB_PASSWORD} + WORDPRESS_DB_NAME=wordpress + WORDPRESS_DB_HOST=db_${DOMAIN}:3306 + + # Website Credentials + WORDPRESS_ADMIN_USER=${ADMIN_USER} + WORDPRESS_ADMIN_PASSWORD=${ADMIN_PASSWORD} + WORDPRESS_ADMIN_EMAIL=${ADMIN_EMAIL} + + ## MYSQL ## + MYSQL_USER=wordpress + MYSQL_PASSWORD=${MYSQL_PASSWORD} + MYSQL_DATABASE=wordpress + MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} + +EOL + + echo "$ENV_FILE generated successfully!" +} + + +create_wp_setup() { + export DOMAIN=$1 + export ADMIN_USER=$2 + export ADMIN_PASSWORD=$3 + export ADMIN_EMAIL=$4 + export SITE_TITLE=$5 + + envsubst '$DOMAIN $ADMIN_USER $ADMIN_PASSWORD $ADMIN_EMAIL $SITE_TITLE' < "${SCRIPT_DIR}/templates/wp-setup.sh.template" > wp-setup.sh + chmod +x wp-setup.sh +} + +run_wp_setup() { + local DOMAIN=$1 + + echo "Running WordPress setup..." + cd "${WORDPRESS_DIR}/${DOMAIN}" + + #if ! wait_for_wordpress "$DOMAIN"; then + # echo -e "${RED}WordPress failed to start properly${NC}" + # return 1 + #fi + + if ./wp-setup.sh 2>&1; then + echo -e "${GREEN}WordPress setup completed successfully${NC}" + return 0 + else + echo -e "${RED}WordPress setup failed${NC}" + return 1 + fi +} \ No newline at end of file diff --git a/templates/caddy.template b/templates/caddy.template new file mode 100644 index 0000000..9e5c83f --- /dev/null +++ b/templates/caddy.template @@ -0,0 +1,22 @@ +${DOMAIN} { + #reverse_proxy wordpress_${DOMAIN}:80 + + tls internal + root * /var/www/${DOMAIN}/html + encode zstd gzip + + # Serve WordPress PHP files through php-fpm: + php_fastcgi wordpress_${DOMAIN}:9000 { + root /var/www/html + } + + # Enable the static file server: + file_server { + precompressed gzip + } + header / { + X-Frame-Options "SAMEORIGIN" + X-Content-Type-Options "nosniff" + } +} + diff --git a/templates/docker-compose.yml.template b/templates/docker-compose.yml.template new file mode 100644 index 0000000..ccedac5 --- /dev/null +++ b/templates/docker-compose.yml.template @@ -0,0 +1,61 @@ +services: + db_${DOMAIN}: + container_name: db_${DOMAIN} + image: mariadb:11.2-jammy + volumes: + - ./db_data:/var/lib/mysql + restart: always + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: wordpress + MYSQL_USER: wordpress + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + healthcheck: + test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "--silent"] + interval: 10s + timeout: 5s + retries: 3 + networks: + - ${DOMAIN}_net + + wordpress_${DOMAIN}: + container_name: wordpress_${DOMAIN} + depends_on: + - db_${DOMAIN} + image: wordpress:fpm-alpine + volumes: + - ./html/:/var/www/html + + restart: always + environment: + WORDPRESS_DB_HOST: db_${DOMAIN}:3306 + WORDPRESS_DB_USER: wordpress + WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD} + WORDPRESS_DB_NAME: wordpress + networks: + - ${DOMAIN}_net + - caddy_net + + wpcli: + depends_on: + - db_${DOMAIN} + - wordpress_${DOMAIN} + environment: + WORDPRESS_DB_HOST: db_${DOMAIN}:3306 + WORDPRESS_DB_USER: wordpress + WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD} + WORDPRESS_DB_NAME: wordpress + image: wordpress:cli + entrypoint: wp + command: "--info" + volumes_from: + - wordpress_${DOMAIN} + networks: + - ${DOMAIN}_net + - caddy_net + +networks: + ${DOMAIN}_net: + driver: bridge + caddy_net: + external: true \ No newline at end of file diff --git a/templates/wp-setup.sh.template b/templates/wp-setup.sh.template new file mode 100644 index 0000000..6a17e89 --- /dev/null +++ b/templates/wp-setup.sh.template @@ -0,0 +1,40 @@ +#!/bin/bash + +wp_cli() { + docker run --rm \ + --network container:wordpress_${DOMAIN} \ + -v "$(pwd):/var/www/html" \ + wordpress:cli-php8.1 \ + wp "$@" +} + +wpcli() { + docker compose run --rm wpcli "$@" +} + +echo "Waiting for MySQL to be ready..." +#while ! docker exec db_${DOMAIN} mysqladmin ping -h localhost --silent; do +# sleep 1 +#done +while [ "$(docker inspect --format='{{.State.Health.Status}}' db_${DOMAIN})" != "healthy" ]; do + echo "MariaDB is not healthy yet. Retrying..." + sleep 5 +done + + +echo "Installing WordPress..." +wpcli core install \ + --url="https://${DOMAIN}" \ + --title="${SITE_TITLE}" \ + --admin_user="${ADMIN_USER}" \ + --admin_password="${ADMIN_PASSWORD}" \ + --admin_email="${ADMIN_EMAIL}" \ + --skip-email + +echo "Installing and activating plugins..." +wpcli plugin install wordfence --activate +wpcli theme install twentytwentyfour --activate +wpcli plugin update --all +wpcli theme update --all + +echo "WordPress setup completed!" \ No newline at end of file diff --git a/uninstall.sh b/uninstall.sh new file mode 100755 index 0000000..a7a60d6 --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Ask for confirmation before proceeding with the uninstallation +read -p "Are you sure you want to uninstall everything? This will stop and remove all containers, and delete files. (y/n): " confirm + +if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then + # Stop all running containers + echo "Stopping all containers..." + docker stop $(docker ps -a -q) + + # Remove all containers + echo "Removing all containers..." + docker rm $(docker ps -a -q) + + # Remove WordPress files + echo "Removing WordPress files..." + rm -rf wordpress + + # Remove Caddy files + echo "Removing Caddy files..." + rm -rf caddy + + echo "Uninstallation complete." +else + echo "Uninstallation aborted." +fi