commit c4cbe966ca838e63d50abc69a1c8b55fbfa13889 Author: nikomiko Date: Thu Oct 29 03:34:30 2026 +0000 init diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bd14147 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Copy to .env, then fill in real secrets: cp .env.example .env && chmod 600 .env +# Generate good passwords with: openssl rand -hex 24 + +# Host-side loopback port nginx will proxy to (must match the upstream in the nginx conf) +WP_HOST_PORT=18080 + +WP_DB_NAME=wordpress +WP_DB_USER=wp_user +WP_DB_PASSWORD=change-me +MARIADB_ROOT_PASSWORD=change-me-too + +# Only change before the very first start +WP_TABLE_PREFIX=wp_ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3858f5f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,69 @@ +# WordPress + MariaDB for Podman (rootless-friendly) +# Used with: podman-compose up -d (or: podman compose up -d) +# +# WordPress is published ONLY on 127.0.0.1 -- the host's nginx is the public face. +# The database is never published at all; it lives on a private network. + +name: wp_viossa_net + +services: + db: + image: docker.io/library/mariadb:11.4 + container_name: wp_viossa_net_db + restart: always + environment: + MARIADB_DATABASE: ${WP_DB_NAME} + MARIADB_USER: ${WP_DB_USER} + MARIADB_PASSWORD: ${WP_DB_PASSWORD} + MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD} + MARIADB_AUTO_UPGRADE: "1" + volumes: + - db_data:/var/lib/mysql + networks: + - backend + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 30s + + wordpress: + image: docker.io/library/wordpress:6-php8.3-apache + container_name: wp_viossa_net_app + restart: always + depends_on: + db: + condition: service_healthy + ports: + # Loopback only. Change WP_HOST_PORT in .env if it collides with something else. + - "127.0.0.1:${WP_HOST_PORT:-18080}:80" + environment: + WORDPRESS_DB_HOST: db:3306 + WORDPRESS_DB_NAME: ${WP_DB_NAME} + WORDPRESS_DB_USER: ${WP_DB_USER} + WORDPRESS_DB_PASSWORD: ${WP_DB_PASSWORD} + WORDPRESS_TABLE_PREFIX: ${WP_TABLE_PREFIX:-wp_} + # NOTE: this is only written into wp-config.php the FIRST time the container + # starts with an empty wp_data volume. "$$" is compose-escaping for a literal "$". + WORDPRESS_CONFIG_EXTRA: | + if (isset($$_SERVER['HTTP_X_FORWARDED_PROTO']) && $$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { + $$_SERVER['HTTPS'] = 'on'; + } + define('FORCE_SSL_ADMIN', true); + define('DISALLOW_FILE_EDIT', true); + volumes: + - wp_data:/var/www/html + # :ro = read-only, :Z = private SELinux label (needed on Fedora/RHEL, harmless elsewhere) + - ./php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro,Z + networks: + - backend + +networks: + backend: + +volumes: + db_data: + name: wp_viossa_net_db_data + wp_data: + name: wp_viossa_net_wp_data diff --git a/uploads.ini b/uploads.ini new file mode 100644 index 0000000..fd8ec70 --- /dev/null +++ b/uploads.ini @@ -0,0 +1,8 @@ +; Mounted into the wordpress container at /usr/local/etc/php/conf.d/uploads.ini +; Keep upload_max_filesize / post_max_size <= client_max_body_size in nginx. +file_uploads = On +upload_max_filesize = 64M +post_max_size = 64M +memory_limit = 256M +max_execution_time = 300 +max_input_vars = 3000 diff --git a/wp.viossa.net.conf b/wp.viossa.net.conf new file mode 100644 index 0000000..47eba9a --- /dev/null +++ b/wp.viossa.net.conf @@ -0,0 +1,87 @@ +# /etc/nginx/conf.d/wp.viossa.net.conf +# (or /etc/nginx/sites-available/wp.viossa.net + symlink into sites-enabled) +# +# Host nginx -> 127.0.0.1:18080 (WordPress container). Names are prefixed +# "wp_viossa_net" so they can't collide with the other sites on this box. +# This file must be included inside http { } (conf.d / sites-enabled both are). + +upstream wp_viossa_net { + server 127.0.0.1:18080; # keep in sync with WP_HOST_PORT in .env + keepalive 16; +} + +# Brute-force brake for the login page (10 requests/minute/IP, small burst) +limit_req_zone $binary_remote_addr zone=wp_viossa_net_login:10m rate=10r/m; + +# ---------- HTTP: ACME challenge + redirect ---------- +server { + listen 80; + listen [::]:80; + server_name wp.viossa.net; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://$host$request_uri; + } +} + +# ---------- HTTPS ---------- +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; # nginx >= 1.25.1. On older nginx, delete this line + # and use: listen 443 ssl http2; + server_name wp.viossa.net; + + ssl_certificate /etc/letsencrypt/live/wp.viossa.net/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/wp.viossa.net/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_session_cache shared:SSL_wp_viossa_net:10m; + ssl_session_timeout 1d; + ssl_session_tickets off; + + access_log /var/log/nginx/wp.viossa.net.access.log; + error_log /var/log/nginx/wp.viossa.net.error.log; + + client_max_body_size 64M; # match php/uploads.ini + + add_header Strict-Transport-Security "max-age=31536000" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Proxy settings at server level are inherited by every location below + # that doesn't define its own proxy_set_header lines. + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_read_timeout 120s; + proxy_send_timeout 120s; + + # Don't expose dotfiles (but allow /.well-known/) + location ~ /\.(?!well-known) { + deny all; + } + + # XML-RPC is a common brute-force/DDoS target; remove this block + # if you need the mobile app or Jetpack. + location = /xmlrpc.php { + return 403; + } + + location = /wp-login.php { + limit_req zone=wp_viossa_net_login burst=5 nodelay; + limit_req_status 429; + proxy_pass http://wp_viossa_net; + } + + location / { + proxy_pass http://wp_viossa_net; + } +}