initial commit
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# local artifacts
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
17
scripts/bs-label-workers.sh
Normal file
17
scripts/bs-label-workers.sh
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# label nodes into pools based on hostname: *wordpressPool*/*mysqlPool*/*redisPool*/*phpPool*/*nginxPool*
|
||||||
|
set -euo pipefail
|
||||||
|
docker node inspect -f '{{ .ID }} {{ .Description.Hostname }} {{ .Spec.Role }}' $(docker node ls -q) \
|
||||||
|
| while read -r id host role; do
|
||||||
|
[[ "$role" == "worker" ]] || continue
|
||||||
|
case "$host" in
|
||||||
|
*wordPressPool*) val=wordpress ;;
|
||||||
|
*mysqlPool*) val=mysql ;;
|
||||||
|
*redisPool*) val=redis ;;
|
||||||
|
*phpPool*) val=php ;;
|
||||||
|
*nginxPool*) val=nginx ;;
|
||||||
|
*) val="" ;;
|
||||||
|
esac
|
||||||
|
[[ -n "$val" ]] && docker node update --label-add "pool=$val" "$id"
|
||||||
|
done
|
||||||
|
echo "Worker labels updated."
|
||||||
5
scripts/bs-networks.sh
Normal file
5
scripts/bs-networks.sh
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
docker network create --driver overlay --opt encrypted --attachable appNet >/dev/null 2>&1 || true
|
||||||
|
docker network create --driver overlay --opt encrypted --internal backendNet >/dev/null 2>&1 || true
|
||||||
|
echo "appNet + backendNet ready."
|
||||||
65
scripts/bs-scaffold-site.sh
Normal file
65
scripts/bs-scaffold-site.sh
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DOMAIN="${1:-${DOMAIN:-}}"
|
||||||
|
if [[ -z "$DOMAIN" ]]; then
|
||||||
|
echo "Usage: $0 <full-domain>"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
STACK="${STACK:-${DOMAIN%.*}}" # domain minus TLD
|
||||||
|
BASE="/mnt/data/sites/$DOMAIN"
|
||||||
|
TPL="/mnt/data/templates/site-stack.allinone.template.yml"
|
||||||
|
OUT="/mnt/data/stacks/site-stack.$DOMAIN.yml"
|
||||||
|
|
||||||
|
mkdir -p "$BASE"/{code,uploads,plugins,themes,mu-plugins,nginx/conf.d,db,redis}
|
||||||
|
chown -R www-data:www-data "$BASE"/{code,uploads,plugins,themes,mu-plugins} || true
|
||||||
|
chown -R nobody:nogroup "$BASE/nginx" || true
|
||||||
|
|
||||||
|
# Seed WordPress core if empty
|
||||||
|
if [ -z "$(ls -A "$BASE/code" 2>/dev/null)" ]; then
|
||||||
|
docker run --rm -v "$BASE/code":/target wordpress:php8.3-fpm \
|
||||||
|
bash -lc 'shopt -s dotglob && cp -a /var/www/html/* /target/'
|
||||||
|
chown -R www-data:www-data "$BASE/code"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# vhost
|
||||||
|
VHOST="$BASE/nginx/conf.d/site.conf"
|
||||||
|
if [[ ! -f "$VHOST" ]]; then
|
||||||
|
cat > "$VHOST" <<'CONF'
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name __DOMAIN__;
|
||||||
|
root /var/www/html;
|
||||||
|
index index.php index.html;
|
||||||
|
|
||||||
|
location / { try_files $uri $uri/ /index.php?$args; }
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
|
||||||
|
fastcgi_param HTTP_X_FORWARDED_HOST $host;
|
||||||
|
fastcgi_pass php:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
client_max_body_size 50m;
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
sed -i "s/__DOMAIN__/$DOMAIN/g" "$VHOST"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# per-site secrets
|
||||||
|
ROOT_SEC="mysql_root_password_${DOMAIN}"
|
||||||
|
WP_SEC="mysql_wp_password_${DOMAIN}"
|
||||||
|
docker secret ls --format '{{.Name}}' | grep -qx "$ROOT_SEC" || openssl rand -base64 32 | docker secret create "$ROOT_SEC" - >/dev/null
|
||||||
|
docker secret ls --format '{{.Name}}' | grep -qx "$WP_SEC" || openssl rand -base64 32 | docker secret create "$WP_SEC" - >/dev/null
|
||||||
|
|
||||||
|
# render stack
|
||||||
|
mkdir -p /mnt/data/stacks /mnt/data/templates
|
||||||
|
cp -n templates/site-stack.allinone.template.yml "$TPL" 2>/dev/null || true
|
||||||
|
sed "s/__DOMAIN__/$DOMAIN/g" "$TPL" > "$OUT"
|
||||||
|
|
||||||
|
# deploy
|
||||||
|
docker stack deploy -c "$OUT" "$STACK"
|
||||||
|
|
||||||
|
echo "Deployed stack '$STACK'. In NPM, proxy https://$DOMAIN -> http://${STACK}_nginx:80 on appNet."
|
||||||
18
stacks/gateway-stack.yml
Normal file
18
stacks/gateway-stack.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
version: "3.9"
|
||||||
|
networks: { appNet: { external: true } }
|
||||||
|
|
||||||
|
services:
|
||||||
|
npm:
|
||||||
|
image: jc21/nginx-proxy-manager:latest
|
||||||
|
networks: [appNet]
|
||||||
|
ports: ["80:80","443:443","81:81"]
|
||||||
|
volumes:
|
||||||
|
- /mnt/data/gateway/npm/data:/data
|
||||||
|
- /mnt/data/gateway/npm/letsencrypt:/etc/letsencrypt
|
||||||
|
deploy:
|
||||||
|
replicas: 2
|
||||||
|
placement:
|
||||||
|
constraints: ["node.role == manager"]
|
||||||
|
resources:
|
||||||
|
limits: { cpus: "0.50", memory: 512M }
|
||||||
|
reservations: { cpus: "0.10", memory: 128M }
|
||||||
18
templates/nginx-site.conf
Normal file
18
templates/nginx-site.conf
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name __DOMAIN__;
|
||||||
|
root /var/www/html;
|
||||||
|
index index.php index.html;
|
||||||
|
|
||||||
|
location / { try_files $uri $uri/ /index.php?$args; }
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
|
||||||
|
fastcgi_param HTTP_X_FORWARDED_HOST $host;
|
||||||
|
fastcgi_pass php:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
client_max_body_size 50m;
|
||||||
|
}
|
||||||
142
templates/site-stack.allinone.template.yml
Normal file
142
templates/site-stack.allinone.template.yml
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
appNet: { external: true }
|
||||||
|
privNet:
|
||||||
|
driver: overlay
|
||||||
|
internal: true
|
||||||
|
attachable: false
|
||||||
|
driver_opts: { encrypted: "true" }
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
mysql_root_password___DOMAIN__: { external: true }
|
||||||
|
mysql_wp_password___DOMAIN__: { external: true }
|
||||||
|
|
||||||
|
x-common-env: &common_env
|
||||||
|
WORDPRESS_DB_HOST: db:3306
|
||||||
|
WORDPRESS_DB_USER: wordpress
|
||||||
|
WORDPRESS_DB_PASSWORD_FILE: /run/secrets/mysql_wp_password___DOMAIN__
|
||||||
|
WORDPRESS_DB_NAME: wordpress
|
||||||
|
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('WP_CACHE_KEY_SALT', '__DOMAIN__');
|
||||||
|
define('WP_REDIS_HOST', 'redis');
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mysql:8.4
|
||||||
|
command: ["--innodb-buffer-pool-size=192M","--max-connections=100","--skip-log-bin"]
|
||||||
|
environment:
|
||||||
|
MYSQL_DATABASE: wordpress
|
||||||
|
MYSQL_USER: wordpress
|
||||||
|
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/mysql_root_password___DOMAIN__
|
||||||
|
MYSQL_PASSWORD_FILE: /run/secrets/mysql_wp_password___DOMAIN__
|
||||||
|
secrets: [mysql_root_password___DOMAIN__, mysql_wp_password___DOMAIN__]
|
||||||
|
networks: [privNet]
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /mnt/data/sites/__DOMAIN__/db
|
||||||
|
target: /var/lib/mysql
|
||||||
|
deploy:
|
||||||
|
placement: { constraints: ["node.labels.pool == mysql","node.role == worker"] }
|
||||||
|
resources:
|
||||||
|
limits: { cpus: "0.50", memory: 512M }
|
||||||
|
reservations: { cpus: "0.10", memory: 256M }
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL","MYSQL_PWD=$$(cat /run/secrets/mysql_root_password___DOMAIN__) mysqladmin ping -h 127.0.0.1 -uroot"]
|
||||||
|
interval: 20s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
command: ["redis-server","--appendonly","yes","--maxmemory","96mb","--maxmemory-policy","allkeys-lru"]
|
||||||
|
networks: [privNet]
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /mnt/data/sites/__DOMAIN__/redis
|
||||||
|
target: /data
|
||||||
|
deploy:
|
||||||
|
placement: { constraints: ["node.labels.pool == redis","node.role == worker"] }
|
||||||
|
resources:
|
||||||
|
limits: { cpus: "0.20", memory: 192M }
|
||||||
|
reservations: { cpus: "0.05", memory: 64M }
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD","redis-cli","ping"]
|
||||||
|
interval: 20s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
php:
|
||||||
|
image: wordpress:php8.3-fpm
|
||||||
|
environment: *common_env
|
||||||
|
secrets: [mysql_wp_password___DOMAIN__]
|
||||||
|
networks: [appNet, privNet]
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /mnt/data/sites/__DOMAIN__/code
|
||||||
|
target: /var/www/html
|
||||||
|
- type: bind
|
||||||
|
source: /mnt/data/sites/__DOMAIN__/uploads
|
||||||
|
target: /var/www/html/wp-content/uploads
|
||||||
|
- type: bind
|
||||||
|
source: /mnt/data/sites/__DOMAIN__/plugins
|
||||||
|
target: /var/www/html/wp-content/plugins
|
||||||
|
- type: bind
|
||||||
|
source: /mnt/data/sites/__DOMAIN__/themes
|
||||||
|
target: /var/www/html/wp-content/themes
|
||||||
|
- type: bind
|
||||||
|
source: /mnt/data/sites/__DOMAIN__/mu-plugins
|
||||||
|
target: /var/www/html/wp-content/mu-plugins
|
||||||
|
tmpfs: ["/tmp"]
|
||||||
|
security_opt: ["no-new-privileges:true"]
|
||||||
|
cap_drop: ["ALL"]
|
||||||
|
ulimits: { nofile: 65536, nproc: 4096 }
|
||||||
|
deploy:
|
||||||
|
placement: { constraints: ["node.labels.pool == php","node.role == worker"] }
|
||||||
|
resources:
|
||||||
|
limits: { cpus: "0.50", memory: 512M }
|
||||||
|
reservations: { cpus: "0.10", memory: 128M }
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:stable-alpine
|
||||||
|
networks: [appNet]
|
||||||
|
depends_on: [php]
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /mnt/data/sites/__DOMAIN__/code
|
||||||
|
target: /var/www/html:ro
|
||||||
|
- type: bind
|
||||||
|
source: /mnt/data/sites/__DOMAIN__/nginx/conf.d
|
||||||
|
target: /etc/nginx/conf.d
|
||||||
|
read_only: true
|
||||||
|
tmpfs: ["/var/cache/nginx","/var/run"]
|
||||||
|
security_opt: ["no-new-privileges:true"]
|
||||||
|
cap_drop: ["ALL"]
|
||||||
|
ulimits: { nofile: 65536 }
|
||||||
|
deploy:
|
||||||
|
placement: { constraints: ["node.labels.pool == nginx","node.role == worker"] }
|
||||||
|
resources:
|
||||||
|
limits: { cpus: "0.25", memory: 256M }
|
||||||
|
reservations: { cpus: "0.05", memory: 64M }
|
||||||
|
|
||||||
|
wp-cron:
|
||||||
|
image: wordpress:cli
|
||||||
|
environment: *common_env
|
||||||
|
secrets: [mysql_wp_password___DOMAIN__]
|
||||||
|
networks: [privNet]
|
||||||
|
working_dir: /var/www/html
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: /mnt/data/sites/__DOMAIN__/code
|
||||||
|
target: /var/www/html
|
||||||
|
command: ["bash","-lc","while true; do wp cron event run --due-now || true; sleep 60; done"]
|
||||||
|
security_opt: ["no-new-privileges:true"]
|
||||||
|
deploy:
|
||||||
|
placement: { constraints: ["node.labels.pool == wp","node.role == worker"] }
|
||||||
|
resources:
|
||||||
|
limits: { cpus: "0.10", memory: 128M }
|
||||||
|
reservations: { cpus: "0.05", memory: 64M }
|
||||||
Reference in New Issue
Block a user