diff --git a/recipe/provision.php b/recipe/provision.php
index 862a48e2f..500eebaed 100644
--- a/recipe/provision.php
+++ b/recipe/provision.php
@@ -5,6 +5,7 @@
require __DIR__ . '/provision/databases.php';
require __DIR__ . '/provision/nodejs.php';
require __DIR__ . '/provision/php.php';
+require __DIR__ . '/provision/user.php';
require __DIR__ . '/provision/website.php';
use Deployer\Task\Context;
@@ -27,12 +28,12 @@
'provision:install',
'provision:ssh',
'provision:firewall',
- 'provision:deployer',
- 'provision:server',
+ 'provision:user',
'provision:php',
+ 'provision:node',
'provision:databases',
'provision:composer',
- 'provision:npm',
+ 'provision:server',
'provision:website',
'provision:verify',
]);
@@ -169,67 +170,6 @@
->verbose()
->oncePerNode();
-desc('Configures a server');
-task('provision:server', function () {
- run('usermod -a -G www-data caddy');
- $html = <<<'HTML'
-
-
-
-
-
- 404 Not Found
-
-
-
-
-
- Not Found
- The requested URL was not found on this server.
-
-
-
- HTML;
- run("mkdir -p /var/dep/html");
- run("echo $'$html' > /var/dep/html/404.html");
-})->oncePerNode();
-
desc('Configures the ssh');
task('provision:ssh', function () {
run("sed -i 's/PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config");
@@ -241,61 +181,6 @@
}
})->oncePerNode();
-set('sudo_password', function () {
- return askHiddenResponse(' Password for sudo: ');
-});
-
-// Specify which key to copy to server.
-// Set to `false` to disable copy of key.
-set('ssh_copy_id', '~/.ssh/id_rsa.pub');
-
-desc('Setups a deployer user');
-task('provision:deployer', function () {
- if (test('id deployer >/dev/null 2>&1')) {
- // TODO: Check what created deployer user configured correctly.
- // TODO: Update sudo_password of deployer user.
- // TODO: Copy ssh_copy_id to deployer ssh dir.
- info('deployer user already exist');
- } else {
- run('useradd deployer');
- run('mkdir -p /home/deployer/.ssh');
- run('mkdir -p /home/deployer/.deployer');
- run('adduser deployer sudo');
-
- run('chsh -s /bin/bash deployer');
- run('cp /root/.profile /home/deployer/.profile');
- run('cp /root/.bashrc /home/deployer/.bashrc');
-
- // Make color prompt.
- run("sed -i 's/#force_color_prompt=yes/force_color_prompt=yes/' /home/deployer/.bashrc");
-
- $password = run("mkpasswd -m sha-512 '%secret%'", ['secret' => get('sudo_password')]);
- run("usermod --password '%secret%' deployer", ['secret' => $password]);
-
- if (!empty(get('ssh_copy_id'))) {
- $file = parse_home_dir(get('ssh_copy_id'));
- if (!file_exists($file)) {
- info('Configure path to your public key.');
- writeln("");
- writeln(" set('ssh_copy_id', '~/.ssh/id_rsa.pub');");
- writeln("");
- $file = ask(' Specify path to your public ssh key: ', '~/.ssh/id_rsa.pub');
- }
- run('echo "$KEY" >> /root/.ssh/authorized_keys', ['env' => ['KEY' => file_get_contents(parse_home_dir($file))]]);
- }
- run('cp /root/.ssh/authorized_keys /home/deployer/.ssh/authorized_keys');
- run('ssh-keygen -f /home/deployer/.ssh/id_rsa -t rsa -N ""');
-
- run('chown -R deployer:deployer /home/deployer');
- run('chmod -R 755 /home/deployer');
- run('chmod 700 /home/deployer/.ssh/id_rsa');
-
- run('usermod -a -G www-data deployer');
- run('usermod -a -G caddy deployer');
- run('groups deployer');
- }
-})->oncePerNode();
-
desc('Setups a firewall');
task('provision:firewall', function () {
run('ufw allow 22');
diff --git a/recipe/provision/nodejs.php b/recipe/provision/nodejs.php
index 402740f8c..96add6c90 100644
--- a/recipe/provision/nodejs.php
+++ b/recipe/provision/nodejs.php
@@ -2,12 +2,34 @@
namespace Deployer;
-// Node.js version from https://github.com/nodesource/distributions.
-set('nodejs_version', 'node_23.x');
+use function Deployer\Support\starts_with;
+
+set('node_version', '23.x');
desc('Installs npm packages');
-task('provision:npm', function () {
- run('npm install -g fx zx pm2');
- run('pm2 startup');
+task('provision:node', function () {
+ if (has('nodejs_version')) {
+ throw new \RuntimeException('nodejs_version is deprecated, use node_version_version instead.');
+ }
+ $arch = run('uname -m');
+
+ if ($arch === 'arm' || starts_with($arch, 'armv7')) {
+ $filename = 'fnm-arm32';
+ } elseif (starts_with($arch, 'aarch') || starts_with($arch, 'armv8')) {
+ $filename = 'fnm-arm64';
+ } else {
+ $filename = 'fnm-linux';
+ }
+
+ $url = "https://github.com/Schniz/fnm/releases/latest/download/$filename.zip";
+ run("curl -sSL $url --output /tmp/$filename.zip");
+
+ run("unzip /tmp/$filename.zip -d /tmp");
+
+ run("mv /tmp/fnm /usr/local/bin/fnm");
+ run('chmod +x /usr/local/bin/fnm');
+
+ run('fnm install {{node_version}}');
+ appendToFile('~/.bashrc', 'eval "`fnm env`"');
})
->oncePerNode();
diff --git a/recipe/provision/php.php b/recipe/provision/php.php
index 72c1af900..26f01683e 100644
--- a/recipe/provision/php.php
+++ b/recipe/provision/php.php
@@ -34,11 +34,11 @@
run('apt-get install -y ' . implode(' ', $packages), ['env' => ['DEBIAN_FRONTEND' => 'noninteractive']]);
// Configure PHP-CLI
- run("sudo sed -i 's/error_reporting = .*/error_reporting = E_ALL/' /etc/php/$version/cli/php.ini");
- run("sudo sed -i 's/display_errors = .*/display_errors = On/' /etc/php/$version/cli/php.ini");
- run("sudo sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/$version/cli/php.ini");
- run("sudo sed -i 's/upload_max_filesize = .*/upload_max_filesize = 128M/' /etc/php/$version/cli/php.ini");
- run("sudo sed -i 's/;date.timezone.*/date.timezone = UTC/' /etc/php/$version/cli/php.ini");
+ run("sed -i 's/error_reporting = .*/error_reporting = E_ALL/' /etc/php/$version/cli/php.ini");
+ run("sed -i 's/display_errors = .*/display_errors = On/' /etc/php/$version/cli/php.ini");
+ run("sed -i 's/memory_limit = .*/memory_limit = 512M/' /etc/php/$version/cli/php.ini");
+ run("sed -i 's/upload_max_filesize = .*/upload_max_filesize = 128M/' /etc/php/$version/cli/php.ini");
+ run("sed -i 's/;date.timezone.*/date.timezone = UTC/' /etc/php/$version/cli/php.ini");
// Configure PHP-FPM
run("sed -i 's/error_reporting = .*/error_reporting = E_ALL/' /etc/php/$version/fpm/php.ini");
diff --git a/recipe/provision/user.php b/recipe/provision/user.php
new file mode 100644
index 000000000..997838953
--- /dev/null
+++ b/recipe/provision/user.php
@@ -0,0 +1,85 @@
+/dev/null 2>&1')) {
+ // TODO: Check what created deployer user configured correctly.
+ // TODO: Update sudo_password of deployer user.
+ // TODO: Copy ssh_copy_id to deployer ssh dir.
+ info('deployer user already exist');
+ } else {
+ run('useradd deployer');
+ run('mkdir -p /home/deployer/.ssh');
+ run('mkdir -p /home/deployer/.deployer');
+ run('adduser deployer sudo');
+
+ run('chsh -s /bin/bash deployer');
+ run('cp /root/.profile /home/deployer/.profile');
+ run('cp /root/.bashrc /home/deployer/.bashrc');
+
+ // Make color prompt.
+ run("sed -i 's/#force_color_prompt=yes/force_color_prompt=yes/' /home/deployer/.bashrc");
+
+ $password = run("mkpasswd -m sha-512 '%secret%'", ['secret' => get('sudo_password')]);
+ run("usermod --password '%secret%' deployer", ['secret' => $password]);
+
+ // Copy root public key to deployer user so user can login without password.
+ run('cp /root/.ssh/authorized_keys /home/deployer/.ssh/authorized_keys');
+
+ // Create ssh key if not already exists.
+ run('ssh-keygen -f /home/deployer/.ssh/id_ed25519 -t ed25519 -N ""');
+
+ run('chown -R deployer:deployer /home/deployer');
+ run('chmod -R 755 /home/deployer');
+ run('chmod 700 /home/deployer/.ssh/id_ed25519');
+
+ run('usermod -a -G www-data deployer');
+ run('usermod -a -G caddy deployer');
+ run('groups deployer');
+ }
+})->oncePerNode();
+
+
+desc('Copy public key to remote server');
+task('provision:ssh_copy_id', function () {
+ $defaultKeys = [
+ '~/.ssh/id_rsa.pub',
+ '~/.ssh/id_ed25519.pub',
+ '~/.ssh/id_ecdsa.pub',
+ '~/.ssh/id_dsa.pub',
+ ];
+
+ $publicKeyContent = false;
+ foreach ($defaultKeys as $key) {
+ $file = parse_home_dir($key);
+ if (file_exists($file)) {
+ $publicKeyContent = file_get_contents($file);
+ break;
+ }
+ }
+
+ if (!$publicKeyContent) {
+ $publicKeyContent = ask(' Public key: ', false);
+ }
+
+ if (!$publicKeyContent) {
+ info('Skipping public key copy as no public key was found or provided.');
+ return;
+ }
+
+ run('echo "$PUBLIC_KEY" >> /home/deployer/.ssh/authorized_keys', [
+ 'env' => [
+ 'PUBLIC_KEY' => $publicKeyContent,
+ ],
+ ]);
+});
diff --git a/recipe/provision/website.php b/recipe/provision/website.php
index bfdad9cf0..2596550d6 100644
--- a/recipe/provision/website.php
+++ b/recipe/provision/website.php
@@ -12,8 +12,71 @@
return ask(' Public path: ', 'public');
});
+desc('Configures a server');
+task('provision:server', function () {
+ run('usermod -a -G www-data caddy');
+ $html = <<<'HTML'
+
+
+
+
+
+ 404 Not Found
+
+
+
+
+
+ Not Found
+ The requested URL was not found on this server.
+
+
+
+ HTML;
+ run("mkdir -p /var/deployer");
+ run("echo $'$html' > /var/deployer/404.html");
+})->oncePerNode();
+
desc('Provision website');
task('provision:website', function () {
+ $restoreBecome = become('deployer');
+
run("[ -d {{deploy_path}} ] || mkdir -p {{deploy_path}}");
run("chown -R deployer:deployer {{deploy_path}}");
@@ -44,6 +107,8 @@
run("echo $'$caddyfile' > Caddyfile");
}
+ $restoreBecome();
+
if (!test("grep -q 'import {{deploy_path}}/Caddyfile' /etc/caddy/Caddyfile")) {
run("echo 'import {{deploy_path}}/Caddyfile' >> /etc/caddy/Caddyfile");
}
@@ -52,12 +117,12 @@
info("Website {{domain}} configured!");
})->limit(1);
-desc('Shows caddy logs');
-task('logs:caddy', function () {
+desc('Shows access logs');
+task('logs:access', function () {
run('tail -f {{deploy_path}}/log/access.log');
})->verbose();
desc('Shows caddy syslog');
-task('logs:caddy:syslog', function () {
+task('logs:caddy', function () {
run('sudo journalctl -u caddy -f');
})->verbose();
diff --git a/src/Component/Ssh/Client.php b/src/Component/Ssh/Client.php
index 71b3e827c..358686fae 100644
--- a/src/Component/Ssh/Client.php
+++ b/src/Component/Ssh/Client.php
@@ -61,7 +61,7 @@ public function run(Host $host, string $command, array $config = []): string
$shellId = bin2hex(random_bytes(10));
$shellCommand = $host->getShell();
- if ($host->has('become')) {
+ if ($host->has('become') && !empty($host->get('become'))) {
$shellCommand = "sudo -H -u {$host->get('become')} " . $shellCommand;
}
diff --git a/src/Support/helpers.php b/src/Support/helpers.php
index 7b4ca2daf..1f07855c3 100644
--- a/src/Support/helpers.php
+++ b/src/Support/helpers.php
@@ -76,10 +76,10 @@ function str_contains(string $haystack, string $needle): bool
/**
* Checks if string stars with given prefix.
*/
-function starts_with(string $string, string $startString): bool
+function starts_with(string $string, string $prefix): bool
{
- $len = strlen($startString);
- return (substr($string, 0, $len) === $startString);
+ $len = strlen($prefix);
+ return (substr($string, 0, $len) === $prefix);
}
/**
diff --git a/src/functions.php b/src/functions.php
index 0a286a2e2..f95978bc4 100644
--- a/src/functions.php
+++ b/src/functions.php
@@ -34,6 +34,7 @@
use function Deployer\Support\array_merge_alternate;
use function Deployer\Support\env_stringify;
+use function Deployer\Support\escape_shell_argument;
use function Deployer\Support\is_closure;
use function Deployer\Support\str_contains;
@@ -312,10 +313,36 @@ function cd(string $path): void
set('working_path', parse($path));
}
+/**
+ * Change the current user.
+ *
+ * Usage:
+ * ```php
+ * $restore = become('deployer');
+ *
+ * // do something
+ *
+ * $restore(); // revert back to the previous user
+ * ```
+ *
+ * @param string $user
+ * @return \Closure
+ * @throws Exception
+ */
+function become(string $user): \Closure
+{
+ $currentBecome = get('become');
+ set('become', $user);
+ return function () use ($currentBecome) {
+ set('become', $currentBecome);
+ };
+}
+
/**
* Execute a callback within a specific directory and revert back to the initial working directory.
*
* @return mixed|null Return value of the $callback function or null if callback doesn't return anything
+ * @throws Exception
*/
function within(string $path, callable $callback)
{
@@ -326,8 +353,6 @@ function within(string $path, callable $callback)
} finally {
set('working_path', $lastWorkingPath);
}
-
- return null;
}
/**
@@ -826,7 +851,7 @@ function askHiddenResponse(string $message): string
}
if (Deployer::isWorker()) {
- return (string) Deployer::proxyCallToMaster(currentHost(), __FUNCTION__, ...func_get_args());
+ return (string)Deployer::proxyCallToMaster(currentHost(), __FUNCTION__, ...func_get_args());
}
/** @var QuestionHelper */
@@ -840,7 +865,7 @@ function askHiddenResponse(string $message): string
$question->setHidden(true);
$question->setHiddenFallback(false);
- return (string) $helper->ask(input(), output(), $question);
+ return (string)$helper->ask(input(), output(), $question);
}
function input(): InputInterface
@@ -955,3 +980,18 @@ function fetch(string $url, string $method = 'get', array $headers = [], ?string
}
return $http->send($info);
}
+
+
+/**
+ * Appends a string to a file.
+ *
+ * @param string $file
+ * @param string $string
+ * @throws Exception
+ * @throws RunException
+ * @throws TimeoutException
+ */
+function appendToFile(string $file, string $string): void
+{
+ run("echo " . escape_shell_argument($string) . " >> $file");
+}