diff --git a/app/Http/Controllers/Api/SecurityController.php b/app/Http/Controllers/Api/SecurityController.php
index a14b0da20..fdd46b100 100644
--- a/app/Http/Controllers/Api/SecurityController.php
+++ b/app/Http/Controllers/Api/SecurityController.php
@@ -195,6 +195,31 @@ class SecurityController extends Controller
if (! $request->description) {
$request->offsetSet('description', 'Created by Coolify via API');
}
+
+ $isPrivateKeyString = str_starts_with($request->private_key, '-----BEGIN');
+ if (! $isPrivateKeyString) {
+ try {
+ $base64PrivateKey = base64_decode($request->private_key);
+ $request->offsetSet('private_key', $base64PrivateKey);
+ } catch (\Exception $e) {
+ return response()->json([
+ 'message' => 'Invalid private key.',
+ ], 422);
+ }
+ }
+ $isPrivateKeyValid = PrivateKey::validatePrivateKey($request->private_key);
+ if (! $isPrivateKeyValid) {
+ return response()->json([
+ 'message' => 'Invalid private key.',
+ ], 422);
+ }
+ $fingerPrint = PrivateKey::generateFingerprint($request->private_key);
+ $isFingerPrintExists = PrivateKey::fingerprintExists($fingerPrint);
+ if ($isFingerPrintExists) {
+ return response()->json([
+ 'message' => 'Private key already exists.',
+ ], 422);
+ }
$key = PrivateKey::create([
'team_id' => $teamId,
'name' => $request->name,
diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php
index b1deb5321..a9a0a2e53 100644
--- a/app/Http/Controllers/Api/ServersController.php
+++ b/app/Http/Controllers/Api/ServersController.php
@@ -530,11 +530,11 @@ class ServersController extends Controller
'user' => $request->user,
'private_key_id' => $privateKey->id,
'team_id' => $teamId,
- 'proxy' => [
- 'type' => $proxyType,
- 'status' => ProxyStatus::EXITED->value,
- ],
]);
+ $server->proxy->set('type', $proxyType);
+ $server->proxy->set('status', ProxyStatus::EXITED->value);
+ $server->save();
+
$server->settings()->update([
'is_build_server' => $request->is_build_server,
]);
@@ -742,6 +742,9 @@ class ServersController extends Controller
if ($server->definedResources()->count() > 0) {
return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400);
}
+ if ($server->isLocalhost()) {
+ return response()->json(['message' => 'Local server cannot be deleted.'], 400);
+ }
$server->delete();
DeleteServer::dispatch($server);
diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php
index 942924437..7d68ce068 100644
--- a/app/Livewire/Project/New/DockerImage.php
+++ b/app/Livewire/Project/New/DockerImage.php
@@ -6,6 +6,7 @@ use App\Models\Application;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
+use App\Services\DockerImageParser;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -28,12 +29,10 @@ class DockerImage extends Component
$this->validate([
'dockerImage' => 'required',
]);
- $image = str($this->dockerImage)->before(':');
- if (str($this->dockerImage)->contains(':')) {
- $tag = str($this->dockerImage)->after(':');
- } else {
- $tag = 'latest';
- }
+
+ $parser = new DockerImageParser;
+ $parser->parse($this->dockerImage);
+
$destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (! $destination) {
@@ -53,8 +52,8 @@ class DockerImage extends Component
'git_branch' => 'main',
'build_pack' => 'dockerimage',
'ports_exposes' => 80,
- 'docker_registry_image_name' => $image,
- 'docker_registry_image_tag' => $tag,
+ 'docker_registry_image_name' => $parser->getFullImageNameWithoutTag(),
+ 'docker_registry_image_tag' => $parser->getTag(),
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination_class,
diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php
index 80015d87f..0e702e460 100644
--- a/app/Models/PrivateKey.php
+++ b/app/Models/PrivateKey.php
@@ -40,6 +40,8 @@ class PrivateKey extends BaseModel
'private_key' => 'encrypted',
];
+ protected $appends = ['public_key'];
+
protected static function booted()
{
static::saving(function ($key) {
@@ -64,6 +66,11 @@ class PrivateKey extends BaseModel
});
}
+ public function getPublicKeyAttribute()
+ {
+ return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key';
+ }
+
public function getPublicKey()
{
return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key';
@@ -208,15 +215,14 @@ class PrivateKey extends BaseModel
{
try {
$key = PublicKeyLoader::load($privateKey);
- $publicKey = $key->getPublicKey();
- return $publicKey->getFingerprint('sha256');
+ return $key->getPublicKey()->getFingerprint('sha256');
} catch (\Throwable $e) {
return null;
}
}
- private static function fingerprintExists($fingerprint, $excludeId = null)
+ public static function fingerprintExists($fingerprint, $excludeId = null)
{
$query = self::query()
->where('fingerprint', $fingerprint)
diff --git a/app/Services/DockerImageParser.php b/app/Services/DockerImageParser.php
new file mode 100644
index 000000000..4987f953d
--- /dev/null
+++ b/app/Services/DockerImageParser.php
@@ -0,0 +1,74 @@
+ strrpos($imageString, '/'))) {
+ $mainPart = substr($imageString, 0, $lastColon);
+ $this->tag = substr($imageString, $lastColon + 1);
+ } else {
+ $mainPart = $imageString;
+ $this->tag = 'latest';
+ }
+
+ // Split the main part by / to handle registry and image name
+ $pathParts = explode('/', $mainPart);
+
+ // If we have more than one part and the first part contains a dot or colon
+ // it's likely a registry URL
+ if (count($pathParts) > 1 && (str_contains($pathParts[0], '.') || str_contains($pathParts[0], ':'))) {
+ $this->registryUrl = array_shift($pathParts);
+ $this->imageName = implode('/', $pathParts);
+ } else {
+ $this->imageName = $mainPart;
+ }
+
+ return $this;
+ }
+
+ public function getFullImageNameWithoutTag(): string
+ {
+ return $this->registryUrl.'/'.$this->imageName;
+ }
+
+ public function getRegistryUrl(): string
+ {
+ return $this->registryUrl;
+ }
+
+ public function getImageName(): string
+ {
+ return $this->imageName;
+ }
+
+ public function getTag(): string
+ {
+ return $this->tag;
+ }
+
+ public function toString(): string
+ {
+ $parts = [];
+ if ($this->registryUrl) {
+ $parts[] = $this->registryUrl;
+ }
+ $parts[] = $this->imageName;
+
+ return implode('/', $parts).':'.$this->tag;
+ }
+}
diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php
index 5d34c63b6..ff2933f72 100644
--- a/bootstrap/helpers/shared.php
+++ b/bootstrap/helpers/shared.php
@@ -2005,7 +2005,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
projectName: $resource->project()->name,
resourceName: $resource->name,
type: 'service',
- subType: $isDatabase ? 'database' : 'application',
+ subType: $isDatabase ? 'database' : 'application',
subId: $savedService->id,
subName: $savedService->name,
environment: $resource->environment->name,
@@ -2872,7 +2872,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
data_forget($service, 'volumes.*.is_directory');
data_forget($service, 'exclude_from_hc');
data_set($service, 'environment', $serviceVariables->toArray());
- updateCompose($savedService);
+ updateCompose($service);
return $service;
});
diff --git a/config/constants.php b/config/constants.php
index 1aff0b787..fc2d92c56 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -2,7 +2,7 @@
return [
'coolify' => [
- 'version' => '4.0.0-beta.382',
+ 'version' => '4.0.0-beta.383',
'self_hosted' => env('SELF_HOSTED', true),
'autoupdate' => env('AUTOUPDATE'),
'base_config_path' => env('BASE_CONFIG_PATH', '/data/coolify'),
diff --git a/other/nightly/install.sh b/other/nightly/install.sh
index 766b1fc63..31d0a1759 100755
--- a/other/nightly/install.sh
+++ b/other/nightly/install.sh
@@ -5,10 +5,10 @@ set -e # Exit immediately if a command exits with a non-zero status
## $1 could be empty, so we need to disable this check
#set -u # Treat unset variables as an error and exit
set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status
-CDN="https://cdn.coollabs.io/coolify"
+CDN="https://cdn.coollabs.io/coolify-nightly"
DATE=$(date +"%Y%m%d-%H%M%S")
-VERSION="1.6"
+VERSION="1.7"
DOCKER_VERSION="27.0"
# TODO: Ask for a user
CURRENT_USER=$USER
@@ -488,13 +488,13 @@ fi
# Add default root user credentials from environment variables
if [ -n "$ROOT_USERNAME" ] && [ -n "$ROOT_USER_EMAIL" ] && [ -n "$ROOT_USER_PASSWORD" ]; then
- if ! grep -q "^ROOT_USERNAME=" "$ENV_FILE-$DATE"; then
+ if grep -q "^ROOT_USERNAME=" "$ENV_FILE-$DATE"; then
sed -i "s|^ROOT_USERNAME=.*|ROOT_USERNAME=$ROOT_USERNAME|" "$ENV_FILE-$DATE"
fi
- if ! grep -q "^ROOT_USER_EMAIL=" "$ENV_FILE-$DATE"; then
+ if grep -q "^ROOT_USER_EMAIL=" "$ENV_FILE-$DATE"; then
sed -i "s|^ROOT_USER_EMAIL=.*|ROOT_USER_EMAIL=$ROOT_USER_EMAIL|" "$ENV_FILE-$DATE"
fi
- if ! grep -q "^ROOT_USER_PASSWORD=" "$ENV_FILE-$DATE"; then
+ if grep -q "^ROOT_USER_PASSWORD=" "$ENV_FILE-$DATE"; then
sed -i "s|^ROOT_USER_PASSWORD=.*|ROOT_USER_PASSWORD=$ROOT_USER_PASSWORD|" "$ENV_FILE-$DATE"
fi
fi
diff --git a/resources/views/livewire/server/new/by-ip.blade.php b/resources/views/livewire/server/new/by-ip.blade.php
index b0786341e..225a59a5a 100644
--- a/resources/views/livewire/server/new/by-ip.blade.php
+++ b/resources/views/livewire/server/new/by-ip.blade.php
@@ -31,7 +31,7 @@
Swarm (experimental)
-
@if ($is_swarm_worker || $is_build_server)
$ENV_FILE
diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh
index 1bdf0f461..369ac067d 100644
--- a/scripts/upgrade.sh
+++ b/scripts/upgrade.sh
@@ -33,6 +33,7 @@ fi
docker network create --attachable coolify 2>/dev/null
# docker network create --attachable --driver=overlay coolify-overlay 2>/dev/null
+echo "If you encounter any issues, please check the log file: $LOGFILE"
if [ -f /data/coolify/source/docker-compose.custom.yml ]; then
echo "docker-compose.custom.yml detected." >> $LOGFILE
docker run -v /data/coolify/source:/data/coolify/source -v /var/run/docker.sock:/var/run/docker.sock --rm ghcr.io/coollabsio/coolify-helper:${LATEST_HELPER_VERSION} bash -c "LATEST_IMAGE=${LATEST_IMAGE} docker compose --env-file /data/coolify/source/.env -f /data/coolify/source/docker-compose.yml -f /data/coolify/source/docker-compose.prod.yml -f /data/coolify/source/docker-compose.custom.yml up -d --remove-orphans --force-recreate --wait --wait-timeout 60" >> $LOGFILE 2>&1
diff --git a/templates/compose/chatwoot.yaml b/templates/compose/chatwoot.yaml
index aaeb73071..b7eec88cc 100644
--- a/templates/compose/chatwoot.yaml
+++ b/templates/compose/chatwoot.yaml
@@ -88,7 +88,7 @@ services:
retries: 3
postgres:
- image: postgres:12
+ image: pgvector/pgvector:pg12
restart: always
volumes:
- postgres-data:/var/lib/postgresql/data
diff --git a/templates/service-templates.json b/templates/service-templates.json
index 4de2e70e6..c703f9bdc 100644
--- a/templates/service-templates.json
+++ b/templates/service-templates.json
@@ -358,7 +358,7 @@
"chatwoot": {
"documentation": "https://www.chatwoot.com/docs/self-hosted/?utm_source=coolify.io",
"slogan": "Delightful customer relationships at scale.",
- "compose": "c2VydmljZXM6CiAgY2hhdHdvb3Q6CiAgICBpbWFnZTogJ2NoYXR3b290L2NoYXR3b290OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcG9zdGdyZXMKICAgICAgLSByZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NIQVRXT09UXzMwMDAKICAgICAgLSBTRUNSRVRfS0VZX0JBU0U9JFNFUlZJQ0VfUEFTU1dPUkRfQ0hBVFdPT1QKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX0NIQVRXT09UfScKICAgICAgLSAnREVGQVVMVF9MT0NBTEU9JHtDSEFUV09PVF9ERUZBVUxUX0xPQ0FMRX0nCiAgICAgIC0gJ0ZPUkNFX1NTTD0ke0ZPUkNFX1NTTDotZmFsc2V9JwogICAgICAtICdFTkFCTEVfQUNDT1VOVF9TSUdOVVA9JHtFTkFCTEVfQUNDT1VOVF9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vZGVmYXVsdEByZWRpczo2Mzc5JwogICAgICAtIFJFRElTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICAgIC0gJ1JFRElTX09QRU5TU0xfVkVSSUZZX01PREU9JHtSRURJU19PUEVOU1NMX1ZFUklGWV9NT0RFOi1ub25lfScKICAgICAgLSAnUE9TVEdSRVNfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotY2hhdHdvb3R9JwogICAgICAtICdQT1NUR1JFU19IT1NUPSR7UE9TVEdSRVNfSE9TVDotcG9zdGdyZXN9JwogICAgICAtIFBPU1RHUkVTX1VTRVJOQU1FPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdSQUlMU19NQVhfVEhSRUFEUz0ke1JBSUxTX01BWF9USFJFQURTOi01fScKICAgICAgLSAnTk9ERV9FTlY9JHtOT0RFX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ1JBSUxTX0VOVj0ke1JBSUxTX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ0lOU1RBTExBVElPTl9FTlY9JHtJTlNUQUxMQVRJT05fRU5WOi1kb2NrZXJ9JwogICAgICAtICdNQUlMRVJfU0VOREVSX0VNQUlMPSR7Q0hBVFdPT1RfTUFJTEVSX1NFTkRFUl9FTUFJTH0nCiAgICAgIC0gJ1NNVFBfQUREUkVTUz0ke0NIQVRXT09UX1NNVFBfQUREUkVTU30nCiAgICAgIC0gJ1NNVFBfQVVUSEVOVElDQVRJT049JHtDSEFUV09PVF9TTVRQX0FVVEhFTlRJQ0FUSU9OfScKICAgICAgLSAnU01UUF9ET01BSU49JHtDSEFUV09PVF9TTVRQX0RPTUFJTn0nCiAgICAgIC0gJ1NNVFBfRU5BQkxFX1NUQVJUVExTX0FVVE89JHtDSEFUV09PVF9TTVRQX0VOQUJMRV9TVEFSVFRMU19BVVRPfScKICAgICAgLSAnU01UUF9QT1JUPSR7Q0hBVFdPT1RfU01UUF9QT1JUfScKICAgICAgLSAnU01UUF9VU0VSTkFNRT0ke0NIQVRXT09UX1NNVFBfVVNFUk5BTUV9JwogICAgICAtICdTTVRQX1BBU1NXT1JEPSR7Q0hBVFdPT1RfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ0FDVElWRV9TVE9SQUdFX1NFUlZJQ0U9JHtBQ1RJVkVfU1RPUkFHRV9TRVJWSUNFOi1sb2NhbH0nCiAgICBlbnRyeXBvaW50OiBkb2NrZXIvZW50cnlwb2ludHMvcmFpbHMuc2gKICAgIGNvbW1hbmQ6ICdzaCAtYyAiYnVuZGxlIGV4ZWMgcmFpbHMgZGI6Y2hhdHdvb3RfcHJlcGFyZSAmJiBidW5kbGUgZXhlYyByYWlscyBzIC1wIDMwMDAgLWIgMC4wLjAuMCInCiAgICB2b2x1bWVzOgogICAgICAtICdyYWlscy1kYXRhOi9hcHAvc3RvcmFnZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgc2lkZWtpcToKICAgIGltYWdlOiAnY2hhdHdvb3QvY2hhdHdvb3Q6bGF0ZXN0JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIHJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRUNSRVRfS0VZX0JBU0U9JFNFUlZJQ0VfUEFTU1dPUkRfQ0hBVFdPT1QKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX0NIQVRXT09UfScKICAgICAgLSAnREVGQVVMVF9MT0NBTEU9JHtDSEFUV09PVF9ERUZBVUxUX0xPQ0FMRX0nCiAgICAgIC0gJ0ZPUkNFX1NTTD0ke0ZPUkNFX1NTTDotZmFsc2V9JwogICAgICAtICdFTkFCTEVfQUNDT1VOVF9TSUdOVVA9JHtFTkFCTEVfQUNDT1VOVF9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vZGVmYXVsdEByZWRpczo2Mzc5JwogICAgICAtIFJFRElTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICAgIC0gJ1JFRElTX09QRU5TU0xfVkVSSUZZX01PREU9JHtSRURJU19PUEVOU1NMX1ZFUklGWV9NT0RFOi1ub25lfScKICAgICAgLSAnUE9TVEdSRVNfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotY2hhdHdvb3R9JwogICAgICAtICdQT1NUR1JFU19IT1NUPSR7UE9TVEdSRVNfSE9TVDotcG9zdGdyZXN9JwogICAgICAtIFBPU1RHUkVTX1VTRVJOQU1FPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdSQUlMU19NQVhfVEhSRUFEUz0ke1JBSUxTX01BWF9USFJFQURTOi01fScKICAgICAgLSAnTk9ERV9FTlY9JHtOT0RFX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ1JBSUxTX0VOVj0ke1JBSUxTX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ0lOU1RBTExBVElPTl9FTlY9JHtJTlNUQUxMQVRJT05fRU5WOi1kb2NrZXJ9JwogICAgICAtICdNQUlMRVJfU0VOREVSX0VNQUlMPSR7Q0hBVFdPT1RfTUFJTEVSX1NFTkRFUl9FTUFJTH0nCiAgICAgIC0gJ1NNVFBfQUREUkVTUz0ke0NIQVRXT09UX1NNVFBfQUREUkVTU30nCiAgICAgIC0gJ1NNVFBfQVVUSEVOVElDQVRJT049JHtDSEFUV09PVF9TTVRQX0FVVEhFTlRJQ0FUSU9OfScKICAgICAgLSAnU01UUF9ET01BSU49JHtDSEFUV09PVF9TTVRQX0RPTUFJTn0nCiAgICAgIC0gJ1NNVFBfRU5BQkxFX1NUQVJUVExTX0FVVE89JHtDSEFUV09PVF9TTVRQX0VOQUJMRV9TVEFSVFRMU19BVVRPfScKICAgICAgLSAnU01UUF9QT1JUPSR7Q0hBVFdPT1RfU01UUF9QT1JUfScKICAgICAgLSAnU01UUF9VU0VSTkFNRT0ke0NIQVRXT09UX1NNVFBfVVNFUk5BTUV9JwogICAgICAtICdTTVRQX1BBU1NXT1JEPSR7Q0hBVFdPT1RfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ0FDVElWRV9TVE9SQUdFX1NFUlZJQ0U9JHtBQ1RJVkVfU1RPUkFHRV9TRVJWSUNFOi1sb2NhbH0nCiAgICBjb21tYW5kOgogICAgICAtIGJ1bmRsZQogICAgICAtIGV4ZWMKICAgICAgLSBzaWRla2lxCiAgICAgIC0gJy1DJwogICAgICAtIGNvbmZpZy9zaWRla2lxLnltbAogICAgdm9sdW1lczoKICAgICAgLSAnc2lkZWtpcS1kYXRhOi9hcHAvc3RvcmFnZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiYnVuZGxlIGV4ZWMgcmFpbHMgcnVubmVyICdwdXRzIFNpZGVraXEucmVkaXMoJjppbmZvKScgPiAvZGV2L251bGwgMj4mMSIKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxMicKICAgIHJlc3RhcnQ6IGFsd2F5cwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotY2hhdHdvb3R9JwogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUyAtZCBjaGF0d29vdCAtaCAxMjcuMC4wLjEnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDUKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6YWxwaW5lJwogICAgcmVzdGFydDogYWx3YXlzCiAgICBjb21tYW5kOgogICAgICAtIHNoCiAgICAgIC0gJy1jJwogICAgICAtICdyZWRpcy1zZXJ2ZXIgLS1yZXF1aXJlcGFzcyAiJFNFUlZJQ0VfUEFTU1dPUkRfUkVESVMiJwogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXMtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtICctYScKICAgICAgICAtICRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICAgICAgLSBQSU5HCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDUK",
+ "compose": "c2VydmljZXM6CiAgY2hhdHdvb3Q6CiAgICBpbWFnZTogJ2NoYXR3b290L2NoYXR3b290OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcG9zdGdyZXMKICAgICAgLSByZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NIQVRXT09UXzMwMDAKICAgICAgLSBTRUNSRVRfS0VZX0JBU0U9JFNFUlZJQ0VfUEFTU1dPUkRfQ0hBVFdPT1QKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX0NIQVRXT09UfScKICAgICAgLSAnREVGQVVMVF9MT0NBTEU9JHtDSEFUV09PVF9ERUZBVUxUX0xPQ0FMRX0nCiAgICAgIC0gJ0ZPUkNFX1NTTD0ke0ZPUkNFX1NTTDotZmFsc2V9JwogICAgICAtICdFTkFCTEVfQUNDT1VOVF9TSUdOVVA9JHtFTkFCTEVfQUNDT1VOVF9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vZGVmYXVsdEByZWRpczo2Mzc5JwogICAgICAtIFJFRElTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICAgIC0gJ1JFRElTX09QRU5TU0xfVkVSSUZZX01PREU9JHtSRURJU19PUEVOU1NMX1ZFUklGWV9NT0RFOi1ub25lfScKICAgICAgLSAnUE9TVEdSRVNfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotY2hhdHdvb3R9JwogICAgICAtICdQT1NUR1JFU19IT1NUPSR7UE9TVEdSRVNfSE9TVDotcG9zdGdyZXN9JwogICAgICAtIFBPU1RHUkVTX1VTRVJOQU1FPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdSQUlMU19NQVhfVEhSRUFEUz0ke1JBSUxTX01BWF9USFJFQURTOi01fScKICAgICAgLSAnTk9ERV9FTlY9JHtOT0RFX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ1JBSUxTX0VOVj0ke1JBSUxTX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ0lOU1RBTExBVElPTl9FTlY9JHtJTlNUQUxMQVRJT05fRU5WOi1kb2NrZXJ9JwogICAgICAtICdNQUlMRVJfU0VOREVSX0VNQUlMPSR7Q0hBVFdPT1RfTUFJTEVSX1NFTkRFUl9FTUFJTH0nCiAgICAgIC0gJ1NNVFBfQUREUkVTUz0ke0NIQVRXT09UX1NNVFBfQUREUkVTU30nCiAgICAgIC0gJ1NNVFBfQVVUSEVOVElDQVRJT049JHtDSEFUV09PVF9TTVRQX0FVVEhFTlRJQ0FUSU9OfScKICAgICAgLSAnU01UUF9ET01BSU49JHtDSEFUV09PVF9TTVRQX0RPTUFJTn0nCiAgICAgIC0gJ1NNVFBfRU5BQkxFX1NUQVJUVExTX0FVVE89JHtDSEFUV09PVF9TTVRQX0VOQUJMRV9TVEFSVFRMU19BVVRPfScKICAgICAgLSAnU01UUF9QT1JUPSR7Q0hBVFdPT1RfU01UUF9QT1JUfScKICAgICAgLSAnU01UUF9VU0VSTkFNRT0ke0NIQVRXT09UX1NNVFBfVVNFUk5BTUV9JwogICAgICAtICdTTVRQX1BBU1NXT1JEPSR7Q0hBVFdPT1RfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ0FDVElWRV9TVE9SQUdFX1NFUlZJQ0U9JHtBQ1RJVkVfU1RPUkFHRV9TRVJWSUNFOi1sb2NhbH0nCiAgICBlbnRyeXBvaW50OiBkb2NrZXIvZW50cnlwb2ludHMvcmFpbHMuc2gKICAgIGNvbW1hbmQ6ICdzaCAtYyAiYnVuZGxlIGV4ZWMgcmFpbHMgZGI6Y2hhdHdvb3RfcHJlcGFyZSAmJiBidW5kbGUgZXhlYyByYWlscyBzIC1wIDMwMDAgLWIgMC4wLjAuMCInCiAgICB2b2x1bWVzOgogICAgICAtICdyYWlscy1kYXRhOi9hcHAvc3RvcmFnZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgc2lkZWtpcToKICAgIGltYWdlOiAnY2hhdHdvb3QvY2hhdHdvb3Q6bGF0ZXN0JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIHJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRUNSRVRfS0VZX0JBU0U9JFNFUlZJQ0VfUEFTU1dPUkRfQ0hBVFdPT1QKICAgICAgLSAnRlJPTlRFTkRfVVJMPSR7U0VSVklDRV9GUUROX0NIQVRXT09UfScKICAgICAgLSAnREVGQVVMVF9MT0NBTEU9JHtDSEFUV09PVF9ERUZBVUxUX0xPQ0FMRX0nCiAgICAgIC0gJ0ZPUkNFX1NTTD0ke0ZPUkNFX1NTTDotZmFsc2V9JwogICAgICAtICdFTkFCTEVfQUNDT1VOVF9TSUdOVVA9JHtFTkFCTEVfQUNDT1VOVF9TSUdOVVA6LWZhbHNlfScKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vZGVmYXVsdEByZWRpczo2Mzc5JwogICAgICAtIFJFRElTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1JFRElTCiAgICAgIC0gJ1JFRElTX09QRU5TU0xfVkVSSUZZX01PREU9JHtSRURJU19PUEVOU1NMX1ZFUklGWV9NT0RFOi1ub25lfScKICAgICAgLSAnUE9TVEdSRVNfREFUQUJBU0U9JHtQT1NUR1JFU19EQjotY2hhdHdvb3R9JwogICAgICAtICdQT1NUR1JFU19IT1NUPSR7UE9TVEdSRVNfSE9TVDotcG9zdGdyZXN9JwogICAgICAtIFBPU1RHUkVTX1VTRVJOQU1FPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdSQUlMU19NQVhfVEhSRUFEUz0ke1JBSUxTX01BWF9USFJFQURTOi01fScKICAgICAgLSAnTk9ERV9FTlY9JHtOT0RFX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ1JBSUxTX0VOVj0ke1JBSUxTX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ0lOU1RBTExBVElPTl9FTlY9JHtJTlNUQUxMQVRJT05fRU5WOi1kb2NrZXJ9JwogICAgICAtICdNQUlMRVJfU0VOREVSX0VNQUlMPSR7Q0hBVFdPT1RfTUFJTEVSX1NFTkRFUl9FTUFJTH0nCiAgICAgIC0gJ1NNVFBfQUREUkVTUz0ke0NIQVRXT09UX1NNVFBfQUREUkVTU30nCiAgICAgIC0gJ1NNVFBfQVVUSEVOVElDQVRJT049JHtDSEFUV09PVF9TTVRQX0FVVEhFTlRJQ0FUSU9OfScKICAgICAgLSAnU01UUF9ET01BSU49JHtDSEFUV09PVF9TTVRQX0RPTUFJTn0nCiAgICAgIC0gJ1NNVFBfRU5BQkxFX1NUQVJUVExTX0FVVE89JHtDSEFUV09PVF9TTVRQX0VOQUJMRV9TVEFSVFRMU19BVVRPfScKICAgICAgLSAnU01UUF9QT1JUPSR7Q0hBVFdPT1RfU01UUF9QT1JUfScKICAgICAgLSAnU01UUF9VU0VSTkFNRT0ke0NIQVRXT09UX1NNVFBfVVNFUk5BTUV9JwogICAgICAtICdTTVRQX1BBU1NXT1JEPSR7Q0hBVFdPT1RfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ0FDVElWRV9TVE9SQUdFX1NFUlZJQ0U9JHtBQ1RJVkVfU1RPUkFHRV9TRVJWSUNFOi1sb2NhbH0nCiAgICBjb21tYW5kOgogICAgICAtIGJ1bmRsZQogICAgICAtIGV4ZWMKICAgICAgLSBzaWRla2lxCiAgICAgIC0gJy1DJwogICAgICAtIGNvbmZpZy9zaWRla2lxLnltbAogICAgdm9sdW1lczoKICAgICAgLSAnc2lkZWtpcS1kYXRhOi9hcHAvc3RvcmFnZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiYnVuZGxlIGV4ZWMgcmFpbHMgcnVubmVyICdwdXRzIFNpZGVraXEucmVkaXMoJjppbmZvKScgPiAvZGV2L251bGwgMj4mMSIKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogIHBvc3RncmVzOgogICAgaW1hZ2U6ICdwZ3ZlY3Rvci9wZ3ZlY3RvcjpwZzEyJwogICAgcmVzdGFydDogYWx3YXlzCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlcy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1jaGF0d29vdH0nCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkU0VSVklDRV9VU0VSX1BPU1RHUkVTIC1kIGNoYXR3b290IC1oIDEyNy4wLjAuMScKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogNQogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczphbHBpbmUnCiAgICByZXN0YXJ0OiBhbHdheXMKICAgIGNvbW1hbmQ6CiAgICAgIC0gc2gKICAgICAgLSAnLWMnCiAgICAgIC0gJ3JlZGlzLXNlcnZlciAtLXJlcXVpcmVwYXNzICIkU0VSVklDRV9QQVNTV09SRF9SRURJUyInCiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gJy1hJwogICAgICAgIC0gJFNFUlZJQ0VfUEFTU1dPUkRfUkVESVMKICAgICAgICAtIFBJTkcKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogNQo=",
"tags": [
"chatwoot",
"chat",
diff --git a/tests/Unit/Services/DockerImageParserTest.php b/tests/Unit/Services/DockerImageParserTest.php
new file mode 100644
index 000000000..876d68ef0
--- /dev/null
+++ b/tests/Unit/Services/DockerImageParserTest.php
@@ -0,0 +1,94 @@
+parser = new DockerImageParser;
+ }
+
+ #[Test]
+ public function it_parses_simple_image_name()
+ {
+ $this->parser->parse('nginx');
+
+ $this->assertEquals('', $this->parser->getRegistryUrl());
+ $this->assertEquals('nginx', $this->parser->getImageName());
+ $this->assertEquals('latest', $this->parser->getTag());
+ }
+
+ #[Test]
+ public function it_parses_image_with_tag()
+ {
+ $this->parser->parse('nginx:1.19');
+
+ $this->assertEquals('', $this->parser->getRegistryUrl());
+ $this->assertEquals('nginx', $this->parser->getImageName());
+ $this->assertEquals('1.19', $this->parser->getTag());
+ }
+
+ #[Test]
+ public function it_parses_image_with_organization()
+ {
+ $this->parser->parse('coollabs/coolify:latest');
+
+ $this->assertEquals('', $this->parser->getRegistryUrl());
+ $this->assertEquals('coollabs/coolify', $this->parser->getImageName());
+ $this->assertEquals('latest', $this->parser->getTag());
+ }
+
+ #[Test]
+ public function it_parses_image_with_registry_url()
+ {
+ $this->parser->parse('ghcr.io/coollabs/coolify:v4');
+
+ $this->assertEquals('ghcr.io', $this->parser->getRegistryUrl());
+ $this->assertEquals('coollabs/coolify', $this->parser->getImageName());
+ $this->assertEquals('v4', $this->parser->getTag());
+ }
+
+ #[Test]
+ public function it_parses_image_with_port_in_registry()
+ {
+ $this->parser->parse('localhost:5000/my-app:dev');
+
+ $this->assertEquals('localhost:5000', $this->parser->getRegistryUrl());
+ $this->assertEquals('my-app', $this->parser->getImageName());
+ $this->assertEquals('dev', $this->parser->getTag());
+ }
+
+ #[Test]
+ public function it_parses_image_without_tag()
+ {
+ $this->parser->parse('ghcr.io/coollabs/coolify');
+
+ $this->assertEquals('ghcr.io', $this->parser->getRegistryUrl());
+ $this->assertEquals('coollabs/coolify', $this->parser->getImageName());
+ $this->assertEquals('latest', $this->parser->getTag());
+ }
+
+ #[Test]
+ public function it_converts_back_to_string()
+ {
+ $originalString = 'ghcr.io/coollabs/coolify:v4';
+ $this->parser->parse($originalString);
+
+ $this->assertEquals($originalString, $this->parser->toString());
+ }
+
+ #[Test]
+ public function it_converts_to_string_with_default_tag()
+ {
+ $this->parser->parse('nginx');
+ $this->assertEquals('nginx:latest', $this->parser->toString());
+ }
+}
diff --git a/versions.json b/versions.json
index f8214d091..a1a01432c 100644
--- a/versions.json
+++ b/versions.json
@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
- "version": "4.0.0-beta.382"
+ "version": "4.0.0-beta.383"
},
"nightly": {
- "version": "4.0.0-beta.383"
+ "version": "4.0.0-beta.384"
},
"helper": {
"version": "1.0.4"