refactor: move PHP docker files to core-php, keep only CLI Dockerfile
Some checks failed
Deploy / build (push) Failing after 4s
Security Scan / security (push) Successful in 14s

Move Dockerfile.app, Dockerfile.web, nginx/, php/, and docker-compose
to core-php where they belong. Promote Dockerfile.core to root Dockerfile.
Simplify deploy workflow to only build the CLI image.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-02-21 21:14:28 +00:00
parent c84ce5265f
commit 260dca0999
9 changed files with 3 additions and 490 deletions

View file

@ -1,5 +1,4 @@
# Host UK Production Deployment Pipeline
# Builds 3 Docker images via reusable workflow, then triggers Coolify deploy.
# Core CLI Docker image build + push
name: Deploy
@ -9,72 +8,10 @@ on:
workflow_dispatch:
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: "8.3"
extensions: bcmath, gd, intl, mbstring, pdo_mysql, redis, zip
coverage: none
- name: Install Composer dependencies
run: composer install --no-interaction --prefer-dist
- name: Run tests
run: composer test
- name: Check code style
run: ./vendor/bin/pint --test
build-app:
name: Build App Image
needs: test
uses: core/go-devops/.forgejo/workflows/docker-publish.yml@main
with:
image: lthn/app
dockerfile: docker/Dockerfile.app
registry: docker.io
secrets: inherit
build-web:
name: Build Web Image
needs: test
uses: core/go-devops/.forgejo/workflows/docker-publish.yml@main
with:
image: lthn/web
dockerfile: docker/Dockerfile.web
registry: docker.io
secrets: inherit
build-core:
name: Build Core Image
needs: test
build:
uses: core/go-devops/.forgejo/workflows/docker-publish.yml@main
with:
image: lthn/core
dockerfile: docker/Dockerfile.core
dockerfile: Dockerfile
registry: docker.io
secrets: inherit
deploy:
name: Deploy to Production
needs: [build-app, build-web, build-core]
runs-on: ubuntu-latest
steps:
- name: Trigger Coolify deploy
run: |
curl -s -X POST \
-H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}" \
"${{ secrets.COOLIFY_URL }}/api/v1/deploy" \
-H "Content-Type: application/json" \
-d '{"uuid": "${{ secrets.COOLIFY_APP_UUID }}", "force": false}'
- name: Wait for deployment
run: |
echo "Deployment triggered. Coolify will perform rolling restart."
echo "Monitor at: ${{ secrets.COOLIFY_URL }}"

View file

@ -1,107 +0,0 @@
# Host UK — Laravel Application Container
# PHP 8.3-FPM with all extensions required by the federated monorepo
#
# Build: docker build -f docker/Dockerfile.app -t host-uk/app:latest ..
# (run from host-uk/ workspace root, not core/)
FROM php:8.3-fpm-alpine AS base
# System dependencies
RUN apk add --no-cache \
git \
curl \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
libwebp-dev \
libzip-dev \
icu-dev \
oniguruma-dev \
libxml2-dev \
linux-headers \
$PHPIZE_DEPS
# PHP extensions
RUN docker-php-ext-configure gd \
--with-freetype \
--with-jpeg \
--with-webp \
&& docker-php-ext-install -j$(nproc) \
bcmath \
exif \
gd \
intl \
mbstring \
opcache \
pcntl \
pdo_mysql \
soap \
xml \
zip
# Redis extension
RUN pecl install redis && docker-php-ext-enable redis
# Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# PHP configuration
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY docker/php/opcache.ini $PHP_INI_DIR/conf.d/opcache.ini
COPY docker/php/php-fpm.conf /usr/local/etc/php-fpm.d/zz-host-uk.conf
# --- Build stage ---
FROM base AS build
WORKDIR /app
# Install dependencies first (cache layer)
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--no-scripts \
--no-autoloader \
--prefer-dist \
--no-interaction
# Copy application
COPY . .
# Generate autoloader and run post-install
RUN composer dump-autoload --optimize --no-dev \
&& php artisan package:discover --ansi
# Build frontend assets
RUN if [ -f package.json ]; then \
apk add --no-cache nodejs npm && \
npm ci --production=false && \
npm run build && \
rm -rf node_modules; \
fi
# --- Production stage ---
FROM base AS production
WORKDIR /app
# Copy built application
COPY --from=build /app /app
# Create storage directories
RUN mkdir -p \
storage/framework/cache/data \
storage/framework/sessions \
storage/framework/views \
storage/logs \
bootstrap/cache
# Permissions
RUN chown -R www-data:www-data storage bootstrap/cache
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD php-fpm-healthcheck || exit 1
USER www-data
EXPOSE 9000

