Private
Public Access
1
0

Initial commit: complete WebsiteBox project

Docker-based self-hosted WordPress deployment system with:
- Four-container stack (nginx, wordpress/php-fpm, mariadb, certbot)
- Automatic SSL via Let's Encrypt with self-signed fallback
- First-boot WordPress setup via WP-CLI (GeneratePress + child theme, plugins)
- Interactive setup wizard and one-line install script
- Backup, update, healthcheck, and SSL renewal scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
constantprojects
2026-02-20 15:24:23 -07:00
commit a440026701
32 changed files with 3397 additions and 0 deletions

41
wordpress/Dockerfile Normal file
View File

@@ -0,0 +1,41 @@
FROM wordpress:php8.2-fpm-alpine
# Install dependencies
RUN apk add --no-cache \
bash \
less \
mysql-client \
fcgi
# Install WP-CLI
RUN curl -o /usr/local/bin/wp https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
chmod +x /usr/local/bin/wp
# Install php-fpm-healthcheck
RUN curl -o /usr/local/bin/php-fpm-healthcheck \
https://raw.githubusercontent.com/renatomefi/php-fpm-healthcheck/master/php-fpm-healthcheck && \
chmod +x /usr/local/bin/php-fpm-healthcheck
# Enable php-fpm status page for healthcheck
RUN echo "pm.status_path = /status" >> /usr/local/etc/php-fpm.d/zz-docker.conf
# Copy PHP upload config
COPY uploads.ini /usr/local/etc/php/conf.d/uploads.ini
# Copy wp-config template
COPY wp-config-docker.php /usr/src/wordpress/wp-config-docker.php
# Copy child theme and mu-plugins
COPY wp-content/themes/websitebox/ /usr/src/websitebox-theme/
COPY wp-content/mu-plugins/ /usr/src/websitebox-mu-plugins/
# Copy custom entrypoint
COPY entrypoint.sh /usr/local/bin/websitebox-entrypoint.sh
RUN chmod +x /usr/local/bin/websitebox-entrypoint.sh
# Create Wordfence-compatible .user.ini
RUN echo "; Wordfence PHP settings" > /usr/src/wordpress/.user.ini && \
echo "auto_prepend_file = /var/www/html/wordfence-waf.php" >> /usr/src/wordpress/.user.ini
ENTRYPOINT ["websitebox-entrypoint.sh"]
CMD ["php-fpm"]

181
wordpress/entrypoint.sh Executable file
View File

