diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml
index 0f4df352bd..f160e38b94 100644
--- a/app/views/install/compose.phtml
+++ b/app/views/install/compose.phtml
@@ -11,6 +11,8 @@ $httpsPort = $this->getParam('httpsPort', '');
$version = $this->getParam('version', '');
$organization = $this->getParam('organization', '');
$image = $this->getParam('image', '');
+$appwriteImage = $this->getParam('appwriteImage', '');
+$useHostSourceMount = $this->getParam('useHostSourceMount', false);
$enableAssistant = $this->getParam('enableAssistant', false);
$dbService = $this->getParam('database', 'mongodb');
$allowedDbServices = ['mariadb', 'mongodb', 'postgresql'];
@@ -18,6 +20,9 @@ if (!\in_array($dbService, $allowedDbServices, true)) {
$dbService = 'mongodb';
}
$hostPath = rtrim($this->getParam('hostPath', ''), '/');
+if ($appwriteImage === '') {
+ $appwriteImage = $organization . '/' . $image . ':' . $version;
+}
?>services:
traefik:
image: traefik:3.6
@@ -46,7 +51,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- appwrite
appwrite:
- image: /:
+ image:
container_name: appwrite
<<: *x-logging
restart: unless-stopped
@@ -67,7 +72,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- traefik.http.routers.appwrite_api_https.service=appwrite_api
- traefik.http.routers.appwrite_api_https.tls=true
volumes:
-
+
- ":/usr/src/code:rw"
- appwrite-uploads:/storage/uploads:rw
@@ -214,7 +219,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- traefik.http.routers.appwrite_console_https.tls=true
appwrite-realtime:
- image: /:
+ image:
entrypoint: realtime
container_name: appwrite-realtime
<<: *x-logging
@@ -261,7 +266,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_LOGGING_CONFIG
appwrite-worker-audits:
- image: /:
+ image:
entrypoint: worker-audits
<<: *x-logging
container_name: appwrite-worker-audits
@@ -291,7 +296,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_LOGGING_CONFIG
appwrite-worker-webhooks:
- image: /:
+ image:
entrypoint: worker-webhooks
<<: *x-logging
container_name: appwrite-worker-webhooks
@@ -323,7 +328,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_LOGGING_CONFIG
appwrite-worker-deletes:
- image: /:
+ image:
entrypoint: worker-deletes
<<: *x-logging
container_name: appwrite-worker-deletes
@@ -390,7 +395,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_EMAIL_CERTIFICATES
appwrite-worker-databases:
- image: /:
+ image:
entrypoint: worker-databases
<<: *x-logging
container_name: appwrite-worker-databases
@@ -420,7 +425,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_LOGGING_CONFIG
appwrite-worker-builds:
- image: /:
+ image:
entrypoint: worker-builds
<<: *x-logging
container_name: appwrite-worker-builds
@@ -492,7 +497,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DOMAIN_SITES
appwrite-worker-certificates:
- image: /:
+ image:
entrypoint: worker-certificates
<<: *x-logging
container_name: appwrite-worker-certificates
@@ -533,7 +538,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_LOGGING_CONFIG
appwrite-worker-functions:
- image: /:
+ image:
entrypoint: worker-functions
<<: *x-logging
container_name: appwrite-worker-functions
@@ -577,7 +582,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_LOGGING_CONFIG
appwrite-worker-mails:
- image: /:
+ image:
entrypoint: worker-mails
<<: *x-logging
container_name: appwrite-worker-mails
@@ -616,7 +621,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_OPTIONS_FORCE_HTTPS
appwrite-worker-messaging:
- image: /:
+ image:
entrypoint: worker-messaging
<<: *x-logging
container_name: appwrite-worker-messaging
@@ -672,7 +677,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_STORAGE_WASABI_BUCKET
appwrite-worker-migrations:
- image: /:
+ image:
entrypoint: worker-migrations
<<: *x-logging
container_name: appwrite-worker-migrations
@@ -711,7 +716,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
appwrite-task-maintenance:
- image: /:
+ image:
entrypoint: maintenance
<<: *x-logging
container_name: appwrite-task-maintenance
@@ -755,7 +760,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_MAINTENANCE_RETENTION_SCHEDULES
appwrite-task-stats-resources:
- image: /:
+ image:
container_name: appwrite-task-stats-resources
entrypoint: stats-resources
<<: *x-logging
@@ -788,7 +793,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_STATS_RESOURCES_INTERVAL
appwrite-worker-stats-resources:
- image: /:
+ image:
entrypoint: worker-stats-resources
container_name: appwrite-worker-stats-resources
<<: *x-logging
@@ -820,7 +825,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_STATS_RESOURCES_INTERVAL
appwrite-worker-stats-usage:
- image: /:
+ image:
entrypoint: worker-stats-usage
container_name: appwrite-worker-stats-usage
<<: *x-logging
@@ -852,7 +857,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_USAGE_AGGREGATION_INTERVAL
appwrite-task-scheduler-functions:
- image: /:
+ image:
entrypoint: schedule-functions
container_name: appwrite-task-scheduler-functions
<<: *x-logging
@@ -881,7 +886,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_ADAPTER
appwrite-task-scheduler-executions:
- image: /:
+ image:
entrypoint: schedule-executions
container_name: appwrite-task-scheduler-executions
<<: *x-logging
@@ -910,7 +915,7 @@ $hostPath = rtrim($this->getParam('hostPath', ''), '/');
- _APP_DB_ADAPTER
appwrite-task-scheduler-messages:
- image: /:
+ image:
entrypoint: schedule-messages
container_name: appwrite-task-scheduler-messages
<<: *x-logging
diff --git a/src/Appwrite/Platform/Tasks/Install.php b/src/Appwrite/Platform/Tasks/Install.php
index af768444f2..0cd2a219b1 100644
--- a/src/Appwrite/Platform/Tasks/Install.php
+++ b/src/Appwrite/Platform/Tasks/Install.php
@@ -106,7 +106,7 @@ class Install extends Action
file_put_contents($this->path . '/' . $composeFileName . '.' . $time . '.backup', $data);
$compose = new Compose($data);
$appwrite = $compose->getService('appwrite');
- $oldVersion = $appwrite?->getImageVersion();
+ $oldImage = $appwrite?->getImage() ?? null;
try {
$ports = $compose->getService('traefik')->getPorts();
} catch (\Throwable $th) {
@@ -117,7 +117,7 @@ class Install extends Action
Console::warning('Traefik not found. Falling back to default ports.');
}
- if ($oldVersion) {
+ if (!empty($oldImage)) {
foreach ($compose->getServices() as $service) {
if (!$service) {
continue;
@@ -509,12 +509,14 @@ class Install extends Action
$this->applyLocalPaths($isLocalInstall, false);
$isCLI = php_sapi_name() === 'cli';
- if ($isLocalInstall || $isUpgrade) {
- $useExistingConfig = false;
- } else {
- $useExistingConfig = file_exists($this->path . '/' . $this->getComposeFileName())
- && file_exists($this->path . '/' . $this->getEnvFileName());
- }
+ $existingComposePath = $this->path . '/' . $this->getComposeFileName();
+ $existingEnvPath = $this->path . '/' . $this->getEnvFileName();
+ $useExistingConfig = $this->shouldReuseExistingConfig(
+ $isLocalInstall,
+ $isUpgrade,
+ file_exists($existingComposePath) && file_exists($existingEnvPath),
+ $this->readExistingComposeImage()
+ );
if ($isLocalInstall) {
$image = 'appwrite';
@@ -531,6 +533,8 @@ class Install extends Action
$version = 'local';
}
+ $appwriteImage = $this->resolveComposeAppwriteImage($isLocalInstall, $organization, $image, $version);
+
$assistantKey = (string) ($input['_APP_ASSISTANT_OPENAI_API_KEY'] ?? '');
$enableAssistant = trim($assistantKey) !== '';
@@ -540,8 +544,10 @@ class Install extends Action
->setParam('version', $version)
->setParam('organization', $organization)
->setParam('image', $image)
+ ->setParam('appwriteImage', $appwriteImage)
->setParam('database', $database)
->setParam('hostPath', $this->hostPath)
+ ->setParam('useHostSourceMount', $isLocalInstall)
->setParam('enableAssistant', $enableAssistant);
$templateForEnv->setParam('vars', $input);
@@ -1096,6 +1102,63 @@ class Install extends Action
return !empty($data) ? $data : '';
}
+ protected function readExistingComposeImage(): ?string
+ {
+ $data = $this->readExistingCompose();
+
+ if ($data === '') {
+ return null;
+ }
+
+ try {
+ $compose = new Compose($data);
+ return $compose->getService('appwrite')?->getImage();
+ } catch (\Throwable) {
+ return null;
+ }
+ }
+
+ protected function shouldUseLocalAppwriteImage(): bool
+ {
+ return \trim((string) \getenv('_APP_VERSION')) === 'dev' && $this->dockerImageExists('appwrite-dev');
+ }
+
+ protected function resolveComposeAppwriteImage(bool $isLocalInstall, string $organization, string $image, string $version): string
+ {
+ if ($isLocalInstall || $this->shouldUseLocalAppwriteImage()) {
+ return 'appwrite-dev';
+ }
+
+ return $organization . '/' . $image . ':' . $version;
+ }
+
+ protected function shouldReuseExistingConfig(bool $isLocalInstall, bool $isUpgrade, bool $configFilesExist, ?string $existingImage = null): bool
+ {
+ if ($isLocalInstall || $isUpgrade || !$configFilesExist) {
+ return false;
+ }
+
+ // Avoid sticky mutable or legacy dev tags from previous failed installer runs.
+ if ($existingImage !== null && $this->isStaleMutableAppwriteImageReference($existingImage)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ protected function isStaleMutableAppwriteImageReference(string $image): bool
+ {
+ return \in_array(\strtolower(\trim($image)), ['appwrite/appwrite:dev', 'appwrite/appwrite:unknown'], true);
+ }
+
+ protected function dockerImageExists(string $image): bool
+ {
+ $result = 1;
+ $output = [];
+ \exec('docker image inspect ' . \escapeshellarg($image) . ' >/dev/null 2>&1', $output, $result);
+ return $result === 0;
+ }
+
protected function generatePasswordValue(string $varName, Password $password): string
{
$value = $password->generate();
diff --git a/tests/unit/Platform/Tasks/InstallTest.php b/tests/unit/Platform/Tasks/InstallTest.php
new file mode 100644
index 0000000000..d54192193c
--- /dev/null
+++ b/tests/unit/Platform/Tasks/InstallTest.php
@@ -0,0 +1,57 @@
+assertSame('appwrite-dev', $this->resolveComposeAppwriteImageForTest(false, 'appwrite', 'appwrite', 'dev', true));
+ }
+
+ public function testShouldNotReuseExistingConfigForDevTaggedCompose(): void
+ {
+ $this->assertFalse($this->shouldReuseExistingConfigForTest(false, false, true, 'appwrite/appwrite:dev'));
+ }
+
+ public function testShouldReuseExistingConfigForLocalAppwriteImageReference(): void
+ {
+ $this->assertTrue($this->shouldReuseExistingConfigForTest(false, false, true, 'appwrite-dev'));
+ }
+
+ private function resolveComposeAppwriteImageForTest(bool $isLocalInstall, string $organization, string $image, string $version, bool $useLocalAppwriteImage): string
+ {
+ $install = new class ($useLocalAppwriteImage) extends Install {
+ public function __construct(private bool $useLocalAppwriteImage)
+ {
+ }
+
+ public function resolveComposeAppwriteImageForTest(bool $isLocalInstall, string $organization, string $image, string $version): string
+ {
+ return $this->resolveComposeAppwriteImage($isLocalInstall, $organization, $image, $version);
+ }
+
+ protected function shouldUseLocalAppwriteImage(): bool
+ {
+ return $this->useLocalAppwriteImage;
+ }
+ };
+
+ return $install->resolveComposeAppwriteImageForTest($isLocalInstall, $organization, $image, $version);
+ }
+
+ private function shouldReuseExistingConfigForTest(bool $isLocalInstall, bool $isUpgrade, bool $configFilesExist, ?string $existingImage = null): bool
+ {
+ $install = new class () extends Install {
+ public function shouldReuseExistingConfigForTest(bool $isLocalInstall, bool $isUpgrade, bool $configFilesExist, ?string $existingImage = null): bool
+ {
+ return $this->shouldReuseExistingConfig($isLocalInstall, $isUpgrade, $configFilesExist, $existingImage);
+ }
+ };
+
+ return $install->shouldReuseExistingConfigForTest($isLocalInstall, $isUpgrade, $configFilesExist, $existingImage);
+ }
+}