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);
$workdir = $service->workdir();
$composeFile = "{$workdir}/docker-compose.yml";
$safeWorkdir = escapeshellarg($workdir);
$safeComposeFile = escapeshellarg($composeFile);
$safeProjectName = escapeshellarg($service->uuid);
$safeComposeServiceName = escapeshellarg($composeServiceName);
$commands = collect([
"echo 'Saved configuration files to {$workdir}.'",
"touch {$workdir}/.env",
'echo '.escapeshellarg("Saved configuration files to {$workdir}."),
'touch '.escapeshellarg("{$workdir}/.env"),
]);
if ($pullLatestImages) {
$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) {
$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) {
$upCommand .= ' --build';
}
$upCommand .= " {$composeServiceName}";
$upCommand .= " {$safeComposeServiceName}";
$commands->push('echo Starting service container.');
$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')) {
$compose = data_get($service, 'docker_compose', []);
$network = $service->destination->network;
$commands->push("docker network connect --alias {$composeServiceName}-{$service->uuid} {$network} {$composeServiceName}-{$service->uuid} >/dev/null 2>&1 || true");
$network = escapeshellarg($service->destination->network);
$containerName = escapeshellarg("{$composeServiceName}-{$service->uuid}");
$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');

View file

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

View file

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

View file

@ -9,12 +9,12 @@
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');
if ($request->has('url')) {
$urlRaw = $request->input('url');
if (array_key_exists('url', $payload)) {
$urlRaw = $payload['url'];
if ($urlRaw !== null && ! is_string($urlRaw)) {
return response()->json([
'message' => 'Validation failed.',
@ -59,38 +59,38 @@ public function execute(ServiceApplication $serviceApplication, Request $request
$serviceApplication->fqdn = $parsed['normalized'];
}
if ($request->has('human_name')) {
$serviceApplication->human_name = $request->input('human_name');
if (array_key_exists('human_name', $payload)) {
$serviceApplication->human_name = $payload['human_name'];
}
if ($request->has('description')) {
$serviceApplication->description = $request->input('description');
if (array_key_exists('description', $payload)) {
$serviceApplication->description = $payload['description'];
}
if ($request->has('image')) {
$serviceApplication->image = $request->input('image');
if (array_key_exists('image', $payload)) {
$serviceApplication->image = $payload['image'];
}
if ($request->has('exclude_from_status')) {
$serviceApplication->exclude_from_status = $request->boolean('exclude_from_status');
if (array_key_exists('exclude_from_status', $payload)) {
$serviceApplication->exclude_from_status = filter_var($payload['exclude_from_status'], FILTER_VALIDATE_BOOLEAN);
}
if ($request->has('is_gzip_enabled')) {
$serviceApplication->is_gzip_enabled = $request->boolean('is_gzip_enabled');
if (array_key_exists('is_gzip_enabled', $payload)) {
$serviceApplication->is_gzip_enabled = filter_var($payload['is_gzip_enabled'], FILTER_VALIDATE_BOOLEAN);
}
if ($request->has('is_stripprefix_enabled')) {
$serviceApplication->is_stripprefix_enabled = $request->boolean('is_stripprefix_enabled');
if (array_key_exists('is_stripprefix_enabled', $payload)) {
$serviceApplication->is_stripprefix_enabled = filter_var($payload['is_stripprefix_enabled'], FILTER_VALIDATE_BOOLEAN);
}
if ($request->has('is_log_drain_enabled')) {
$enabled = $request->boolean('is_log_drain_enabled');
if (array_key_exists('is_log_drain_enabled', $payload)) {
$enabled = filter_var($payload['is_log_drain_enabled'], FILTER_VALIDATE_BOOLEAN);
$server = $serviceApplication->service->destination->server;
if ($enabled && ! $server->isLogDrainEnabled()) {
return response()->json([
'message' => 'Validation failed.',
'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);
}

View file

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

View file

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

View file

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

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 () {
test('rejects semicolon injection in install_command', function () {
$rules = sharedDataApplications();

View file

@ -20,20 +20,13 @@
beforeEach(function () {
Queue::fake();
InstanceSettings::updateOrCreate(['id' => 0]);
InstanceSettings::forceCreate(['id' => 0, 'is_api_enabled' => true]);
$this->team = Team::factory()->create();
$this->user = User::factory()->create();
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
$plainTextToken = Str::random(40);
$token = $this->user->tokens()->create([
'name' => 'test-token',
'token' => hash('sha256', $plainTextToken),
'abilities' => ['*'],
'team_id' => $this->team->id,
]);
$this->bearerToken = $token->getKey().'|'.$plainTextToken;
$this->bearerToken = createBearerTokenForServiceApplicationsApiTest($this->user, $this->team);
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
$this->server->settings->update([
@ -46,6 +39,19 @@
$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
{
$service = Service::factory()->create([
@ -108,6 +114,32 @@ function createServiceWithoutApplicationsForApiTest(object $ctx): Service
$response->assertStatus(404);
$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 () {
@ -149,7 +181,7 @@ function createServiceWithoutApplicationsForApiTest(object $ctx): Service
'human_name' => 'x',
], ['Accept' => 'application/json', 'Content-Type' => 'application/json']);
$response->assertStatus(400);
$response->assertStatus(401);
});
test('updates human_name', function () {
@ -191,13 +223,51 @@ function createServiceWithoutApplicationsForApiTest(object $ctx): Service
$response->assertStatus(422);
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 () {
test('returns 400 without valid token', function () {
$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 () {
@ -214,6 +284,17 @@ function createServiceWithoutApplicationsForApiTest(object $ctx): Service
});
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 () {
$ctx = createServiceWithApplicationForApiTest($this);