@@ -0,0 +1,181 @@
#!/bin/bash
set -eo pipefail
MARKER_COMPLETE="/var/www/html/.websitebox-setup-complete"
MARKER_PARTIAL="/var/www/html/.websitebox-setup-partial"
# Run the default WordPress docker-entrypoint.sh first
# This sets up wp-config.php, copies WordPress files, then execs php-fpm
docker-entrypoint.sh php-fpm &
WP_PID=$!
# Forward signals to PHP-FPM for graceful shutdown
trap 'kill -TERM $WP_PID; wait $WP_PID; exit $?' TERM INT
# Wait for WordPress files AND wp-config.php to be available
echo "WebsiteBox: Waiting for WordPress files..."
for i in $(seq 1 60); do
if [ -f /var/www/html/wp-includes/version.php ] && [ -f /var/www/html/wp-config.php ]; then
echo "WebsiteBox: WordPress files ready."
break
fi
if [ "$i" -eq 60 ]; then
echo "ERROR: WordPress files did not appear after 120 seconds."
exit 1
fi
sleep 2
done
# Copy mu-plugins into place
mkdir -p /var/www/html/wp-content/mu-plugins
cp -f /usr/src/websitebox-mu-plugins/*.php /var/www/html/wp-content/mu-plugins/ 2>/dev/null || true
chown -R www-data:www-data /var/www/html/wp-content/mu-plugins/
# Check if setup is already complete
if [ -f "${MARKER_COMPLETE}" ]; then
echo "WebsiteBox: Setup already complete. Starting normally."
wait $WP_PID
exit $?
fi
echo "WebsiteBox: Running first-boot setup..."
# If partial marker exists, we're retrying
if [ -f "${MARKER_PARTIAL}" ]; then
echo "WebsiteBox: Previous setup was partial. Re-running setup idempotently..."
fi
# Wait for MariaDB to be ready
echo "WebsiteBox: Waiting for database..."
DB_READY=false
for i in $(seq 1 30); do
if su -s /bin/sh -c 'wp db check --path=/var/www/html 2>/dev/null' www-data; then
DB_READY=true
echo "WebsiteBox: Database is ready."
break
fi
echo "WebsiteBox: Database not ready, attempt ${i}/30..."
sleep 2
done
if [ "${DB_READY}" != "true" ]; then
echo "ERROR: WebsiteBox could not connect to database after 30 attempts."
echo "Check that the db container is running: docker compose ps db"
exit 1
fi
# Track if any step fails
SETUP_FAILED=false
# Install WordPress core if not already installed
# Using env vars directly (inherited by su without -l) avoids shell quoting issues
echo "WebsiteBox: Installing WordPress core..."
if ! su -s /bin/sh -c 'wp core is-installed --path=/var/www/html 2>/dev/null' www-data; then
# shellcheck disable=SC2016
# Single quotes intentional: inner shell (via su) expands env vars
if ! su -s /bin/sh www-data -c 'wp core install \
--path=/var/www/html \
--url="https://${DOMAIN}" \
--title="${SITE_TITLE:-My Portfolio}" \
--admin_user="${ADMIN_USER}" \
--admin_password="${ADMIN_PASSWORD}" \
--admin_email="${ADMIN_EMAIL}" \
--skip-email'; then
echo "ERROR: WordPress core install failed."
SETUP_FAILED=true
fi
else
echo "WebsiteBox: WordPress core already installed."
fi
# Install and activate GeneratePress theme
if [ "${SETUP_FAILED}" != "true" ]; then
echo "WebsiteBox: Installing GeneratePress theme..."
su -s /bin/sh -c 'wp theme install generatepress --path=/var/www/html 2>/dev/null' www-data || true
fi
# Copy and activate child theme
echo "WebsiteBox: Installing WebsiteBox child theme..."
cp -r /usr/src/websitebox-theme/ /var/www/html/wp-content/themes/websitebox/
chown -R www-data:www-data /var/www/html/wp-content/themes/websitebox/
if [ "${SETUP_FAILED}" != "true" ]; then
su -s /bin/sh -c 'wp theme activate websitebox --path=/var/www/html' www-data || {
echo "WARNING: Could not activate child theme. Activating GeneratePress instead."
su -s /bin/sh -c 'wp theme activate generatepress --path=/var/www/html' www-data || true
}
fi
# Install and activate plugins
if [ "${SETUP_FAILED}" != "true" ]; then
echo "WebsiteBox: Installing plugins..."
for plugin in age-gate wordfence updraftplus; do
echo "WebsiteBox: Installing ${plugin}..."
if ! su -s /bin/sh -c "wp plugin install ${plugin} --activate --path=/var/www/html 2>/dev/null" www-data; then
echo "WARNING: Failed to install ${plugin}. Will continue with remaining setup."
SETUP_FAILED=true
fi
done
fi
# Configure WordPress settings
if [ "${SETUP_FAILED}" != "true" ]; then
echo "WebsiteBox: Configuring WordPress settings..."
# Set permalink structure
su -s /bin/sh -c "wp rewrite structure '/%postname%/' --path=/var/www/html" www-data || true
# Delete default content
su -s /bin/sh -c 'wp post delete 1 --force --path=/var/www/html 2>/dev/null' www-data || true
su -s /bin/sh -c 'wp post delete 2 --force --path=/var/www/html 2>/dev/null' www-data || true
# Set timezone
su -s /bin/sh -c "wp option update timezone_string 'UTC' --path=/var/www/html" www-data || true
# Discourage search engines until user is ready
su -s /bin/sh -c "wp option update blog_public 0 --path=/var/www/html" www-data || true
fi
# Configure Age Gate
if [ "${SETUP_FAILED}" != "true" ] && [ "${AGE_GATE_ENABLED}" = "true" ]; then
echo "WebsiteBox: Configuring Age Gate..."
# shellcheck disable=SC2016
su -s /bin/sh -c 'wp option update age_gate_min_age "${AGE_GATE_MIN_AGE:-18}" --path=/var/www/html' www-data || true
su -s /bin/sh -c 'wp option update age_gate_restrict_all "1" --path=/var/www/html' www-data || true
fi
# Disable Age Gate if not enabled
if [ "${AGE_GATE_ENABLED}" != "true" ]; then
echo "WebsiteBox: Age Gate disabled. Deactivating plugin..."
su -s /bin/sh -c 'wp plugin deactivate age-gate --path=/var/www/html 2>/dev/null' www-data || true
fi
# Configure UpdraftPlus backup path
echo "WebsiteBox: Configuring UpdraftPlus backup path..."
su -s /bin/sh -c "wp option update updraft_dir '/var/backups/websitebox' --path=/var/www/html" www-data || true
# Set backup retention
RETENTION="${BACKUP_RETENTION_DAYS:-30}"
DB_RETAIN=$((RETENTION > 30 ? 30 : RETENTION))
FILE_RETAIN=$((RETENTION / 7))
[ "${FILE_RETAIN}" -lt 1 ] && FILE_RETAIN=1
su -s /bin/sh -c "wp option update updraft_retain '${FILE_RETAIN}' --path=/var/www/html" www-data || true
su -s /bin/sh -c "wp option update updraft_retain_db '${DB_RETAIN}' --path=/var/www/html" www-data || true
# Create marker file
if [ "${SETUP_FAILED}" = "true" ]; then
touch "${MARKER_PARTIAL}"
chown www-data:www-data "${MARKER_PARTIAL}"
echo "WARNING: WebsiteBox setup completed with errors."
echo "Some plugins/themes may not have installed correctly."
echo "To retry: docker compose restart wordpress"
else
rm -f "${MARKER_PARTIAL}"
touch "${MARKER_COMPLETE}"
chown www-data:www-data "${MARKER_COMPLETE}"
echo "WebsiteBox: First-run setup complete!"
fi
# Wait for the background PHP-FPM process
wait $WP_PID

5
wordpress/uploads.ini Normal file
View File

@@ -0,0 +1,5 @@
file_uploads = On
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 300
memory_limit = 256M

View File

@@ -0,0 +1,26 @@
<?php
/**
* WebsiteBox WordPress Configuration (Extra Settings)
*
* This file is included by the auto-generated wp-config.php from the official
* WordPress Docker image. It must NOT redefine DB constants, $table_prefix,
* auth keys/salts, or call wp-settings.php — those are all handled by the
* parent wp-config.php.
*/
// Security hardening
define('DISALLOW_FILE_EDIT', true);
define('FORCE_SSL_ADMIN', true);
define('WP_POST_REVISIONS', 10);
define('WP_AUTO_UPDATE_CORE', 'minor');
// Debug settings (disabled in production)
define('WP_DEBUG', false);
define('WP_DEBUG_LOG', false);
define('WP_DEBUG_DISPLAY', false);
// WordPress URLs
if (getenv('DOMAIN')) {
define('WP_HOME', 'https://' . getenv('DOMAIN'));
define('WP_SITEURL', 'https://' . getenv('DOMAIN'));
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* Plugin Name: WebsiteBox Setup Status
* Description: Displays setup status notices and dashboard widget. Disables XML-RPC.
* Version: 1.0.0
* Author: WebsiteBox
*/
if (!defined('ABSPATH')) {
exit;
}
// Disable XML-RPC
add_filter('xmlrpc_enabled', '__return_false');
// Admin notices based on setup status
add_action('admin_notices', function () {
$complete = ABSPATH . '.websitebox-setup-complete';
$partial = ABSPATH . '.websitebox-setup-partial';
if (file_exists($complete)) {
return; // Normal operation
}
if (file_exists($partial)) {
echo '<div class="notice notice-warning"><p>';
echo '<strong>WebsiteBox:</strong> Initial setup completed with errors. ';
echo 'Some plugins or themes may not have installed correctly. ';
echo 'To retry, run: <code>docker compose restart wordpress</code>';
echo '</p></div>';
return;
}
echo '<div class="notice notice-info"><p>';
echo '<strong>WebsiteBox:</strong> Initial setup is still in progress. ';
echo 'The container may still be installing themes and plugins. Please wait a moment and refresh this page.';
echo '</p></div>';
});
// Dashboard widget
add_action('wp_dashboard_setup', function () {
wp_add_dashboard_widget(
'websitebox_status',
'WebsiteBox Status',
'websitebox_dashboard_widget'
);
});
function websitebox_dashboard_widget() {
$complete = file_exists(ABSPATH . '.websitebox-setup-complete');
$partial = file_exists(ABSPATH . '.websitebox-setup-partial');
echo '<table class="widefat" style="border:0">';
// Setup status
echo '<tr><td><strong>Setup Status</strong></td><td>';
if ($complete) {
echo '<span style="color:green">&#10003; Complete</span>';
} elseif ($partial) {
echo '<span style="color:orange">&#9888; Partial — retry with: <code>docker compose restart wordpress</code></span>';
} else {
echo '<span style="color:blue">&#8987; In progress...</span>';
}
echo '</td></tr>';
// Age Gate status
if (is_plugin_active('age-gate/age-gate.php')) {
$min_age = get_option('age_gate_min_age', '18');
$restrict_all = get_option('age_gate_restrict_all', '0');
echo '<tr><td><strong>Age Gate</strong></td><td>';
echo $restrict_all ? "Enabled (minimum age: {$min_age})" : 'Installed but not restricting all content';
echo '</td></tr>';
} else {
echo '<tr><td><strong>Age Gate</strong></td><td>Not active</td></tr>';
}
// Backup status
if (is_plugin_active('updraftplus/updraftplus.php')) {
$last_backup = get_option('updraft_last_backup', []);
echo '<tr><td><strong>Backups</strong></td><td>';
if (!empty($last_backup['backup_time'])) {
echo 'Last backup: ' . date('Y-m-d H:i', $last_backup['backup_time']);
} else {
echo 'No backups yet';
}
echo '</td></tr>';
}
echo '</table>';
}

View File

@@ -0,0 +1,37 @@
<?php
/**
* WebsiteBox Child Theme Functions
*/
if (!defined('ABSPATH')) {
exit;
}
// Enqueue parent theme styles
add_action('wp_enqueue_scripts', function () {
wp_enqueue_style(
'generatepress-parent',
get_template_directory_uri() . '/style.css',
[],
wp_get_theme('generatepress')->get('Version')
);
wp_enqueue_style(
'websitebox-child',
get_stylesheet_uri(),
['generatepress-parent'],
wp_get_theme()->get('Version')
);
});
// Remove GeneratePress footer branding
add_filter('generate_copyright', function () {
return '';
});
// Set portfolio-friendly defaults
add_action('after_setup_theme', function () {
add_theme_support('post-thumbnails');
set_post_thumbnail_size(1200, 800, true);
add_image_size('portfolio-thumb', 600, 400, true);
add_image_size('portfolio-full', 1920, 1080, false);
});

View File

@@ -0,0 +1,13 @@
<?php
// Silence is golden. GeneratePress handles all template rendering.
// This file exists only because WordPress requires index.php in every theme.
get_header();
if (have_posts()) :
while (have_posts()) : the_post();
get_template_part('content', get_post_type());
endwhile;
the_posts_navigation();
else :
get_template_part('content', 'none');
endif;
get_footer();

View File

@@ -0,0 +1,12 @@
/*
Theme Name: WebsiteBox
Theme URI: https://github.com/websitebox/websitebox
Description: A clean portfolio child theme for WebsiteBox, built on GeneratePress.
Author: WebsiteBox
Author URI: https://github.com/websitebox/websitebox
Template: generatepress
Version: 1.0.0
License: GNU General Public License v3 or later
License URI: https://www.gnu.org/licenses/gpl-3.0.html
Text Domain: websitebox
*/

View File

@@ -0,0 +1,66 @@
{
"$schema": "https://schemas.wp.org/trunk/theme.json",
"version": 3,
"settings": {
"color": {
"palette": [
{
"slug": "primary",
"color": "#1a1a2e",
"name": "Primary"
},
{
"slug": "secondary",
"color": "#16213e",
"name": "Secondary"
},
{
"slug": "accent",
"color": "#e94560",
"name": "Accent"
},
{
"slug": "light",
"color": "#f8f9fa",
"name": "Light"
},
{
"slug": "dark",
"color": "#0f0f1a",
"name": "Dark"
}
]
},
"typography": {
"fontSizes": [
{
"slug": "small",
"size": "0.875rem",
"name": "Small"
},
{
"slug": "medium",
"size": "1rem",
"name": "Medium"
},
{
"slug": "large",
"size": "1.5rem",
"name": "Large"
},
{
"slug": "x-large",
"size": "2.25rem",
"name": "Extra Large"
}
]
},
"spacing": {
"units": ["px", "em", "rem", "%", "vh", "vw"]
},
"layout": {
"contentSize": "1200px",
"wideSize": "1400px"
}
}
}