8.0 KiB
8.0 KiB
Cleft Lip Data — Docker Deployment Plan
Overview
Multi-container Docker deployment for a Laravel 11 cleft lip/palate medical records management app.
Target: Coolify Database: MariaDB 10.6.25 (matching existing SQL dump) PHP: 8.2 FPM Alpine Web Server: Nginx 1.25 Alpine
Architecture
Coolify auto-generates Traefik labels
│
┌─────▼─────┐
│ nginx │ :80
│ Dockerfile.nginx
└─────┬─────┘
│ volume: app-photos:ro
┌─────▼──────┐
│ app (fpm) │ :9000
│ Dockerfile
└─────┬──────┘
│ volume: app-storage
┌───────────▼───────────┐
│ mariadb:10.6.25 │ :3306
│ healthcheck: custom │ volume: mariadb_data
└───────────────────────┘
Files to Create
| File | Content |
|---|---|
Dockerfile |
Multi-stage: Node build → Composer deps → PHP-FPM runtime |
Dockerfile.nginx |
Nginx with Laravel config baked in |
docker-nginx.conf |
Laravel-compatible Nginx site config |
docker-compose.yml |
3 services: mariadb, app, nginx |
docker-entrypoint.sh |
Startup orchestration script |
.dockerignore |
Build context filter |
docker-healthcheck.sh |
MariaDB healthcheck (not used directly; Healthcheck is inline in compose for MariaDB) |
1. Dockerfile — Multi-Stage Build
Stage 1: Node Build
- Base:
node:20-alpine npm cinpm run build→ outputspublic/build/assets/+public/build/manifest.json
Stage 2: Composer Dependencies
- Base:
php:8.2-cli-alpine - Copy
composerbinary from officialcomposer:2image composer install --no-dev --optimize-autoloader
Stage 3: PHP-FPM Runtime
- Base:
php:8.2-fpm-alpine - Install missing PHP extensions only:
pdo_mysql(not in base)gdwith--with-freetype --with-jpeg(not in base)zip(not in base)intl(recommended)bcmath(recommended)
- System packages:
libpng-dev,libjpeg-turbo-dev,freetype-dev,libzip-dev,icu-dev,oniguruma-dev - Create
appuser (non-root, UID 1001) - Copy application source (excluding
node_modules,vendor,storage,.env) - Copy
vendor/from Stage 2 - Copy
public/build/from Stage 1 - Copy
docker-entrypoint.sh→/usr/local/bin/ - Set
WORKDIR /var/www/html ENTRYPOINT ["docker-entrypoint.sh"]CMD ["php-fpm"]USER app
2. Dockerfile.nginx
- Base:
nginx:1.25-alpine(pinned) - Copy
docker-nginx.conf→/etc/nginx/conf.d/default.conf
3. docker-nginx.conf
server {
listen 80;
server_name _;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
location ~ /\.(?!well-known) {
deny all;
}
}
4. docker-entrypoint.sh
#!/bin/sh
set -e
# === 1. Wait for MySQL ===
# PDO connection loop, 60s timeout, 2s intervals
# Uses getenv('DB_HOST'), getenv('DB_PORT'), getenv('DB_DATABASE'),
# getenv('DB_USERNAME'), getenv('DB_PASSWORD')
# === 2. Generate APP_KEY if missing or default ===
# php artisan key:generate if getenv('APP_KEY') is empty
# But only if running in PHP-FPM context (not during build)
# === 3. Import SQL dump if tables don't exist ===
# Check if any tables exist via mysql --ssl=0 ...
# If not, import humicpro_cleftlipdata.sql
# === 4. Run migrations ===
# php artisan migrate --force
# === 5. Cache ===
# php artisan config:cache
# php artisan route:cache
# php artisan view:cache
# === 6. Storage link ===
# php artisan storage:link (if not already linked)
# === 7. Start PHP-FPM ===
exec php-fpm
Critical rules applied:
set -e— any failure stops the script (rule 9)getenv()in PHP — no bash variable interpolation (rule 4)--ssl=0for mysql client (rule 8)exec php-fpm— final process replaces shell (rule 9)
5. docker-compose.yml
services:
mariadb:
image: mariadb:10.6.25
volumes:
- mariadb_data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost --ssl=0 -u root -p$$MYSQL_ROOT_PASSWORD"]
interval: 10s
timeout: 5s
retries: 10
start_period: 60s
restart: unless-stopped
app:
build:
context: .
dockerfile: Dockerfile
volumes:
- app-storage:/var/www/html/storage
- app-photos:/var/www/html/public/photos
environment:
APP_ENV: production
APP_DEBUG: "false"
APP_URL: ${APP_URL}
APP_KEY: ${APP_KEY}
DB_HOST: mariadb
DB_PORT: "3306"
DB_DATABASE: ${DB_DATABASE}
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
SESSION_DRIVER: database
QUEUE_CONNECTION: database
CACHE_STORE: database
MAIL_MAILER: ${MAIL_MAILER:-log}
LOG_CHANNEL: stack
LOG_LEVEL: ${LOG_LEVEL:-warning}
depends_on:
mariadb:
condition: service_healthy
restart: unless-stopped
nginx:
build:
context: .
dockerfile: Dockerfile.nginx
volumes:
- app-storage:/var/www/html/storage:ro
- app-photos:/var/www/html/public/photos:ro
ports:
- "${NGINX_PORT:-80}:80"
depends_on:
- app
restart: unless-stopped
volumes:
mariadb_data:
app-storage:
app-photos:
No Traefik labels — Coolify auto-generates them (rule 6).
6. .dockerignore
.git
.gitattributes
.gitignore
.env
.env.example
local.env
node_modules
vendor
storage
.DS_Store
*.md
.dockerignore
docker-compose.yml
Dockerfile.nginx
docker-nginx.conf
humicpro_cleftlipdata.sql
AGENTS.MD
DEPLOY.htaccess
DEPLOYindex.php
database/database.sqlite
error_log
.build/
.cgi-bin/
.well-known/
Key Compliance with AGENTS.md Rules
| Rule | How We Comply |
|---|---|
| 1. Pin image tags | php:8.2-fpm-alpine, nginx:1.25-alpine, mariadb:10.6.25, node:20-alpine — all pinned |
| 2. Verify PHP extensions | Base image has most; install only: pdo_mysql, gd, zip, intl, bcmath |
| 3. Shared volumes | app-storage and app-photos shared between app and nginx |
| 4. No bash var interpolation | Entrypoint uses PHP's getenv() where needed, not shell ${VAR} |
| 5. No bind mounts | Nginx config baked into Dockerfile.nginx; all configs in images |
| 6. No Traefik labels | Zero labels in compose |
| 7. Match DB image | mariadb:10.6.25 matches dump's 10.6.25-MariaDB server version |
8. --ssl=0 |
Entrypoint and healthcheck use --ssl=0 |
| 9. Entrypoint flow | set -e, step-by-step checks, exec php-fpm as final process |
| 10. DB healthcheck | Custom mysqladmin ping with 10 retries, 60s start period |
Queue Worker
Not needed. The app contains:
- Zero job classes (
app/Jobs/empty) - Zero
dispatch()calls in application code - Zero
ShouldQueueimplementations QUEUE_CONNECTION=databaseis an unused default
All operations (data CRUD, file uploads, spreadsheet export, zip creation) are synchronous in HTTP requests.
External Connections
None. The app is fully self-contained:
- No outgoing HTTP/API calls
- No third-party services
- No webhooks or callbacks
- No social login
Browser-only external resources:
fonts.bunny.net(Google Fonts proxy)cdn.jsdelivr.net/npm/flatpickr(datepicker)