2022-07-13 07:02:55 +00:00
|
|
|
<?php
|
2022-07-14 02:04:31 +00:00
|
|
|
|
2022-11-14 10:01:41 +00:00
|
|
|
namespace Appwrite\Platform\Tasks;
|
2022-07-13 07:02:55 +00:00
|
|
|
|
|
|
|
|
use Appwrite\Docker\Compose;
|
|
|
|
|
use Appwrite\Docker\Env;
|
2026-01-22 14:22:46 +00:00
|
|
|
use Appwrite\Platform\Installer\Server as InstallerServer;
|
2022-07-13 07:02:55 +00:00
|
|
|
use Appwrite\Utopia\View;
|
2025-11-04 07:51:03 +00:00
|
|
|
use Utopia\Auth\Proofs\Password;
|
|
|
|
|
use Utopia\Auth\Proofs\Token;
|
2022-07-13 07:02:55 +00:00
|
|
|
use Utopia\CLI\Console;
|
|
|
|
|
use Utopia\Config\Config;
|
2024-03-08 12:57:20 +00:00
|
|
|
use Utopia\Platform\Action;
|
2024-10-08 07:54:40 +00:00
|
|
|
use Utopia\Validator\Boolean;
|
|
|
|
|
use Utopia\Validator\Text;
|
2022-07-13 07:02:55 +00:00
|
|
|
|
2022-07-14 02:04:31 +00:00
|
|
|
class Install extends Action
|
|
|
|
|
{
|
2026-01-22 14:22:46 +00:00
|
|
|
private const int INSTALL_STEP_DELAY_SECONDS = 2;
|
|
|
|
|
|
2023-07-24 22:21:34 +00:00
|
|
|
protected string $path = '/usr/src/code/appwrite';
|
|
|
|
|
|
2022-08-02 01:58:36 +00:00
|
|
|
public static function getName(): string
|
|
|
|
|
{
|
|
|
|
|
return 'install';
|
|
|
|
|
}
|
2022-07-14 02:04:31 +00:00
|
|
|
|
2022-07-13 07:02:55 +00:00
|
|
|
public function __construct()
|
|
|
|
|
{
|
|
|
|
|
$this
|
|
|
|
|
->desc('Install Appwrite')
|
2024-01-08 20:56:51 +00:00
|
|
|
->param('http-port', '', new Text(4), 'Server HTTP port', true)
|
|
|
|
|
->param('https-port', '', new Text(4), 'Server HTTPS port', true)
|
2022-07-13 07:02:55 +00:00
|
|
|
->param('organization', 'appwrite', new Text(0), 'Docker Registry organization', true)
|
|
|
|
|
->param('image', 'appwrite', new Text(0), 'Main appwrite docker image', true)
|
|
|
|
|
->param('interactive', 'Y', new Text(1), 'Run an interactive session', true)
|
2024-01-08 20:56:51 +00:00
|
|
|
->param('no-start', false, new Boolean(true), 'Run an interactive session', true)
|
2025-06-04 08:37:43 +00:00
|
|
|
->callback($this->action(...));
|
2025-12-02 10:33:21 +00:00
|
|
|
|
2022-07-13 07:02:55 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-22 11:58:23 +00:00
|
|
|
public function action(string $httpPort, string $httpsPort, string $organization, string $image, string $interactive, bool $noStart, bool $isUpgrade = false): void
|
2022-07-13 07:02:55 +00:00
|
|
|
{
|
|
|
|
|
$config = Config::getParam('variables');
|
|
|
|
|
$defaultHTTPPort = '80';
|
|
|
|
|
$defaultHTTPSPort = '443';
|
2025-12-02 10:33:21 +00:00
|
|
|
/** @var array<string, array<string, string>> $vars array where key is variable name and value is variable */
|
2022-07-13 07:02:55 +00:00
|
|
|
$vars = [];
|
|
|
|
|
|
|
|
|
|
foreach ($config as $category) {
|
|
|
|
|
foreach ($category['variables'] ?? [] as $var) {
|
2023-09-04 17:08:50 +00:00
|
|
|
$vars[$var['name']] = $var;
|
2022-07-13 07:02:55 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Console::success('Starting Appwrite installation...');
|
|
|
|
|
|
2026-01-22 12:42:41 +00:00
|
|
|
$isLocalInstall = !empty(getenv('APPWRITE_INSTALLER_LOCAL'));
|
|
|
|
|
|
|
|
|
|
// Create directory with write permissions
|
2023-07-24 22:21:34 +00:00
|
|
|
if (!\file_exists(\dirname($this->path))) {
|
|
|
|
|
if (!@\mkdir(\dirname($this->path), 0755, true)) {
|
|
|
|
|
Console::error('Can\'t create directory ' . \dirname($this->path));
|
2022-07-13 07:02:55 +00:00
|
|
|
Console::exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 12:42:41 +00:00
|
|
|
// Check for existing installation
|
2023-07-24 22:21:34 +00:00
|
|
|
$data = @file_get_contents($this->path . '/docker-compose.yml');
|
2025-12-02 10:33:21 +00:00
|
|
|
$existingInstallation = $data !== false;
|
2022-07-13 07:02:55 +00:00
|
|
|
|
2025-12-02 10:33:21 +00:00
|
|
|
if ($existingInstallation) {
|
2022-07-13 07:02:55 +00:00
|
|
|
$time = \time();
|
2026-01-22 12:42:41 +00:00
|
|
|
if (!$isLocalInstall) {
|
|
|
|
|
Console::info('Compose file found, creating backup: docker-compose.yml.' . $time . '.backup');
|
|
|
|
|
file_put_contents($this->path . '/docker-compose.yml.' . $time . '.backup', $data);
|
|
|
|
|
}
|
2022-07-13 07:02:55 +00:00
|
|
|
$compose = new Compose($data);
|
|
|
|
|
$appwrite = $compose->getService('appwrite');
|
2023-10-26 20:25:35 +00:00
|
|
|
$oldVersion = $appwrite?->getImageVersion();
|
2022-07-13 07:02:55 +00:00
|
|
|
try {
|
|
|
|
|
$ports = $compose->getService('traefik')->getPorts();
|
|
|
|
|
} catch (\Throwable $th) {
|
|
|
|
|
$ports = [
|
|
|
|
|
$defaultHTTPPort => $defaultHTTPPort,
|
|
|
|
|
$defaultHTTPSPort => $defaultHTTPSPort
|
|
|
|
|
];
|
|
|
|
|
Console::warning('Traefik not found. Falling back to default ports.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($oldVersion) {
|
2025-12-02 10:33:21 +00:00
|
|
|
foreach ($compose->getServices() as $service) {
|
2022-07-13 07:02:55 +00:00
|
|
|
if (!$service) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$env = $service->getEnvironment()->list();
|
|
|
|
|
|
|
|
|
|
foreach ($env as $key => $value) {
|
|
|
|
|
if (is_null($value)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2023-09-04 17:08:50 +00:00
|
|
|
|
|
|
|
|
$configVar = $vars[$key] ?? [];
|
|
|
|
|
if (!empty($configVar) && !($configVar['overwrite'] ?? false)) {
|
|
|
|
|
$vars[$key]['default'] = $value;
|
2022-07-13 07:02:55 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 10:33:21 +00:00
|
|
|
$envData = @file_get_contents($this->path . '/.env');
|
2022-07-13 07:02:55 +00:00
|
|
|
|
2025-12-02 10:33:21 +00:00
|
|
|
if ($envData !== false) {
|
2026-01-22 12:42:41 +00:00
|
|
|
if (!$isLocalInstall) {
|
|
|
|
|
Console::info('Env file found, creating backup: .env.' . $time . '.backup');
|
|
|
|
|
file_put_contents($this->path . '/.env.' . $time . '.backup', $envData);
|
|
|
|
|
}
|
2025-12-02 10:33:21 +00:00
|
|
|
$env = new Env($envData);
|
2022-07-13 07:02:55 +00:00
|
|
|
|
|
|
|
|
foreach ($env->list() as $key => $value) {
|
|
|
|
|
if (is_null($value)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2023-09-04 17:08:50 +00:00
|
|
|
|
|
|
|
|
$configVar = $vars[$key] ?? [];
|
|
|
|
|
if (!empty($configVar) && !($configVar['overwrite'] ?? false)) {
|
|
|
|
|
$vars[$key]['default'] = $value;
|
2022-07-13 07:02:55 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($ports as $key => $value) {
|
|
|
|
|
if ($value === $defaultHTTPPort) {
|
|
|
|
|
$defaultHTTPPort = $key;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($value === $defaultHTTPSPort) {
|
|
|
|
|
$defaultHTTPSPort = $key;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 12:42:41 +00:00
|
|
|
// If interactive and web mode enabled, start web server
|
2025-12-02 10:33:21 +00:00
|
|
|
if ($interactive === 'Y' && Console::isInteractive()) {
|
|
|
|
|
Console::success('Starting web installer...');
|
2026-01-22 14:22:46 +00:00
|
|
|
Console::info('Open your browser at: http://localhost:' . InstallerServer::INSTALLER_WEB_PORT);
|
2025-12-02 10:33:21 +00:00
|
|
|
Console::info('Press Ctrl+C to cancel installation');
|
|
|
|
|
|
|
|
|
|
$this->startWebServer($defaultHTTPPort, $defaultHTTPSPort, $organization, $image, $noStart, $vars);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 12:42:41 +00:00
|
|
|
// Fall back to CLI mode
|
2026-01-22 14:22:46 +00:00
|
|
|
$enableAssistant = false;
|
|
|
|
|
$assistantExistsInOldCompose = false;
|
|
|
|
|
if ($existingInstallation && isset($compose)) {
|
|
|
|
|
try {
|
|
|
|
|
$assistantService = $compose->getService('appwrite-assistant');
|
|
|
|
|
$assistantExistsInOldCompose = $assistantService !== null;
|
|
|
|
|
} catch (\Throwable) {
|
|
|
|
|
/* ignore */
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($interactive === 'Y' && Console::isInteractive()) {
|
|
|
|
|
$prompt = 'Add Appwrite Assistant? (Y/n)' . ($assistantExistsInOldCompose ? ' [Currently enabled]' : '');
|
|
|
|
|
$answer = Console::confirm($prompt);
|
|
|
|
|
|
|
|
|
|
if (empty($answer)) {
|
|
|
|
|
$enableAssistant = $assistantExistsInOldCompose;
|
|
|
|
|
} else {
|
|
|
|
|
$enableAssistant = \strtolower($answer) === 'y';
|
|
|
|
|
}
|
|
|
|
|
} elseif ($assistantExistsInOldCompose) {
|
|
|
|
|
$enableAssistant = true;
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-13 07:02:55 +00:00
|
|
|
if (empty($httpPort)) {
|
|
|
|
|
$httpPort = Console::confirm('Choose your server HTTP port: (default: ' . $defaultHTTPPort . ')');
|
|
|
|
|
$httpPort = ($httpPort) ? $httpPort : $defaultHTTPPort;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (empty($httpsPort)) {
|
|
|
|
|
$httpsPort = Console::confirm('Choose your server HTTPS port: (default: ' . $defaultHTTPSPort . ')');
|
|
|
|
|
$httpsPort = ($httpsPort) ? $httpsPort : $defaultHTTPSPort;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 14:22:46 +00:00
|
|
|
$userInput = [];
|
2023-04-14 23:18:31 +00:00
|
|
|
foreach ($vars as $var) {
|
2026-01-22 14:22:46 +00:00
|
|
|
if ($var['name'] === '_APP_ASSISTANT_OPENAI_API_KEY') {
|
|
|
|
|
if (!$enableAssistant) {
|
|
|
|
|
$userInput[$var['name']] = '';
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!empty($var['default'])) {
|
|
|
|
|
$userInput[$var['name']] = $var['default'];
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Console::isInteractive() && $interactive === 'Y') {
|
|
|
|
|
$userInput[$var['name']] = Console::confirm('Enter your OpenAI API key for Appwrite Assistant:');
|
|
|
|
|
if (empty($userInput[$var['name']])) {
|
|
|
|
|
Console::warning('No API key provided. Assistant will be disabled.');
|
|
|
|
|
$enableAssistant = false;
|
|
|
|
|
$userInput[$var['name']] = '';
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$userInput[$var['name']] = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 10:33:21 +00:00
|
|
|
if (!$var['required'] || !Console::isInteractive() || $interactive !== 'Y') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2022-07-13 07:02:55 +00:00
|
|
|
|
2025-12-02 10:33:21 +00:00
|
|
|
$value = Console::confirm($var['question'] . ' (default: \'' . $var['default'] . '\')');
|
2022-07-13 07:02:55 +00:00
|
|
|
|
2025-12-02 10:33:21 +00:00
|
|
|
if (!empty($value)) {
|
|
|
|
|
$userInput[$var['name']] = $value;
|
2022-07-13 07:02:55 +00:00
|
|
|
}
|
2025-12-02 10:33:21 +00:00
|
|
|
|
|
|
|
|
if ($var['filter'] === 'domainTarget' && !empty($value) && $value !== 'localhost') {
|
|
|
|
|
Console::warning("\nIf you haven't already done so, set the following record for {$value} on your DNS provider:\n");
|
|
|
|
|
$mask = "%-15.15s %-10.10s %-30.30s\n";
|
|
|
|
|
printf($mask, "Type", "Name", "Value");
|
|
|
|
|
printf($mask, "A or AAAA", "@", "<YOUR PUBLIC IP>");
|
|
|
|
|
Console::warning("\nUse 'AAAA' if you're using an IPv6 address and 'A' if you're using an IPv4 address.\n");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$input = $this->prepareEnvironmentVariables($userInput, $vars);
|
2026-01-22 11:58:23 +00:00
|
|
|
$this->performInstallation($httpPort, $httpsPort, $organization, $image, $input, $noStart, null, null, $isUpgrade);
|
2025-12-02 10:33:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
protected function startWebServer(string $defaultHTTPPort, string $defaultHTTPSPort, string $organization, string $image, bool $noStart, array $vars, bool $isUpgrade = false, ?string $lockedDatabase = null): void
|
|
|
|
|
{
|
2026-01-22 14:22:46 +00:00
|
|
|
$port = getenv('APPWRITE_INSTALLER_LOCAL')
|
|
|
|
|
? InstallerServer::INSTALLER_WEB_PORT_INTERNAL
|
|
|
|
|
: InstallerServer::INSTALLER_WEB_PORT;
|
2025-12-02 10:33:21 +00:00
|
|
|
$host = '0.0.0.0';
|
|
|
|
|
|
2026-01-22 12:42:41 +00:00
|
|
|
// Create a router script for handling requests
|
2025-12-02 10:33:21 +00:00
|
|
|
$routerScript = \sys_get_temp_dir() . '/appwrite-installer-router.php';
|
|
|
|
|
$this->createRouterScript($routerScript, $defaultHTTPPort, $defaultHTTPSPort, $organization, $image, $noStart, $vars, $isUpgrade, $lockedDatabase);
|
|
|
|
|
|
2026-01-22 12:42:41 +00:00
|
|
|
// Start PHP built-in server in background
|
2025-12-02 10:33:21 +00:00
|
|
|
$command = \sprintf(
|
2026-01-22 11:58:23 +00:00
|
|
|
'php -S %s:%d %s >/dev/null 2>&1 & echo $!',
|
2025-12-02 10:33:21 +00:00
|
|
|
$host,
|
|
|
|
|
$port,
|
|
|
|
|
\escapeshellarg($routerScript)
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-22 12:42:41 +00:00
|
|
|
// Start the server
|
2026-01-22 11:58:23 +00:00
|
|
|
$output = [];
|
|
|
|
|
$exitCode = 0;
|
2025-12-02 10:33:21 +00:00
|
|
|
\exec($command, $output, $exitCode);
|
2026-01-22 11:58:23 +00:00
|
|
|
$pid = isset($output[0]) ? (int) $output[0] : 0;
|
|
|
|
|
|
|
|
|
|
\register_shutdown_function(function () use ($routerScript, $pid) {
|
|
|
|
|
if (\file_exists($routerScript)) {
|
|
|
|
|
\unlink($routerScript);
|
|
|
|
|
}
|
|
|
|
|
if ($pid > 0 && \function_exists('posix_kill')) {
|
|
|
|
|
@\posix_kill($pid, SIGTERM);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-12-02 10:33:21 +00:00
|
|
|
\sleep(3);
|
|
|
|
|
|
2026-01-22 12:42:41 +00:00
|
|
|
// Check if the server actually started
|
2025-12-02 10:33:21 +00:00
|
|
|
$handle = @fsockopen('localhost', $port, $errno, $errstr, 1);
|
|
|
|
|
if ($handle === false) {
|
|
|
|
|
Console::exit(1);
|
|
|
|
|
}
|
|
|
|
|
\fclose($handle);
|
|
|
|
|
|
2026-01-22 12:42:41 +00:00
|
|
|
// Wait for the server process to finish
|
2025-12-02 10:33:21 +00:00
|
|
|
while (true) {
|
|
|
|
|
$handle = @fsockopen('localhost', $port, $errno, $errstr, 1);
|
|
|
|
|
if ($handle === false) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
\fclose($handle);
|
|
|
|
|
\sleep(1);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 12:42:41 +00:00
|
|
|
// Cleanup
|
2025-12-02 10:33:21 +00:00
|
|
|
if (\file_exists($routerScript)) {
|
|
|
|
|
\unlink($routerScript);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function createRouterScript(string $path, string $defaultHTTPPort, string $defaultHTTPSPort, string $organization, string $image, bool $noStart, array $vars, bool $isUpgrade = false, ?string $lockedDatabase = null): void
|
|
|
|
|
{
|
|
|
|
|
$installPhpPath = __FILE__;
|
|
|
|
|
$appViewsPath = __DIR__ . '/../../../../app/views/install';
|
|
|
|
|
$publicPath = __DIR__ . '/../../../../public';
|
|
|
|
|
$vendorPath = __DIR__ . '/../../../../vendor/autoload.php';
|
|
|
|
|
$appwritePath = __DIR__ . '/../../../../app/init.php';
|
|
|
|
|
|
2026-01-22 14:22:46 +00:00
|
|
|
InstallerServer::writeRouterScript(
|
|
|
|
|
$path,
|
2025-12-02 10:33:21 +00:00
|
|
|
$installPhpPath,
|
|
|
|
|
$appViewsPath,
|
|
|
|
|
$publicPath,
|
2026-01-22 14:22:46 +00:00
|
|
|
$vendorPath,
|
|
|
|
|
$appwritePath,
|
2025-12-02 10:33:21 +00:00
|
|
|
$defaultHTTPPort,
|
|
|
|
|
$defaultHTTPSPort,
|
|
|
|
|
$organization,
|
|
|
|
|
$image,
|
2026-01-22 14:22:46 +00:00
|
|
|
$noStart,
|
|
|
|
|
$vars,
|
|
|
|
|
$isUpgrade,
|
|
|
|
|
$lockedDatabase
|
2025-12-02 10:33:21 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function prepareEnvironmentVariables(array $userInput, array $vars): array
|
|
|
|
|
{
|
|
|
|
|
$input = [];
|
|
|
|
|
$password = new Password();
|
|
|
|
|
$token = new Token();
|
|
|
|
|
|
2026-01-22 12:42:41 +00:00
|
|
|
// Start with all defaults
|
2025-12-02 10:33:21 +00:00
|
|
|
foreach ($vars as $var) {
|
|
|
|
|
if (!empty($var['filter']) && $var['filter'] === 'token') {
|
|
|
|
|
$input[$var['name']] = $token->generate();
|
|
|
|
|
} elseif (!empty($var['filter']) && $var['filter'] === 'password') {
|
|
|
|
|
$input[$var['name']] = $password->generate();
|
|
|
|
|
} else {
|
2022-07-13 07:02:55 +00:00
|
|
|
$input[$var['name']] = $var['default'];
|
|
|
|
|
}
|
2025-12-02 10:33:21 +00:00
|
|
|
}
|
2022-10-14 09:32:17 +00:00
|
|
|
|
2026-01-22 12:42:41 +00:00
|
|
|
// Override with user inputs
|
2025-12-02 10:33:21 +00:00
|
|
|
foreach ($userInput as $key => $value) {
|
2026-01-22 14:22:46 +00:00
|
|
|
if ($value !== null && ($value !== '' || $key === '_APP_ASSISTANT_OPENAI_API_KEY')) {
|
2025-12-02 10:33:21 +00:00
|
|
|
$input[$key] = $value;
|
2022-10-14 09:32:17 +00:00
|
|
|
}
|
2022-07-13 07:02:55 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-22 12:42:41 +00:00
|
|
|
// Set database-specific connection details
|
2025-12-02 10:33:21 +00:00
|
|
|
$database = $input['_APP_DB_ADAPTER'] ?? 'mongodb';
|
|
|
|
|
if ($database === 'mongodb') {
|
|
|
|
|
$input['_APP_DB_HOST'] = 'mongodb';
|
|
|
|
|
$input['_APP_DB_PORT'] = 27017;
|
|
|
|
|
} elseif ($database === 'mariadb') {
|
|
|
|
|
$input['_APP_DB_HOST'] = 'mariadb';
|
|
|
|
|
$input['_APP_DB_PORT'] = 3306;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $input;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 11:58:23 +00:00
|
|
|
private function reportProgress(?callable $progress, string $step, string $status, string $message, array $details = []): void
|
|
|
|
|
{
|
|
|
|
|
if (!$progress) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
$progress($step, $status, $message, $details);
|
|
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function delayProgress(?callable $progress, string $status): void
|
|
|
|
|
{
|
|
|
|
|
if ($progress && $status !== 'error') {
|
|
|
|
|
sleep(self::INSTALL_STEP_DELAY_SECONDS);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-02 10:33:21 +00:00
|
|
|
public function performInstallation(
|
|
|
|
|
string $httpPort,
|
|
|
|
|
string $httpsPort,
|
|
|
|
|
string $organization,
|
|
|
|
|
string $image,
|
|
|
|
|
array $input,
|
2026-01-22 11:58:23 +00:00
|
|
|
bool $noStart,
|
|
|
|
|
?callable $progress = null,
|
|
|
|
|
?string $resumeFromStep = null,
|
|
|
|
|
bool $isUpgrade = false
|
2025-12-02 10:33:21 +00:00
|
|
|
): void {
|
|
|
|
|
$isCLI = php_sapi_name() === 'cli';
|
2026-01-22 12:42:41 +00:00
|
|
|
$useExistingConfig = !empty(getenv('APPWRITE_INSTALLER_LOCAL'))
|
|
|
|
|
&& file_exists($this->path . '/docker-compose.yml')
|
|
|
|
|
&& file_exists($this->path . '/.env');
|
2025-12-02 10:33:21 +00:00
|
|
|
|
2026-01-22 11:58:23 +00:00
|
|
|
if (getenv('APPWRITE_INSTALLER_LOCAL')) {
|
|
|
|
|
$organization = 'appwrite';
|
|
|
|
|
$image = 'appwrite';
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-28 22:09:37 +00:00
|
|
|
$templateForCompose = new View(__DIR__ . '/../../../../app/views/install/compose.phtml');
|
|
|
|
|
$templateForEnv = new View(__DIR__ . '/../../../../app/views/install/env.phtml');
|
2022-07-13 07:02:55 +00:00
|
|
|
|
2025-12-02 10:33:21 +00:00
|
|
|
$database = $input['_APP_DB_ADAPTER'] ?? 'mongodb';
|
|
|
|
|
|
2026-01-22 11:58:23 +00:00
|
|
|
$version = \defined('APP_VERSION_STABLE') ? APP_VERSION_STABLE : 'latest';
|
|
|
|
|
if (getenv('APPWRITE_INSTALLER_LOCAL')) {
|
|
|
|
|
$version = 'local';
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-13 07:02:55 +00:00
|
|
|
$templateForCompose
|
|
|
|
|
->setParam('httpPort', $httpPort)
|
|
|
|
|
->setParam('httpsPort', $httpsPort)
|
2026-01-22 11:58:23 +00:00
|
|
|
->setParam('version', $version)
|
2022-07-13 07:02:55 +00:00
|
|
|
->setParam('organization', $organization)
|
2025-12-02 10:33:21 +00:00
|
|
|
->setParam('image', $image)
|
2026-01-22 14:22:46 +00:00
|
|
|
->setParam('database', $database);
|
2022-07-13 07:02:55 +00:00
|
|
|
|
2023-08-28 22:09:37 +00:00
|
|
|
$templateForEnv->setParam('vars', $input);
|
2022-07-13 07:02:55 +00:00
|
|
|
|
2026-01-22 11:58:23 +00:00
|
|
|
$steps = ['docker-compose', 'env-vars', 'docker-containers'];
|
|
|
|
|
$startIndex = 0;
|
|
|
|
|
if ($resumeFromStep !== null) {
|
|
|
|
|
$resumeIndex = array_search($resumeFromStep, $steps, true);
|
|
|
|
|
if ($resumeIndex !== false) {
|
|
|
|
|
$startIndex = $resumeIndex;
|
|
|
|
|
}
|
2022-07-13 07:02:55 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-22 11:58:23 +00:00
|
|
|
$currentStep = null;
|
|
|
|
|
|
|
|
|
|
$messages = $isUpgrade ? [
|
|
|
|
|
'config-files' => [
|
|
|
|
|
'start' => 'Updating configuration files...',
|
|
|
|
|
'done' => 'Configuration files updated',
|
|
|
|
|
],
|
|
|
|
|
'docker-compose' => [
|
|
|
|
|
'start' => 'Updating Docker Compose file...',
|
|
|
|
|
'done' => 'Docker Compose file updated',
|
|
|
|
|
],
|
|
|
|
|
'env-vars' => [
|
|
|
|
|
'start' => 'Updating environment variables...',
|
|
|
|
|
'done' => 'Environment variables updated',
|
|
|
|
|
],
|
|
|
|
|
'docker-containers' => [
|
|
|
|
|
'start' => 'Restarting Docker containers...',
|
|
|
|
|
'done' => 'Docker containers restarted',
|
|
|
|
|
],
|
|
|
|
|
] : [
|
|
|
|
|
'config-files' => [
|
|
|
|
|
'start' => 'Creating configuration files...',
|
|
|
|
|
'done' => 'Configuration files created',
|
|
|
|
|
],
|
|
|
|
|
'docker-compose' => [
|
|
|
|
|
'start' => 'Generating Docker Compose file...',
|
|
|
|
|
'done' => 'Docker Compose file generated',
|
|
|
|
|
],
|
|
|
|
|
'env-vars' => [
|
|
|
|
|
'start' => 'Configuring environment variables...',
|
|
|
|
|
'done' => 'Environment variables configured',
|
|
|
|
|
],
|
|
|
|
|
'docker-containers' => [
|
|
|
|
|
'start' => 'Starting Docker containers...',
|
|
|
|
|
'done' => 'Docker containers started',
|
|
|
|
|
],
|
|
|
|
|
];
|
2022-07-13 07:02:55 +00:00
|
|
|
|
2026-01-22 11:58:23 +00:00
|
|
|
try {
|
|
|
|
|
if ($startIndex <= 1) {
|
|
|
|
|
$this->reportProgress($progress, 'config-files', 'in-progress', $messages['config-files']['start']);
|
|
|
|
|
$this->delayProgress($progress, 'in-progress');
|
2022-07-13 07:02:55 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-22 11:58:23 +00:00
|
|
|
if ($startIndex <= 0) {
|
|
|
|
|
$currentStep = 'docker-compose';
|
|
|
|
|
$this->reportProgress($progress, 'docker-compose', 'in-progress', $messages['docker-compose']['start']);
|
|
|
|
|
$this->delayProgress($progress, 'in-progress');
|
|
|
|
|
|
2026-01-22 12:42:41 +00:00
|
|
|
if (!$useExistingConfig) {
|
|
|
|
|
if (!\file_put_contents($this->path . '/docker-compose.yml', $templateForCompose->render(false))) {
|
|
|
|
|
throw new \Exception('Failed to save Docker Compose file');
|
|
|
|
|
}
|
2025-12-02 10:33:21 +00:00
|
|
|
}
|
2026-01-22 11:58:23 +00:00
|
|
|
|
|
|
|
|
$this->reportProgress($progress, 'docker-compose', 'completed', $messages['docker-compose']['done']);
|
2025-12-02 10:33:21 +00:00
|
|
|
}
|
2022-07-13 07:02:55 +00:00
|
|
|
|
2026-01-22 11:58:23 +00:00
|
|
|
if ($startIndex <= 1) {
|
|
|
|
|
$currentStep = 'env-vars';
|
|
|
|
|
$this->reportProgress($progress, 'env-vars', 'in-progress', $messages['env-vars']['start']);
|
|
|
|
|
$this->delayProgress($progress, 'in-progress');
|
|
|
|
|
|
2026-01-22 12:42:41 +00:00
|
|
|
if (!$useExistingConfig) {
|
|
|
|
|
if (!\file_put_contents($this->path . '/.env', $templateForEnv->render(false))) {
|
|
|
|
|
throw new \Exception('Failed to save environment variables file');
|
|
|
|
|
}
|
2026-01-22 11:58:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->reportProgress($progress, 'env-vars', 'completed', $messages['env-vars']['done']);
|
|
|
|
|
$this->reportProgress($progress, 'config-files', 'completed', $messages['config-files']['done']);
|
2025-12-02 10:33:21 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-22 12:42:41 +00:00
|
|
|
if ($database === 'mongodb' && !$useExistingConfig) {
|
2026-01-22 11:58:23 +00:00
|
|
|
$mongoEntrypoint = __DIR__ . '/../../../../mongo-entrypoint.sh';
|
2025-12-02 10:33:21 +00:00
|
|
|
|
2026-01-22 11:58:23 +00:00
|
|
|
if (file_exists($mongoEntrypoint)) {
|
|
|
|
|
copy($mongoEntrypoint, $this->path . '/mongo-entrypoint.sh');
|
|
|
|
|
}
|
2025-12-02 10:33:21 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-22 11:58:23 +00:00
|
|
|
if (!$noStart && $startIndex <= 2) {
|
|
|
|
|
$currentStep = 'docker-containers';
|
|
|
|
|
$this->reportProgress($progress, 'docker-containers', 'in-progress', $messages['docker-containers']['start']);
|
|
|
|
|
$this->delayProgress($progress, 'in-progress');
|
|
|
|
|
$envVars = getenv();
|
|
|
|
|
if (!is_array($envVars)) {
|
|
|
|
|
$envVars = [];
|
|
|
|
|
}
|
2026-01-22 12:42:41 +00:00
|
|
|
if (!$useExistingConfig) {
|
|
|
|
|
foreach ($input as $key => $value) {
|
|
|
|
|
if ($value === null || $value === '') {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!preg_match('/^[A-Z0-9_]+$/', $key)) {
|
|
|
|
|
throw new \Exception('Invalid environment variable name');
|
|
|
|
|
}
|
|
|
|
|
$envVars[$key] = (string) $value;
|
2026-01-22 11:58:23 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($isCLI) {
|
|
|
|
|
Console::log("Running \"docker compose up -d --remove-orphans --renew-anon-volumes\"");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$command = [
|
|
|
|
|
'docker',
|
|
|
|
|
'compose',
|
|
|
|
|
'--project-directory',
|
|
|
|
|
$this->path,
|
|
|
|
|
'up',
|
|
|
|
|
'-d',
|
|
|
|
|
'--remove-orphans',
|
|
|
|
|
'--renew-anon-volumes'
|
|
|
|
|
];
|
|
|
|
|
$descriptorSpec = [
|
|
|
|
|
1 => ['pipe', 'w'],
|
|
|
|
|
2 => ['pipe', 'w'],
|
|
|
|
|
];
|
|
|
|
|
$process = \proc_open($command, $descriptorSpec, $pipes, null, $envVars);
|
|
|
|
|
if (!is_resource($process)) {
|
|
|
|
|
throw new \Exception('Failed to start Docker Compose');
|
|
|
|
|
}
|
|
|
|
|
$stdout = stream_get_contents($pipes[1]);
|
|
|
|
|
$stderr = stream_get_contents($pipes[2]);
|
|
|
|
|
if (is_resource($pipes[1])) {
|
|
|
|
|
fclose($pipes[1]);
|
|
|
|
|
}
|
|
|
|
|
if (is_resource($pipes[2])) {
|
|
|
|
|
fclose($pipes[2]);
|
|
|
|
|
}
|
|
|
|
|
$exit = \proc_close($process);
|
|
|
|
|
|
|
|
|
|
if ($exit !== 0) {
|
|
|
|
|
$output = trim(($stderr ?: '') . ($stdout ?: ''));
|
|
|
|
|
$previous = $output !== '' ? new \RuntimeException($output) : null;
|
|
|
|
|
throw new \RuntimeException('Failed to start containers', 0, $previous);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->reportProgress($progress, 'docker-containers', 'completed', $messages['docker-containers']['done']);
|
|
|
|
|
|
|
|
|
|
if ($isCLI) {
|
|
|
|
|
Console::success('Appwrite installed successfully');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if ($isCLI) {
|
|
|
|
|
Console::success('Installation files created. Run "docker compose up -d" to start Appwrite');
|
|
|
|
|
}
|
2025-12-02 10:33:21 +00:00
|
|
|
}
|
2026-01-22 11:58:23 +00:00
|
|
|
} catch (\Throwable $e) {
|
|
|
|
|
if ($currentStep) {
|
|
|
|
|
$details = [
|
|
|
|
|
'trace' => $e->getTraceAsString()
|
|
|
|
|
];
|
|
|
|
|
$previous = $e->getPrevious();
|
|
|
|
|
if ($previous instanceof \Throwable && $previous->getMessage() !== '') {
|
|
|
|
|
$details['output'] = $previous->getMessage();
|
|
|
|
|
}
|
|
|
|
|
$this->reportProgress($progress, $currentStep, 'error', $e->getMessage(), $details);
|
2025-12-02 10:33:21 +00:00
|
|
|
}
|
2026-01-22 11:58:23 +00:00
|
|
|
throw $e;
|
2022-07-13 07:02:55 +00:00
|
|
|
}
|
|
|
|
|
}
|
2022-07-14 02:04:31 +00:00
|
|
|
}
|