diff --git a/recipe/provision-fedora.php b/recipe/provision-fedora.php
new file mode 100644
index 000000000..506c38239
--- /dev/null
+++ b/recipe/provision-fedora.php
@@ -0,0 +1,216 @@
+ $name, 'VERSION_ID' => $version] = parse_ini_string($release);
+ if ($name !== 'Fedora Linux') {
+ warning('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
+ warning('!! !!');
+ warning('!! Only Fedora Linux is supported! !!');
+ warning('!! !!');
+ warning('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
+ if (!askConfirmation(' Do you want to continue? (Not recommended)', false)) {
+ throw new \RuntimeException('Provision aborted due to incompatible OS.');
+ }
+ }
+ // Also only version 43 and newer are supported.
+ if (version_compare($version, '43', '<')) {
+ warning("Fedora $version is not supported. Use Fedora 43 or newer.");
+ if (!askConfirmation(' Do you want to continue? (Not recommended)', false)) {
+ throw new \RuntimeException('Provision aborted due to incompatible OS.');
+ }
+ }
+})->oncePerNode();
+
+desc('Collects required params');
+task('provision:configure', function () {
+ set('remote_user', get('provision_user'));
+
+ $params = [
+ 'sudo_password',
+ 'domain',
+ 'public_path',
+ 'db_type',
+ ];
+ $dbparams = [
+ 'db_user',
+ 'db_name',
+ 'db_password',
+ ];
+
+ $showCode = false;
+
+ foreach ($params as $name) {
+ if (!Context::get()->getConfig()->hasOwn($name)) {
+ $showCode = true;
+ }
+ get($name);
+ }
+
+ if (get('db_type') !== 'none') {
+ foreach ($dbparams as $name) {
+ if (!Context::get()->getConfig()->hasOwn($name)) {
+ $showCode = true;
+ }
+ get($name);
+ }
+ }
+
+ if ($showCode) {
+ $code = "\n\n====== Configuration Start ======";
+ $code .= "\nhost('{{alias}}')";
+ $codeParams = $params;
+ if (get('db_type') !== 'none') {
+ $codeParams = array_merge($codeParams, $dbparams);
+ }
+ foreach ($codeParams as $name) {
+ $code .= "\n ->set('$name', '" . get($name) . "')";
+ }
+ $code .= ";\n";
+ $code .= "====== Configuration End ======\n\n";
+ writeln($code);
+ }
+});
+
+
+desc('Adds repositories and update');
+task('provision:update', function () {
+ set('remote_user', get('provision_user'));
+
+ // Update before installing anything
+ run('dnf -y makecache');
+
+ // Pre-requisites
+ run('dnf install -y curl gnupg2 dnf-plugins-core');
+
+ // Caddy
+ run('dnf copr enable -y @caddy/caddy');
+
+ // Update
+ run('dnf -y makecache');
+})
+ ->oncePerNode()
+ ->verbose();
+
+desc('Upgrades all packages');
+task('provision:upgrade', function () {
+ set('remote_user', get('provision_user'));
+ run('dnf upgrade -y', timeout: 900);
+})
+ ->oncePerNode()
+ ->verbose();
+
+desc('Installs packages');
+task('provision:install', function () {
+ set('remote_user', get('provision_user'));
+ $packages = [
+ 'acl',
+ 'caddy',
+ 'curl',
+ 'fail2ban',
+ 'firewalld',
+ 'gcc',
+ 'gcc-c++',
+ 'git',
+ 'make',
+ 'memcached',
+ 'ncdu',
+ 'nodejs',
+ 'openssh-server',
+ 'pcre-devel',
+ 'pkgconf-pkg-config',
+ 'policycoreutils-python-utils',
+ 'python3',
+ 'sendmail',
+ 'sqlite',
+ 'sqlite-devel',
+ 'unzip',
+ 'util-linux',
+ 'valkey',
+ 'whois',
+ ];
+ run('dnf install -y ' . implode(' ', $packages), timeout: 900);
+
+ run('systemctl enable --now caddy');
+ run('systemctl enable --now valkey');
+ run('systemctl enable --now memcached');
+ run('systemctl enable --now fail2ban');
+})
+ ->verbose()
+ ->oncePerNode();
+
+desc('Configures the ssh');
+task('provision:ssh', function () {
+ set('remote_user', get('provision_user'));
+ run("sed -i 's/PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config");
+ run('ssh-keygen -A');
+ run('systemctl enable sshd');
+ run('systemctl restart sshd');
+ if (test('[ ! -d /root/.ssh ]')) {
+ run('mkdir -p /root/.ssh');
+ run('touch /root/.ssh/authorized_keys');
+ }
+})->oncePerNode();
+
+desc('Setups a firewall');
+task('provision:firewall', function () {
+ set('remote_user', get('provision_user'));
+ run('systemctl enable --now firewalld');
+ run('firewall-cmd --permanent --add-service=ssh');
+ run('firewall-cmd --permanent --add-service=http');
+ run('firewall-cmd --permanent --add-service=https');
+ run('firewall-cmd --reload');
+})->oncePerNode();
+
+desc('Verifies what provision was successful');
+task('provision:verify', function () {
+ fetch('{{domain}}', 'get', [], null, $info, true);
+ if ($info['http_code'] === 404) {
+ info("provisioned successfully!");
+ }
+});
diff --git a/recipe/provision-fedora/404.html b/recipe/provision-fedora/404.html
new file mode 100644
index 000000000..58665778d
--- /dev/null
+++ b/recipe/provision-fedora/404.html
@@ -0,0 +1,51 @@
+
+
+
+
+
+ 404 Not Found
+
+
+
+
+
+ Not Found
+ The requested URL was not found on this server.
+
+
+
diff --git a/recipe/provision-fedora/Caddyfile b/recipe/provision-fedora/Caddyfile
new file mode 100644
index 000000000..727c9a9d4
--- /dev/null
+++ b/recipe/provision-fedora/Caddyfile
@@ -0,0 +1,25 @@
+{{domain}} {
+ root * {{deploy_path}}/current/{{public_path}}
+ encode zstd gzip
+ file_server
+ php_fastcgi * unix//run/php-fpm/www.sock {
+ resolve_root_symlink
+ }
+
+ log {
+ output file {{deploy_path}}/log/access.log {
+ mode 0644
+ }
+ }
+
+ handle_errors {
+ @404 {
+ expression {http.error.status_code} == 404
+ }
+ rewrite @404 /404.html
+ encode zstd gzip
+ file_server {
+ root /var/deployer
+ }
+ }
+}
diff --git a/recipe/provision-fedora/databases.php b/recipe/provision-fedora/databases.php
new file mode 100644
index 000000000..bbe34ba7f
--- /dev/null
+++ b/recipe/provision-fedora/databases.php
@@ -0,0 +1,72 @@
+limit(1);
+
+desc('Provision MariaDB');
+task('provision:mariadb', function () {
+ run('dnf install -y mariadb-server', timeout: 900);
+ run('systemctl enable --now mariadb');
+ run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'0.0.0.0' IDENTIFIED BY '%db_password%';\"", secrets: ['db_password' => get('db_password')]);
+ run("mysql --user=\"root\" -e \"CREATE USER IF NOT EXISTS '{{db_user}}'@'%' IDENTIFIED BY '%db_password%';\"", secrets: ['db_password' => get('db_password')]);
+ run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'0.0.0.0' WITH GRANT OPTION;\"");
+ run("mysql --user=\"root\" -e \"GRANT ALL PRIVILEGES ON *.* TO '{{db_user}}'@'%' WITH GRANT OPTION;\"");
+ run("mysql --user=\"root\" -e \"FLUSH PRIVILEGES;\"");
+ run("mysql --user=\"root\" -e \"CREATE DATABASE IF NOT EXISTS {{db_name}} character set UTF8mb4 collate utf8mb4_bin;\"");
+});
+
+desc('Provision PostgreSQL');
+task('provision:postgresql', function () {
+ $version = get('postgresql_version');
+
+ // Fedora's own repos only ship a single PostgreSQL version. PGDG gives us
+ // a properly maintained, versioned package instead.
+ run('dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/F-{{fedora_version}}-x86_64/pgdg-fedora-repo-latest.noarch.rpm');
+ run('dnf -y -q module disable postgresql', nothrow: true);
+
+ run("dnf install -y postgresql$version-server postgresql$version-contrib", timeout: 900);
+ run("/usr/pgsql-$version/bin/postgresql-$version-setup initdb", nothrow: true);
+ run("systemctl enable --now postgresql-$version");
+
+ run("sudo -u postgres /usr/pgsql-$version/bin/psql <<< $'CREATE DATABASE {{db_name}};'");
+ run("sudo -u postgres /usr/pgsql-$version/bin/psql <<< $'CREATE USER {{db_user}} WITH ENCRYPTED PASSWORD \'%db_password%\';'", secrets: ['db_password' => get('db_password')]);
+ run("sudo -u postgres /usr/pgsql-$version/bin/psql <<< $'GRANT ALL PRIVILEGES ON DATABASE {{db_name}} TO {{db_user}};'");
+});
diff --git a/recipe/provision-fedora/nodejs.php b/recipe/provision-fedora/nodejs.php
new file mode 100644
index 000000000..45eba13e5
--- /dev/null
+++ b/recipe/provision-fedora/nodejs.php
@@ -0,0 +1,36 @@
+> /etc/profile.d/fnm.sh");
+})
+ ->oncePerNode();
diff --git a/recipe/provision-fedora/php.php b/recipe/provision-fedora/php.php
new file mode 100644
index 000000000..d5d5a58e1
--- /dev/null
+++ b/recipe/provision-fedora/php.php
@@ -0,0 +1,81 @@
+verbose()
+ ->limit(1);
+
+desc('Shows php-fpm logs');
+task('logs:php-fpm', function () {
+ run('sudo tail -f /var/log/php-fpm/www-error.log');
+})->verbose();
+
+desc('Installs Composer');
+task('provision:composer', function () {
+ run('curl -sS https://getcomposer.org/installer | php');
+ run('mv composer.phar /usr/local/bin/composer');
+})->oncePerNode();
diff --git a/recipe/provision-fedora/user.php b/recipe/provision-fedora/user.php
new file mode 100644
index 000000000..019edc93a
--- /dev/null
+++ b/recipe/provision-fedora/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('usermod -aG wheel deployer');
+
+ run('chsh -s /bin/bash deployer');
+ 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("openssl passwd -6 '%password%'", secrets: ['password' => get('sudo_password')]);
+ run("usermod --password '%password%' deployer", secrets: ['password' => $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 ""');
+
+ try {
+ run('chown -R deployer:deployer /home/deployer');
+ run('chmod -R 755 /home/deployer');
+ run('chmod 700 /home/deployer/.ssh');
+ run('chmod 600 /home/deployer/.ssh/id_ed25519');
+ run('chmod 600 /home/deployer/.ssh/authorized_keys');
+ } catch (\Throwable $e) {
+ warning($e->getMessage());
+ }
+
+ run('usermod -a -G caddy 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: ', '');
+ }
+
+ if (empty($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-fedora/website.php b/recipe/provision-fedora/website.php
new file mode 100644
index 000000000..7844148da
--- /dev/null
+++ b/recipe/provision-fedora/website.php
@@ -0,0 +1,83 @@
+ /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}}");
+
+ set('deploy_path', run("realpath {{deploy_path}}"));
+ cd('{{deploy_path}}');
+
+ run("[ -d log ] || mkdir log");
+ run("chgrp caddy log");
+ run("chmod g+w log");
+
+ $caddyfile = parse(file_get_contents(__DIR__ . '/Caddyfile'));
+
+ if (test('[ -f Caddyfile ]')) {
+ run("echo $'$caddyfile' > Caddyfile.new");
+ $diff = run('diff -U5 --color=always Caddyfile Caddyfile.new', nothrow: true);
+ if (empty($diff)) {
+ run('rm Caddyfile.new');
+ } else {
+ info('Found Caddyfile changes');
+ writeln("\n" . $diff);
+ $answer = askChoice(' Which Caddyfile to save? ', ['old', 'new'], 0);
+ if ($answer === 'old') {
+ run('rm Caddyfile.new');
+ } else {
+ run('mv Caddyfile.new Caddyfile');
+ }
+ }
+ } else {
+ run("echo $'$caddyfile' > Caddyfile");
+ }
+
+ $restoreBecome();
+
+ // Caddy runs confined under the httpd_t SELinux domain. deploy_path lives
+ // under the deployer user's home (user_home_t), which httpd_t can't read
+ // or write, so relabel the whole tree as webserver content.
+ run("semanage fcontext -a -t httpd_sys_rw_content_t '{{deploy_path}}(/.*)?'", nothrow: true);
+ run('restorecon -R {{deploy_path}}');
+
+ if (!test("grep -q 'import {{deploy_path}}/Caddyfile' /etc/caddy/Caddyfile")) {
+ run("echo 'import {{deploy_path}}/Caddyfile' >> /etc/caddy/Caddyfile");
+ }
+ run('systemctl reload caddy');
+
+ info("Website {{domain}} configured!");
+})->limit(1);
+
+desc('Shows access logs');
+task('logs:access', function () {
+ run('tail -f {{deploy_path}}/log/access.log');
+})->verbose();
+
+desc('Shows caddy syslog');
+task('logs:caddy', function () {
+ run('sudo journalctl -u caddy -f');
+})->verbose();