fix(service-apps): harden updates and docker commands

Escape service application lifecycle command arguments for deploy, restart, stop, and log status checks.
Validate API update payloads from JSON/form data before applying allowed fields, preserving explicit boolean and null values.
Add coverage for service application API authorization, team isolation, validation, and command escaping.
This commit is contained in:
Andras Bacsai 2026-07-07 15:10:24 +02:00
parent 95a3c453d8
commit a165e03d06
9 changed files with 356 additions and 233 deletions

View file

@ -22,35 +22,42 @@ public function handle(ServiceApplication $serviceApplication, bool $pullLatestI
$service->isConfigurationChanged(save: true); $service->isConfigurationChanged(save: true);
$workdir = $service->workdir(); $workdir = $service->workdir();
$composeFile = "{$workdir}/docker-compose.yml";
$safeWorkdir = escapeshellarg($workdir);
$safeComposeFile = escapeshellarg($composeFile);
$safeProjectName = escapeshellarg($service->uuid);
$safeComposeServiceName = escapeshellarg($composeServiceName);
$commands = collect([ $commands = collect([
"echo 'Saved configuration files to {$workdir}.'", 'echo '.escapeshellarg("Saved configuration files to {$workdir}."),
"touch {$workdir}/.env", 'touch '.escapeshellarg("{$workdir}/.env"),
]); ]);
if ($pullLatestImages) { if ($pullLatestImages) {
$commands->push('echo Pulling image for service.'); $commands->push('echo Pulling image for service.');
$commands->push("docker compose --project-directory {$workdir} -f {$workdir}/docker-compose.yml --project-name {$service->uuid} pull {$composeServiceName}"); $commands->push("docker compose --project-directory {$safeWorkdir} -f {$safeComposeFile} --project-name {$safeProjectName} pull {$safeComposeServiceName}");
} }
if ($service->networks()->count() > 0) { if ($service->networks()->count() > 0) {
$commands->push('echo Creating Docker network.'); $commands->push('echo Creating Docker network.');
$commands->push("docker network inspect {$service->uuid} >/dev/null 2>&1 || docker network create --attachable {$service->uuid}"); $commands->push("docker network inspect {$safeProjectName} >/dev/null 2>&1 || docker network create --attachable {$safeProjectName}");
} }
$upCommand = "docker compose --project-directory {$workdir} -f {$workdir}/docker-compose.yml --project-name {$service->uuid} up -d --no-deps"; $upCommand = "docker compose --project-directory {$safeWorkdir} -f {$safeComposeFile} --project-name {$safeProjectName} up -d --no-deps";
if ($forceRebuild) { if ($forceRebuild) {
$upCommand .= ' --build'; $upCommand .= ' --build';
} }
$upCommand .= " {$composeServiceName}"; $upCommand .= " {$safeComposeServiceName}";
$commands->push('echo Starting service container.'); $commands->push('echo Starting service container.');
$commands->push($upCommand); $commands->push($upCommand);
$commands->push("docker network connect {$service->uuid} coolify-proxy >/dev/null 2>&1 || true"); $commands->push("docker network connect {$safeProjectName} coolify-proxy >/dev/null 2>&1 || true");
if (data_get($service, 'connect_to_docker_network')) { if (data_get($service, 'connect_to_docker_network')) {
$compose = data_get($service, 'docker_compose', []); $network = escapeshellarg($service->destination->network);
$network = $service->destination->network; $containerName = escapeshellarg("{$composeServiceName}-{$service->uuid}");
$commands->push("docker network connect --alias {$composeServiceName}-{$service->uuid} {$network} {$composeServiceName}-{$service->uuid} >/dev/null 2>&1 || true"); $networkAlias = escapeshellarg("{$composeServiceName}-{$service->uuid}");
$commands->push("docker network connect --alias {$networkAlias} {$network} {$containerName} >/dev/null 2>&1 || true");
} }
return remote_process($commands->toArray(), $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged'); return remote_process($commands->toArray(), $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');

View file

@ -15,7 +15,7 @@ public function handle(ServiceApplication $serviceApplication): void
{ {
$service = $serviceApplication->service; $service = $serviceApplication->service;
$server = $service->destination->server; $server = $service->destination->server;
$containerName = $serviceApplication->name.'-'.$service->uuid; $containerName = escapeshellarg($serviceApplication->name.'-'.$service->uuid);
instant_remote_process([ instant_remote_process([
"docker restart {$containerName}", "docker restart {$containerName}",

View file

@ -15,7 +15,7 @@ public function handle(ServiceApplication $serviceApplication): void
{ {
$service = $serviceApplication->service; $service = $serviceApplication->service;
$server = $service->destination->server; $server = $service->destination->server;
$containerName = $serviceApplication->name.'-'.$service->uuid; $containerName = escapeshellarg($serviceApplication->name.'-'.$service->uuid);
instant_remote_process([ instant_remote_process([
"docker stop {$containerName}", "docker stop {$containerName}",

View file

@ -9,12 +9,12 @@
class UpdateServiceApplicationFromApi class UpdateServiceApplicationFromApi
{ {
public function execute(ServiceApplication $serviceApplication, Request $request, string $teamId): ?JsonResponse public function execute(ServiceApplication $serviceApplication, Request $request, string $teamId, array $payload): ?JsonResponse
{ {
$forceDomainOverride = $request->boolean('force_domain_override'); $forceDomainOverride = $request->boolean('force_domain_override');
if ($request->has('url')) { if (array_key_exists('url', $payload)) {
$urlRaw = $request->input('url'); $urlRaw = $payload['url'];
if ($urlRaw !== null && ! is_string($urlRaw)) { if ($urlRaw !== null && ! is_string($urlRaw)) {
return response()->json([ return response()->json([
'message' => 'Validation failed.', 'message' => 'Validation failed.',
@ -59,38 +59,38 @@ public function execute(ServiceApplication $serviceApplication, Request $request
$serviceApplication->fqdn = $parsed['normalized']; $serviceApplication->fqdn = $parsed['normalized'];
} }
if ($request->has('human_name')) { if (array_key_exists('human_name', $payload)) {
$serviceApplication->human_name = $request->input('human_name'); $serviceApplication->human_name = $payload['human_name'];
} }
if ($request->has('description')) { if (array_key_exists('description', $payload)) {
$serviceApplication->description = $request->input('description'); $serviceApplication->description = $payload['description'];
} }
if ($request->has('image')) { if (array_key_exists('image', $payload)) {
$serviceApplication->image = $request->input('image'); $serviceApplication->image = $payload['image'];
} }
if ($request->has('exclude_from_status')) { if (array_key_exists('exclude_from_status', $payload)) {
$serviceApplication->exclude_from_status = $request->boolean('exclude_from_status'); $serviceApplication->exclude_from_status = filter_var($payload['exclude_from_status'], FILTER_VALIDATE_BOOLEAN);
} }
if ($request->has('is_gzip_enabled')) { if (array_key_exists('is_gzip_enabled', $payload)) {
$serviceApplication->is_gzip_enabled = $request->boolean('is_gzip_enabled'); $serviceApplication->is_gzip_enabled = filter_var($payload['is_gzip_enabled'], FILTER_VALIDATE_BOOLEAN);
} }
if ($request->has('is_stripprefix_enabled')) { if (array_key_exists('is_stripprefix_enabled', $payload)) {
$serviceApplication->is_stripprefix_enabled = $request->boolean('is_stripprefix_enabled'); $serviceApplication->is_stripprefix_enabled = filter_var($payload['is_stripprefix_enabled'], FILTER_VALIDATE_BOOLEAN);
} }
if ($request->has('is_log_drain_enabled')) { if (array_key_exists('is_log_drain_enabled', $payload)) {
$enabled = $request->boolean('is_log_drain_enabled'); $enabled = filter_var($payload['is_log_drain_enabled'], FILTER_VALIDATE_BOOLEAN);
$server = $serviceApplication->service->destination->server; $server = $serviceApplication->service->destination->server;
if ($enabled && ! $server->isLogDrainEnabled()) { if ($enabled && ! $server->isLogDrainEnabled()) {
return response()->json([ return response()->json([
'message' => 'Validation failed.', 'message' => 'Validation failed.',
'errors' => [ 'errors' => [
'is_log_drain_enabled' => 'Log drain is not enabled on the server for this service.', 'is_log_drain_enabled' => ['Log drain is not enabled on the server for this service.'],
], ],
], 422); ], 422);
} }

View file

@ -306,6 +306,11 @@ public function update(Request $request, UpdateServiceApplicationFromApi $update
$this->authorize('update', $serviceApplication); $this->authorize('update', $serviceApplication);
$payload = $request->json()->all();
if (empty($payload)) {
$payload = $request->request->all();
}
$allowedFields = [ $allowedFields = [
'url', 'url',
'human_name', 'human_name',
@ -328,9 +333,9 @@ public function update(Request $request, UpdateServiceApplicationFromApi $update
'is_stripprefix_enabled' => 'sometimes|boolean', 'is_stripprefix_enabled' => 'sometimes|boolean',
]; ];
$validator = Validator::make($request->all(), $validationRules); $validator = Validator::make($payload, $validationRules);
$extraFields = array_diff(array_keys($request->all()), $allowedFields); $extraFields = array_diff(array_keys($payload), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) { if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors(); $errors = $validator->errors();
foreach ($extraFields as $field) { foreach ($extraFields as $field) {
@ -343,7 +348,7 @@ public function update(Request $request, UpdateServiceApplicationFromApi $update
], 422); ], 422);
} }
$response = $updateServiceApplicationFromApi->execute($serviceApplication, $request, $teamId); $response = $updateServiceApplicationFromApi->execute($serviceApplication, $request, $teamId, $payload);
if ($response instanceof JsonResponse) { if ($response instanceof JsonResponse) {
return $response; return $response;
} }
@ -450,8 +455,9 @@ public function logs_by_uuid(Request $request): JsonResponse
} }
$containerName = $serviceApplication->name.'-'.$serviceApplication->service->uuid; $containerName = $serviceApplication->name.'-'.$serviceApplication->service->uuid;
$safeContainerName = escapeshellarg($containerName);
$status = getContainerStatus($server, $containerName); $status = getContainerStatus($server, $safeContainerName);
if ($status !== 'running') { if ($status !== 'running') {
return response()->json([ return response()->json([
'message' => 'Service application container is not running.', 'message' => 'Service application container is not running.',

View file

@ -31,7 +31,7 @@
"category": "finance", "category": "finance",
"logo": "svgs/actualbudget.png", "logo": "svgs/actualbudget.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "5006" "port": "5006"
}, },
"affine": { "affine": {
@ -64,7 +64,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/alexandrie.svg", "logo": "svgs/alexandrie.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-27T21:03:32+01:00", "template_last_updated_at": "2026-04-05T13:36:24+02:00",
"port": "8200" "port": "8200"
}, },
"anythingllm": { "anythingllm": {
@ -175,7 +175,7 @@
"category": "ai", "category": "ai",
"logo": "svgs/argilla.png", "logo": "svgs/argilla.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "6900" "port": "6900"
}, },
"audiobookshelf": { "audiobookshelf": {
@ -192,7 +192,7 @@
"category": "media", "category": "media",
"logo": "svgs/audiobookshelf.svg", "logo": "svgs/audiobookshelf.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-05-12T09:09:36+02:00",
"port": "80" "port": "80"
}, },
"authentik": { "authentik": {
@ -231,7 +231,7 @@
"category": "database", "category": "database",
"logo": "svgs/autobase.svg", "logo": "svgs/autobase.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-19T18:13:51+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"babybuddy": { "babybuddy": {
@ -280,7 +280,7 @@
"category": "monitoring", "category": "monitoring",
"logo": "svgs/beszel.svg", "logo": "svgs/beszel.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-02-21T23:17:23+05:30" "template_last_updated_at": "2026-04-24T02:22:08+05:30"
}, },
"beszel": { "beszel": {
"documentation": "https://github.com/henrygd/beszel?tab=readme-ov-file#getting-started?utm_source=coolify.io", "documentation": "https://github.com/henrygd/beszel?tab=readme-ov-file#getting-started?utm_source=coolify.io",
@ -296,7 +296,7 @@
"category": "monitoring", "category": "monitoring",
"logo": "svgs/beszel.svg", "logo": "svgs/beszel.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-02-21T23:17:23+05:30", "template_last_updated_at": "2026-04-24T02:21:33+05:30",
"port": "8090" "port": "8090"
}, },
"bitcoin-core": { "bitcoin-core": {
@ -326,7 +326,7 @@
"category": "backend", "category": "backend",
"logo": "svgs/bluesky.svg", "logo": "svgs/bluesky.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-02-03T22:12:21+01:00", "template_last_updated_at": "2026-05-09T19:19:48+05:30",
"port": "3000" "port": "3000"
}, },
"bookstack": { "bookstack": {
@ -377,7 +377,7 @@
"category": "finance", "category": "finance",
"logo": "svgs/budge.png", "logo": "svgs/budge.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00" "template_last_updated_at": "2026-04-06T11:35:16-05:00"
}, },
"budibase": { "budibase": {
"documentation": "https://docs.budibase.com/docs/docker-compose?utm_source=coolify.io", "documentation": "https://docs.budibase.com/docs/docker-compose?utm_source=coolify.io",
@ -438,7 +438,7 @@
"category": "media", "category": "media",
"logo": "svgs/calibre-web-automated-with-downloader.png", "logo": "svgs/calibre-web-automated-with-downloader.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-08T21:32:32+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8083" "port": "8083"
}, },
"calibre-web": { "calibre-web": {
@ -475,7 +475,7 @@
"category": "security", "category": "security",
"logo": "svgs/cap-captcha.png", "logo": "svgs/cap-captcha.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null, "template_last_updated_at": "2026-04-23T01:07:14+05:30",
"port": "3000" "port": "3000"
}, },
"cap": { "cap": {
@ -493,7 +493,7 @@
"category": "media", "category": "media",
"logo": "svgs/cap.svg", "logo": "svgs/cap.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-10-14T23:42:45+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "5679" "port": "5679"
}, },
"castopod": { "castopod": {
@ -552,7 +552,7 @@
"category": "helpdesk", "category": "helpdesk",
"logo": "svgs/chaskiq.png", "logo": "svgs/chaskiq.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "3000" "port": "3000"
}, },
"chatwoot": { "chatwoot": {
@ -573,7 +573,7 @@
"category": "helpdesk", "category": "helpdesk",
"logo": "svgs/chatwoot.svg", "logo": "svgs/chatwoot.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-05-27T09:31:29-03:00",
"port": "3000" "port": "3000"
}, },
"checkmate": { "checkmate": {
@ -605,7 +605,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/chibisafe.svg", "logo": "svgs/chibisafe.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-12T22:51:57+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"chroma": { "chroma": {
@ -704,7 +704,7 @@
"category": "automation", "category": "automation",
"logo": "svgs/cloudflare-ddns.svg", "logo": "svgs/cloudflare-ddns.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null "template_last_updated_at": "2026-05-18T15:42:31+10:00"
}, },
"cloudflared": { "cloudflared": {
"documentation": "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/?utm_source=coolify.io", "documentation": "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/?utm_source=coolify.io",
@ -803,7 +803,7 @@
"category": "backend", "category": "backend",
"logo": "svgs/convex.svg", "logo": "svgs/convex.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-11-10T14:16:14+01:00", "template_last_updated_at": "2026-06-12T10:45:52+02:00",
"port": "6791" "port": "6791"
}, },
"cryptgeon": { "cryptgeon": {
@ -903,7 +903,7 @@
"category": "cms", "category": "cms",
"logo": "svgs/directus.svg", "logo": "svgs/directus.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-03T19:25:51+05:30",
"port": "8055" "port": "8055"
}, },
"directus": { "directus": {
@ -919,7 +919,7 @@
"category": "cms", "category": "cms",
"logo": "svgs/directus.svg", "logo": "svgs/directus.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-03T19:26:06+05:30",
"port": "8055" "port": "8055"
}, },
"diun": { "diun": {
@ -968,7 +968,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/docmost.png", "logo": "svgs/docmost.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-09-27T04:00:56+02:00", "template_last_updated_at": "2026-05-15T13:36:02+02:00",
"port": "3000" "port": "3000"
}, },
"documenso": { "documenso": {
@ -1044,7 +1044,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/dolibarr.png", "logo": "svgs/dolibarr.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-12-18T10:27:24+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"dozzle-with-auth": { "dozzle-with-auth": {
@ -1093,7 +1093,7 @@
"category": "devtools", "category": "devtools",
"logo": "svgs/drizzle.jpeg", "logo": "svgs/drizzle.jpeg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "4983" "port": "4983"
}, },
"drupal-with-postgresql": { "drupal-with-postgresql": {
@ -1161,7 +1161,7 @@
"category": "monitoring", "category": "monitoring",
"logo": "svgs/elasticsearch.svg", "logo": "svgs/elasticsearch.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-27T21:34:40+05:30", "template_last_updated_at": "2026-04-09T00:43:59-05:00",
"port": "5601" "port": "5601"
}, },
"elasticsearch": { "elasticsearch": {
@ -1248,7 +1248,7 @@
"category": "Networking", "category": "Networking",
"logo": "svgs/emqx-enterprise.svg", "logo": "svgs/emqx-enterprise.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null, "template_last_updated_at": "2026-04-27T09:25:48+03:00",
"port": "18083" "port": "18083"
}, },
"ente-photos-with-s3": { "ente-photos-with-s3": {
@ -1322,7 +1322,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/espocrm.svg", "logo": "svgs/espocrm.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-03-16T18:23:47+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"evolution-api": { "evolution-api": {
@ -1436,7 +1436,7 @@
"category": "finance", "category": "finance",
"logo": "svgs/firefly.svg", "logo": "svgs/firefly.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8080" "port": "8080"
}, },
"firefox": { "firefox": {
@ -1625,7 +1625,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/formbricks.png", "logo": "svgs/formbricks.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-27T21:12:57+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "3000" "port": "3000"
}, },
"foundryvtt": { "foundryvtt": {
@ -1642,7 +1642,7 @@
"category": "games", "category": "games",
"logo": "svgs/foundryvtt.png", "logo": "svgs/foundryvtt.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "30000" "port": "30000"
}, },
"freescout": { "freescout": {
@ -1658,7 +1658,7 @@
"category": "helpdesk", "category": "helpdesk",
"logo": "svgs/freescout.png", "logo": "svgs/freescout.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"freshrss-with-mariadb": { "freshrss-with-mariadb": {
@ -1672,7 +1672,7 @@
"category": "RSS", "category": "RSS",
"logo": "svgs/freshrss.png", "logo": "svgs/freshrss.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"freshrss-with-mysql": { "freshrss-with-mysql": {
@ -1686,7 +1686,7 @@
"category": "RSS", "category": "RSS",
"logo": "svgs/freshrss.png", "logo": "svgs/freshrss.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"freshrss-with-postgresql": { "freshrss-with-postgresql": {
@ -1700,7 +1700,7 @@
"category": "RSS", "category": "RSS",
"logo": "svgs/freshrss.png", "logo": "svgs/freshrss.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-04T17:41:25+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"freshrss": { "freshrss": {
@ -1714,7 +1714,7 @@
"category": "RSS", "category": "RSS",
"logo": "svgs/freshrss.png", "logo": "svgs/freshrss.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"garage": { "garage": {
@ -1732,7 +1732,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/garage.svg", "logo": "svgs/garage.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-12-10T12:57:34+01:00", "template_last_updated_at": "2026-05-31T23:57:46+02:00",
"port": "3900" "port": "3900"
}, },
"getoutline": { "getoutline": {
@ -1746,7 +1746,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/getoutline.jpeg", "logo": "svgs/getoutline.jpeg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-12-16T14:12:15+07:00", "template_last_updated_at": "2026-05-09T19:19:29+05:30",
"port": "3000" "port": "3000"
}, },
"ghost": { "ghost": {
@ -1779,7 +1779,7 @@
"category": "devtools", "category": "devtools",
"logo": "svgs/gitea.svg", "logo": "svgs/gitea.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null "template_last_updated_at": "2026-06-06T00:11:24+02:00"
}, },
"gitea-with-mariadb": { "gitea-with-mariadb": {
"documentation": "https://docs.gitea.com?utm_source=coolify.io", "documentation": "https://docs.gitea.com?utm_source=coolify.io",
@ -1910,7 +1910,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/glance.png", "logo": "svgs/glance.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-18T19:18:07+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8080" "port": "8080"
}, },
"glances": { "glances": {
@ -2000,7 +2000,7 @@
"category": "messaging", "category": "messaging",
"logo": "svgs/gotify.png", "logo": "svgs/gotify.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-10-08T11:43:21+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"gowa": { "gowa": {
@ -2017,7 +2017,7 @@
"category": "messaging", "category": "messaging",
"logo": "svgs/gowa.svg", "logo": "svgs/gowa.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "3000" "port": "3000"
}, },
"grafana-with-postgresql": { "grafana-with-postgresql": {
@ -2080,7 +2080,7 @@
"category": null, "category": null,
"logo": "svgs/grimmory.svg", "logo": "svgs/grimmory.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null, "template_last_updated_at": "2026-04-03T18:59:26+05:30",
"port": "80" "port": "80"
}, },
"grist": { "grist": {
@ -2114,7 +2114,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/grocy.svg", "logo": "svgs/grocy.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00" "template_last_updated_at": "2026-05-12T09:10:44+02:00"
}, },
"hatchet": { "hatchet": {
"documentation": "https://docs.hatchet.run/self-hosting/docker-compose?utm_source=coolify.io", "documentation": "https://docs.hatchet.run/self-hosting/docker-compose?utm_source=coolify.io",
@ -2129,7 +2129,7 @@
"category": "automation", "category": "automation",
"logo": "svgs/hatchet.svg", "logo": "svgs/hatchet.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-16T22:27:56+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"healthchecks": { "healthchecks": {
@ -2148,7 +2148,7 @@
"category": "monitoring", "category": "monitoring",
"logo": "svgs/healthchecks.webp", "logo": "svgs/healthchecks.webp",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null, "template_last_updated_at": "2026-05-25T13:06:59-04:00",
"port": "80000" "port": "80000"
}, },
"heimdall": { "heimdall": {
@ -2183,7 +2183,7 @@
"category": "ai", "category": "ai",
"logo": "svgs/hermes-agent.png", "logo": "svgs/hermes-agent.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null, "template_last_updated_at": "2026-05-29T12:14:17+05:30",
"port": "8787" "port": "8787"
}, },
"heyform": { "heyform": {
@ -2218,7 +2218,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/homarr.svg", "logo": "svgs/homarr.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-10-07T12:19:45+02:00", "template_last_updated_at": "2026-05-09T19:30:07+05:30",
"port": "7575" "port": "7575"
}, },
"home-assistant": { "home-assistant": {
@ -2253,7 +2253,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/homebox.svg", "logo": "svgs/homebox.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "7745" "port": "7745"
}, },
"homepage": { "homepage": {
@ -2361,7 +2361,7 @@
"category": "automation", "category": "automation",
"logo": "svgs/inngest.png", "logo": "svgs/inngest.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null, "template_last_updated_at": "2026-07-02T13:25:47+02:00",
"port": "8288" "port": "8288"
}, },
"invoice-ninja": { "invoice-ninja": {
@ -2378,7 +2378,7 @@
"category": "finance", "category": "finance",
"logo": "svgs/invoiceninja.png", "logo": "svgs/invoiceninja.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "9000" "port": "9000"
}, },
"it-tools": { "it-tools": {
@ -2410,7 +2410,7 @@
"category": "media", "category": "media",
"logo": "svgs/jellyfin.svg", "logo": "svgs/jellyfin.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-05-12T08:58:05+02:00",
"port": "8096" "port": "8096"
}, },
"jenkins": { "jenkins": {
@ -2443,7 +2443,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/jitsi.svg", "logo": "svgs/jitsi.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-24T09:40:01+05:30",
"port": "80" "port": "80"
}, },
"joomla-with-mariadb": { "joomla-with-mariadb": {
@ -2672,7 +2672,7 @@
"category": "ai", "category": "ai",
"logo": "svgs/langfuse.svg", "logo": "svgs/langfuse.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-03-28T16:21:57+01:00", "template_last_updated_at": "2026-04-23T18:08:40+02:00",
"port": "3000" "port": "3000"
}, },
"leantime": { "leantime": {
@ -2713,7 +2713,7 @@
"category": "ai", "category": "ai",
"logo": "svgs/librechat.svg", "logo": "svgs/librechat.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-03T18:29:44+05:30",
"port": "3080" "port": "3080"
}, },
"libreoffice": { "libreoffice": {
@ -2881,7 +2881,7 @@
"category": "auth", "category": "auth",
"logo": "svgs/logto_dark.svg", "logo": "svgs/logto_dark.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00" "template_last_updated_at": "2026-04-01T13:19:47Z"
}, },
"lowcoder": { "lowcoder": {
"documentation": "https://docs.lowcoder.cloud/?utm_source=coolify.io", "documentation": "https://docs.lowcoder.cloud/?utm_source=coolify.io",
@ -3005,7 +3005,7 @@
"category": "messaging", "category": "messaging",
"logo": "svgs/mattermost.svg", "logo": "svgs/mattermost.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-09-04T16:02:48+03:30", "template_last_updated_at": "2026-04-06T13:57:40-05:00",
"port": "8065" "port": "8065"
}, },
"mealie": { "mealie": {
@ -3021,7 +3021,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/mealie.png", "logo": "svgs/mealie.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-05-12T09:22:15+02:00",
"port": "9000" "port": "9000"
}, },
"mediawiki": { "mediawiki": {
@ -3149,7 +3149,7 @@
"category": "games", "category": "games",
"logo": "svgs/minecraft.svg", "logo": "svgs/minecraft.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-01T15:06:17-05:00",
"port": "25565" "port": "25565"
}, },
"miniflux": { "miniflux": {
@ -3166,7 +3166,7 @@
"category": "RSS", "category": "RSS",
"logo": "svgs/miniflux.svg", "logo": "svgs/miniflux.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8080" "port": "8080"
}, },
"mixpost": { "mixpost": {
@ -3242,7 +3242,7 @@
"category": "automation", "category": "automation",
"logo": "svgs/n8n.png", "logo": "svgs/n8n.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-03-08T01:08:18-05:00", "template_last_updated_at": "2026-03-30T20:40:07+02:00",
"port": "5678" "port": "5678"
}, },
"n8n-with-postgresql": { "n8n-with-postgresql": {
@ -3261,7 +3261,7 @@
"category": "automation", "category": "automation",
"logo": "svgs/n8n.png", "logo": "svgs/n8n.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-02-27T22:41:52-05:00", "template_last_updated_at": "2026-03-30T20:40:07+02:00",
"port": "5678" "port": "5678"
}, },
"n8n": { "n8n": {
@ -3280,7 +3280,7 @@
"category": "automation", "category": "automation",
"logo": "svgs/n8n.png", "logo": "svgs/n8n.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-02-27T22:41:52-05:00", "template_last_updated_at": "2026-03-30T20:40:07+02:00",
"port": "5678" "port": "5678"
}, },
"navidrome": { "navidrome": {
@ -3328,7 +3328,7 @@
"category": "vpn", "category": "vpn",
"logo": "svgs/netbird.png", "logo": "svgs/netbird.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-11-16T10:33:10+05:30" "template_last_updated_at": "2026-04-09T00:00:38+05:30"
}, },
"newapi": { "newapi": {
"documentation": "https://docs.newapi.pro/en/getting-started/?utm_source=coolify.io", "documentation": "https://docs.newapi.pro/en/getting-started/?utm_source=coolify.io",
@ -3360,7 +3360,7 @@
"category": "proxy", "category": "proxy",
"logo": "svgs/pangolin-logo.png", "logo": "svgs/pangolin-logo.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-11-12T11:59:36-03:00" "template_last_updated_at": "2026-04-06T11:35:16-05:00"
}, },
"next-image-transformation": { "next-image-transformation": {
"documentation": "https://github.com/coollabsio/next-image-transformation?utm_source=coolify.io", "documentation": "https://github.com/coollabsio/next-image-transformation?utm_source=coolify.io",
@ -3392,7 +3392,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/nextcloud.svg", "logo": "svgs/nextcloud.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-07T23:32:57+05:30",
"port": "80" "port": "80"
}, },
"nextcloud-with-mysql": { "nextcloud-with-mysql": {
@ -3409,7 +3409,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/nextcloud.svg", "logo": "svgs/nextcloud.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-07T23:32:57+05:30",
"port": "80" "port": "80"
}, },
"nextcloud-with-postgres": { "nextcloud-with-postgres": {
@ -3426,7 +3426,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/nextcloud.svg", "logo": "svgs/nextcloud.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-07T23:32:57+05:30",
"port": "80" "port": "80"
}, },
"nextcloud": { "nextcloud": {
@ -3443,7 +3443,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/nextcloud.svg", "logo": "svgs/nextcloud.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-07T23:32:57+05:30",
"port": "80" "port": "80"
}, },
"nexus-arm": { "nexus-arm": {
@ -3554,7 +3554,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/nocodb.svg", "logo": "svgs/nocodb.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8080" "port": "8080"
}, },
"nodebb": { "nodebb": {
@ -3621,7 +3621,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/odoo.svg", "logo": "svgs/odoo.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8069" "port": "8069"
}, },
"ollama-with-open-webui": { "ollama-with-open-webui": {
@ -3712,7 +3712,7 @@
"category": "email", "category": "email",
"logo": "svgs/openarchiver.svg", "logo": "svgs/openarchiver.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-12T23:41:36+01:00", "template_last_updated_at": "2026-05-09T19:35:15+05:30",
"port": "3000" "port": "3000"
}, },
"open-webui": { "open-webui": {
@ -3770,7 +3770,7 @@
"category": "monitoring", "category": "monitoring",
"logo": "svgs/openobserve.svg", "logo": "svgs/openobserve.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null, "template_last_updated_at": "2026-05-19T16:40:18+05:30",
"port": "5080" "port": "5080"
}, },
"openpanel": { "openpanel": {
@ -3789,7 +3789,7 @@
"category": "analytics", "category": "analytics",
"logo": "svgs/openpanel.svg", "logo": "svgs/openpanel.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-27T20:48:57+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "3000" "port": "3000"
}, },
"opnform": { "opnform": {
@ -3810,7 +3810,7 @@
"category": "productivity", "category": "productivity",
"logo": "svg/opnform.svg", "logo": "svg/opnform.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-11-24T10:13:08+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"orangehrm": { "orangehrm": {
@ -3828,7 +3828,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/orangehrm.svg", "logo": "svgs/orangehrm.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"organizr": { "organizr": {
@ -3857,7 +3857,7 @@
"category": "helpdesk", "category": "helpdesk",
"logo": "svgs/osticket.png", "logo": "svgs/osticket.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"overseerr": { "overseerr": {
@ -3891,7 +3891,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/owncloud.svg", "logo": "svgs/owncloud.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-06-02T12:12:42+03:00",
"port": "8080" "port": "8080"
}, },
"pairdrop": { "pairdrop": {
@ -3918,7 +3918,7 @@
"category": "games", "category": "games",
"logo": "svgs/default.webp", "logo": "svgs/default.webp",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-11-15T21:55:50+01:00" "template_last_updated_at": "2026-04-06T11:35:16-05:00"
}, },
"paperless": { "paperless": {
"documentation": "https://docs.paperless-ngx.com/configuration/?utm_source=coolify.io", "documentation": "https://docs.paperless-ngx.com/configuration/?utm_source=coolify.io",
@ -3959,7 +3959,7 @@
"category": "finance", "category": "finance",
"logo": "svgs/paymenter.svg", "logo": "svgs/paymenter.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-11-23T12:08:21+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"penpot-with-s3": { "penpot-with-s3": {
@ -4075,7 +4075,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/plane.svg", "logo": "svgs/plane.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-02-24T02:34:35+05:30", "template_last_updated_at": "2026-04-24T00:12:17+05:30",
"port": "80" "port": "80"
}, },
"plex": { "plex": {
@ -4108,7 +4108,7 @@
"category": "email", "category": "email",
"logo": "svgs/plunk.svg", "logo": "svgs/plunk.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "3000" "port": "3000"
}, },
"pocket-id-with-postgresql": { "pocket-id-with-postgresql": {
@ -4265,7 +4265,7 @@
"category": "proxy", "category": "proxy",
"logo": "svgs/hoppscotch.png", "logo": "svgs/hoppscotch.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-10-13T19:24:05+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "9159" "port": "9159"
}, },
"qbittorrent": { "qbittorrent": {
@ -4351,7 +4351,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/rallly.svg", "logo": "svgs/rallly.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-03-19T16:11:50+01:00",
"port": "3000" "port": "3000"
}, },
"reactive-resume": { "reactive-resume": {
@ -4428,7 +4428,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/redmine.svg", "logo": "svgs/redmine.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-16T22:27:56+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "3000" "port": "3000"
}, },
"rivet-engine": { "rivet-engine": {
@ -4446,7 +4446,7 @@
"category": "development", "category": "development",
"logo": "svgs/rivet.svg", "logo": "svgs/rivet.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-10-22T16:07:42+02:00", "template_last_updated_at": "2026-04-01T19:58:30+02:00",
"port": "6420" "port": "6420"
}, },
"rocketchat": { "rocketchat": {
@ -4502,7 +4502,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/ryot.svg", "logo": "svgs/ryot.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-05-12T08:34:57+02:00",
"port": "8000" "port": "8000"
}, },
"satisfactory": { "satisfactory": {
@ -4621,7 +4621,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/sftpgo.png", "logo": "svgs/sftpgo.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-12T23:44:06+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8080" "port": "8080"
}, },
"shlink": { "shlink": {
@ -4660,7 +4660,7 @@
"category": "monitoring", "category": "monitoring",
"logo": "svgs/signoz.svg", "logo": "svgs/signoz.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-11T17:51:10+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8080" "port": "8080"
}, },
"silverbullet": { "silverbullet": {
@ -4675,7 +4675,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/silverbullet.png", "logo": "svgs/silverbullet.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-05T15:42:12+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "3000" "port": "3000"
}, },
"siyuan": { "siyuan": {
@ -4759,7 +4759,7 @@
"category": "devtools", "category": "devtools",
"logo": "svgs/soketi-app-manager.svg", "logo": "svgs/soketi-app-manager.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-16T22:27:17+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8080" "port": "8080"
}, },
"soketi": { "soketi": {
@ -4898,7 +4898,7 @@
"category": "backend", "category": "backend",
"logo": "svgs/supabase.svg", "logo": "svgs/supabase.svg",
"minversion": "4.0.0-beta.228", "minversion": "4.0.0-beta.228",
"template_last_updated_at": "2026-01-20T19:10:07+01:00", "template_last_updated_at": "2026-04-05T20:21:11+02:00",
"port": "8000" "port": "8000"
}, },
"superset-with-postgresql": { "superset-with-postgresql": {
@ -5176,7 +5176,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/twenty.svg", "logo": "svgs/twenty.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-08T18:51:23+01:00", "template_last_updated_at": "2026-04-16T23:18:19+05:30",
"port": "3000" "port": "3000"
}, },
"typesense": { "typesense": {
@ -5224,7 +5224,7 @@
"category": "devtools", "category": "devtools",
"logo": "svgs/unleash.svg", "logo": "svgs/unleash.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-04T19:45:35+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "4242" "port": "4242"
}, },
"unleash-without-database": { "unleash-without-database": {
@ -5241,7 +5241,7 @@
"category": "devtools", "category": "devtools",
"logo": "svgs/unleash.svg", "logo": "svgs/unleash.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "4242" "port": "4242"
}, },
"unstructured": { "unstructured": {
@ -5339,7 +5339,7 @@
"category": "email", "category": "email",
"logo": "svgs/usesend.svg", "logo": "svgs/usesend.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-12-07T17:32:23+11:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "3000" "port": "3000"
}, },
"vaultwarden": { "vaultwarden": {
@ -5774,7 +5774,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/cells.svg", "logo": "svgs/cells.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-02-13T18:47:54+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8080" "port": "8080"
} }
} }

View file

@ -31,7 +31,7 @@
"category": "finance", "category": "finance",
"logo": "svgs/actualbudget.png", "logo": "svgs/actualbudget.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "5006" "port": "5006"
}, },
"affine": { "affine": {
@ -64,7 +64,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/alexandrie.svg", "logo": "svgs/alexandrie.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-27T21:03:32+01:00", "template_last_updated_at": "2026-04-05T13:36:24+02:00",
"port": "8200" "port": "8200"
}, },
"anythingllm": { "anythingllm": {
@ -175,7 +175,7 @@
"category": "ai", "category": "ai",
"logo": "svgs/argilla.png", "logo": "svgs/argilla.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "6900" "port": "6900"
}, },
"audiobookshelf": { "audiobookshelf": {
@ -192,7 +192,7 @@
"category": "media", "category": "media",
"logo": "svgs/audiobookshelf.svg", "logo": "svgs/audiobookshelf.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-05-12T09:09:36+02:00",
"port": "80" "port": "80"
}, },
"authentik": { "authentik": {
@ -231,7 +231,7 @@
"category": "database", "category": "database",
"logo": "svgs/autobase.svg", "logo": "svgs/autobase.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-19T18:13:51+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"babybuddy": { "babybuddy": {
@ -280,7 +280,7 @@
"category": "monitoring", "category": "monitoring",
"logo": "svgs/beszel.svg", "logo": "svgs/beszel.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-02-21T23:17:23+05:30" "template_last_updated_at": "2026-04-24T02:22:08+05:30"
}, },
"beszel": { "beszel": {
"documentation": "https://github.com/henrygd/beszel?tab=readme-ov-file#getting-started?utm_source=coolify.io", "documentation": "https://github.com/henrygd/beszel?tab=readme-ov-file#getting-started?utm_source=coolify.io",
@ -296,7 +296,7 @@
"category": "monitoring", "category": "monitoring",
"logo": "svgs/beszel.svg", "logo": "svgs/beszel.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-02-21T23:17:23+05:30", "template_last_updated_at": "2026-04-24T02:21:33+05:30",
"port": "8090" "port": "8090"
}, },
"bitcoin-core": { "bitcoin-core": {
@ -326,7 +326,7 @@
"category": "backend", "category": "backend",
"logo": "svgs/bluesky.svg", "logo": "svgs/bluesky.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-02-03T22:12:21+01:00", "template_last_updated_at": "2026-05-09T19:19:48+05:30",
"port": "3000" "port": "3000"
}, },
"bookstack": { "bookstack": {
@ -377,7 +377,7 @@
"category": "finance", "category": "finance",
"logo": "svgs/budge.png", "logo": "svgs/budge.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00" "template_last_updated_at": "2026-04-06T11:35:16-05:00"
}, },
"budibase": { "budibase": {
"documentation": "https://docs.budibase.com/docs/docker-compose?utm_source=coolify.io", "documentation": "https://docs.budibase.com/docs/docker-compose?utm_source=coolify.io",
@ -438,7 +438,7 @@
"category": "media", "category": "media",
"logo": "svgs/calibre-web-automated-with-downloader.png", "logo": "svgs/calibre-web-automated-with-downloader.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-08T21:32:32+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8083" "port": "8083"
}, },
"calibre-web": { "calibre-web": {
@ -475,7 +475,7 @@
"category": "security", "category": "security",
"logo": "svgs/cap-captcha.png", "logo": "svgs/cap-captcha.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null, "template_last_updated_at": "2026-04-23T01:07:14+05:30",
"port": "3000" "port": "3000"
}, },
"cap": { "cap": {
@ -493,7 +493,7 @@
"category": "media", "category": "media",
"logo": "svgs/cap.svg", "logo": "svgs/cap.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-10-14T23:42:45+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "5679" "port": "5679"
}, },
"castopod": { "castopod": {
@ -552,7 +552,7 @@
"category": "helpdesk", "category": "helpdesk",
"logo": "svgs/chaskiq.png", "logo": "svgs/chaskiq.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "3000" "port": "3000"
}, },
"chatwoot": { "chatwoot": {
@ -573,7 +573,7 @@
"category": "helpdesk", "category": "helpdesk",
"logo": "svgs/chatwoot.svg", "logo": "svgs/chatwoot.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-05-27T09:31:29-03:00",
"port": "3000" "port": "3000"
}, },
"checkmate": { "checkmate": {
@ -605,7 +605,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/chibisafe.svg", "logo": "svgs/chibisafe.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-12T22:51:57+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"chroma": { "chroma": {
@ -704,7 +704,7 @@
"category": "automation", "category": "automation",
"logo": "svgs/cloudflare-ddns.svg", "logo": "svgs/cloudflare-ddns.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null "template_last_updated_at": "2026-05-18T15:42:31+10:00"
}, },
"cloudflared": { "cloudflared": {
"documentation": "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/?utm_source=coolify.io", "documentation": "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/?utm_source=coolify.io",
@ -803,7 +803,7 @@
"category": "backend", "category": "backend",
"logo": "svgs/convex.svg", "logo": "svgs/convex.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-11-10T14:16:14+01:00", "template_last_updated_at": "2026-06-12T10:45:52+02:00",
"port": "6791" "port": "6791"
}, },
"cryptgeon": { "cryptgeon": {
@ -903,7 +903,7 @@
"category": "cms", "category": "cms",
"logo": "svgs/directus.svg", "logo": "svgs/directus.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-03T19:25:51+05:30",
"port": "8055" "port": "8055"
}, },
"directus": { "directus": {
@ -919,7 +919,7 @@
"category": "cms", "category": "cms",
"logo": "svgs/directus.svg", "logo": "svgs/directus.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-03T19:26:06+05:30",
"port": "8055" "port": "8055"
}, },
"diun": { "diun": {
@ -968,7 +968,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/docmost.png", "logo": "svgs/docmost.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-09-27T04:00:56+02:00", "template_last_updated_at": "2026-05-15T13:36:02+02:00",
"port": "3000" "port": "3000"
}, },
"documenso": { "documenso": {
@ -1044,7 +1044,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/dolibarr.png", "logo": "svgs/dolibarr.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-12-18T10:27:24+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"dozzle-with-auth": { "dozzle-with-auth": {
@ -1093,7 +1093,7 @@
"category": "devtools", "category": "devtools",
"logo": "svgs/drizzle.jpeg", "logo": "svgs/drizzle.jpeg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "4983" "port": "4983"
}, },
"drupal-with-postgresql": { "drupal-with-postgresql": {
@ -1161,7 +1161,7 @@
"category": "monitoring", "category": "monitoring",
"logo": "svgs/elasticsearch.svg", "logo": "svgs/elasticsearch.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-27T21:34:40+05:30", "template_last_updated_at": "2026-04-09T00:43:59-05:00",
"port": "5601" "port": "5601"
}, },
"elasticsearch": { "elasticsearch": {
@ -1248,7 +1248,7 @@
"category": "Networking", "category": "Networking",
"logo": "svgs/emqx-enterprise.svg", "logo": "svgs/emqx-enterprise.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null, "template_last_updated_at": "2026-04-27T09:25:48+03:00",
"port": "18083" "port": "18083"
}, },
"ente-photos-with-s3": { "ente-photos-with-s3": {
@ -1322,7 +1322,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/espocrm.svg", "logo": "svgs/espocrm.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-03-16T18:23:47+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"evolution-api": { "evolution-api": {
@ -1436,7 +1436,7 @@
"category": "finance", "category": "finance",
"logo": "svgs/firefly.svg", "logo": "svgs/firefly.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8080" "port": "8080"
}, },
"firefox": { "firefox": {
@ -1625,7 +1625,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/formbricks.png", "logo": "svgs/formbricks.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-27T21:12:57+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "3000" "port": "3000"
}, },
"foundryvtt": { "foundryvtt": {
@ -1642,7 +1642,7 @@
"category": "games", "category": "games",
"logo": "svgs/foundryvtt.png", "logo": "svgs/foundryvtt.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "30000" "port": "30000"
}, },
"freescout": { "freescout": {
@ -1658,7 +1658,7 @@
"category": "helpdesk", "category": "helpdesk",
"logo": "svgs/freescout.png", "logo": "svgs/freescout.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"freshrss-with-mariadb": { "freshrss-with-mariadb": {
@ -1672,7 +1672,7 @@
"category": "RSS", "category": "RSS",
"logo": "svgs/freshrss.png", "logo": "svgs/freshrss.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"freshrss-with-mysql": { "freshrss-with-mysql": {
@ -1686,7 +1686,7 @@
"category": "RSS", "category": "RSS",
"logo": "svgs/freshrss.png", "logo": "svgs/freshrss.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"freshrss-with-postgresql": { "freshrss-with-postgresql": {
@ -1700,7 +1700,7 @@
"category": "RSS", "category": "RSS",
"logo": "svgs/freshrss.png", "logo": "svgs/freshrss.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-04T17:41:25+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"freshrss": { "freshrss": {
@ -1714,7 +1714,7 @@
"category": "RSS", "category": "RSS",
"logo": "svgs/freshrss.png", "logo": "svgs/freshrss.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"garage": { "garage": {
@ -1732,7 +1732,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/garage.svg", "logo": "svgs/garage.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-12-10T12:57:34+01:00", "template_last_updated_at": "2026-05-31T23:57:46+02:00",
"port": "3900" "port": "3900"
}, },
"getoutline": { "getoutline": {
@ -1746,7 +1746,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/getoutline.jpeg", "logo": "svgs/getoutline.jpeg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-12-16T14:12:15+07:00", "template_last_updated_at": "2026-05-09T19:19:29+05:30",
"port": "3000" "port": "3000"
}, },
"ghost": { "ghost": {
@ -1779,7 +1779,7 @@
"category": "devtools", "category": "devtools",
"logo": "svgs/gitea.svg", "logo": "svgs/gitea.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null "template_last_updated_at": "2026-06-06T00:11:24+02:00"
}, },
"gitea-with-mariadb": { "gitea-with-mariadb": {
"documentation": "https://docs.gitea.com?utm_source=coolify.io", "documentation": "https://docs.gitea.com?utm_source=coolify.io",
@ -1910,7 +1910,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/glance.png", "logo": "svgs/glance.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-18T19:18:07+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8080" "port": "8080"
}, },
"glances": { "glances": {
@ -2000,7 +2000,7 @@
"category": "messaging", "category": "messaging",
"logo": "svgs/gotify.png", "logo": "svgs/gotify.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-10-08T11:43:21+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"gowa": { "gowa": {
@ -2017,7 +2017,7 @@
"category": "messaging", "category": "messaging",
"logo": "svgs/gowa.svg", "logo": "svgs/gowa.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "3000" "port": "3000"
}, },
"grafana-with-postgresql": { "grafana-with-postgresql": {
@ -2080,7 +2080,7 @@
"category": null, "category": null,
"logo": "svgs/grimmory.svg", "logo": "svgs/grimmory.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null, "template_last_updated_at": "2026-04-03T18:59:26+05:30",
"port": "80" "port": "80"
}, },
"grist": { "grist": {
@ -2114,7 +2114,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/grocy.svg", "logo": "svgs/grocy.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00" "template_last_updated_at": "2026-05-12T09:10:44+02:00"
}, },
"hatchet": { "hatchet": {
"documentation": "https://docs.hatchet.run/self-hosting/docker-compose?utm_source=coolify.io", "documentation": "https://docs.hatchet.run/self-hosting/docker-compose?utm_source=coolify.io",
@ -2129,7 +2129,7 @@
"category": "automation", "category": "automation",
"logo": "svgs/hatchet.svg", "logo": "svgs/hatchet.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-16T22:27:56+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"healthchecks": { "healthchecks": {
@ -2148,7 +2148,7 @@
"category": "monitoring", "category": "monitoring",
"logo": "svgs/healthchecks.webp", "logo": "svgs/healthchecks.webp",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null, "template_last_updated_at": "2026-05-25T13:06:59-04:00",
"port": "80000" "port": "80000"
}, },
"heimdall": { "heimdall": {
@ -2183,7 +2183,7 @@
"category": "ai", "category": "ai",
"logo": "svgs/hermes-agent.png", "logo": "svgs/hermes-agent.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null, "template_last_updated_at": "2026-05-29T12:14:17+05:30",
"port": "8787" "port": "8787"
}, },
"heyform": { "heyform": {
@ -2218,7 +2218,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/homarr.svg", "logo": "svgs/homarr.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-10-07T12:19:45+02:00", "template_last_updated_at": "2026-05-09T19:30:07+05:30",
"port": "7575" "port": "7575"
}, },
"home-assistant": { "home-assistant": {
@ -2253,7 +2253,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/homebox.svg", "logo": "svgs/homebox.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "7745" "port": "7745"
}, },
"homepage": { "homepage": {
@ -2361,7 +2361,7 @@
"category": "automation", "category": "automation",
"logo": "svgs/inngest.png", "logo": "svgs/inngest.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null, "template_last_updated_at": "2026-07-02T13:25:47+02:00",
"port": "8288" "port": "8288"
}, },
"invoice-ninja": { "invoice-ninja": {
@ -2378,7 +2378,7 @@
"category": "finance", "category": "finance",
"logo": "svgs/invoiceninja.png", "logo": "svgs/invoiceninja.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "9000" "port": "9000"
}, },
"it-tools": { "it-tools": {
@ -2410,7 +2410,7 @@
"category": "media", "category": "media",
"logo": "svgs/jellyfin.svg", "logo": "svgs/jellyfin.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-05-12T08:58:05+02:00",
"port": "8096" "port": "8096"
}, },
"jenkins": { "jenkins": {
@ -2443,7 +2443,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/jitsi.svg", "logo": "svgs/jitsi.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-24T09:40:01+05:30",
"port": "80" "port": "80"
}, },
"joomla-with-mariadb": { "joomla-with-mariadb": {
@ -2672,7 +2672,7 @@
"category": "ai", "category": "ai",
"logo": "svgs/langfuse.svg", "logo": "svgs/langfuse.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-03-28T16:21:57+01:00", "template_last_updated_at": "2026-04-23T18:08:40+02:00",
"port": "3000" "port": "3000"
}, },
"leantime": { "leantime": {
@ -2713,7 +2713,7 @@
"category": "ai", "category": "ai",
"logo": "svgs/librechat.svg", "logo": "svgs/librechat.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-03T18:29:44+05:30",
"port": "3080" "port": "3080"
}, },
"libreoffice": { "libreoffice": {
@ -2881,7 +2881,7 @@
"category": "auth", "category": "auth",
"logo": "svgs/logto_dark.svg", "logo": "svgs/logto_dark.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00" "template_last_updated_at": "2026-04-01T13:19:47Z"
}, },
"lowcoder": { "lowcoder": {
"documentation": "https://docs.lowcoder.cloud/?utm_source=coolify.io", "documentation": "https://docs.lowcoder.cloud/?utm_source=coolify.io",
@ -3005,7 +3005,7 @@
"category": "messaging", "category": "messaging",
"logo": "svgs/mattermost.svg", "logo": "svgs/mattermost.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-09-04T16:02:48+03:30", "template_last_updated_at": "2026-04-06T13:57:40-05:00",
"port": "8065" "port": "8065"
}, },
"mealie": { "mealie": {
@ -3021,7 +3021,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/mealie.png", "logo": "svgs/mealie.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-05-12T09:22:15+02:00",
"port": "9000" "port": "9000"
}, },
"mediawiki": { "mediawiki": {
@ -3149,7 +3149,7 @@
"category": "games", "category": "games",
"logo": "svgs/minecraft.svg", "logo": "svgs/minecraft.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-01T15:06:17-05:00",
"port": "25565" "port": "25565"
}, },
"miniflux": { "miniflux": {
@ -3166,7 +3166,7 @@
"category": "RSS", "category": "RSS",
"logo": "svgs/miniflux.svg", "logo": "svgs/miniflux.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8080" "port": "8080"
}, },
"mixpost": { "mixpost": {
@ -3242,7 +3242,7 @@
"category": "automation", "category": "automation",
"logo": "svgs/n8n.png", "logo": "svgs/n8n.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-03-08T01:08:18-05:00", "template_last_updated_at": "2026-03-30T20:40:07+02:00",
"port": "5678" "port": "5678"
}, },
"n8n-with-postgresql": { "n8n-with-postgresql": {
@ -3261,7 +3261,7 @@
"category": "automation", "category": "automation",
"logo": "svgs/n8n.png", "logo": "svgs/n8n.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-02-27T22:41:52-05:00", "template_last_updated_at": "2026-03-30T20:40:07+02:00",
"port": "5678" "port": "5678"
}, },
"n8n": { "n8n": {
@ -3280,7 +3280,7 @@
"category": "automation", "category": "automation",
"logo": "svgs/n8n.png", "logo": "svgs/n8n.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-02-27T22:41:52-05:00", "template_last_updated_at": "2026-03-30T20:40:07+02:00",
"port": "5678" "port": "5678"
}, },
"navidrome": { "navidrome": {
@ -3328,7 +3328,7 @@
"category": "vpn", "category": "vpn",
"logo": "svgs/netbird.png", "logo": "svgs/netbird.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-11-16T10:33:10+05:30" "template_last_updated_at": "2026-04-09T00:00:38+05:30"
}, },
"newapi": { "newapi": {
"documentation": "https://docs.newapi.pro/en/getting-started/?utm_source=coolify.io", "documentation": "https://docs.newapi.pro/en/getting-started/?utm_source=coolify.io",
@ -3360,7 +3360,7 @@
"category": "proxy", "category": "proxy",
"logo": "svgs/pangolin-logo.png", "logo": "svgs/pangolin-logo.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-11-12T11:59:36-03:00" "template_last_updated_at": "2026-04-06T11:35:16-05:00"
}, },
"next-image-transformation": { "next-image-transformation": {
"documentation": "https://github.com/coollabsio/next-image-transformation?utm_source=coolify.io", "documentation": "https://github.com/coollabsio/next-image-transformation?utm_source=coolify.io",
@ -3392,7 +3392,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/nextcloud.svg", "logo": "svgs/nextcloud.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-07T23:32:57+05:30",
"port": "80" "port": "80"
}, },
"nextcloud-with-mysql": { "nextcloud-with-mysql": {
@ -3409,7 +3409,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/nextcloud.svg", "logo": "svgs/nextcloud.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-07T23:32:57+05:30",
"port": "80" "port": "80"
}, },
"nextcloud-with-postgres": { "nextcloud-with-postgres": {
@ -3426,7 +3426,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/nextcloud.svg", "logo": "svgs/nextcloud.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-07T23:32:57+05:30",
"port": "80" "port": "80"
}, },
"nextcloud": { "nextcloud": {
@ -3443,7 +3443,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/nextcloud.svg", "logo": "svgs/nextcloud.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-07T23:32:57+05:30",
"port": "80" "port": "80"
}, },
"nexus-arm": { "nexus-arm": {
@ -3554,7 +3554,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/nocodb.svg", "logo": "svgs/nocodb.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8080" "port": "8080"
}, },
"nodebb": { "nodebb": {
@ -3621,7 +3621,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/odoo.svg", "logo": "svgs/odoo.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8069" "port": "8069"
}, },
"ollama-with-open-webui": { "ollama-with-open-webui": {
@ -3712,7 +3712,7 @@
"category": "email", "category": "email",
"logo": "svgs/openarchiver.svg", "logo": "svgs/openarchiver.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-12T23:41:36+01:00", "template_last_updated_at": "2026-05-09T19:35:15+05:30",
"port": "3000" "port": "3000"
}, },
"open-webui": { "open-webui": {
@ -3770,7 +3770,7 @@
"category": "monitoring", "category": "monitoring",
"logo": "svgs/openobserve.svg", "logo": "svgs/openobserve.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": null, "template_last_updated_at": "2026-05-19T16:40:18+05:30",
"port": "5080" "port": "5080"
}, },
"openpanel": { "openpanel": {
@ -3789,7 +3789,7 @@
"category": "analytics", "category": "analytics",
"logo": "svgs/openpanel.svg", "logo": "svgs/openpanel.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-27T20:48:57+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "3000" "port": "3000"
}, },
"opnform": { "opnform": {
@ -3810,7 +3810,7 @@
"category": "productivity", "category": "productivity",
"logo": "svg/opnform.svg", "logo": "svg/opnform.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-11-24T10:13:08+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"orangehrm": { "orangehrm": {
@ -3828,7 +3828,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/orangehrm.svg", "logo": "svgs/orangehrm.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"organizr": { "organizr": {
@ -3857,7 +3857,7 @@
"category": "helpdesk", "category": "helpdesk",
"logo": "svgs/osticket.png", "logo": "svgs/osticket.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"overseerr": { "overseerr": {
@ -3891,7 +3891,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/owncloud.svg", "logo": "svgs/owncloud.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-06-02T12:12:42+03:00",
"port": "8080" "port": "8080"
}, },
"pairdrop": { "pairdrop": {
@ -3918,7 +3918,7 @@
"category": "games", "category": "games",
"logo": "svgs/default.webp", "logo": "svgs/default.webp",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-11-15T21:55:50+01:00" "template_last_updated_at": "2026-04-06T11:35:16-05:00"
}, },
"paperless": { "paperless": {
"documentation": "https://docs.paperless-ngx.com/configuration/?utm_source=coolify.io", "documentation": "https://docs.paperless-ngx.com/configuration/?utm_source=coolify.io",
@ -3959,7 +3959,7 @@
"category": "finance", "category": "finance",
"logo": "svgs/paymenter.svg", "logo": "svgs/paymenter.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-11-23T12:08:21+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "80" "port": "80"
}, },
"penpot-with-s3": { "penpot-with-s3": {
@ -4075,7 +4075,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/plane.svg", "logo": "svgs/plane.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-02-24T02:34:35+05:30", "template_last_updated_at": "2026-04-24T00:12:17+05:30",
"port": "80" "port": "80"
}, },
"plex": { "plex": {
@ -4108,7 +4108,7 @@
"category": "email", "category": "email",
"logo": "svgs/plunk.svg", "logo": "svgs/plunk.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "3000" "port": "3000"
}, },
"pocket-id-with-postgresql": { "pocket-id-with-postgresql": {
@ -4265,7 +4265,7 @@
"category": "proxy", "category": "proxy",
"logo": "svgs/hoppscotch.png", "logo": "svgs/hoppscotch.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-10-13T19:24:05+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "9159" "port": "9159"
}, },
"qbittorrent": { "qbittorrent": {
@ -4351,7 +4351,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/rallly.svg", "logo": "svgs/rallly.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-03-19T16:11:50+01:00",
"port": "3000" "port": "3000"
}, },
"reactive-resume": { "reactive-resume": {
@ -4428,7 +4428,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/redmine.svg", "logo": "svgs/redmine.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-16T22:27:56+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "3000" "port": "3000"
}, },
"rivet-engine": { "rivet-engine": {
@ -4446,7 +4446,7 @@
"category": "development", "category": "development",
"logo": "svgs/rivet.svg", "logo": "svgs/rivet.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-10-22T16:07:42+02:00", "template_last_updated_at": "2026-04-01T19:58:30+02:00",
"port": "6420" "port": "6420"
}, },
"rocketchat": { "rocketchat": {
@ -4502,7 +4502,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/ryot.svg", "logo": "svgs/ryot.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-05-12T08:34:57+02:00",
"port": "8000" "port": "8000"
}, },
"satisfactory": { "satisfactory": {
@ -4621,7 +4621,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/sftpgo.png", "logo": "svgs/sftpgo.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-12T23:44:06+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8080" "port": "8080"
}, },
"shlink": { "shlink": {
@ -4660,7 +4660,7 @@
"category": "monitoring", "category": "monitoring",
"logo": "svgs/signoz.svg", "logo": "svgs/signoz.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-11T17:51:10+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8080" "port": "8080"
}, },
"silverbullet": { "silverbullet": {
@ -4675,7 +4675,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/silverbullet.png", "logo": "svgs/silverbullet.png",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-05T15:42:12+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "3000" "port": "3000"
}, },
"siyuan": { "siyuan": {
@ -4759,7 +4759,7 @@
"category": "devtools", "category": "devtools",
"logo": "svgs/soketi-app-manager.svg", "logo": "svgs/soketi-app-manager.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-16T22:27:17+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8080" "port": "8080"
}, },
"soketi": { "soketi": {
@ -4898,7 +4898,7 @@
"category": "backend", "category": "backend",
"logo": "svgs/supabase.svg", "logo": "svgs/supabase.svg",
"minversion": "4.0.0-beta.228", "minversion": "4.0.0-beta.228",
"template_last_updated_at": "2026-01-20T19:10:07+01:00", "template_last_updated_at": "2026-04-05T20:21:11+02:00",
"port": "8000" "port": "8000"
}, },
"superset-with-postgresql": { "superset-with-postgresql": {
@ -5176,7 +5176,7 @@
"category": "productivity", "category": "productivity",
"logo": "svgs/twenty.svg", "logo": "svgs/twenty.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-08T18:51:23+01:00", "template_last_updated_at": "2026-04-16T23:18:19+05:30",
"port": "3000" "port": "3000"
}, },
"typesense": { "typesense": {
@ -5224,7 +5224,7 @@
"category": "devtools", "category": "devtools",
"logo": "svgs/unleash.svg", "logo": "svgs/unleash.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-01-04T19:45:35+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "4242" "port": "4242"
}, },
"unleash-without-database": { "unleash-without-database": {
@ -5241,7 +5241,7 @@
"category": "devtools", "category": "devtools",
"logo": "svgs/unleash.svg", "logo": "svgs/unleash.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-08-17T18:23:57+02:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "4242" "port": "4242"
}, },
"unstructured": { "unstructured": {
@ -5339,7 +5339,7 @@
"category": "email", "category": "email",
"logo": "svgs/usesend.svg", "logo": "svgs/usesend.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2025-12-07T17:32:23+11:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "3000" "port": "3000"
}, },
"vaultwarden": { "vaultwarden": {
@ -5774,7 +5774,7 @@
"category": "storage", "category": "storage",
"logo": "svgs/cells.svg", "logo": "svgs/cells.svg",
"minversion": "0.0.0", "minversion": "0.0.0",
"template_last_updated_at": "2026-02-13T18:47:54+01:00", "template_last_updated_at": "2026-04-06T11:35:16-05:00",
"port": "8080" "port": "8080"
} }
} }

View file

@ -996,6 +996,35 @@
}); });
}); });
describe('service application lifecycle command escaping', function () {
test('service application deploy action escapes docker command arguments', function () {
$source = file_get_contents(app_path('Actions/Service/DeployServiceApplication.php'));
expect($source)->toContain('escapeshellarg($workdir)')
->and($source)->toContain('escapeshellarg($composeFile)')
->and($source)->toContain('escapeshellarg($service->uuid)')
->and($source)->toContain('escapeshellarg($composeServiceName)')
->and($source)->toContain('escapeshellarg($service->destination->network)');
});
test('service application restart and stop actions escape container names', function (string $path) {
$source = file_get_contents(app_path($path));
expect($source)->toContain('escapeshellarg($serviceApplication->name');
})->with([
'restart action' => 'Actions/Service/RestartServiceApplication.php',
'stop action' => 'Actions/Service/StopServiceApplication.php',
]);
test('service application logs endpoint escapes container name before docker helpers', function () {
$source = file_get_contents(app_path('Http/Controllers/Api/ServiceApplicationsController.php'));
expect($source)->toContain('$safeContainerName = escapeshellarg($containerName)')
->and($source)->toContain('getContainerStatus($server, $safeContainerName)')
->and($source)->toContain('getContainerLogs($server, $containerName, $lines)');
});
});
describe('install/build/start command validation', function () { describe('install/build/start command validation', function () {
test('rejects semicolon injection in install_command', function () { test('rejects semicolon injection in install_command', function () {
$rules = sharedDataApplications(); $rules = sharedDataApplications();

View file

@ -20,20 +20,13 @@
beforeEach(function () { beforeEach(function () {
Queue::fake(); Queue::fake();
InstanceSettings::updateOrCreate(['id' => 0]); InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]);
$this->team = Team::factory()->create(); $this->team = Team::factory()->create();
$this->user = User::factory()->create(); $this->user = User::factory()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']); $this->team->members()->attach($this->user->id, ['role' => 'owner']);
$plainTextToken = Str::random(40); $this->bearerToken = createBearerTokenForServiceApplicationsApiTest($this->user, $this->team);
$token = $this->user->tokens()->create([
'name' => 'test-token',
'token' => hash('sha256', $plainTextToken),
'abilities' => ['*'],
'team_id' => $this->team->id,
]);
$this->bearerToken = $token->getKey().'|'.$plainTextToken;
$this->server = Server::factory()->create(['team_id' => $this->team->id]); $this->server = Server::factory()->create(['team_id' => $this->team->id]);
$this->server->settings->update([ $this->server->settings->update([
@ -46,6 +39,19 @@
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]); $this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
}); });
function createBearerTokenForServiceApplicationsApiTest(User $user, Team $team, array $abilities = ['*']): string
{
$plainTextToken = Str::random(40);
$token = $user->tokens()->create([
'name' => 'test-token',
'token' => hash('sha256', $plainTextToken),
'abilities' => $abilities,
'team_id' => $team->id,
]);
return $token->getKey().'|'.$plainTextToken;
}
function createServiceWithApplicationForApiTest(object $ctx): object function createServiceWithApplicationForApiTest(object $ctx): object
{ {
$service = Service::factory()->create([ $service = Service::factory()->create([
@ -108,6 +114,32 @@ function createServiceWithoutApplicationsForApiTest(object $ctx): Service
$response->assertStatus(404); $response->assertStatus(404);
$response->assertJsonFragment(['message' => 'Service not found.']); $response->assertJsonFragment(['message' => 'Service not found.']);
}); });
test('does not list applications from another team', function () {
$otherTeam = Team::factory()->create();
$otherServer = Server::factory()->create(['team_id' => $otherTeam->id]);
$otherServer->settings->update([
'is_reachable' => true,
'is_usable' => true,
'force_disabled' => false,
]);
$otherCtx = (object) [
'server' => $otherServer,
'destination' => StandaloneDocker::where('server_id', $otherServer->id)->first(),
'environment' => Environment::factory()->create([
'project_id' => Project::factory()->create(['team_id' => $otherTeam->id])->id,
]),
];
$ctx = createServiceWithApplicationForApiTest($otherCtx);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
])->getJson("/api/v1/services/{$ctx->service->uuid}/applications");
$response->assertStatus(404);
$response->assertJsonFragment(['message' => 'Service not found.']);
});
}); });
describe('GET /api/v1/services/{uuid}/applications/{app_uuid}', function () { describe('GET /api/v1/services/{uuid}/applications/{app_uuid}', function () {
@ -149,7 +181,7 @@ function createServiceWithoutApplicationsForApiTest(object $ctx): Service
'human_name' => 'x', 'human_name' => 'x',
], ['Accept' => 'application/json', 'Content-Type' => 'application/json']); ], ['Accept' => 'application/json', 'Content-Type' => 'application/json']);
$response->assertStatus(400); $response->assertStatus(401);
}); });
test('updates human_name', function () { test('updates human_name', function () {
@ -191,13 +223,51 @@ function createServiceWithoutApplicationsForApiTest(object $ctx): Service
$response->assertStatus(422); $response->assertStatus(422);
expect((string) $response->json('errors.is_log_drain_enabled.0'))->toContain('Log drain'); expect((string) $response->json('errors.is_log_drain_enabled.0'))->toContain('Log drain');
}); });
test('read-only token cannot update a service application', function () {
$ctx = createServiceWithApplicationForApiTest($this);
$readOnlyToken = createBearerTokenForServiceApplicationsApiTest($this->user, $this->team, ['read']);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$readOnlyToken,
])->patchJson("/api/v1/services/{$ctx->service->uuid}/applications/{$ctx->serviceApplication->uuid}", [
'human_name' => 'Blocked',
]);
$response->assertStatus(403);
});
test('returns 409 for domain conflicts unless forced', function () {
$ctx = createServiceWithApplicationForApiTest($this);
$ctx->serviceApplication->update(['fqdn' => 'http://used.example.com']);
$otherApplication = ServiceApplication::create([
'name' => 'admin',
'service_id' => $ctx->service->id,
'image' => 'nginx:alpine',
]);
$this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
])->patchJson("/api/v1/services/{$ctx->service->uuid}/applications/{$otherApplication->uuid}", [
'url' => 'http://used.example.com',
])->assertStatus(409);
$this->withHeaders([
'Authorization' => 'Bearer '.$this->bearerToken,
])->patchJson("/api/v1/services/{$ctx->service->uuid}/applications/{$otherApplication->uuid}?force_domain_override=true", [
'url' => 'http://used.example.com',
])->assertStatus(200);
$otherApplication->refresh();
expect($otherApplication->fqdn)->toBe('http://used.example.com');
});
}); });
describe('POST /api/v1/services/{uuid}/applications/{app_uuid}/restart', function () { describe('POST /api/v1/services/{uuid}/applications/{app_uuid}/restart', function () {
test('returns 400 without valid token', function () { test('returns 400 without valid token', function () {
$response = $this->postJson('/api/v1/services/some-uuid/applications/some-app/restart'); $response = $this->postJson('/api/v1/services/some-uuid/applications/some-app/restart');
$response->assertStatus(400); $response->assertStatus(401);
}); });
test('queues restart for a service application', function () { test('queues restart for a service application', function () {
@ -214,6 +284,17 @@ function createServiceWithoutApplicationsForApiTest(object $ctx): Service
}); });
describe('POST /api/v1/services/{uuid}/applications/{app_uuid}/start', function () { describe('POST /api/v1/services/{uuid}/applications/{app_uuid}/start', function () {
test('write token cannot start a service application', function () {
$ctx = createServiceWithApplicationForApiTest($this);
$writeToken = createBearerTokenForServiceApplicationsApiTest($this->user, $this->team, ['write']);
$response = $this->withHeaders([
'Authorization' => 'Bearer '.$writeToken,
])->postJson("/api/v1/services/{$ctx->service->uuid}/applications/{$ctx->serviceApplication->uuid}/start");
$response->assertStatus(403);
});
test('queues deploy for a service application', function () { test('queues deploy for a service application', function () {
$ctx = createServiceWithApplicationForApiTest($this); $ctx = createServiceWithApplicationForApiTest($this);