'array', 'postgres_password' => 'encrypted', 'restart_count' => 'integer', 'last_restart_at' => 'datetime', 'last_restart_type' => 'string', ]; protected static function booted() { static::created(function ($database) { // This is really stupid and it took me 1h to figure out why the image was not loading properly. This is exactly the reason why we need to use the action pattern because Model events and Accessors are a fragile mess! $image = (string) ($database->getAttributes()['image'] ?? ''); $majorVersion = 0; if (preg_match('/:(?:pg)?(\d+)/i', $image, $matches)) { $majorVersion = (int) $matches[1]; } // PostgreSQL 18+ uses /var/lib/postgresql as mount path // Older versions use /var/lib/postgresql/data $mountPath = $majorVersion >= 18 ? '/var/lib/postgresql' : '/var/lib/postgresql/data'; LocalPersistentVolume::create([ 'name' => 'postgres-data-'.$database->uuid, 'mount_path' => $mountPath, 'host_path' => null, 'resource_id' => $database->id, 'resource_type' => $database->getMorphClass(), ]); }); static::forceDeleting(function ($database) { $database->persistentStorages()->delete(); $database->scheduledBackups()->delete(); $database->environment_variables()->delete(); $database->tags()->detach(); }); static::saving(function ($database) { if ($database->isDirty('status')) { $database->forceFill(['last_online_at' => now()]); } }); } /** * Get query builder for PostgreSQL databases owned by current team. * If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead. */ public static function ownedByCurrentTeam() { return StandalonePostgresql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); } /** * Get all PostgreSQL databases owned by current team (cached for request duration). */ public static function ownedByCurrentTeamCached() { return once(function () { return StandalonePostgresql::ownedByCurrentTeam()->get(); }); } public function workdir() { return database_configuration_dir()."/{$this->uuid}"; } protected function serverStatus(): Attribute { return Attribute::make( get: function () { return $this->destination->server->isFunctional(); } ); } public function deleteConfigurations() { $server = data_get($this, 'destination.server'); $workdir = $this->workdir(); if (str($workdir)->endsWith($this->uuid)) { instant_remote_process(['rm -rf '.$this->workdir()], $server, false); } } public function deleteVolumes() { $persistentStorages = $this->persistentStorages()->get() ?? collect(); if ($persistentStorages->count() === 0) { return; } $server = data_get($this, 'destination.server'); foreach ($persistentStorages as $storage) { instant_remote_process(["docker volume rm -f $storage->name"], $server, false); } } public function isConfigurationChanged(bool $save = false) { $newConfigHash = $this->image.$this->ports_mappings.$this->postgres_initdb_args.$this->postgres_host_auth_method; $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort()); $newConfigHash = md5($newConfigHash); $oldConfigHash = data_get($this, 'config_hash'); if ($oldConfigHash === null) { if ($save) { $this->config_hash = $newConfigHash; $this->save(); } return true; } if ($oldConfigHash === $newConfigHash) { return false; } else { if ($save) { $this->config_hash = $newConfigHash; $this->save(); } return true; } } public function isRunning() { return (bool) str($this->status)->contains('running'); } public function isExited() { return (bool) str($this->status)->startsWith('exited'); } public function realStatus() { return $this->getRawOriginal('status'); } public function status(): Attribute { return Attribute::make( set: function ($value) { if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } return "$status:$health"; }, get: function ($value) { if (str($value)->contains('(')) { $status = str($value)->before('(')->trim()->value(); $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; } elseif (str($value)->contains(':')) { $status = str($value)->before(':')->trim()->value(); $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; } else { $status = $value; $health = 'unhealthy'; } return "$status:$health"; }, ); } public function tags() { return $this->morphToMany(Tag::class, 'taggable'); } public function project() { return data_get($this, 'environment.project'); } public function link() { if (data_get($this, 'environment.project.uuid')) { return route('project.database.configuration', [ 'project_uuid' => data_get($this, 'environment.project.uuid'), 'environment_uuid' => data_get($this, 'environment.uuid'), 'database_uuid' => data_get($this, 'uuid'), ]); } return null; } public function isLogDrainEnabled() { return data_get($this, 'is_log_drain_enabled', false); } public function portsMappings(): Attribute { return Attribute::make( set: fn ($value) => $value === '' ? null : $value, ); } public function portsMappingsArray(): Attribute { return Attribute::make( get: fn () => is_null($this->ports_mappings) ? [] : explode(',', $this->ports_mappings), ); } public function team() { return data_get($this, 'environment.project.team'); } public function databaseType(): Attribute { return new Attribute( get: fn () => $this->type(), ); } public function type(): string { return 'standalone-postgresql'; } protected function internalDbUrl(): Attribute { return new Attribute( get: function () { $encodedUser = rawurlencode($this->postgres_user); $encodedPass = rawurlencode($this->postgres_password); $url = "postgres://{$encodedUser}:{$encodedPass}@{$this->uuid}:5432/{$this->postgres_db}"; if ($this->enable_ssl) { $url .= "?sslmode={$this->ssl_mode}"; if (in_array($this->ssl_mode, ['verify-ca', 'verify-full'])) { $url .= '&sslrootcert=/etc/ssl/certs/coolify-ca.crt'; } } return $url; }, ); } protected function externalDbUrl(): Attribute { return new Attribute( get: function () { if ($this->is_public && $this->public_port) { $serverIp = $this->destination->server->getIp; if (empty($serverIp)) { return null; } $encodedUser = rawurlencode($this->postgres_user); $encodedPass = rawurlencode($this->postgres_password); $url = "postgres://{$encodedUser}:{$encodedPass}@{$serverIp}:{$this->public_port}/{$this->postgres_db}"; if ($this->enable_ssl) { $url .= "?sslmode={$this->ssl_mode}"; if (in_array($this->ssl_mode, ['verify-ca', 'verify-full'])) { $url .= '&sslrootcert=/etc/ssl/certs/coolify-ca.crt'; } } return $url; } return null; } ); } public function environment() { return $this->belongsTo(Environment::class); } public function persistentStorages() { return $this->morphMany(LocalPersistentVolume::class, 'resource'); } public function fileStorages() { return $this->morphMany(LocalFileVolume::class, 'resource'); } public function sslCertificates() { return $this->morphMany(SslCertificate::class, 'resource'); } public function destination() { return $this->morphTo(); } public function runtime_environment_variables() { return $this->morphMany(EnvironmentVariable::class, 'resourceable'); } public function scheduledBackups() { return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } public function environment_variables() { return $this->morphMany(EnvironmentVariable::class, 'resourceable'); } public function isBackupSolutionAvailable() { return true; } }