View file

@ -1,20 +0,0 @@
# Host UK — Nginx Web Server
# Serves static files and proxies PHP to FPM container
#
# Build: docker build -f docker/Dockerfile.web -t host-uk/web:latest .
FROM nginx:1.27-alpine
# Copy nginx configuration
COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY docker/nginx/security-headers.conf /etc/nginx/snippets/security-headers.conf
# Copy static assets from app build
# (In production, these are volume-mounted from the app container)
# COPY --from=host-uk/app:latest /app/public /app/public
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost/health || exit 1
USER nginx
EXPOSE 80

View file

@ -1,200 +0,0 @@
# Host UK Production Docker Compose
# Deployed to de.host.uk.com and de2.host.uk.com via Coolify
#
# Container topology per app server:
# app - PHP 8.3-FPM (all Laravel modules)
# web - Nginx (static files + FastCGI proxy)
# horizon - Laravel Horizon (queue worker)
# scheduler - Laravel scheduler
# mcp - Go MCP server
# redis - Redis 7 (local cache + sessions)
# galera - MariaDB 11 (Galera cluster node)
services:
app:
image: ${REGISTRY:-gitea.snider.dev}/host-uk/app:${TAG:-latest}
restart: unless-stopped
volumes:
- app-storage:/app/storage
environment:
- APP_ENV=production
- APP_DEBUG=false
- APP_URL=${APP_URL:-https://host.uk.com}
- DB_HOST=galera
- DB_PORT=3306
- DB_DATABASE=${DB_DATABASE:-hostuk}
- DB_USERNAME=${DB_USERNAME:-hostuk}
- DB_PASSWORD=${DB_PASSWORD}
- REDIS_HOST=redis
- REDIS_PORT=6379
- CACHE_DRIVER=redis
- SESSION_DRIVER=redis
- QUEUE_CONNECTION=redis
depends_on:
redis:
condition: service_healthy
galera:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "php-fpm-healthcheck || exit 1"]
interval: 30s
timeout: 3s
start_period: 10s
retries: 3
networks:
- app-net
web:
image: ${REGISTRY:-gitea.snider.dev}/host-uk/web:${TAG:-latest}
restart: unless-stopped
ports:
- "${WEB_PORT:-80}:80"
volumes:
- app-storage:/app/storage:ro
depends_on:
app:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/health"]
interval: 30s
timeout: 3s
start_period: 5s
retries: 3
networks:
- app-net
horizon:
image: ${REGISTRY:-gitea.snider.dev}/host-uk/app:${TAG:-latest}
restart: unless-stopped
command: php artisan horizon
volumes:
- app-storage:/app/storage
environment:
- APP_ENV=production
- DB_HOST=galera
- DB_PORT=3306
- DB_DATABASE=${DB_DATABASE:-hostuk}
- DB_USERNAME=${DB_USERNAME:-hostuk}
- DB_PASSWORD=${DB_PASSWORD}
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
app:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "php artisan horizon:status | grep -q running"]
interval: 60s
timeout: 5s
start_period: 30s
retries: 3
networks:
- app-net
scheduler:
image: ${REGISTRY:-gitea.snider.dev}/host-uk/app:${TAG:-latest}
restart: unless-stopped
command: php artisan schedule:work
volumes:
- app-storage:/app/storage
environment:
- APP_ENV=production
- DB_HOST=galera
- DB_PORT=3306
- DB_DATABASE=${DB_DATABASE:-hostuk}
- DB_USERNAME=${DB_USERNAME:-hostuk}
- DB_PASSWORD=${DB_PASSWORD}
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
app:
condition: service_healthy
networks:
- app-net
mcp:
image: ${REGISTRY:-gitea.snider.dev}/host-uk/core:${TAG:-latest}
restart: unless-stopped
command: core mcp serve
ports:
- "${MCP_PORT:-9001}:9000"
environment:
- MCP_ADDR=:9000
healthcheck:
test: ["CMD-SHELL", "nc -z localhost 9000 || exit 1"]
interval: 30s
timeout: 3s
retries: 3
networks:
- app-net
redis:
image: redis:7-alpine
restart: unless-stopped
command: >
redis-server
--maxmemory 512mb
--maxmemory-policy allkeys-lru
--appendonly yes
--appendfsync everysec
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- app-net
galera:
image: mariadb:11
restart: unless-stopped
environment:
- MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
- MARIADB_DATABASE=${DB_DATABASE:-hostuk}
- MARIADB_USER=${DB_USERNAME:-hostuk}
- MARIADB_PASSWORD=${DB_PASSWORD}
- WSREP_CLUSTER_NAME=hostuk-galera
- WSREP_CLUSTER_ADDRESS=${GALERA_CLUSTER_ADDRESS:-gcomm://}
- WSREP_NODE_ADDRESS=${GALERA_NODE_ADDRESS}
- WSREP_NODE_NAME=${GALERA_NODE_NAME}
- WSREP_SST_METHOD=mariabackup
command: >
--wsrep-on=ON
--wsrep-provider=/usr/lib/galera/libgalera_smm.so
--wsrep-cluster-name=hostuk-galera
--wsrep-cluster-address=${GALERA_CLUSTER_ADDRESS:-gcomm://}
--wsrep-node-address=${GALERA_NODE_ADDRESS}
--wsrep-node-name=${GALERA_NODE_NAME}
--wsrep-sst-method=mariabackup
--binlog-format=ROW
--default-storage-engine=InnoDB
--innodb-autoinc-lock-mode=2
--innodb-buffer-pool-size=1G
--innodb-log-file-size=256M
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
volumes:
- galera-data:/var/lib/mysql
ports:
- "${GALERA_PORT:-3306}:3306"
- "4567:4567"
- "4568:4568"
- "4444:4444"
healthcheck:
test: ["CMD-SHELL", "mariadb -u root -p${DB_ROOT_PASSWORD} -e 'SHOW STATUS LIKE \"wsrep_ready\"' | grep -q ON"]
interval: 30s
timeout: 10s
start_period: 60s
retries: 5
networks:
- app-net
volumes:
app-storage:
redis-data:
galera-data:
networks:
app-net:
driver: bridge

View file

@ -1,59 +0,0 @@
# Host UK Nginx Configuration
# Proxies PHP to the app (FPM) container, serves static files directly
server {
listen 80;
server_name _;
root /app/public;
index index.php;
charset utf-8;
# Security headers
include /etc/nginx/snippets/security-headers.conf;
# Health check endpoint (no logging)
location = /health {
access_log off;
try_files $uri /index.php?$query_string;
}
# Static file caching
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|webp|avif)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
try_files $uri =404;
}
# Laravel application
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP-FPM upstream
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
fastcgi_buffer_size 32k;
fastcgi_buffers 16 16k;
fastcgi_read_timeout 300;
# Pass real client IP from LB proxy protocol
fastcgi_param REMOTE_ADDR $http_x_forwarded_for;
}
# Block dotfiles (except .well-known)
location ~ /\.(?!well-known) {
deny all;
}
# Block access to sensitive files
location ~* \.(env|log|yaml|yml|toml|lock|bak|sql)$ {
deny all;
}
}

View file

@ -1,6 +0,0 @@
# Security headers for Host UK
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;

View file

@ -1,10 +0,0 @@
; OPcache configuration for production
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
opcache.save_comments=1
opcache.fast_shutdown=1
opcache.jit_buffer_size=128M
opcache.jit=1255

View file

@ -1,22 +0,0 @@
; Host UK PHP-FPM pool configuration
[www]
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 1000
pm.process_idle_timeout = 10s
; Status page for health checks
pm.status_path = /fpm-status
ping.path = /fpm-ping
ping.response = pong
; Logging
access.log = /proc/self/fd/2
slowlog = /proc/self/fd/2
request_slowlog_timeout = 5s
; Security
security.limit_extensions = .php