feat: Implement Docker-based deployment for ClinicPro on Coolify
- Added Dockerfile for multi-stage build including PHP, Node.js, and Nginx. - Created docker-compose.coolify.yaml for service orchestration with app, workers, MariaDB, and Redis. - Introduced entrypoint.sh for initialization tasks like JWT key generation and database migrations. - Configured Nginx with default.conf for handling requests and routing to PHP-FPM. - Added php.ini with production settings and opcache configuration. - Set up supervisord.conf to manage PHP-FPM and Nginx processes. - Created frontend-domains.json for managing allowed frontend domains. - Added gen-cors-env.php script to generate CORS environment variables from frontend domains. - Updated framework.yaml to configure trusted proxies and headers. - Created .dockerignore to exclude unnecessary files from the Docker context. - Added .env.coolify.example for environment variable configuration. - Documented deployment steps and troubleshooting in coolify.md.
This commit is contained in:
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Volume-mounted dirs (jwt_keys, uploads) are created/owned by root on first run.
|
||||
# Ensure the runtime user can write to them. Runs every start; cheap and idempotent.
|
||||
chown -R www-data:www-data var public/uploads config/jwt 2>/dev/null || true
|
||||
|
||||
# One-time init tasks — only the web service runs these (RUN_INIT=1).
|
||||
# Workers set RUN_INIT=0 so DB migrations / JWT generation don't race.
|
||||
if [ "${RUN_INIT:-1}" = "1" ]; then
|
||||
# Generate JWT keypair if not already persisted on the jwt_keys volume.
|
||||
php bin/console lexik:jwt:generate-keypair --skip-if-exists --no-interaction
|
||||
|
||||
# Rebuild the prod cache (vendor was installed with --no-scripts at build time).
|
||||
php bin/console cache:clear --no-warmup
|
||||
php bin/console cache:warmup
|
||||
|
||||
# Apply pending migrations. --all-or-nothing wraps them in a transaction.
|
||||
php bin/console doctrine:migrations:migrate --all-or-nothing --no-interaction
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"_comment": "Frontend domains served by this backend. Add an entry per city, then run: ddev exec php docker/gen-cors-env.php — paste the output into Coolify env (CORS_ALLOW_ORIGIN + ALLOWED_FRONTEND_HOSTS), add the same domain to the app service in Coolify UI, and redeploy.",
|
||||
"domains": [
|
||||
{ "domain": "nobat724.com", "label": "اصلی" },
|
||||
{ "domain": "ahvaz-nobat.ir", "label": "اهواز" },
|
||||
{ "domain": "arak-nobat.ir", "label": "اراک" },
|
||||
{ "domain": "ardabil-nobat.ir", "label": "اردبیل" },
|
||||
{ "domain": "bandar-nobat.ir", "label": "بندرعباس" },
|
||||
{ "domain": "behbahan-nobat.ir", "label": "بهبهان" },
|
||||
{ "domain": "birjand-nobat.ir", "label": "بیرجند" },
|
||||
{ "domain": "bojnord-nobat.ir", "label": "بجنورد" },
|
||||
{ "domain": "bushehr-nobat.ir", "label": "بوشهر" },
|
||||
{ "domain": "dehdasht-nobat.ir", "label": "دهدشت" },
|
||||
{ "domain": "esf-nobat.ir", "label": "اصفهان" },
|
||||
{ "domain": "golestan-nobat.ir", "label": "گلستان" },
|
||||
{ "domain": "hamadan-nobat.ir", "label": "همدان" },
|
||||
{ "domain": "ilam-nobat.ir", "label": "ایلام" },
|
||||
{ "domain": "karaj-nobat.ir", "label": "کرج" },
|
||||
{ "domain": "kerman-nobat.ir", "label": "کرمان" },
|
||||
{ "domain": "kermanshah-nobat.ir", "label": "کرمانشاه" },
|
||||
{ "domain": "lorestan-nobat.ir", "label": "لرستان" },
|
||||
{ "domain": "mashhad-nobat.ir", "label": "مشهد" },
|
||||
{ "domain": "qazvin-nobat.ir", "label": "قزوین" },
|
||||
{ "domain": "qom-nobat.ir", "label": "قم" },
|
||||
{ "domain": "rasht-nobat.ir", "label": "رشت" },
|
||||
{ "domain": "sanandaj-nobat.ir", "label": "سنندج" },
|
||||
{ "domain": "sari-nobat.ir", "label": "ساری" },
|
||||
{ "domain": "semnan-nobat.ir", "label": "سمنان" },
|
||||
{ "domain": "shiraz-nobat.ir", "label": "شیراز" },
|
||||
{ "domain": "shkord-nobat.ir", "label": "شهرکرد" },
|
||||
{ "domain": "tabriz-nobat.ir", "label": "تبریز" },
|
||||
{ "domain": "tehran-nobat.ir", "label": "تهران" },
|
||||
{ "domain": "urmia-nobat.ir", "label": "ارومیه" },
|
||||
{ "domain": "yasuj-nobat.ir", "label": "یاسوج" },
|
||||
{ "domain": "yazd-nobat.ir", "label": "یزد" },
|
||||
{ "domain": "zahedan-nobat.ir", "label": "زاهدان" },
|
||||
{ "domain": "zanjan-nobat.ir", "label": "زنجان" }
|
||||
]
|
||||
}
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* Generates the multi-domain env values for Coolify from docker/frontend-domains.json.
|
||||
*
|
||||
* Usage:
|
||||
* php docker/gen-cors-env.php
|
||||
*
|
||||
* Output: CORS_ALLOW_ORIGIN (single regex, explicit alternation) and
|
||||
* ALLOWED_FRONTEND_HOSTS (comma-separated host list).
|
||||
* Paste both into the Coolify Environment Variables tab.
|
||||
*/
|
||||
|
||||
$jsonFile = __DIR__ . '/frontend-domains.json';
|
||||
if (!is_file($jsonFile)) {
|
||||
fwrite(STDERR, "Missing $jsonFile\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
$data = json_decode(file_get_contents($jsonFile), true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $e) {
|
||||
fwrite(STDERR, "Invalid JSON in $jsonFile: {$e->getMessage()}\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
if (!isset($data['domains']) || !is_array($data['domains'])) {
|
||||
fwrite(STDERR, "Expected a \"domains\" array in $jsonFile\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$domains = [];
|
||||
foreach ($data['domains'] as $entry) {
|
||||
$host = is_array($entry) ? ($entry['domain'] ?? null) : $entry;
|
||||
$host = is_string($host) ? trim($host) : '';
|
||||
if ($host === '') {
|
||||
continue;
|
||||
}
|
||||
if (!preg_match('/^[a-z0-9.-]+$/i', $host)) {
|
||||
fwrite(STDERR, "Skipping invalid domain: \"$host\"\n");
|
||||
continue;
|
||||
}
|
||||
$domains[strtolower($host)] = true; // dedupe, case-insensitive
|
||||
}
|
||||
$domains = array_keys($domains);
|
||||
sort($domains);
|
||||
|
||||
if (empty($domains)) {
|
||||
fwrite(STDERR, "No valid domains found in $jsonFile\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// CORS: explicit alternation, anchored, dots escaped. https only.
|
||||
$alternation = implode('|', array_map(static fn (string $d): string => preg_quote($d, '/'), $domains));
|
||||
$cors = "^https://($alternation)$";
|
||||
|
||||
// Frontend hosts: bare hostnames, comma-separated (matched via in_array in PaymentController).
|
||||
$hosts = implode(',', $domains);
|
||||
|
||||
echo "# ---- paste into Coolify env (" . count($domains) . " domains) ----\n\n";
|
||||
echo "CORS_ALLOW_ORIGIN='" . $cors . "'\n\n";
|
||||
echo "ALLOWED_FRONTEND_HOSTS=" . $hosts . "\n";
|
||||
@@ -0,0 +1,30 @@
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name _;
|
||||
root /app/public;
|
||||
|
||||
# Symfony front controller
|
||||
location / {
|
||||
try_files $uri /index.php$is_args$args;
|
||||
}
|
||||
|
||||
location ~ ^/index\.php(/|$) {
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.*)$;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
|
||||
fastcgi_param DOCUMENT_ROOT $realpath_root;
|
||||
# Forwarded headers from Traefik (Coolify) are trusted via Symfony trusted_proxies
|
||||
internal;
|
||||
}
|
||||
|
||||
# Block direct access to any other .php file
|
||||
location ~ \.php$ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
client_max_body_size 16m; # keep in sync with MAX_FILE_SIZE_BYTES / php.ini
|
||||
|
||||
error_log /dev/stderr warn;
|
||||
access_log /dev/stdout;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
; Production PHP settings for ClinicPro on Coolify
|
||||
memory_limit = 256M
|
||||
upload_max_filesize = 16M
|
||||
post_max_size = 16M
|
||||
max_execution_time = 60
|
||||
expose_php = Off
|
||||
date.timezone = Asia/Tehran
|
||||
|
||||
; OPcache (production)
|
||||
opcache.enable = 1
|
||||
opcache.enable_cli = 0
|
||||
opcache.memory_consumption = 256
|
||||
opcache.max_accelerated_files = 20000
|
||||
opcache.validate_timestamps = 0
|
||||
opcache.interned_strings_buffer = 16
|
||||
opcache.preload = /app/config/preload.php
|
||||
opcache.preload_user = www-data
|
||||
|
||||
; Realpath cache (perf)
|
||||
realpath_cache_size = 4096k
|
||||
realpath_cache_ttl = 600
|
||||
@@ -0,0 +1,24 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
user=root
|
||||
logfile=/dev/stdout
|
||||
logfile_maxbytes=0
|
||||
pidfile=/run/supervisord.pid
|
||||
|
||||
[program:php-fpm]
|
||||
command=php-fpm -F
|
||||
autorestart=true
|
||||
priority=10
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:nginx]
|
||||
command=nginx -g 'daemon off;'
|
||||
autorestart=true
|
||||
priority=20
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
Reference in New Issue
Block a user