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
master
Guillermo Pages 1 month ago
commit 90776a45ce

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

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

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

15
.gitignore vendored

@ -0,0 +1,15 @@
# Environment files with secrets
.env*
# IDE
.idea/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Logs
*.log

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

@ -0,0 +1,8 @@
{
"port": 80,
"baseURL": "",
"address": "",
"log": "stdout",
"database": "/database/filebrowser.db",
"root": "/srv"
}

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

@ -0,0 +1,4 @@
memory_limit = 512M
upload_max_filesize = 100M
post_max_size = 100M
max_execution_time = 300

@ -0,0 +1,24 @@
<?php
/**
* Convert environment variable strings to boolean values
*/
$env_to_bool = function ($v): bool {
if (is_bool($v)) {
return $v;
}
if (is_numeric($v)) {
return (bool) intval($v);
}
if (is_string($v)) {
$n = mb_strtolower(trim($v));
if (in_array($n, ['1', 'true', 'on', 'yes', 'y', 't'], true)) {
return true;
}
if (in_array($n, ['0', 'false', 'off', 'no', 'n', 'f', ''], true)) {
return false;
}
}
throw new Exception('Could not convert value to bool: ' . var_export($v, true));
};
return $env_to_bool;

@ -0,0 +1,6 @@
FROM alpine:latest
RUN apk add --no-cache curl \
&& echo "*/5 * * * * curl -s http://wp:80/wp-cron.php?doing_wp_cron > /dev/null 2>&1" > /etc/crontabs/root
CMD ["crond", "-f"]

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

@ -0,0 +1,90 @@
#!/usr/bin/env php
<?php
/**
* Strip WPBakery shortcodes from content while preserving inner text
*
* Usage: php strip-wpbakery-shortcodes.php < input.sql > 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);
}
}
Loading…
Cancel
Save