feat(container): support comma-separated roles

Allow s6 services to start for specific roles like horizon,
scheduler, nightwatch, and flux while keeping all as the default.
This commit is contained in:
Andras Bacsai
2026-06-15 23:36:47 +02:00
parent dcd325dc44
commit 762daf83a1
14 changed files with 163 additions and 108 deletions
+1
View File
@@ -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
+7 -2
View File
@@ -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
@@ -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."
@@ -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."
@@ -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..."
@@ -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."
+26
View File
@@ -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
}
@@ -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."
@@ -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."
@@ -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..."
@@ -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."
+26
View File
@@ -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
}
+2 -10
View File
@@ -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",
+53
View File
@@ -0,0 +1,53 @@
<?php
use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Support\Facades\Process;
it('matches comma separated container roles and lets all override every service', function (string $roles, string $serviceRole) {
$result = runContainerRoleHelper($roles, $serviceRole);
expect($result->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);
}