This note records a simple SSL certificate workflow with certbot: acquire, verify, renew, and deploy.
The example domain is api.example.com. Replace it with your own domain.
Acquire A Certificate
For a standalone HTTP-01 challenge, port 80 must be reachable and not occupied by another process:
sudo certbot certonly --standalone -d api.example.comIf Caddy already owns port 80, first consider letting Caddy manage its own certificates, or using DNS-01 / a properly served webroot. The standalone alternative below causes downtime and is only for this port-conflict branch, not every service on the host.
Create certbot-caddy-stop.sh in your working directory:
#!/usr/bin/env bashset -euo pipefailif systemctl is-active --quiet caddy; then install -d -o root -g root -m 0700 /run/certbot-caddy-renewal touch /run/certbot-caddy-renewal/was-active systemctl stop caddyfiCreate certbot-caddy-start.sh:
#!/usr/bin/env bashset -euo pipefailif [[ -f /run/certbot-caddy-renewal/was-active ]]; then systemctl start caddy rm -f /run/certbot-caddy-renewal/was-activefiInstall the inspected scripts as root-owned hooks, then request the certificate with those hooks:
sudo install -o root -g root -m 0755 certbot-caddy-stop.sh /usr/local/sbin/certbot-caddy-stopsudo install -o root -g root -m 0755 certbot-caddy-start.sh /usr/local/sbin/certbot-caddy-startsudo certbot certonly --standalone -d api.example.com \ --pre-hook /usr/local/sbin/certbot-caddy-stop \ --post-hook /usr/local/sbin/certbot-caddy-startsudo systemctl status caddyThe marker restores Caddy only if it was active before the attempt. Certbot normally runs the post-hook after renewal attempts even when validation fails; a killed process or host failure can still prevent it. After an interrupted run, inspect Caddy and the marker and restore service manually if needed. Serialize renewal jobs using this pair; the shared marker is not designed for concurrent Certbot instances. Hook files and all ancestor directories must not be writable by unprivileged users.
Check The Certificate
sudo certbot certificatessudo openssl x509 \ -in /etc/letsencrypt/live/api.example.com/fullchain.pem \ -noout -datesEnable Renewal
sudo systemctl enable --now certbot.timersystemctl list-timers | grep certbotFor the Caddy-conflict branch, check that the certificate’s /etc/letsencrypt/renewal/api.example.com.conf retains the standalone authenticator and the intended pre/post hook paths before relying on the timer. An existing certificate obtained without these hooks needs its renewal configuration updated using the installed Certbot version’s supported reconfiguration workflow. Keep the hooks off lineages that do not require this downtime.
Run a dry test (the pre/post hooks may stop and restart Caddy):
sudo certbot renew --dry-runDeploy Hook
Some services cannot read directly from /etc/letsencrypt/live/..., or they need certificate files copied into a service-owned directory.
Example hook for a service called myapp. Confirm that the actual service identity is a member of group myapp and that all parent directories allow traversal by that group. Here the directory remains root-owned, with read/traverse access for the service group; the private key is never world-readable. Replace the domain/group/service names for your deployment. Save the following as deploy-myapp-cert.sh:
#!/usr/bin/env bashset -euo pipefail
DOMAIN="api.example.com"TARGET_DIR="/etc/myapp/cert"SERVICE_GROUP="myapp"
# Deploy only the lineage that this service uses.case " ${RENEWED_DOMAINS:-} " in *" $DOMAIN "*) ;; *) exit 0 ;;esacLINEAGE="${RENEWED_LINEAGE:?Certbot must provide RENEWED_LINEAGE}"getent group "$SERVICE_GROUP" >/dev/null
install -d -o root -g "$SERVICE_GROUP" -m 0750 "$TARGET_DIR"install -o root -g "$SERVICE_GROUP" -m 0644 "$LINEAGE/fullchain.pem" "$TARGET_DIR/fullchain.pem"install -o root -g "$SERVICE_GROUP" -m 0640 "$LINEAGE/privkey.pem" "$TARGET_DIR/privkey.pem"
systemctl reload myappInstall it as a deploy hook:
sudo install -o root -g root -m 0755 deploy-myapp-cert.sh \ /etc/letsencrypt/renewal-hooks/deploy/myapp.shVerify the directory/key owner, group and mode, then test certificate reads as the actual service identity. A successful copy does not prove the service can read the key or supports reload. Plain renew --dry-run does not normally run deploy hooks; use the installed version’s --run-deploy-hooks support deliberately (it can reload the real service). This example does not provide atomic replacement of the certificate/key pair; coordinate readers or use the service’s documented deployment mechanism when concurrent reads matter. No live issuance, renewal or myapp reload was tested here.