initial commit
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user