Compare commits

...
Sign in to create a new pull request.

12 commits
main ... dev

Author SHA1 Message Date
Snider
a7963928d1 updates
Some checks are pending
Build Images / Docker (developer) (push) Waiting to run
Build Images / Docker (server-php) (push) Waiting to run
Build Images / LinuxKit (developer-amd64) (push) Blocked by required conditions
Build Images / LinuxKit (server-php-amd64) (push) Blocked by required conditions
Build Images / LinuxKit (developer-arm64) (push) Blocked by required conditions
Build Images / LinuxKit (server-php-arm64) (push) Blocked by required conditions
Build Images / Release LinuxKit Images (push) Blocked by required conditions
2026-02-08 15:17:35 +00:00
Snider
861e5b00c4 fix(developer): pin Go tools to versions compatible with Go 1.24
gopls@latest requires Go 1.25, scc/v3@latest requires Go 1.25.2.
Pin to known-working versions for Alpine's Go 1.24.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 17:38:27 +00:00
Snider
a847c7bf5c fix(developer): allow pestphp/pest-plugin in composer
Composer blocks plugins by default for security. Need to explicitly
allow pest-plugin before running composer global require.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 17:27:25 +00:00
Snider
f7d1a2cce0 fix(developer): remove phpdocumentor (no PHP 8.4 support)
phpdocumentor/phpdocumentor doesn't support PHP 8.4 yet, causing
the composer global require to fail during image build.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 17:16:08 +00:00
Snider
9901887233 feat(ci): auto-cancel superseded builds
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 16:43:48 +00:00
Snider
62ac2e4dc6 feat(ci): add Docker Hub publishing for releases
- Dev branch: GHCR only (ghcr.io/host-uk/*)
- Main branch + tags: GHCR + Docker Hub (lthn/*)
- Requires DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 16:41:41 +00:00
Snider
d8f26497c2 fix(ci): correct GHCR image names and branch triggers
- Use separate image repos (ghcr.io/host-uk/core-dev, server-php)
- Trigger on dev branch instead of main
- Only build LinuxKit on tags
- Add fail-fast: false to continue if one image fails

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 16:34:16 +00:00
Snider
bd10a3f377 fix(server-php): add minimal product placeholder for base image
The Dockerfile requires product/ directory for the builder stage.
For the base image, this is a minimal placeholder that returns JSON.
Applications should mount their own code at /var/www/html.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 23:54:26 +00:00
Snider
7659c15787 fix(server-php): add empty patch directory for builder stage
The builder stage copies patch/ to allow overriding vendor files.
For the base image this is empty with a README explaining usage.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 23:53:15 +00:00
Snider
7c7edae110 fix: add missing server-php configs, fix developer git-delta
server-php:
- Add nginx.conf, fpm-pool.conf.template, supervisord.conf
- Add php.ini.template, php-prod.ini, php-dev.ini
- Add opcache-prod.ini, xdebug.ini
- Add nginx-performance.conf for production
- Add entrypoint.sh script

developer:
- Remove git-delta (not available in Alpine 3.22 repos)

Closes #3

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 23:51:48 +00:00
Snider
be8a20786f feat: dual-registry publishing (GHCR + Docker Hub)
Publishes container images to both registries:
- GHCR: ghcr.io/host-uk/core-images:{image} (org access)
- Docker Hub: lthn/{image}:{version} (public distribution)

Changes:
- Add Docker Hub login step (requires DOCKERHUB_USERNAME, DOCKERHUB_TOKEN secrets)
- Update metadata to generate tags for both registries
- Enable dev branch builds
- Fix LinuxKit build to use linuxkit directly (not core CLI)
- Use correct double-dash flags for linuxkit

Naming:
- developer -> ghcr.io/host-uk/core-images:developer + lthn/core-dev:latest
- server-php -> ghcr.io/host-uk/core-images:server-php + lthn/server-php:latest

Closes #1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 23:18:16 +00:00
Snider
10b3b69572 chore: add CodeRabbit configuration
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 21:41:26 +00:00
19 changed files with 732 additions and 76 deletions

13
.coderabbit.yaml Normal file
View file

@ -0,0 +1,13 @@
# CodeRabbit Configuration
# Inherits from: https://github.com/host-uk/coderabbit/.coderabbit.yaml
reviews:
review_status: false
path_instructions:
- path: "**/Dockerfile*"
instructions: "Check for security best practices, multi-stage builds, and pinned versions"
- path: "**/*.yml"
instructions: "Ansible/Docker Compose - validate syntax and idempotency"
- path: "**/*.sh"
instructions: "Shell scripts - check for shellcheck compliance and proper error handling"

View file

@ -1,15 +1,24 @@
# Host UK Container Images
#
# Dev branch: ghcr.io/host-uk/{core-dev,server-php}:dev
# Tags/Main: ghcr.io/host-uk/{core-dev,server-php}:latest + lthn/{core-dev,server-php}:latest
name: Build Images name: Build Images
on: on:
push: push:
branches: [main] branches: [dev, main]
tags: ['v*'] tags: ['v*']
pull_request: pull_request:
branches: [main] branches: [dev, main]
workflow_dispatch: workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env: env:
REGISTRY: ghcr.io GHCR_REGISTRY: ghcr.io
jobs: jobs:
# ============================================================ # ============================================================
@ -23,10 +32,18 @@ jobs:
packages: write packages: write
strategy: strategy:
fail-fast: false
matrix: matrix:
image: image:
- developer - developer
- server-php - server-php
include:
- image: developer
ghcr_name: core-dev
dockerhub_name: core-dev
- image: server-php
ghcr_name: server-php
dockerhub_name: server-php
steps: steps:
- name: Checkout - name: Checkout
@ -42,21 +59,57 @@ jobs:
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GHCR_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata - name: Login to Docker Hub
id: meta if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Determine if release build
id: release
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]] || [[ "${{ github.ref }}" == refs/tags/v* ]]; then
echo "is_release=true" >> $GITHUB_OUTPUT
else
echo "is_release=false" >> $GITHUB_OUTPUT
fi
- name: Extract metadata (GHCR only - dev builds)
if: steps.release.outputs.is_release == 'false'
id: meta-dev
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }}/host-uk/${{ matrix.image == 'developer' && 'core-dev' || matrix.image }} images: |
${{ env.GHCR_REGISTRY }}/host-uk/${{ matrix.ghcr_name }}
tags: | tags: |
type=ref,event=branch type=ref,event=branch
type=ref,event=pr type=ref,event=pr
flavor: |
latest=false
- name: Extract metadata (GHCR + Docker Hub - release builds)
if: steps.release.outputs.is_release == 'true'
id: meta-release
uses: docker/metadata-action@v5
with:
images: |
${{ env.GHCR_REGISTRY }}/host-uk/${{ matrix.ghcr_name }}
lthn/${{ matrix.dockerhub_name }}
tags: |
# main branch -> latest
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
# Semver tags (v1.0.0 -> 1.0.0, 1.0, 1, latest)
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable={{is_default_branch}} type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
flavor: |
latest=false
- name: Build and push - name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
@ -64,62 +117,64 @@ jobs:
context: ./${{ matrix.image }} context: ./${{ matrix.image }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta-dev.outputs.tags || steps.meta-release.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta-dev.outputs.labels || steps.meta-release.outputs.labels }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
# ============================================================ # ============================================================
# Build LinuxKit Images # Build LinuxKit Images (only on tags)
# ============================================================ # ============================================================
linuxkit: linuxkit:
name: LinuxKit (${{ matrix.image }}-${{ matrix.arch }}) name: LinuxKit (${{ matrix.image }}-${{ matrix.arch }})
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: docker # Needs Docker images to be built first needs: docker
if: startsWith(github.ref, 'refs/tags/v')
strategy: strategy:
fail-fast: false
matrix: matrix:
image: [developer, server-php] image: [developer, server-php]
arch: [amd64, arm64] arch: [amd64, arm64]
format: [qcow2-bios, iso-bios] format: [qcow2-bios, iso-bios]
include:
- image: developer
output_name: core-dev
- image: server-php
output_name: server-php
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Core CLI - name: Install LinuxKit
run: | run: |
# Download latest core binary curl -fsSL "https://github.com/linuxkit/linuxkit/releases/download/v1.5.3/linuxkit-linux-amd64" -o linuxkit
curl -fsSL "https://github.com/host-uk/core/releases/latest/download/core-linux-amd64.tar.gz" -o core.tar.gz chmod +x linuxkit
tar -xzf core.tar.gz sudo mv linuxkit /usr/local/bin/
sudo mv core /usr/local/bin/core
chmod +x /usr/local/bin/core
core --version
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Login to GHCR - name: Login to GHCR
if: github.event_name != 'pull_request'
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.GHCR_REGISTRY }}
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build LinuxKit Image - name: Build LinuxKit Image
run: | run: |
mkdir -p dist mkdir -p dist
core build --type linuxkit \ linuxkit build \
--config ./${{ matrix.image }}/linuxkit.yml \
--format ${{ matrix.format }} \ --format ${{ matrix.format }} \
--arch ${{ matrix.arch }} \ --name ./dist/${{ matrix.output_name }}-${{ matrix.arch }} \
-o ./dist/${{ matrix.image == 'developer' && 'core-dev' || matrix.image }}-${{ matrix.arch }} ./${{ matrix.image }}/linuxkit.yml
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ matrix.image == 'developer' && 'core-dev' || matrix.image }}-${{ matrix.arch }}-${{ matrix.format }} name: ${{ matrix.output_name }}-${{ matrix.arch }}-${{ matrix.format }}
path: ./dist/* path: ./dist/*
# ============================================================ # ============================================================
@ -152,39 +207,3 @@ jobs:
dist/* dist/*
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ============================================================
# Build TIM Bundles (when core build --type tim is ready)
# ============================================================
# tim:
# name: TIM (${{ matrix.image }})
# runs-on: ubuntu-latest
# needs: docker
#
# strategy:
# matrix:
# image: [developer, server-php]
# os: [linux, darwin]
# arch: [amd64, arm64]
#
# steps:
# - uses: actions/checkout@v4
#
# - name: Install Core
# run: |
# curl -fsSL https://github.com/host-uk/core/releases/latest/download/core-linux-amd64 -o /usr/local/bin/core
# chmod +x /usr/local/bin/core
#
# - name: Build TIM
# run: |
# core build --type tim \
# --borgfile ./${{ matrix.image }}/Borgfile \
# --os ${{ matrix.os }} \
# --arch ${{ matrix.arch }} \
# -o ./dist/${{ matrix.image }}-${{ matrix.os }}-${{ matrix.arch }}.tim
#
# - name: Upload artifact
# uses: actions/upload-artifact@v4
# with:
# name: ${{ matrix.image }}-${{ matrix.os }}-${{ matrix.arch }}
# path: ./dist/*.tim

View file

@ -25,6 +25,9 @@ ENV LC_ALL=C.UTF-8
ENV TERM=xterm-256color ENV TERM=xterm-256color
ENV EDITOR=nvim ENV EDITOR=nvim
ENV SHELL=/bin/zsh ENV SHELL=/bin/zsh
ENV CODEX_HOME=/root/.codex
ENV CORE_DEV_HOST_HOME=/host-home
ENV CODEX_AUTH_SYNC=1
ENV GOPATH=/root/go ENV GOPATH=/root/go
ENV CARGO_HOME=/root/.cargo ENV CARGO_HOME=/root/.cargo
ENV RUSTUP_HOME=/root/.rustup ENV RUSTUP_HOME=/root/.rustup
@ -65,8 +68,9 @@ RUN apk add --no-cache \
# ============================================================ # ============================================================
# VCS & Git Tools # VCS & Git Tools
# ============================================================ # ============================================================
# Note: git-delta not in Alpine repos, install via cargo if needed
RUN apk add --no-cache \ RUN apk add --no-cache \
git git-lfs github-cli lazygit git-delta git git-lfs github-cli lazygit
# ============================================================ # ============================================================
# Node.js Ecosystem # Node.js Ecosystem
@ -123,13 +127,13 @@ RUN ln -sf /usr/bin/php84 /usr/bin/php
RUN curl -sS https://getcomposer.org/installer | php -- \ RUN curl -sS https://getcomposer.org/installer | php -- \
--install-dir=/usr/bin --filename=composer --install-dir=/usr/bin --filename=composer
# PHP tools via Composer # Allow Pest plugin and install PHP tools globally
RUN composer global require --no-interaction \ RUN composer global config allow-plugins.pestphp/pest-plugin true && \
composer global require --no-interaction \
phpunit/phpunit:^11.0 \ phpunit/phpunit:^11.0 \
pestphp/pest:^3.0 \ pestphp/pest:^3.0 \
phpstan/phpstan:^2.0 \ phpstan/phpstan:^2.0 \
laravel/pint:^1.0 \ laravel/pint:^1.0
phpdocumentor/phpdocumentor:^3.0
# FrankenPHP (static binary) # FrankenPHP (static binary)
RUN curl -fsSL "https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-$(uname -m | sed 's/aarch64/arm64/' | sed 's/x86_64/x86_64/')" -o /usr/local/bin/frankenphp && \ RUN curl -fsSL "https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-$(uname -m | sed 's/aarch64/arm64/' | sed 's/x86_64/x86_64/')" -o /usr/local/bin/frankenphp && \
@ -140,12 +144,13 @@ RUN curl -fsSL "https://github.com/dunglas/frankenphp/releases/latest/download/f
# ============================================================ # ============================================================
RUN apk add --no-cache go RUN apk add --no-cache go
RUN go install golang.org/x/tools/gopls@latest && \ # Note: Some tools require newer Go versions, so we pin compatible versions
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest && \ # or make installations optional with || true
go install github.com/go-task/task/v3/cmd/task@latest && \ RUN go install golang.org/x/tools/gopls@v0.17.1 || true && \
go install github.com/casey/just@latest && \ go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.2 && \
go install github.com/watchexec/watchexec/cmd/watchexec@latest 2>/dev/null || true && \ go install github.com/go-task/task/v3/cmd/task@v3.40.1 && \
go install github.com/boyter/scc/v3@latest go install github.com/casey/just@latest || true && \
go install github.com/boyter/scc/v3@v3.4.0 || true
# ============================================================ # ============================================================
# Rust Ecosystem # Rust Ecosystem
@ -281,7 +286,7 @@ COPY --chmod=644 config/aliases.sh /etc/profile.d/aliases.sh
COPY --chmod=755 scripts/entrypoint.sh /usr/local/bin/entrypoint.sh COPY --chmod=755 scripts/entrypoint.sh /usr/local/bin/entrypoint.sh
# Create directories # Create directories
RUN mkdir -p /root/.config /root/.claude /workspace RUN mkdir -p /root/.config /root/.claude /root/.codex /workspace
WORKDIR /workspace WORKDIR /workspace

View file

@ -3,6 +3,28 @@
set -e set -e
sync_dir() {
src="$1"
dst="$2"
if [ ! -d "$src" ]; then
return 0
fi
mkdir -p "$dst"
# Copy host state into container state each boot so auth/extensions are portable.
cp -a "$src"/. "$dst"/
}
# Sync Codex auth/state from host profile if mounted.
# Example: -v "$HOME:/host-home:ro"
if [ "${CODEX_AUTH_SYNC:-1}" = "1" ]; then
HOST_HOME="${CORE_DEV_HOST_HOME:-/host-home}"
HOST_CODEX_HOME="${HOST_HOME}/.codex"
CONTAINER_CODEX_HOME="${CODEX_HOME:-${HOME}/.codex}"
sync_dir "$HOST_CODEX_HOME" "$CONTAINER_CODEX_HOME"
fi
# Run pre-start hooks if they exist # Run pre-start hooks if they exist
if [ -d "/root/.config/core-dev/hooks/pre-start" ]; then if [ -d "/root/.config/core-dev/hooks/pre-start" ]; then
for hook in /root/.config/core-dev/hooks/pre-start/*; do for hook in /root/.config/core-dev/hooks/pre-start/*; do
@ -26,8 +48,8 @@ fi
# Setup SSH agent if keys exist # Setup SSH agent if keys exist
if [ -d "$HOME/.ssh" ] && [ -z "$SSH_AUTH_SOCK" ]; then if [ -d "$HOME/.ssh" ] && [ -z "$SSH_AUTH_SOCK" ]; then
eval "$(ssh-agent -s)" > /dev/null 2>&1 eval "$(ssh-agent -s)" > /dev/null 2>&1
for key in $HOME/.ssh/id_* ; do for key in "$HOME"/.ssh/id_*; do
[ -f "$key" ] && [ ! -f "$key.pub" ] || ssh-add "$key" 2>/dev/null || true [ -f "$key" ] && [ ! -f "$key.pub" ] && ssh-add "$key" 2>/dev/null || true
done done
fi fi

View file

@ -0,0 +1,182 @@
# Unified nginx configuration
# Routes traffic based on domain:
# - host.uk.com → Laravel Host Hub (PHP-FPM)
# - *.host.uk.com → WordPress (PHP-FPM)
# Map for allowed CORS origins (WordPress REST API)
map $http_origin $cors_origin {
default "";
"~^https?://host\.uk\.com$" $http_origin;
"~^https?://social\.host\.uk\.com$" $http_origin;
"~^https?://link\.host\.uk\.com$" $http_origin;
"~^https?://analytics\.host\.uk\.com$" $http_origin;
"~^https?://trust\.host\.uk\.com$" $http_origin;
"~^https?://notify\.host\.uk\.com$" $http_origin;
"~^https?://localhost(:[0-9]+)?$" $http_origin;
"~^https?://127\.0\.0\.1(:[0-9]+)?$" $http_origin;
}
# ============================================
# LARAVEL HOST HUB - Apex Domain
# ============================================
server {
listen 80;
listen [::]:80;
server_name host.uk.com www.host.uk.com;
root /app/public;
index index.php;
client_max_body_size 64M;
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
# Health check - returns 200 if nginx is up (no PHP needed)
location = /healthz {
access_log off;
add_header Content-Type text/plain;
return 200 "ok\n";
}
# WordPress REST API proxy
# host.uk.com/api/wordpress/* → WordPress /wp-json/*
# Same-origin, no CORS needed
location ~ ^/api/wordpress/(.*)$ {
# Pass to WordPress index.php with rest_route
fastcgi_pass unix:/run/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME /var/www/html/index.php;
fastcgi_param REQUEST_URI /wp-json/$1$is_args$args;
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
fastcgi_index index.php;
fastcgi_buffering off;
fastcgi_read_timeout 300;
include fastcgi_params;
}
# Laravel routing
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP-FPM for Laravel
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
fastcgi_index index.php;
fastcgi_buffering off;
fastcgi_read_timeout 300;
include fastcgi_params;
}
# Livewire and Flux - must go to Laravel (not static files)
location ~ ^/(admin|flux)/ {
try_files $uri $uri/ /index.php?$query_string;
}
# Laravel static assets (build, vendor directories only)
location ~* ^/(build|vendor)/.*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires max;
add_header Cache-Control "public, immutable";
log_not_found off;
access_log off;
}
# Deny hidden files
location ~ /\. {
deny all;
}
# PHP-FPM status (internal only)
location ~ ^/(fpm-status|fpm-ping)$ {
access_log off;
allow 127.0.0.1;
deny all;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_pass unix:/run/php-fpm.sock;
}
}
# ============================================
# LARAVEL SATELLITES - Subdomains
# ============================================
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name *.host.uk.com;
root /app/public;
index index.php;
client_max_body_size 64M;
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
# Health check
location = /healthz {
access_log off;
add_header Content-Type text/plain;
return 200 "ok\n";
}
# WordPress REST API (for internal content sync)
# Routes /wp-json/* requests to WordPress
# Host header determines which multisite blog to serve
location ~ ^/wp-json/(.*)$ {
fastcgi_pass unix:/run/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME /var/www/html/index.php;
fastcgi_param REQUEST_URI /wp-json/$1$is_args$args;
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
fastcgi_index index.php;
fastcgi_buffering off;
fastcgi_read_timeout 300;
include fastcgi_params;
}
# Laravel routing
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP-FPM for Laravel
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
fastcgi_index index.php;
fastcgi_buffering off;
fastcgi_read_timeout 300;
include fastcgi_params;
}
# Livewire and Flux
location ~ ^/(admin|flux)/ {
try_files $uri $uri/ /index.php?$query_string;
}
# Static assets
location ~* ^/(build|vendor)/.*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires max;
add_header Cache-Control "public, immutable";
log_not_found off;
access_log off;
}
# Deny hidden files
location ~ /\. {
deny all;
}
}

View file

@ -0,0 +1,155 @@
# WordPress Multisite server configuration
# Map for allowed CORS origins
map $http_origin $cors_origin {
default "";
"~^https?://host\.uk\.com$" $http_origin;
"~^https?://social\.host\.uk\.com$" $http_origin;
"~^https?://link\.host\.uk\.com$" $http_origin;
"~^https?://analytics\.host\.uk\.com$" $http_origin;
"~^https?://trust\.host\.uk\.com$" $http_origin;
"~^https?://notify\.host\.uk\.com$" $http_origin;
"~^https?://localhost(:[0-9]+)?$" $http_origin;
"~^https?://127\.0\.0\.1(:[0-9]+)?$" $http_origin;
}
server {
listen [::]:80 default_server;
listen 80 default_server;
# Only accept subdomain traffic (*.host.uk.com), not apex domain
# The apex domain (host.uk.com) should route to Host Hub (Laravel)
server_name ~^(?<subdomain>.+)\.host\.uk\.com$ hestia.host.uk.com *.host.uk.com;
# Serve error page for apex domain - this shouldn't hit WordPress
# If it does, Coolify routing is misconfigured
error_page 503 /wp-content/routing-error.html;
if ($host = "host.uk.com") {
return 503;
}
# Reject completely unknown hosts with connection close
if ($host !~ "\.host\.uk\.com$") {
return 444;
}
sendfile off;
tcp_nodelay on;
absolute_redirect off;
root /var/www/html;
index index.php;
client_max_body_size 64M;
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
# WordPress multisite rewrite rules
location / {
try_files $uri $uri/ /index.php?$args;
}
# REST API with CORS headers for headless operation
location /wp-json/ {
# Handle preflight OPTIONS requests
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-WP-Nonce' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400 always;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain';
return 204;
}
# Add CORS headers to actual requests
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Expose-Headers' 'X-WP-Total, X-WP-TotalPages, Link' always;
try_files $uri $uri/ /index.php?$args;
}
# Pass the PHP scripts to PHP-FPM listening on unix socket
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
fastcgi_index index.php;
fastcgi_buffering off;
fastcgi_read_timeout 300;
include fastcgi_params;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires max;
log_not_found off;
access_log off;
}
location = /favicon.ico {
log_not_found off;
access_log off;
}
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
# Block XML-RPC by default, allow with secret token
# Usage: /xmlrpc.php?token=YOUR_XMLRPC_TOKEN
location = /xmlrpc.php {
set $xmlrpc_allowed 0;
# Allow if valid token provided (set in environment or change here)
if ($arg_token = "xrpc-9f8e7d6c5b4a") {
set $xmlrpc_allowed 1;
}
# Block if no valid token
if ($xmlrpc_allowed = 0) {
return 403;
}
# Pass to PHP if allowed
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_index index.php;
include fastcgi_params;
}
# Deny access to hidden files
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Deny access to backup files
location ~ ~$ {
access_log off;
log_not_found off;
deny all;
}
# Allow fpm ping and status from localhost
location ~ ^/(fpm-status|fpm-ping)$ {
access_log off;
allow 127.0.0.1;
deny all;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_pass unix:/run/php-fpm.sock;
}
}

View file

@ -0,0 +1,43 @@
[global]
; Log to stderr
error_log = /dev/stderr
[www]
; User and group for PHP-FPM processes
user = nobody
group = nobody
; The address on which to accept FastCGI requests.
listen = /run/php-fpm.sock
; Set permissions for unix socket
listen.owner = nobody
listen.group = nobody
listen.mode = 0666
; Enable status page
pm.status_path = /fpm-status
; Ondemand process manager
pm = ondemand
; The maximum number of child processes
pm.max_children = 100
; The number of seconds after which an idle process will be killed.
pm.process_idle_timeout = 10s
; The number of requests each child process should execute before respawning.
pm.max_requests = 1000
; Make sure the FPM workers can reach the environment variables for configuration
clear_env = no
; Catch output from PHP
catch_workers_output = yes
; Remove the 'child 10 said into stderr' prefix in the log and only show the actual message
decorate_workers_output = no
; Enable ping page to use in healthcheck
ping.path = /fpm-ping

View file

@ -0,0 +1,22 @@
# Production performance optimizations
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
# Brotli (if available)
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
# Cache static files
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;

View file

@ -0,0 +1,63 @@
upstream php-fpm {
server host-uk-dev-wordpress:9000;
}
server {
listen 80;
server_name _;
root /var/www/html;
index index.php;
client_max_body_size 64M;
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php-fpm;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
fastcgi_buffering off;
fastcgi_read_timeout 300;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires max;
log_not_found off;
access_log off;
}
location = /favicon.ico {
log_not_found off;
access_log off;
}
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
location ~ ~$ {
access_log off;
log_not_found off;
deny all;
}
}

View file

@ -0,0 +1,10 @@
[opcache]
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.save_comments=1
opcache.enable_cli=1
opcache.jit=1255
opcache.jit_buffer_size=128M

View file

@ -0,0 +1,11 @@
; Development PHP settings
display_errors = On
display_startup_errors = On
error_reporting = E_ALL
log_errors = On
memory_limit = 512M
max_execution_time = 300
max_input_time = 300
post_max_size = 128M
upload_max_filesize = 128M

View file

@ -0,0 +1,17 @@
; Production PHP settings
display_errors = Off
display_startup_errors = Off
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
log_errors = On
error_log = /var/log/php/error.log
memory_limit = 256M
max_execution_time = 60
max_input_time = 60
post_max_size = 64M
upload_max_filesize = 64M
expose_php = Off
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1

View file

@ -0,0 +1,9 @@
[Date]
date.timezone="UTC"
[PHP]
expose_php = Off
upload_max_filesize = 64M
post_max_size = 64M
memory_limit = 256M
max_execution_time = 300

View file

@ -0,0 +1,24 @@
[supervisord]
nodaemon=true
user=root
logfile=/dev/null
logfile_maxbytes=0
pidfile=/run/supervisord.pid
[program:php-fpm]
command=php-fpm84 -F
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=false
startretries=0
[program:nginx]
command=nginx -g 'daemon off;'
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=false
startretries=0

View file

@ -0,0 +1,7 @@
[xdebug]
xdebug.mode = develop,debug
xdebug.start_with_request = trigger
xdebug.client_host = host.docker.internal
xdebug.client_port = 9003
xdebug.idekey = PHPSTORM
xdebug.log_level = 0

View file

@ -0,0 +1,20 @@
# Patch Directory
This directory contains files that override vendor packages after composer install.
## Usage
Place files here that should overwrite files in `vendor/` or elsewhere after dependencies are installed. The entire contents of this directory are copied over the application root.
## Example
To patch a vendor file:
```
patch/vendor/some-package/src/File.php
```
This will overwrite `vendor/some-package/src/File.php` in the built image.
## Empty by Default
For the base image, this directory is empty. Applications using this image should mount or copy their own patches.

View file

@ -0,0 +1,6 @@
{
"name": "host-uk/server-php-placeholder",
"description": "Placeholder for base image",
"type": "project",
"require": {}
}

View file

@ -0,0 +1,8 @@
<?php
// Placeholder for base image
// Applications should mount/copy their own code
echo json_encode([
'status' => 'ok',
'message' => 'Host UK Server PHP Base Image',
'note' => 'Mount your Laravel app to /var/www/html'
]);

View file

@ -0,0 +1,20 @@
#!/bin/sh
set -e
# Install PHP dependencies if vendor directory is empty
if [ ! -f "/app/vendor/autoload.php" ]; then
echo "Installing PHP dependencies..."
composer install --no-interaction --prefer-dist
fi
# Install Node dependencies if node_modules is empty
if [ ! -d "/app/node_modules" ] || [ -z "$(ls -A /app/node_modules 2>/dev/null)" ]; then
echo "Installing Node dependencies..."
npm install
fi
# Run database migrations (skip errors if DB not ready)
php artisan migrate --force 2>/dev/null || echo "Migrations skipped (DB may not be ready)"
# Execute the main command
exec "$@"