Phase 3: site templates & base images
Add the building blocks the provisioning CLI renders per site. Base images (site-templates/images): - php-fpm: non-root (www-data) php:<ver>-fpm-alpine with pdo_mysql, mysqli, gd, intl, zip, opcache, exif; tuned php.ini + pool; built per PHP version. - nginx: hardened nginx:1.27-alpine with shared security + fastcgi snippets; per-site server block mounted at runtime. - build.sh: build + Trivy-scan (+ optional push) for both images. Profile templates (site-templates/profiles), Jinja2 rendered: - static, redirect (tiny nginx 301/302), custom-php (nginx + our php-fpm, optional DB), wordpress (nginx + official wordpress-fpm, DB required, upload-exec denied). Only nginx carries Traefik labels and joins proxy; php-fpm uses the private <slug>_net and joins platform only when a DB is needed. Secrets stay in a git-ignored .env, not the compose. CI: .forgejo/workflows/images.yml builds/scans images (gitleaks + Trivy). README documents the render context and the Phase 4 web-root ownership item. Templates validated: all profiles render to valid compose YAML across the database on/off branches and single/multi-domain host rules. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
41a01c60ef
commit
edfb4c44b8
20 changed files with 511 additions and 6 deletions
29
.forgejo/workflows/images.yml
Normal file
29
.forgejo/workflows/images.yml
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Build + scan the base images on push. Runs on a Forgejo Actions runner (set up
|
||||||
|
# in a later phase). Uses the built-in registry; TOKEN/REGISTRY come from repo
|
||||||
|
# secrets/vars.
|
||||||
|
name: images
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- "site-templates/images/**"
|
||||||
|
- ".forgejo/workflows/images.yml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-scan:
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Secret scan (gitleaks)
|
||||||
|
uses: gitleaks/gitleaks-action@v2
|
||||||
|
|
||||||
|
- name: Log in to the registry
|
||||||
|
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "${{ vars.REGISTRY_HOST }}" -u "${{ vars.REGISTRY_USER }}" --password-stdin
|
||||||
|
|
||||||
|
- name: Build + Trivy-scan + push images
|
||||||
|
env:
|
||||||
|
REGISTRY: ${{ vars.REGISTRY_HOST }}/heleos
|
||||||
|
PUSH: "1"
|
||||||
|
SCAN: "1"
|
||||||
|
run: ./site-templates/images/build.sh
|
||||||
|
|
@ -1,10 +1,70 @@
|
||||||
# site-templates
|
# site-templates
|
||||||
|
|
||||||
Building blocks the provisioning CLI renders from (Phase 3). Empty until then.
|
Building blocks the provisioning CLI (Phase 4) renders into
|
||||||
|
`deployments/<customer>/<site>/`. Two parts: **base images** and **profile
|
||||||
|
templates**.
|
||||||
|
|
||||||
**Planned contents:**
|
## Images (`images/`)
|
||||||
- Dockerfiles for standard images: php-fpm (non-root), nginx, static base.
|
|
||||||
- One compose template per profile: `static`, `redirect`, `custom-php`,
|
|
||||||
`wordpress` — placeholders filled from a site's `site.yaml`.
|
|
||||||
|
|
||||||
Profiles are specified in [../docs/04-site-profiles.md](../docs/04-site-profiles.md).
|
Hardened base images, built and pushed to the Forgejo registry.
|
||||||
|
|
||||||
|
| Image | Base | Notes |
|
||||||
|
|-------|------|-------|
|
||||||
|
| `nginx` | `nginx:1.27-alpine` | Hardened `nginx.conf` + reusable snippets (`security.conf`, `fastcgi-php.conf`). Per-site server block mounted at runtime. |
|
||||||
|
| `php-fpm` | `php:<ver>-fpm-alpine` | **Runs as non-root** (`www-data`). Extensions: pdo_mysql, mysqli, gd, intl, zip, opcache, exif. OPcache tuned. Built per PHP version. |
|
||||||
|
|
||||||
|
Build (and Trivy-scan) locally:
|
||||||
|
```bash
|
||||||
|
REGISTRY=git.example.com/heleos ./images/build.sh # build + scan
|
||||||
|
REGISTRY=git.example.com/heleos PUSH=1 ./images/build.sh # also push
|
||||||
|
```
|
||||||
|
CI does the same on push — see [`.forgejo/workflows/images.yml`](../.forgejo/workflows/images.yml).
|
||||||
|
|
||||||
|
WordPress uses the official `wordpress:<ver>-fpm-alpine` image directly (it
|
||||||
|
bundles WordPress + php-fpm); only `custom-php` uses our `php-fpm` image.
|
||||||
|
|
||||||
|
## Profiles (`profiles/`)
|
||||||
|
|
||||||
|
One directory per profile (see
|
||||||
|
[../docs/04-site-profiles.md](../docs/04-site-profiles.md)). Each has a
|
||||||
|
`docker-compose.yml.j2` and its nginx config template.
|
||||||
|
|
||||||
|
| Profile | Containers | DB | Networks |
|
||||||
|
|---------|-----------|----|---------|
|
||||||
|
| `static` | nginx (web root ro) | — | proxy |
|
||||||
|
| `redirect` | nginx (301/302) | — | proxy |
|
||||||
|
| `custom-php` | nginx + php-fpm | optional | proxy, site, [platform] |
|
||||||
|
| `wordpress` | nginx + wordpress-fpm | required | proxy, site, platform |
|
||||||
|
|
||||||
|
Routing pattern (all profiles): only **nginx** carries Traefik labels and joins
|
||||||
|
`proxy`; php-fpm joins the private `site` network (`<slug>_net`) and, when a DB
|
||||||
|
is used, the shared internal `platform` network to reach MariaDB. nginx reaches
|
||||||
|
php-fpm at `fpm:9000`.
|
||||||
|
|
||||||
|
## Render context
|
||||||
|
|
||||||
|
The CLI renders the `.j2` files with these variables (source: the site's
|
||||||
|
`site.yaml` + platform config):
|
||||||
|
|
||||||
|
| Variable | Example | Meaning |
|
||||||
|
|----------|---------|---------|
|
||||||
|
| `customer` / `site` | `acme` / `shop` | Ids. |
|
||||||
|
| `slug` | `acme-shop` | `<customer>-<site>`; Docker/router key. |
|
||||||
|
| `slug_underscored` | `acme_shop` | For DB names (`db_`, `u_`). |
|
||||||
|
| `domains` | `[shop.acme.com]` | Host rule is `Host(\`d1\`) || Host(\`d2\`)…` |
|
||||||
|
| `webroot` | `/tank/customers/acme/shop/web` | Bind-mounted web root. |
|
||||||
|
| `database` | `true`/`false` | custom-php only; wordpress is always true. |
|
||||||
|
| `redirect_to` / `redirect_code` | `https://acme.com` / `301` | redirect profile. |
|
||||||
|
| `nginx_image` / `php_image` / `wordpress_image` | `git.example.com/heleos/nginx:latest` | Resolved image refs. |
|
||||||
|
| `resources.cpu` / `resources.memory` | `1.0` / `512m` | Per-project limits. |
|
||||||
|
|
||||||
|
Secrets (DB password) are **not** rendered into the compose file — they go into a
|
||||||
|
git-ignored `.env` (from `secrets.enc.yaml`) that the compose reads via
|
||||||
|
`env_file`.
|
||||||
|
|
||||||
|
## Provisioning note (for Phase 4)
|
||||||
|
|
||||||
|
The web root dataset must be writable by the php-fpm user (`www-data`, uid 82 in
|
||||||
|
these alpine images) **and** by the customer's SFTP user. Provisioning will set
|
||||||
|
web-root ownership/permissions accordingly (e.g. shared group + setgid) — this is
|
||||||
|
an open item to finalize in the Phase 4 CLI, not baked into these templates.
|
||||||
|
|
|
||||||
38
site-templates/images/build.sh
Executable file
38
site-templates/images/build.sh
Executable file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build (and optionally scan/push) the heleos base images.
|
||||||
|
#
|
||||||
|
# REGISTRY=git.example.com/heleos ./build.sh # build + Trivy scan
|
||||||
|
# REGISTRY=git.example.com/heleos PUSH=1 ./build.sh # also push
|
||||||
|
#
|
||||||
|
# PHP_VERSIONS controls which php-fpm tags are built.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REGISTRY="${REGISTRY:-heleos}" # e.g. git.example.com/heleos
|
||||||
|
PUSH="${PUSH:-0}"
|
||||||
|
SCAN="${SCAN:-1}" # run Trivy if available
|
||||||
|
PHP_VERSIONS="${PHP_VERSIONS:-8.3 8.2}"
|
||||||
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
scan() {
|
||||||
|
if [ "$SCAN" = "1" ] && command -v trivy >/dev/null 2>&1; then
|
||||||
|
trivy image --severity HIGH,CRITICAL --exit-code 1 --no-progress "$1"
|
||||||
|
else
|
||||||
|
echo " (skipping Trivy scan for $1)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
maybe_push() { [ "$PUSH" = "1" ] && docker push "$1" || true; }
|
||||||
|
|
||||||
|
echo "==> nginx"
|
||||||
|
docker build -t "${REGISTRY}/nginx:latest" "${HERE}/nginx"
|
||||||
|
scan "${REGISTRY}/nginx:latest"
|
||||||
|
maybe_push "${REGISTRY}/nginx:latest"
|
||||||
|
|
||||||
|
for v in $PHP_VERSIONS; do
|
||||||
|
echo "==> php-fpm ${v}"
|
||||||
|
docker build --build-arg "PHP_VERSION=${v}" -t "${REGISTRY}/php-fpm:${v}" "${HERE}/php-fpm"
|
||||||
|
scan "${REGISTRY}/php-fpm:${v}"
|
||||||
|
maybe_push "${REGISTRY}/php-fpm:${v}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Done."
|
||||||
10
site-templates/images/nginx/Dockerfile
Normal file
10
site-templates/images/nginx/Dockerfile
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# heleos nginx base image. Ships a hardened main config + reusable snippets.
|
||||||
|
# The per-site server block is provided at runtime as a mounted
|
||||||
|
# /etc/nginx/conf.d/default.conf (rendered from a profile template).
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
COPY conf/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
COPY conf/snippets/ /etc/nginx/snippets/
|
||||||
|
|
||||||
|
# Access/error logs already go to stdout/stderr in the base image.
|
||||||
|
EXPOSE 80
|
||||||
32
site-templates/images/nginx/conf/nginx.conf
Normal file
32
site-templates/images/nginx/conf/nginx.conf
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
user nginx;
|
||||||
|
worker_processes auto;
|
||||||
|
error_log /dev/stderr warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
access_log /dev/stdout main;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
client_max_body_size 64m;
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript
|
||||||
|
text/xml application/xml image/svg+xml;
|
||||||
|
|
||||||
|
# Per-site server block is mounted here at runtime.
|
||||||
|
include /etc/nginx/conf.d/*.conf;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Standard FastCGI params for passing .php requests to php-fpm.
|
||||||
|
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_read_timeout 60s;
|
||||||
13
site-templates/images/nginx/conf/snippets/security.conf
Normal file
13
site-templates/images/nginx/conf/snippets/security.conf
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Shared hardening rules, included by every site server block.
|
||||||
|
|
||||||
|
# Deny dotfiles (e.g. .git, .env, .htaccess) except ACME well-known.
|
||||||
|
location ~ /\.(?!well-known).* {
|
||||||
|
deny all;
|
||||||
|
access_log off;
|
||||||
|
log_not_found off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Block common sensitive files.
|
||||||
|
location ~* \.(engine|inc|ini|log|sh|sql|conf|bak)$ {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
27
site-templates/images/php-fpm/Dockerfile
Normal file
27
site-templates/images/php-fpm/Dockerfile
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# heleos php-fpm base image — runs as non-root (www-data, uid 82 on alpine).
|
||||||
|
# Build arg PHP_VERSION selects the runtime; tag the image to match.
|
||||||
|
ARG PHP_VERSION=8.3
|
||||||
|
FROM php:${PHP_VERSION}-fpm-alpine
|
||||||
|
|
||||||
|
# Build extensions with dev headers, then keep only the runtime shared libs.
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache --virtual .build-deps \
|
||||||
|
icu-dev libzip-dev libpng-dev libjpeg-turbo-dev freetype-dev oniguruma-dev; \
|
||||||
|
docker-php-ext-configure gd --with-freetype --with-jpeg; \
|
||||||
|
docker-php-ext-install -j"$(nproc)" \
|
||||||
|
pdo_mysql mysqli gd intl zip opcache exif; \
|
||||||
|
runDeps="$( \
|
||||||
|
scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \
|
||||||
|
| tr ',' '\n' | sort -u | awk 'system("[ -e /usr/local/lib/"$1" ]") == 0 { next } { print "so:" $1 }' \
|
||||||
|
)"; \
|
||||||
|
apk add --no-cache $runDeps icu-libs libzip libpng libjpeg-turbo freetype oniguruma; \
|
||||||
|
apk del .build-deps
|
||||||
|
|
||||||
|
# Platform php + pool config.
|
||||||
|
COPY php.ini /usr/local/etc/php/conf.d/zz-heleos.ini
|
||||||
|
COPY www.conf /usr/local/etc/php-fpm.d/zz-heleos.conf
|
||||||
|
|
||||||
|
# Drop privileges: the container runs entirely as www-data. php-fpm listens on
|
||||||
|
# TCP 9000 (unprivileged), so no root is needed.
|
||||||
|
USER www-data
|
||||||
|
EXPOSE 9000
|
||||||
22
site-templates/images/php-fpm/php.ini
Normal file
22
site-templates/images/php-fpm/php.ini
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
; heleos php defaults. Sites may override via a mounted conf.d snippet.
|
||||||
|
expose_php = Off
|
||||||
|
memory_limit = 256M
|
||||||
|
upload_max_filesize = 64M
|
||||||
|
post_max_size = 66M
|
||||||
|
max_execution_time = 60
|
||||||
|
max_input_vars = 3000
|
||||||
|
date.timezone = UTC
|
||||||
|
|
||||||
|
; OPcache — tuned for typical PHP/WordPress workloads.
|
||||||
|
opcache.enable = 1
|
||||||
|
opcache.enable_cli = 0
|
||||||
|
opcache.memory_consumption = 128
|
||||||
|
opcache.interned_strings_buffer = 16
|
||||||
|
opcache.max_accelerated_files = 10000
|
||||||
|
opcache.revalidate_freq = 2
|
||||||
|
opcache.validate_timestamps = 1
|
||||||
|
|
||||||
|
; Do not leak errors to visitors (log to stderr → container logs → Loki).
|
||||||
|
display_errors = Off
|
||||||
|
log_errors = On
|
||||||
|
error_log = /dev/stderr
|
||||||
21
site-templates/images/php-fpm/www.conf
Normal file
21
site-templates/images/php-fpm/www.conf
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
; heleos php-fpm pool. The container already runs as www-data (see Dockerfile
|
||||||
|
; USER), so the pool does not set user/group (it can't setuid as non-root).
|
||||||
|
[www]
|
||||||
|
listen = 9000
|
||||||
|
|
||||||
|
; Process manager — dynamic, conservative defaults for a shared host.
|
||||||
|
pm = dynamic
|
||||||
|
pm.max_children = 10
|
||||||
|
pm.start_servers = 2
|
||||||
|
pm.min_spare_servers = 1
|
||||||
|
pm.max_spare_servers = 3
|
||||||
|
pm.max_requests = 500
|
||||||
|
|
||||||
|
; Send worker stdout/stderr to the container logs.
|
||||||
|
catch_workers_output = yes
|
||||||
|
decorate_workers_output = no
|
||||||
|
clear_env = no
|
||||||
|
|
||||||
|
; Health/status endpoints (used by monitoring in Phase 6).
|
||||||
|
pm.status_path = /status
|
||||||
|
ping.path = /ping
|
||||||
4
site-templates/profiles/custom-php/.env.example
Normal file
4
site-templates/profiles/custom-php/.env.example
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Generated per-site by the CLI (Phase 4) into deployments/<customer>/<site>/.env
|
||||||
|
# (git-ignored; real value comes from secrets.enc.yaml). Present only when the
|
||||||
|
# site has a database.
|
||||||
|
DB_PASSWORD=generated_per_site
|
||||||
59
site-templates/profiles/custom-php/docker-compose.yml.j2
Normal file
59
site-templates/profiles/custom-php/docker-compose.yml.j2
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Rendered by the provisioning CLI (Phase 4) into deployments/<customer>/<site>/.
|
||||||
|
# Render context is documented in site-templates/README.md.
|
||||||
|
name: {{ slug }}
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: {{ nginx_image }}
|
||||||
|
container_name: {{ slug }}_nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on: [fpm]
|
||||||
|
volumes:
|
||||||
|
- {{ webroot }}:/var/www/html:ro
|
||||||
|
- ./nginx-site.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
networks: [proxy, site]
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.{{ slug }}.rule=Host(`{{ domains | join('`) || Host(`') }}`)"
|
||||||
|
- "traefik.http.routers.{{ slug }}.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.{{ slug }}.tls.certresolver=le"
|
||||||
|
- "traefik.http.routers.{{ slug }}.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.services.{{ slug }}.loadbalancer.server.port=80"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "{{ resources.cpu }}"
|
||||||
|
memory: {{ resources.memory }}
|
||||||
|
|
||||||
|
fpm:
|
||||||
|
image: {{ php_image }}
|
||||||
|
container_name: {{ slug }}_fpm
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- {{ webroot }}:/var/www/html
|
||||||
|
{% if database %}
|
||||||
|
env_file: [.env]
|
||||||
|
environment:
|
||||||
|
DB_HOST: mariadb
|
||||||
|
DB_NAME: db_{{ slug_underscored }}
|
||||||
|
DB_USER: u_{{ slug_underscored }}
|
||||||
|
# DB_PASSWORD is injected from .env
|
||||||
|
networks: [site, platform]
|
||||||
|
{% else %}
|
||||||
|
networks: [site]
|
||||||
|
{% endif %}
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "{{ resources.cpu }}"
|
||||||
|
memory: {{ resources.memory }}
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
|
{% if database %}
|
||||||
|
platform:
|
||||||
|
external: true
|
||||||
|
{% endif %}
|
||||||
|
site:
|
||||||
|
name: {{ slug }}_net
|
||||||
16
site-templates/profiles/custom-php/nginx-site.conf.j2
Normal file
16
site-templates/profiles/custom-php/nginx-site.conf.j2
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
root /var/www/html;
|
||||||
|
index index.php index.html;
|
||||||
|
|
||||||
|
include /etc/nginx/snippets/security.conf;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$args;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
include /etc/nginx/snippets/fastcgi-php.conf;
|
||||||
|
fastcgi_pass fpm:9000;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
site-templates/profiles/redirect/docker-compose.yml.j2
Normal file
28
site-templates/profiles/redirect/docker-compose.yml.j2
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Rendered by the provisioning CLI (Phase 4). Redirect-only site: a tiny nginx
|
||||||
|
# that returns a permanent (or temporary) redirect. No PHP, no DB, no web root.
|
||||||
|
name: {{ slug }}
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: {{ nginx_image }}
|
||||||
|
container_name: {{ slug }}_nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./nginx-redirect.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
networks: [proxy]
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.{{ slug }}.rule=Host(`{{ domains | join('`) || Host(`') }}`)"
|
||||||
|
- "traefik.http.routers.{{ slug }}.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.{{ slug }}.tls.certresolver=le"
|
||||||
|
- "traefik.http.routers.{{ slug }}.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.services.{{ slug }}.loadbalancer.server.port=80"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "{{ resources.cpu }}"
|
||||||
|
memory: {{ resources.memory }}
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
7
site-templates/profiles/redirect/nginx-redirect.conf.j2
Normal file
7
site-templates/profiles/redirect/nginx-redirect.conf.j2
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
# {{ redirect_code | default(301) }} redirect, preserving the request path.
|
||||||
|
return {{ redirect_code | default(301) }} {{ redirect_to }}$request_uri;
|
||||||
|
}
|
||||||
29
site-templates/profiles/static/docker-compose.yml.j2
Normal file
29
site-templates/profiles/static/docker-compose.yml.j2
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Rendered by the provisioning CLI (Phase 4). Static site: nginx only, no PHP,
|
||||||
|
# no DB. Web root served read-only.
|
||||||
|
name: {{ slug }}
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: {{ nginx_image }}
|
||||||
|
container_name: {{ slug }}_nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- {{ webroot }}:/var/www/html:ro
|
||||||
|
- ./nginx-site.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
networks: [proxy]
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.{{ slug }}.rule=Host(`{{ domains | join('`) || Host(`') }}`)"
|
||||||
|
- "traefik.http.routers.{{ slug }}.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.{{ slug }}.tls.certresolver=le"
|
||||||
|
- "traefik.http.routers.{{ slug }}.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.services.{{ slug }}.loadbalancer.server.port=80"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "{{ resources.cpu }}"
|
||||||
|
memory: {{ resources.memory }}
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
16
site-templates/profiles/static/nginx-site.conf.j2
Normal file
16
site-templates/profiles/static/nginx-site.conf.j2
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
root /var/www/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
include /etc/nginx/snippets/security.conf;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||||
|
expires 30d;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
site-templates/profiles/wordpress/.env.example
Normal file
3
site-templates/profiles/wordpress/.env.example
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# Generated per-site by the CLI (Phase 4) into deployments/<customer>/<site>/.env
|
||||||
|
# (git-ignored; real value comes from secrets.enc.yaml).
|
||||||
|
WORDPRESS_DB_PASSWORD=generated_per_site
|
||||||
53
site-templates/profiles/wordpress/docker-compose.yml.j2
Normal file
53
site-templates/profiles/wordpress/docker-compose.yml.j2
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Rendered by the provisioning CLI (Phase 4). WordPress always has a database.
|
||||||
|
# Uses the official WordPress php-fpm image; nginx serves it and proxies PHP.
|
||||||
|
name: {{ slug }}
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
image: {{ nginx_image }}
|
||||||
|
container_name: {{ slug }}_nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on: [fpm]
|
||||||
|
volumes:
|
||||||
|
- {{ webroot }}:/var/www/html:ro
|
||||||
|
- ./nginx-site.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
networks: [proxy, site]
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.{{ slug }}.rule=Host(`{{ domains | join('`) || Host(`') }}`)"
|
||||||
|
- "traefik.http.routers.{{ slug }}.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.{{ slug }}.tls.certresolver=le"
|
||||||
|
- "traefik.http.routers.{{ slug }}.middlewares=security-headers@file"
|
||||||
|
- "traefik.http.services.{{ slug }}.loadbalancer.server.port=80"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "{{ resources.cpu }}"
|
||||||
|
memory: {{ resources.memory }}
|
||||||
|
|
||||||
|
fpm:
|
||||||
|
image: {{ wordpress_image }}
|
||||||
|
container_name: {{ slug }}_fpm
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: [.env]
|
||||||
|
environment:
|
||||||
|
WORDPRESS_DB_HOST: mariadb
|
||||||
|
WORDPRESS_DB_NAME: db_{{ slug_underscored }}
|
||||||
|
WORDPRESS_DB_USER: u_{{ slug_underscored }}
|
||||||
|
# WORDPRESS_DB_PASSWORD is injected from .env
|
||||||
|
volumes:
|
||||||
|
- {{ webroot }}:/var/www/html
|
||||||
|
networks: [site, platform]
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "{{ resources.cpu }}"
|
||||||
|
memory: {{ resources.memory }}
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
|
platform:
|
||||||
|
external: true
|
||||||
|
site:
|
||||||
|
name: {{ slug }}_net
|
||||||
31
site-templates/profiles/wordpress/nginx-site.conf.j2
Normal file
31
site-templates/profiles/wordpress/nginx-site.conf.j2
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
root /var/www/html;
|
||||||
|
index index.php;
|
||||||
|
|
||||||
|
include /etc/nginx/snippets/security.conf;
|
||||||
|
|
||||||
|
# WordPress pretty permalinks.
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$args;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
include /etc/nginx/snippets/fastcgi-php.conf;
|
||||||
|
fastcgi_pass fpm:9000;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Never execute PHP uploaded into wp-content/uploads (blunts upload RCE).
|
||||||
|
location ~* /wp-content/uploads/.*\.php$ {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Reduce attack surface / noise.
|
||||||
|
location = /xmlrpc.php { deny all; }
|
||||||
|
|
||||||
|
# Long-cache static assets.
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
|
||||||
|
expires 30d;
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue