diff --git a/.env.development.example b/.env.development.example index d02b8ba59..887f62974 100644 --- a/.env.development.example +++ b/.env.development.example @@ -7,6 +7,7 @@ APP_URL=http://localhost APP_PORT=8000 APP_DEBUG=true SSH_MUX_ENABLED=true +COOLIFY_CONTAINER_ROLE=all # PostgreSQL Database Configuration DB_DATABASE=coolify diff --git a/README.md b/README.md index f4e289bcd..378e75ac7 100644 --- a/README.md +++ b/README.md @@ -32,13 +32,18 @@ You can find the installation script source [here](./scripts/install.sh). ## Container roles and Flux -The Coolify image can run different process roles with `COOLIFY_CONTAINER_ROLE`: +The Coolify image can run different process roles with `COOLIFY_CONTAINER_ROLE`. The value can be a single role or a comma-separated list. If `all` is present anywhere in the list, every role starts. - `all` (default): self-hosted mode; runs the web process, worker services, and Flux when configured. -- `web`: web/API process only; s6 worker services and Flux sleep. +- `web`: web/API process only; s6 worker services and Flux sleep unless they are also listed. - `worker`: Horizon, Laravel scheduler worker, and optional Nightwatch agent. +- `horizon`: Horizon only. +- `scheduler` or `scheduler-worker`: Laravel scheduler worker only. +- `nightwatch` or `nightwatch-agent`: optional Nightwatch agent only. - `flux`: Flux only; used by Cloud/HA deployments that scale coold connection routers separately. +For example, `COOLIFY_CONTAINER_ROLE=web,flux` starts the web container plus Flux, while `COOLIFY_CONTAINER_ROLE=web,all` still starts all services. + Flux is installed from the coold nightly release into `/usr/local/bin/flux`. Containers running the `all` or `flux` role expose Flux on port `6443` and use these runtime variables: ```env diff --git a/docker/development/etc/s6-overlay/s6-rc.d/flux/run b/docker/development/etc/s6-overlay/s6-rc.d/flux/run index 95f648f8e..d99bdaac4 100755 --- a/docker/development/etc/s6-overlay/s6-rc.d/flux/run +++ b/docker/development/etc/s6-overlay/s6-rc.d/flux/run @@ -2,19 +2,13 @@ cd /var/www/html -role="${COOLIFY_CONTAINER_ROLE:-}" -if [ -z "$role" ]; then - role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")" -fi -role="${role:-all}" +. /etc/s6-overlay/scripts/container-role +role="$(coolify_container_role_value)" -case "$role" in - all|flux) ;; - *) - echo " INFO Flux is disabled for role '$role', sleeping." - exec sleep infinity - ;; -esac +if ! coolify_container_has_role flux; then + echo " INFO Flux is disabled for role '$role', sleeping." + exec sleep infinity +fi if grep -qE '^COOLIFY_FLUX_ENABLED=false' .env 2>/dev/null || [ "${COOLIFY_FLUX_ENABLED:-}" = "false" ]; then echo " INFO Flux is disabled, sleeping." diff --git a/docker/development/etc/s6-overlay/s6-rc.d/horizon/run b/docker/development/etc/s6-overlay/s6-rc.d/horizon/run index 9a8d96d2a..584bd0d9d 100644 --- a/docker/development/etc/s6-overlay/s6-rc.d/horizon/run +++ b/docker/development/etc/s6-overlay/s6-rc.d/horizon/run @@ -2,19 +2,13 @@ cd /var/www/html -role="${COOLIFY_CONTAINER_ROLE:-}" -if [ -z "$role" ]; then - role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")" -fi -role="${role:-all}" +. /etc/s6-overlay/scripts/container-role +role="$(coolify_container_role_value)" -case "$role" in - all|worker) ;; - *) - echo " INFO Horizon is disabled for role '$role', sleeping." - exec sleep infinity - ;; -esac +if ! coolify_container_has_role worker && ! coolify_container_has_role horizon; then + echo " INFO Horizon is disabled for role '$role', sleeping." + exec sleep infinity +fi if grep -qE '^HORIZON_ENABLED=false' .env 2>/dev/null; then echo " INFO Horizon is disabled, sleeping." diff --git a/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run b/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run index 7242b3f79..1f8b37079 100644 --- a/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run +++ b/docker/development/etc/s6-overlay/s6-rc.d/nightwatch-agent/run @@ -2,19 +2,13 @@ cd /var/www/html -role="${COOLIFY_CONTAINER_ROLE:-}" -if [ -z "$role" ]; then - role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")" -fi -role="${role:-all}" +. /etc/s6-overlay/scripts/container-role +role="$(coolify_container_role_value)" -case "$role" in - all|worker) ;; - *) - echo " INFO Nightwatch is disabled for role '$role', sleeping." - exec sleep infinity - ;; -esac +if ! coolify_container_has_role worker && ! coolify_container_has_role nightwatch && ! coolify_container_has_role nightwatch-agent; then + echo " INFO Nightwatch is disabled for role '$role', sleeping." + exec sleep infinity +fi if grep -qE '^NIGHTWATCH_ENABLED=true' .env 2>/dev/null; then echo " INFO Nightwatch is enabled, starting..." diff --git a/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run b/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run index 0dbc2e716..2dcd493a1 100644 --- a/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run +++ b/docker/development/etc/s6-overlay/s6-rc.d/scheduler-worker/run @@ -2,19 +2,13 @@ cd /var/www/html -role="${COOLIFY_CONTAINER_ROLE:-}" -if [ -z "$role" ]; then - role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")" -fi -role="${role:-all}" +. /etc/s6-overlay/scripts/container-role +role="$(coolify_container_role_value)" -case "$role" in - all|worker) ;; - *) - echo " INFO Scheduler worker is disabled for role '$role', sleeping." - exec sleep infinity - ;; -esac +if ! coolify_container_has_role worker && ! coolify_container_has_role scheduler && ! coolify_container_has_role scheduler-worker; then + echo " INFO Scheduler worker is disabled for role '$role', sleeping." + exec sleep infinity +fi if grep -qE '^SCHEDULER_ENABLED=false' .env 2>/dev/null; then echo " INFO Scheduler is disabled, sleeping." diff --git a/docker/development/etc/s6-overlay/scripts/container-role b/docker/development/etc/s6-overlay/scripts/container-role new file mode 100755 index 000000000..c7ee7dd3c --- /dev/null +++ b/docker/development/etc/s6-overlay/scripts/container-role @@ -0,0 +1,26 @@ +#!/bin/sh + +coolify_container_role_value() { + role="${COOLIFY_CONTAINER_ROLE:-}" + + if [ -z "$role" ]; then + role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")" + fi + + printf '%s\n' "${role:-all}" +} + +coolify_container_has_role() { + wanted_role="$1" + roles="$(coolify_container_role_value | tr '[:upper:]' '[:lower:]' | tr ',' ' ')" + + for role in $roles; do + case "$role" in + all|"$wanted_role") + return 0 + ;; + esac + done + + return 1 +} diff --git a/docker/production/etc/s6-overlay/s6-rc.d/flux/run b/docker/production/etc/s6-overlay/s6-rc.d/flux/run index 95f648f8e..d99bdaac4 100755 --- a/docker/production/etc/s6-overlay/s6-rc.d/flux/run +++ b/docker/production/etc/s6-overlay/s6-rc.d/flux/run @@ -2,19 +2,13 @@ cd /var/www/html -role="${COOLIFY_CONTAINER_ROLE:-}" -if [ -z "$role" ]; then - role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")" -fi -role="${role:-all}" +. /etc/s6-overlay/scripts/container-role +role="$(coolify_container_role_value)" -case "$role" in - all|flux) ;; - *) - echo " INFO Flux is disabled for role '$role', sleeping." - exec sleep infinity - ;; -esac +if ! coolify_container_has_role flux; then + echo " INFO Flux is disabled for role '$role', sleeping." + exec sleep infinity +fi if grep -qE '^COOLIFY_FLUX_ENABLED=false' .env 2>/dev/null || [ "${COOLIFY_FLUX_ENABLED:-}" = "false" ]; then echo " INFO Flux is disabled, sleeping." diff --git a/docker/production/etc/s6-overlay/s6-rc.d/horizon/run b/docker/production/etc/s6-overlay/s6-rc.d/horizon/run index 9a8d96d2a..584bd0d9d 100644 --- a/docker/production/etc/s6-overlay/s6-rc.d/horizon/run +++ b/docker/production/etc/s6-overlay/s6-rc.d/horizon/run @@ -2,19 +2,13 @@ cd /var/www/html -role="${COOLIFY_CONTAINER_ROLE:-}" -if [ -z "$role" ]; then - role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")" -fi -role="${role:-all}" +. /etc/s6-overlay/scripts/container-role +role="$(coolify_container_role_value)" -case "$role" in - all|worker) ;; - *) - echo " INFO Horizon is disabled for role '$role', sleeping." - exec sleep infinity - ;; -esac +if ! coolify_container_has_role worker && ! coolify_container_has_role horizon; then + echo " INFO Horizon is disabled for role '$role', sleeping." + exec sleep infinity +fi if grep -qE '^HORIZON_ENABLED=false' .env 2>/dev/null; then echo " INFO Horizon is disabled, sleeping." diff --git a/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run b/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run index 7242b3f79..1f8b37079 100644 --- a/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run +++ b/docker/production/etc/s6-overlay/s6-rc.d/nightwatch-agent/run @@ -2,19 +2,13 @@ cd /var/www/html -role="${COOLIFY_CONTAINER_ROLE:-}" -if [ -z "$role" ]; then - role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")" -fi -role="${role:-all}" +. /etc/s6-overlay/scripts/container-role +role="$(coolify_container_role_value)" -case "$role" in - all|worker) ;; - *) - echo " INFO Nightwatch is disabled for role '$role', sleeping." - exec sleep infinity - ;; -esac +if ! coolify_container_has_role worker && ! coolify_container_has_role nightwatch && ! coolify_container_has_role nightwatch-agent; then + echo " INFO Nightwatch is disabled for role '$role', sleeping." + exec sleep infinity +fi if grep -qE '^NIGHTWATCH_ENABLED=true' .env 2>/dev/null; then echo " INFO Nightwatch is enabled, starting..." diff --git a/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run b/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run index 0dbc2e716..2dcd493a1 100644 --- a/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run +++ b/docker/production/etc/s6-overlay/s6-rc.d/scheduler-worker/run @@ -2,19 +2,13 @@ cd /var/www/html -role="${COOLIFY_CONTAINER_ROLE:-}" -if [ -z "$role" ]; then - role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")" -fi -role="${role:-all}" +. /etc/s6-overlay/scripts/container-role +role="$(coolify_container_role_value)" -case "$role" in - all|worker) ;; - *) - echo " INFO Scheduler worker is disabled for role '$role', sleeping." - exec sleep infinity - ;; -esac +if ! coolify_container_has_role worker && ! coolify_container_has_role scheduler && ! coolify_container_has_role scheduler-worker; then + echo " INFO Scheduler worker is disabled for role '$role', sleeping." + exec sleep infinity +fi if grep -qE '^SCHEDULER_ENABLED=false' .env 2>/dev/null; then echo " INFO Scheduler is disabled, sleeping." diff --git a/docker/production/etc/s6-overlay/scripts/container-role b/docker/production/etc/s6-overlay/scripts/container-role new file mode 100755 index 000000000..c7ee7dd3c --- /dev/null +++ b/docker/production/etc/s6-overlay/scripts/container-role @@ -0,0 +1,26 @@ +#!/bin/sh + +coolify_container_role_value() { + role="${COOLIFY_CONTAINER_ROLE:-}" + + if [ -z "$role" ]; then + role="$(grep -E '^COOLIFY_CONTAINER_ROLE=' .env 2>/dev/null | tail -n1 | cut -d= -f2- | tr -d '"' | tr -d "'")" + fi + + printf '%s\n' "${role:-all}" +} + +coolify_container_has_role() { + wanted_role="$1" + roles="$(coolify_container_role_value | tr '[:upper:]' '[:lower:]' | tr ',' ' ')" + + for role in $roles; do + case "$role" in + all|"$wanted_role") + return 0 + ;; + esac + done + + return 1 +} diff --git a/package-lock.json b/package-lock.json index 288474a88..04a133240 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,7 +70,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1614,8 +1613,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/baseline-browser-mapping": { "version": "2.10.33", @@ -1650,7 +1648,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2296,7 +2293,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2408,7 +2404,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2418,7 +2413,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -2525,8 +2519,7 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -2600,7 +2593,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/tests/Feature/ContainerRoleScriptTest.php b/tests/Feature/ContainerRoleScriptTest.php new file mode 100644 index 000000000..5d55a3877 --- /dev/null +++ b/tests/Feature/ContainerRoleScriptTest.php @@ -0,0 +1,53 @@ +successful())->toBeTrue(); +})->with([ + ['worker,flux', 'worker'], + ['worker,flux', 'flux'], + ['web, all', 'flux'], + ['flux,all,worker', 'worker'], + ['horizon,scheduler,nightwatch,flux', 'horizon'], + ['horizon,scheduler,nightwatch,flux', 'scheduler'], + ['horizon,scheduler,nightwatch,flux', 'nightwatch'], +]); + +it('rejects services missing from the comma separated container roles', function () { + $result = runContainerRoleHelper('web,flux', 'worker'); + + expect($result->failed())->toBeTrue(); +}); + +it('falls back to the container role from the local env file', function () { + $directory = sys_get_temp_dir().'/coolify-container-role-'.bin2hex(random_bytes(4)); + mkdir($directory); + file_put_contents($directory.'/.env', 'COOLIFY_CONTAINER_ROLE=worker,flux'.PHP_EOL); + + $result = runContainerRoleHelper('', 'flux', $directory); + + expect($result->successful())->toBeTrue(); +}); + +it('keeps production and development role helpers in sync', function () { + expect(file_get_contents(base_path('docker/production/etc/s6-overlay/scripts/container-role'))) + ->toBe(file_get_contents(base_path('docker/development/etc/s6-overlay/scripts/container-role'))); +}); + +function runContainerRoleHelper(string $roles, string $serviceRole, ?string $workingDirectory = null): ProcessResult +{ + $script = base_path('docker/development/etc/s6-overlay/scripts/container-role'); + $command = sprintf( + '. %s && coolify_container_has_role %s', + escapeshellarg($script), + escapeshellarg($serviceRole), + ); + + return Process::path($workingDirectory ?? base_path()) + ->env(['COOLIFY_CONTAINER_ROLE' => $roles]) + ->run($command); +}