initial commit

This commit is contained in:
2026-06-06 10:05:39 +07:00
commit b22608adbc
1577 changed files with 641460 additions and 0 deletions
+306
View File
@@ -0,0 +1,306 @@
# 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 ci`
- `npm run build` → outputs `public/build/assets/` + `public/build/manifest.json`
### Stage 2: Composer Dependencies
- Base: `php:8.2-cli-alpine`
- Copy `composer` binary from official `composer:2` image
- `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)
- `gd` with `--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 `app` user (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`
```nginx
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`
```bash
#!/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=0` for mysql client (rule 8)
- `exec php-fpm` — final process replaces shell (rule 9)
---
## 5. `docker-compose.yml`
```yaml
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 `ShouldQueue` implementations
- `QUEUE_CONNECTION=database` is 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)