From 08eb7c726e9e29373359fe55c44fb2b3e1ccac04 Mon Sep 17 00:00:00 2001 From: "A. Stewart" Date: Fri, 26 Jun 2026 15:49:29 -0500 Subject: [PATCH 1/2] provision: support sudo-only VPS (no direct root) hosts Shared-hosting VPS offerings (e.g. GoDaddy) grant sudo access but no direct root login, which breaks multiple assumptions in the provision recipes. All changes are guarded so the default root-login path is unaffected. provision:user: - /root/.ssh/authorized_keys may not exist when you connect as a non-root provision_user. Fall back to that user's authorized_keys (resolved via getent) so the deployer user still gets a login key. - Create {{deploy_path}} and chown it to deployer while still running as the privileged host user, since a non-root user cannot chown. provision:website: - Set remote_user to provision_user (every other provision task does) so commands connect as that user and elevate via the host's become:root; otherwise a standalone run never establishes root and the mkdir/chown run unprivileged -> Permission denied. - Create the log dir and assign owner deployer / group caddy in the root context. The deployer user's supplementary caddy group is not reliably active in the non-login 'sudo -H -u deployer' shell, so a later 'chgrp caddy' as deployer fails with Operation not permitted. --- recipe/provision/user.php | 25 +++++++++++++++++++++---- recipe/provision/website.php | 11 +++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/recipe/provision/user.php b/recipe/provision/user.php index 9940daa75..a9e10a502 100644 --- a/recipe/provision/user.php +++ b/recipe/provision/user.php @@ -35,11 +35,25 @@ $password = run("mkpasswd -m sha-512 '%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'); + $authorizedKeys = '/root/.ssh/authorized_keys'; + if (!test("[ -f $authorizedKeys ]")) { + $provisionHome = run('getent passwd ' . get('provision_user') . ' | cut -d: -f6'); + $authorizedKeys = "$provisionHome/.ssh/authorized_keys"; + } + + try { + // Copy the public key to the deployer user so it can login without password. + run("cp $authorizedKeys /home/deployer/.ssh/authorized_keys"); - // Create ssh key if not already exists. - run('ssh-keygen -f /home/deployer/.ssh/id_ed25519 -t ed25519 -N ""'); + // Create ssh key if not already exists. + run('ssh-keygen -f /home/deployer/.ssh/id_ed25519 -t ed25519 -N ""'); + } catch (\Throwable $e) { + // Copy the public key to the deployer user so it can login without password. + run("sudo cp $authorizedKeys /home/deployer/.ssh/authorized_keys"); + + // Create ssh key if not already exists. + run('sudo ssh-keygen -f /home/deployer/.ssh/id_ed25519 -t ed25519 -N ""'); + } try { run('chown -R deployer:deployer /home/deployer'); @@ -54,6 +68,9 @@ run('usermod -a -G www-data deployer'); run('usermod -a -G caddy deployer'); } + + run("[ -d {{deploy_path}} ] || mkdir -p {{deploy_path}}"); + run("chown -R deployer:deployer {{deploy_path}}"); })->oncePerNode(); diff --git a/recipe/provision/website.php b/recipe/provision/website.php index b7403d295..243555469 100644 --- a/recipe/provision/website.php +++ b/recipe/provision/website.php @@ -23,17 +23,16 @@ desc('Provision website'); task('provision:website', function () { - $restoreBecome = become('deployer'); + set('remote_user', get('provision_user')); - run("[ -d {{deploy_path}} ] || mkdir -p {{deploy_path}}"); - run("chown -R deployer:deployer {{deploy_path}}"); + run("[ -d {{deploy_path}}/log ] || mkdir -p {{deploy_path}}/log"); + run("chown deployer:caddy {{deploy_path}}/log"); + + $restoreBecome = become('deployer'); set('deploy_path', run("realpath {{deploy_path}}")); cd('{{deploy_path}}'); - run("[ -d log ] || mkdir log"); - run("chgrp caddy log"); - $caddyfile = parse(file_get_contents(__DIR__ . '/Caddyfile')); if (test('[ -f Caddyfile ]')) { From f44f2a3b2aa4669f0f213fea742a1cbd6938bb83 Mon Sep 17 00:00:00 2001 From: "A. Stewart" Date: Tue, 30 Jun 2026 04:27:31 -0500 Subject: [PATCH 2/2] provision:website: Set log directory group-writable for Caddy Ensures the caddy daemon (which runs as group `caddy`) can create `access.log` within `{{deploy_path}}/log`. Without `775` permissions, `service caddy reload` fails to open the log writer. --- recipe/provision/website.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/recipe/provision/website.php b/recipe/provision/website.php index 243555469..307bdb729 100644 --- a/recipe/provision/website.php +++ b/recipe/provision/website.php @@ -27,6 +27,9 @@ run("[ -d {{deploy_path}}/log ] || mkdir -p {{deploy_path}}/log"); run("chown deployer:caddy {{deploy_path}}/log"); + // Group-writable so the caddy daemon (group caddy) can create access.log; + // otherwise `service caddy reload` fails to open the log writer. + run("chmod 775 {{deploy_path}}/log"); $restoreBecome = become('deployer');