# 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)