Files
cleftlipdata/DEPLOYMENT_PLAN.md
T
2026-06-06 10:05:39 +07:00

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

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=0 for 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 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)