commit 90776a45ce2910c045f31205fbb35be1c4c8c3a7 Author: Guillermo Pages Date: Sun Jan 4 21:30:11 2026 +0100 feat: initial Docker setup for shop.dev.tb.meow.ch - WordPress 6.9 with WP-CLI - MariaDB 11, phpMyAdmin, FileBrowser - Traefik routing with HTTPS - WP-Cron container - Shortcode cleanup script for WPBakery migration diff --git a/.deploy.yml b/.deploy.yml new file mode 100644 index 0000000..329acef --- /dev/null +++ b/.deploy.yml @@ -0,0 +1,9 @@ +sn48: + vault: + hydrate: + branches: [master] + copy_files: + - docker/wordpress/php.ini + - docker/wordpress/to-bool.php + - docker/filebrowser/settings.json + deploy: 2 diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..722b962 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,67 @@ +kind: pipeline +type: docker +name: default + +steps: + - name: debug-secrets + image: alpine + commands: + - echo "Vault API URL is $${VAULT_API_URL}" + environment: + VAULT_API_URL: + from_secret: VAULT_API_URL + + - name: wordpress + image: plugins/docker + settings: + repo: registry.sn48.zivili.ch/meow/wp-shop-dev-tb + tags: + - amd64-1.0.0 + - latest + registry: registry.sn48.zivili.ch + dockerfile: docker/wordpress/Dockerfile + context: docker/wordpress + username: + from_secret: PORTUS_USER + password: + from_secret: PORTUS_PASSWORD + + - name: cron + image: plugins/docker + settings: + repo: registry.sn48.zivili.ch/meow/wp-cron-shop-dev-tb + tags: + - amd64-1.0.0 + - latest + registry: registry.sn48.zivili.ch + dockerfile: docker/wp-cron/Dockerfile + context: docker/wp-cron + username: + from_secret: PORTUS_USER + password: + from_secret: PORTUS_PASSWORD + + - name: deploy + image: registry.sn48.zivili.ch/meow/drone-deploy:amd64-1.0.0 + pull: always + environment: + SSH_HOST: + from_secret: SSH_HOST + SSH_USER: + from_secret: SSH_USER + SSH_KEY: + from_secret: SSH_KEY + SSH_PORT: + from_secret: SSH_PORT + SSH_FINGERPRINT: + from_secret: SSH_FINGERPRINT + PORTUS_USER: + from_secret: PORTUS_USER + PORTUS_PASSWORD: + from_secret: PORTUS_PASSWORD + VAULT_API_URL: + from_secret: VAULT_API_URL + DRONE_AGENT1_TOKEN: + from_secret: DRONE_AGENT1_TOKEN + dockerconfigjson: + from_secret: dockerconfigjson diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a97caa8 --- /dev/null +++ b/.env.example @@ -0,0 +1,27 @@ +REVERSE_DOMAIN=ch_meow_tb_dev_shop +APPLICATION_DOMAIN_NAME=shop.dev.tb.meow.ch + +DB_ROOT_PASSWORD=CHANGE_ME_ROOT_PASSWORD +DB_NAME=wordpress +DB_USER=wordpress +DB_PASSWORD=CHANGE_ME_DB_PASSWORD + +DOCKER_IMAGE=registry.sn48.zivili.ch/meow/wp-shop-dev-tb +DOCKER_IMAGE_TAG=latest +DOCKER_IMAGE_WP_CRON=registry.sn48.zivili.ch/meow/wp-cron-shop-dev-tb:latest + +TABLE_PREFIX=wp_ +WP_DEBUG=true +WP_DEBUG_LOG=true +WP_DEBUG_DISPLAY=false +DISABLE_WP_CRON=true + +# WordPress salts - generate fresh at https://api.wordpress.org/secret-key/1.1/salt/ +AUTH_KEY=CHANGE_ME +SECURE_AUTH_KEY=CHANGE_ME +LOGGED_IN_KEY=CHANGE_ME +NONCE_KEY=CHANGE_ME +AUTH_SALT=CHANGE_ME +SECURE_AUTH_SALT=CHANGE_ME +LOGGED_IN_SALT=CHANGE_ME +NONCE_SALT=CHANGE_ME diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b1a2e5b --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Environment files with secrets +.env* + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a195ceb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,140 @@ +version: "3.9" + +services: + wp_db: + image: mariadb:11 + container_name: "${REVERSE_DOMAIN}_wp_db" + environment: + MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}" + MYSQL_DATABASE: "${DB_NAME}" + MYSQL_USER: "${DB_USER}" + MYSQL_PASSWORD: "${DB_PASSWORD}" + expose: + - 3306 + volumes: + - wp_mysql:/var/lib/mysql + restart: always + networks: + - app_network + + wp_db_phpmyadmin: + image: phpmyadmin/phpmyadmin + container_name: "${REVERSE_DOMAIN}_db_phpmyadmin" + depends_on: + - wp_db + environment: + PMA_HOST: "${REVERSE_DOMAIN}_wp_db" + PMA_PORT: 3306 + MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD}" + MYSQL_DATABASE: "${DB_NAME}" + MYSQL_USER: "${DB_USER}" + MYSQL_PASSWORD: "${DB_PASSWORD}" + UPLOAD_LIMIT: 300M + expose: + - 80 + labels: + - "traefik.enable=true" + - "traefik.http.routers.${REVERSE_DOMAIN}_pma.rule=Host(`pma.${APPLICATION_DOMAIN_NAME}`)" + - "traefik.http.routers.${REVERSE_DOMAIN}_pma.entrypoints=websecure" + - "traefik.http.routers.${REVERSE_DOMAIN}_pma.tls.certresolver=myresolver" + - "traefik.http.services.${REVERSE_DOMAIN}_pma.loadbalancer.server.port=80" + - "traefik.docker.network=shared_network" + restart: always + networks: + - shared_network + - app_network + + wp_filebrowser: + image: filebrowser/filebrowser:latest + container_name: "${REVERSE_DOMAIN}_filebrowser" + volumes: + - wp_data:/srv + - filebrowser_db:/database + - ./docker/filebrowser/settings.json:/config/settings.json + environment: + - PUID=$(id -u) + - PGID=$(id -g) + expose: + - 80 + labels: + - "traefik.enable=true" + - "traefik.http.routers.${REVERSE_DOMAIN}_filebrowser.rule=Host(`ftp.${APPLICATION_DOMAIN_NAME}`)" + - "traefik.http.routers.${REVERSE_DOMAIN}_filebrowser.entrypoints=websecure" + - "traefik.http.routers.${REVERSE_DOMAIN}_filebrowser.tls.certresolver=myresolver" + - "traefik.http.services.${REVERSE_DOMAIN}_filebrowser.loadbalancer.server.port=80" + - "traefik.docker.network=shared_network" + restart: always + networks: + - shared_network + - app_network + + wp: + image: "${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG}" + container_name: "${REVERSE_DOMAIN}_wp" + depends_on: + - wp_db + environment: + WORDPRESS_DB_HOST: "${REVERSE_DOMAIN}_wp_db" + WORDPRESS_DB_NAME: "${DB_NAME}" + WORDPRESS_DB_USER: "${DB_USER}" + WORDPRESS_DB_PASSWORD: "${DB_PASSWORD}" + WORDPRESS_DB_CHARSET: "utf8" + WORDPRESS_DB_COLLATE: "" + WORDPRESS_AUTH_KEY: "${AUTH_KEY}" + WORDPRESS_SECURE_AUTH_KEY: "${SECURE_AUTH_KEY}" + WORDPRESS_LOGGED_IN_KEY: "${LOGGED_IN_KEY}" + WORDPRESS_NONCE_KEY: "${NONCE_KEY}" + WORDPRESS_AUTH_SALT: "${AUTH_SALT}" + WORDPRESS_SECURE_AUTH_SALT: "${SECURE_AUTH_SALT}" + WORDPRESS_LOGGED_IN_SALT: "${LOGGED_IN_SALT}" + WORDPRESS_NONCE_SALT: "${NONCE_SALT}" + WORDPRESS_TABLE_PREFIX: "${TABLE_PREFIX}" + WORDPRESS_CONFIG_EXTRA: | + $$to_bool = include __DIR__ . '/to-bool.php'; + define( 'WP_DEBUG', $$to_bool('${WP_DEBUG}') ); + define( 'WP_DEBUG_LOG', $$to_bool('${WP_DEBUG_LOG}') ); + define( 'WP_DEBUG_DISPLAY', $$to_bool('${WP_DEBUG_DISPLAY}') ); + define( 'DISABLE_WP_CRON', $$to_bool('${DISABLE_WP_CRON}') ); + expose: + - 80 + labels: + - "traefik.enable=true" + - "traefik.http.routers.${REVERSE_DOMAIN}.rule=Host(`${APPLICATION_DOMAIN_NAME}`)" + - "traefik.http.routers.${REVERSE_DOMAIN}.entrypoints=websecure" + - "traefik.http.routers.${REVERSE_DOMAIN}.tls.certresolver=myresolver" + - "traefik.http.services.${REVERSE_DOMAIN}.loadbalancer.server.port=80" + - "traefik.docker.network=shared_network" + volumes: + - wp_data:/var/www/html + - ./docker/wordpress/php.ini:/usr/local/etc/php/php.ini + - ./docker/wordpress/to-bool.php:/var/www/html/to-bool.php:ro + restart: always + networks: + - shared_network + - app_network + + wp_cron: + image: ${DOCKER_IMAGE_WP_CRON} + container_name: "${REVERSE_DOMAIN}_wp_cron" + depends_on: + - wp + networks: + - app_network + +networks: + shared_network: + name: shared_network + external: true + app_network: + name: ${REVERSE_DOMAIN}-app_network + +volumes: + wp_mysql: + name: "${REVERSE_DOMAIN}_wp_db-volume" + external: true + wp_data: + name: "${REVERSE_DOMAIN}_wp-data" + external: true + filebrowser_db: + name: "${REVERSE_DOMAIN}_filebrowser_db" + external: true diff --git a/docker/filebrowser/settings.json b/docker/filebrowser/settings.json new file mode 100644 index 0000000..cf7fb4e --- /dev/null +++ b/docker/filebrowser/settings.json @@ -0,0 +1,8 @@ +{ + "port": 80, + "baseURL": "", + "address": "", + "log": "stdout", + "database": "/database/filebrowser.db", + "root": "/srv" +} diff --git a/docker/wordpress/Dockerfile b/docker/wordpress/Dockerfile new file mode 100644 index 0000000..5a06644 --- /dev/null +++ b/docker/wordpress/Dockerfile @@ -0,0 +1,18 @@ +FROM wordpress:6.9-php8.2-apache + +# Install WP-CLI +RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \ + && chmod +x wp-cli.phar \ + && mv wp-cli.phar /usr/local/bin/wp + +# Install additional PHP extensions if needed +RUN apt-get update && apt-get install -y \ + less \ + mariadb-client \ + && rm -rf /var/lib/apt/lists/* + +# Copy PHP config +COPY php.ini /usr/local/etc/php/php.ini +COPY to-bool.php /usr/src/to-bool.php + +EXPOSE 80 diff --git a/docker/wordpress/php.ini b/docker/wordpress/php.ini new file mode 100644 index 0000000..f9bcb3e --- /dev/null +++ b/docker/wordpress/php.ini @@ -0,0 +1,4 @@ +memory_limit = 512M +upload_max_filesize = 100M +post_max_size = 100M +max_execution_time = 300 diff --git a/docker/wordpress/to-bool.php b/docker/wordpress/to-bool.php new file mode 100644 index 0000000..8095fda --- /dev/null +++ b/docker/wordpress/to-bool.php @@ -0,0 +1,24 @@ + /dev/null 2>&1" > /etc/crontabs/root + +CMD ["crond", "-f"] diff --git a/generate-env-secrets.sh b/generate-env-secrets.sh new file mode 100755 index 0000000..9a89f81 --- /dev/null +++ b/generate-env-secrets.sh @@ -0,0 +1,70 @@ +#!/bin/bash +# Generate secure passwords and WordPress salts for .env.prod +# Usage: ./generate-env-secrets.sh + +set -e + +PROJECT_NAME="ch_meow_tb_dev_shop" +DOMAIN="shop.dev.tb.meow.ch" +TABLE_PREFIX="wp_" +DOCKER_IMAGE="registry.sn48.zivili.ch/meow/wp-shop-dev-tb" +DOCKER_IMAGE_WP_CRON="registry.sn48.zivili.ch/meow/wp-cron-shop-dev-tb:latest" + +# Generate random passwords +DB_ROOT_PASSWORD=$(openssl rand -base64 32) +DB_PASSWORD=$(openssl rand -base64 32) +FTP_PASSWORD=$(openssl rand -base64 24) + +# Fetch WordPress salts from official API +echo "Fetching WordPress salts from api.wordpress.org..." +SALTS=$(curl -s https://api.wordpress.org/secret-key/1.1/salt/) + +# Extract individual salt values and escape for shell +extract_salt() { + echo "$SALTS" | grep "define('$1'" | sed "s/define('$1', *'//" | sed "s/');$//" | sed "s/\\\$/\\\\\$/g" | sed "s/\`/\\\\\`/g" +} + +AUTH_KEY=$(extract_salt "AUTH_KEY") +SECURE_AUTH_KEY=$(extract_salt "SECURE_AUTH_KEY") +LOGGED_IN_KEY=$(extract_salt "LOGGED_IN_KEY") +NONCE_KEY=$(extract_salt "NONCE_KEY") +AUTH_SALT=$(extract_salt "AUTH_SALT") +SECURE_AUTH_SALT=$(extract_salt "SECURE_AUTH_SALT") +LOGGED_IN_SALT=$(extract_salt "LOGGED_IN_SALT") +NONCE_SALT=$(extract_salt "NONCE_SALT") + +# Write .env.prod +cat > .env.prod << EOF +REVERSE_DOMAIN=${PROJECT_NAME} +APPLICATION_DOMAIN_NAME=${DOMAIN} + +DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD} +DB_NAME=${PROJECT_NAME}-db_name +DB_USER=${PROJECT_NAME}-db_user +DB_PASSWORD=${DB_PASSWORD} + +DOCKER_IMAGE=${DOCKER_IMAGE} +DOCKER_IMAGE_TAG=latest +DOCKER_IMAGE_WP_CRON=${DOCKER_IMAGE_WP_CRON} + +TABLE_PREFIX=${TABLE_PREFIX} + +AUTH_KEY="'${AUTH_KEY}'" +SECURE_AUTH_KEY="'${SECURE_AUTH_KEY}'" +LOGGED_IN_KEY="'${LOGGED_IN_KEY}'" +NONCE_KEY="'${NONCE_KEY}'" +AUTH_SALT="'${AUTH_SALT}'" +SECURE_AUTH_SALT="'${SECURE_AUTH_SALT}'" +LOGGED_IN_SALT="'${LOGGED_IN_SALT}'" +NONCE_SALT="'${NONCE_SALT}'" + +WP_DEBUG=1 +WP_DEBUG_LOG=1 +WP_DEBUG_DISPLAY=false +DISABLE_WP_CRON=true + +FTP_USERNAME=admin +FTP_PASSWORD=${FTP_PASSWORD} +EOF + +echo "Generated .env.prod for ${PROJECT_NAME} (${DOMAIN})" diff --git a/scripts/strip-wpbakery-shortcodes.php b/scripts/strip-wpbakery-shortcodes.php new file mode 100644 index 0000000..6eb29da --- /dev/null +++ b/scripts/strip-wpbakery-shortcodes.php @@ -0,0 +1,90 @@ +#!/usr/bin/env php + output.sql + * Or: php strip-wpbakery-shortcodes.php --test "content with [vc_row]shortcodes[/vc_row]" + */ + +function strip_wpbakery_shortcodes(string $content): string { + // List of WPBakery shortcode prefixes to remove + $prefixes = [ + 'vc_', // Visual Composer / WPBakery + 'rev_', // Slider Revolution + 'ozy_', // Theme-specific (ozy-diwine-essentials) + ]; + + // Build regex pattern for opening tags: [vc_anything attr="value"] + $pattern_open = '/\[(' . implode('|', $prefixes) . ')[^\]]*\]/i'; + + // Build regex pattern for closing tags: [/vc_anything] + $pattern_close = '/\[\/(' . implode('|', $prefixes) . ')[^\]]*\]/i'; + + // Remove opening tags + $content = preg_replace($pattern_open, '', $content); + + // Remove closing tags + $content = preg_replace($pattern_close, '', $content); + + // Clean up excessive newlines (more than 2 in a row) + $content = preg_replace('/\n{3,}/', "\n\n", $content); + + // Clean up excessive spaces + $content = preg_replace('/[ \t]{2,}/', ' ', $content); + + // Trim leading/trailing whitespace + $content = trim($content); + + return $content; +} + +/** + * Process SQL dump, stripping shortcodes from post_content + */ +function process_sql_dump(string $sql): string { + // Match INSERT INTO ... post_content patterns + // This is a simplified approach - for production, consider using proper SQL parsing + + return preg_replace_callback( + "/'((?:[^'\\\\]|\\\\.)*?)'/", + function($matches) { + $content = $matches[1]; + // Only process if it looks like it contains WPBakery shortcodes + if (preg_match('/\[(vc_|rev_|ozy_)/', $content)) { + $cleaned = strip_wpbakery_shortcodes(stripslashes($content)); + return "'" . addslashes($cleaned) . "'"; + } + return $matches[0]; + }, + $sql + ); +} + +// CLI handling +if (php_sapi_name() === 'cli') { + $args = getopt('', ['test:', 'help', 'sql']); + + if (isset($args['help'])) { + echo "Usage:\n"; + echo " php strip-wpbakery-shortcodes.php --test \"content\" Test stripping on a string\n"; + echo " php strip-wpbakery-shortcodes.php --sql < dump.sql Process SQL dump from stdin\n"; + echo " php strip-wpbakery-shortcodes.php < input.txt Process plain text from stdin\n"; + exit(0); + } + + if (isset($args['test'])) { + echo "Input:\n" . $args['test'] . "\n\n"; + echo "Output:\n" . strip_wpbakery_shortcodes($args['test']) . "\n"; + exit(0); + } + + // Read from stdin + $input = file_get_contents('php://stdin'); + + if (isset($args['sql'])) { + echo process_sql_dump($input); + } else { + echo strip_wpbakery_shortcodes($input); + } +}