DEHYDRATED Deployment Guide for SSL Certificate Automation

Mar 26, 2026

What Is This Guide About?

This guide walks you through everything you need to do after installing the Dehydrated tool to get your SSL certificate up and running on your website. We cover how to put the certificate on your web server, how to make it renew itself automatically, and how to keep everything healthy over time.

Think of Dehydrated as a helper robot that talks to a certificate authority (like Let's Encrypt or the Acme server) on your behalf, gets a trusted SSL certificate, and puts it in the right place so your visitors see the padlock icon in their browser.

? Before You Start: Make sure you have already completed the Installation Guide (INSTALLATION_GUIDE.md). This guide picks up right where that one leaves off.

Section 1 — Deploying Your SSL Certificate to a Web Server

Once Dehydrated has fetched your certificate files, you need to copy them to your web server and tell the server where to find them. Below are instructions for the two most popular web servers: Apache and Nginx.

1A — Installing the Certificate on Apache

Apache is a widely-used web server. Follow these steps to install your certificate into it.

1Create the Apache SSL folder
This is the folder where Apache will look for your certificate files.
mkdir -p /etc/apache2/ssl
2Copy the certificate files across
Replace 'demo.example.com' with your own domain name.
cp /etc/dehydrated/certs/demo.example.com/fullchain.pem \
/etc/apache2/ssl/demo.example.com.fullchain.pem

cp /etc/dehydrated/certs/demo.example.com/privkey.pem \
/etc/apache2/ssl/demo.example.com.key
3Set the correct file permissions
The certificate can be readable by anyone, but the private key must be locked down tightly.
chmod 644 /etc/apache2/ssl/demo.example.com.fullchain.pem
chmod 600 /etc/apache2/ssl/demo.example.com.key
4Add the SSL block to your Apache configuration
Open your Apache virtual host config file and add the settings below inside a <VirtualHost *:443> block.
<VirtualHost *:443>
ServerName demo.example.com
DocumentRoot /var/www/demo.example.com

SSLEngine on
SSLCertificateFile /etc/apache2/ssl/demo.example.com.fullchain.pem
SSLCertificateKeyFile /etc/apache2/ssl/demo.example.com.key

# Only allow modern, secure TLS versions
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
SSLHonorCipherOrder off

# Tell browsers to always use HTTPS for the next 2 years
Header always set Strict-Transport-Security "max-age=63072000"
</VirtualHost>
5Test your Apache configuration, then reload
Always test before reloading so you catch mistakes early.
apache2ctl configtest
systemctl reload apache2
? Success sign: You should see 'Syntax OK' from the configtest command. After reloading, visit your website with https:// and check for the padlock.

1B — Installing the Certificate on Nginx

Nginx is another very popular web server. The process is nearly identical to Apache.

1Create the Nginx SSL folder
mkdir -p /etc/nginx/ssl
2Copy the certificate files
cp /etc/dehydrated/certs/demo.example.com/fullchain.pem \
/etc/nginx/ssl/demo.example.com.fullchain.pem

cp /etc/dehydrated/certs/demo.example.com/privkey.pem \
/etc/nginx/ssl/demo.example.com.key
3Set file permissions
chmod 644 /etc/nginx/ssl/demo.example.com.fullchain.pem
chmod 600 /etc/nginx/ssl/demo.example.com.key
4Update your Nginx site configuration file
Open your server block config and add the SSL settings. Also add a second block that redirects plain HTTP traffic to HTTPS automatically.
server {
listen 443 ssl http2;
server_name demo.example.com;
root /var/www/demo.example.com;

ssl_certificate /etc/nginx/ssl/demo.example.com.fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/demo.example.com.key;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;

# Force HTTPS for 2 years
add_header Strict-Transport-Security "max-age=63072000" always;

location / {
try_files $uri $uri/ =404;
}
}

# Redirect HTTP to HTTPS
server {
listen 80;
server_name demo.example.com;
return 301 https://$host$request_uri;
}
5Test and reload Nginx
nginx -t
systemctl reload nginx

Instead of running the copy commands manually every time your certificate renews, you can make Dehydrated do it automatically by updating the 'deploy_cert' function inside the hook script. This is the recommended approach for production servers.

1Open the Cloudflare hook script in a text editor
nano /etc/dehydrated/hooks/cloudflare.sh
2Find the deploy_cert section and update it
Look for the line that says 'deploy_cert)' and update the block so it copies your new certificate files and reloads Nginx automatically whenever Dehydrated fetches a fresh certificate.
deploy_cert)
local domain="$1"
local keyfile="$2"
local fullchainfile="$4"
local chainfile="$5"
local timestamp="$6"

log "=== deploy_cert ==="
log "Domain: ${domain}"

# Copy new certificate files to Nginx
cp "$fullchainfile" "/etc/nginx/ssl/${domain}.fullchain.pem"
cp "$keyfile" "/etc/nginx/ssl/${domain}.key"
chmod 644 "/etc/nginx/ssl/${domain}.fullchain.pem"
chmod 600 "/etc/nginx/ssl/${domain}.key"

# Reload Nginx so it picks up the new certificate
systemctl reload nginx

echo "Certificate deployed and Nginx reloaded"
;;
???? Tip: You can also add an optional notification here — for example, send a Slack message or email to let your team know a new certificate was deployed.

Section 2 — Setting Up Automatic Certificate Renewal

SSL certificates expire (usually after 90 days). You never want your site to show a scary 'Your connection is not secure' warning. Dehydrated can renew your certificate automatically — you just need to schedule it to run regularly.

There are two common ways to schedule automatic renewal: using a Cron Job (the classic Linux scheduler) or using a Systemd Timer (a more modern alternative). Pick the one you are most comfortable with.

Option A — Set Up a Cron Job (Classic Method)

A cron job is a task you schedule to run at a specific time every day. Think of it like an alarm clock for your server.

1Open the crontab editor
sudo crontab -e
2Add the renewal job (runs every day at 3:00 AM)
# Dehydrated SSL certificate renewal
0 3 * * * source /etc/dehydrated/cloudflare.env && \
/usr/local/bin/dehydrated --cron >> /var/log/dehydrated/cron.log 2>&1
3(Optional) Add email alerts if the renewal fails
If something goes wrong at 3 AM, this will send an email to your admin address.
0 3 * * * source /etc/dehydrated/cloudflare.env && \
/usr/local/bin/dehydrated --cron >> /var/log/dehydrated/cron.log 2>&1 \
|| echo "Dehydrated renewal failed" | mail -s "SSL Renewal Error" admin@example.com
4(Optional) Multiple domains with separate settings
If you have production and staging environments with different DNS providers, you can add separate jobs for each.
# Production (Cloudflare DNS)
0 3 * * * source /etc/dehydrated/cloudflare.env && \
/usr/local/bin/dehydrated --cron --config /etc/dehydrated/config \
>> /var/log/dehydrated/cron.log 2>&1

# Staging (different DNS provider)
0 4 * * * source /etc/dehydrated/staging.env && \
/usr/local/bin/dehydrated --cron --config /etc/dehydrated/staging-config \
>> /var/log/dehydrated/staging-cron.log 2>&1
???? Tip: Dehydrated is smart — it only actually renews the certificate when there are 30 days or fewer left before it expires. So running it daily costs very little.

Option B — Set Up a Systemd Timer (Modern Method)

Systemd timers are a more structured way to schedule tasks on modern Linux systems. They also restart automatically if the system reboots.

1Create the service file — This tells systemd what to run.
sudo nano /etc/systemd/system/dehydrated.service

Paste the following content into the file:

[Unit]
Description=Dehydrated Certificate Renewal
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=root
EnvironmentFile=/etc/dehydrated/cloudflare.env
ExecStart=/usr/local/bin/dehydrated --cron
StandardOutput=append:/var/log/dehydrated/systemd.log
StandardError=append:/var/log/dehydrated/systemd.log
2Create the timer file — This tells systemd when to run the service.
sudo nano /etc/systemd/system/dehydrated.timer

Paste the following content:

[Unit]
Description=Dehydrated Certificate Renewal Timer
Requires=dehydrated.service

[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true

[Install]
WantedBy=timers.target
? What does RandomizedDelaySec do? It adds a random delay of up to 1 hour so that all your servers do not hit the ACME server at exactly the same time.
3Enable and start the timer
sudo systemctl daemon-reload
sudo systemctl enable dehydrated.timer
sudo systemctl start dehydrated.timer
4Check the timer is active
sudo systemctl status dehydrated.timer
sudo systemctl list-timers dehydrated.timer

Test That Renewal Works

1Run a forced renewal to check everything works end-to-end
source /etc/dehydrated/cloudflare.env
dehydrated --cron --force --verbose
2(Optional) Test for a specific domain only
dehydrated --cron --domain demo.example.com --force
? What to look for: No error messages in the output, and a line that says 'Certificate deployed and Nginx reloaded' (if you set up the hook in Section 1C).

Section 3 — Connecting Different DNS Providers

When Dehydrated needs to prove you own your domain, it creates a temporary DNS record called an ACME challenge. The hook script is a small program that creates and deletes this DNS record automatically. Dehydrated works with any DNS provider — you just need the right hook script.

GoDaddy DNS

You will need your GoDaddy API Key and API Secret. Get them from GoDaddy's Developer Portal.

#!/usr/bin/env bash

GD_API_KEY="YOUR_GODADDY_API_KEY"
GD_API_SECRET="YOUR_GODADDY_API_SECRET"
GD_API_URL="https://api.godaddy.com/v1"

create_txt_record() {
local domain="$1"
local token="$2"
curl -s -X PATCH "${GD_API_URL}/domains/${domain}/records" \
-H "Authorization: sso-key ${GD_API_KEY}:${GD_API_SECRET}" \
-H "Content-Type: application/json" \
--data '[{"data":"'"${token}"'","name":"_acme-challenge","ttl":600,"type":"TXT"}]'
}

delete_txt_record() {
local domain="$1"
curl -s -X DELETE "${GD_API_URL}/domains/${domain}/records/TXT/_acme-challenge" \
-H "Authorization: sso-key ${GD_API_KEY}:${GD_API_SECRET}"
}

case "$1" in
deploy_challenge) create_txt_record "$2" "$4"; sleep 120 ;;
clean_challenge) delete_txt_record "$2" ;;
esac

AWS Route 53

You will need the AWS CLI installed and configured with permissions to manage Route 53 records.

#!/usr/bin/env bash

AWS_REGION="us-east-1"

create_txt_record() {
local domain="$1"
local token="$2"
local zone_id
zone_id=$(aws route53 list-hosted-zones \
--query "HostedZones[?Name=='${domain}.'].Id" --output text)

aws route53 change-resource-record-sets \
--hosted-zone-id "$zone_id" \
--change-batch '{"Changes":[{"Action":"UPSERT","ResourceRecordSet":
{"Name":"_acme-challenge.'"${domain}"'","Type":"TXT","TTL":60,
"ResourceRecords":[{"Value":"\"'"${token}"'\""}]}}]}'
sleep 60
}

delete_txt_record() {
local domain="$1"
local zone_id
zone_id=$(aws route53 list-hosted-zones \
--query "HostedZones[?Name=='${domain}.'].Id" --output text)
# (fetch record value then delete it — see full script for details)
}

case "$1" in
deploy_challenge) create_txt_record "$2" "$4" ;;
clean_challenge) delete_txt_record "$2" ;;
esac

DigitalOcean DNS

You will need a DigitalOcean Personal Access Token with write access.

#!/usr/bin/env bash

DO_API_TOKEN="YOUR_DIGITALOCEAN_TOKEN"
DO_API_URL="https://api.digitalocean.com/v2"

create_txt_record() {
local domain="$1"
local token="$2"
curl -s -X POST "${DO_API_URL}/domains/${domain}/records" \
-H "Authorization: Bearer ${DO_API_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"type":"TXT","name":"_acme-challenge","data":"'"${token}"'","ttl":1800}'
sleep 120
}

delete_txt_record() {
local domain="$1"
local record_id
record_id=$(curl -s "${DO_API_URL}/domains/${domain}/records" \
-H "Authorization: Bearer ${DO_API_TOKEN}" | \
jq -r '.domain_records[] | select(.type=="TXT" and .name=="_acme-challenge") | .id')
[[ -n "$record_id" ]] && curl -s -X DELETE \
"${DO_API_URL}/domains/${domain}/records/${record_id}" \
-H "Authorization: Bearer ${DO_API_TOKEN}"
}

case "$1" in
deploy_challenge) create_txt_record "$2" "$4" ;;
clean_challenge) delete_txt_record "$2" ;;
esac

Section 4 — Advanced Deployment Scenarios

Deploy to Multiple Servers at Once

If your website runs on several servers behind a load balancer, you need to copy the fresh certificate to all of them whenever it renews. This script does that via SSH.

#!/bin/bash

DOMAIN="demo.example.com"
CERT_DIR="/etc/dehydrated/certs/${DOMAIN}"
SERVERS=("server1.local" "server2.local" "server3.local")

for server in "${SERVERS[@]}"; do
echo "Deploying to $server..."
scp "${CERT_DIR}/fullchain.pem" root@$server:/etc/ssl/certs/${DOMAIN}.pem
scp "${CERT_DIR}/privkey.pem" root@$server:/etc/ssl/private/${DOMAIN}.key
ssh root@$server "systemctl reload nginx"
echo "Done: $server"
done
? SSH keys required: This script uses passwordless SSH. Make sure your server's root user has the public key of the machine running Dehydrated.

Deploy to Docker Containers

If your web server runs inside a Docker container, copy the certificate files to a shared Docker volume, then restart the relevant containers.

#!/bin/bash

DOMAIN="demo.example.com"
CERT_DIR="/etc/dehydrated/certs/${DOMAIN}"
DOCKER_VOL="/var/docker/ssl"

mkdir -p "$DOCKER_VOL"

cp "${CERT_DIR}/fullchain.pem" "${DOCKER_VOL}/${DOMAIN}.pem"
cp "${CERT_DIR}/privkey.pem" "${DOCKER_VOL}/${DOMAIN}.key"

chmod 644 "${DOCKER_VOL}/${DOMAIN}.pem"
chmod 600 "${DOCKER_VOL}/${DOMAIN}.key"

docker restart nginx-proxy
docker restart web-app

echo "Docker containers updated with new certificates"

Deploy to HAProxy

HAProxy requires a single combined file (certificate + private key joined together).

#!/bin/bash

DOMAIN="demo.example.com"
CERT_DIR="/etc/dehydrated/certs/${DOMAIN}"
HAPROXY_CERT_DIR="/etc/haproxy/certs"

# Combine certificate and private key into one file
cat "${CERT_DIR}/fullchain.pem" "${CERT_DIR}/privkey.pem" \
> "${HAPROXY_CERT_DIR}/${DOMAIN}.pem"

chmod 600 "${HAPROXY_CERT_DIR}/${DOMAIN}.pem"

systemctl reload haproxy

echo "HAProxy certificate deployed"

Deploy to Kubernetes

In Kubernetes, SSL certificates are stored as TLS secrets. This script creates or updates that secret whenever the certificate renews.

#!/bin/bash

DOMAIN="demo.example.com"
CERT_DIR="/etc/dehydrated/certs/${DOMAIN}"
NAMESPACE="production"
SECRET_NAME="tls-secret"

kubectl create secret tls $SECRET_NAME \
--cert="${CERT_DIR}/fullchain.pem" \
--key="${CERT_DIR}/privkey.pem" \
--namespace=$NAMESPACE \
--dry-run=client -o yaml | kubectl apply -f -

# Restart the ingress controller to pick up the new cert
kubectl rollout restart deployment/nginx-ingress-controller -n ingress-nginx

echo "Kubernetes secret updated"

Deploy to Azure Key Vault

This script converts your certificate to PFX format and uploads it to Azure Key Vault where other Azure services can use it.

#!/bin/bash

DOMAIN="demo.example.com"
CERT_DIR="/etc/dehydrated/certs/${DOMAIN}"
VAULT_NAME="mykeyvault"
CERT_NAME="dikshatest-site"
PFX_PASSWORD="SecurePassword123!"

# Convert to PFX format
openssl pkcs12 -export \
-out "/tmp/${CERT_NAME}.pfx" \
-inkey "${CERT_DIR}/privkey.pem" \
-in "${CERT_DIR}/fullchain.pem" \
-password "pass:$PFX_PASSWORD"

# Upload to Azure Key Vault
az keyvault certificate import \
--vault-name $VAULT_NAME \
--name $CERT_NAME \
--file "/tmp/${CERT_NAME}.pfx" \
--password $PFX_PASSWORD

# Clean up the temporary PFX file
rm "/tmp/${CERT_NAME}.pfx"

echo "Certificate uploaded to Azure Key Vault"

Section 5 — Complete One-Click Automation Script

This all-in-one script sets up directories, registers your ACME account, issues the certificate, and deploys it to Nginx — all in a single run. It is great for setting up a brand-new server.

? Important: Replace the placeholder values (EAB_KEY, HMAC_KEY, CF_TOKEN, EMAIL, DOMAIN) with your own real credentials before running this script.
#!/bin/bash

set -e # Stop immediately if any command fails

# ?? Your configuration ??????????????????????????????????????
DOMAIN="demo.example.com"
ACME_SERVER="https://www.freessl.in/acme-new/acme/directory"
EAB_KEY="YOUR_EAB_KEY_HERE"
HMAC_KEY="YOUR_HMAC_KEY_HERE"
EMAIL="you@yourdomain.com"
CF_TOKEN="YOUR_CLOUDFLARE_API_TOKEN"
# ?????????????????????????????????????????????????????????????

export CF_API_TOKEN="$CF_TOKEN"

# Step 1: Create required folders
mkdir -p /etc/dehydrated/{accounts,certs,hooks,chains}
mkdir -p /var/log/dehydrated

# Step 2: Register your ACME account (only needed once)
if [[ -z "$(ls -A /etc/dehydrated/accounts)" ]]; then
echo "Registering account..."
dehydrated --register --accept-terms \
--eab-kid "$EAB_KEY" \
--eab-hmac-key "$HMAC_KEY"
fi

# Step 3: Add your domain to the domains list
[[ ! -f /etc/dehydrated/domains.txt ]] && echo "$DOMAIN" > /etc/dehydrated/domains.txt

# Step 4: Issue the certificate
echo "Issuing certificate for $DOMAIN..."
dehydrated --cron --force --verbose

# Step 5: Deploy to Nginx
CERT_DIR="/etc/dehydrated/certs/${DOMAIN}"
NGINX_SSL_DIR="/etc/nginx/ssl"

mkdir -p "$NGINX_SSL_DIR"

cp "${CERT_DIR}/fullchain.pem" "${NGINX_SSL_DIR}/${DOMAIN}.pem"
cp "${CERT_DIR}/privkey.pem" "${NGINX_SSL_DIR}/${DOMAIN}.key"

chmod 644 "${NGINX_SSL_DIR}/${DOMAIN}.pem"
chmod 600 "${NGINX_SSL_DIR}/${DOMAIN}.key"

# Step 6: Test and reload Nginx
nginx -t && systemctl reload nginx

echo "Certificate deployment completed successfully!"

Section 6 — Monitoring and Maintenance

Check When Your Certificate Expires

Check from the certificate file on disk:

openssl x509 -in /etc/dehydrated/certs/demo.example.com/cert.pem \
-noout -dates

Check live from your running web server:

echo | openssl s_client -connect demo.example.com:443 \
-servername demo.example.com 2>/dev/null | openssl x509 -noout -dates

Show the exact number of days remaining:

echo $(( ($(date -d "$(openssl x509 -in /etc/dehydrated/certs/demo.example.com/cert.pem \
-noout -enddate | cut -d= -f2)" +%s) - $(date +%s)) / 86400 )) days

Set Up a Certificate Expiry Alert

This monitoring script will automatically send you an email if any certificate is getting close to its expiry date. Save it as /usr/local/bin/cert-monitor.sh and make it executable.

#!/bin/bash

ALERT_DAYS=14 # Send alert when fewer than 14 days remain
EMAIL="admin@example.com"

for cert_dir in /etc/dehydrated/certs/*/; do
domain=$(basename "$cert_dir")
cert_file="${cert_dir}/cert.pem"

[[ ! -f "$cert_file" ]] && continue

expiry_date=$(openssl x509 -in "$cert_file" -noout -enddate | cut -d= -f2)
expiry_epoch=$(date -d "$expiry_date" +%s)
days_remaining=$(( ($expiry_epoch - $(date +%s)) / 86400 ))

echo "Domain: $domain | Days remaining: $days_remaining"

if [[ $days_remaining -lt $ALERT_DAYS ]]; then
echo "WARNING: $domain expires in $days_remaining days!" | \
mail -s "SSL Expiry Warning: $domain" $EMAIL
fi
done

Add it to crontab to run daily at 6 AM:

0 6 * * * /usr/local/bin/cert-monitor.sh >> /var/log/dehydrated/monitor.log 2>&1

List All Managed Certificates

# Quick list of certificate folders
ls -la /etc/dehydrated/certs/

# Show domain name + expiry date for each
for domain in /etc/dehydrated/certs/*/; do
d=$(basename "$domain")
exp=$(openssl x509 -in "${domain}/cert.pem" -noout -enddate 2>/dev/null | cut -d= -f2)
echo "Domain: $d | Expires: $exp"
done

View Logs

# Watch the cron renewal log live
tail -f /var/log/dehydrated/cron.log

# Watch the hook script log
tail -f /var/log/dehydrated/cloudflare-hook.log

# View systemd logs (if using systemd timer)
journalctl -u dehydrated.service -f

# Show the last 50 lines from all Dehydrated logs
tail -n 50 /var/log/dehydrated/*.log

Back Up Your Certificates

Quick one-off backup commands:

# Back up everything (all domains and config)
tar -czf dehydrated-backup-$(date +%Y%m%d).tar.gz /etc/dehydrated/

# Back up only a specific domain
tar -czf demo.example.com-backup-$(date +%Y%m%d).tar.gz \
/etc/dehydrated/certs/demo.example.com/

# Upload backup to another server
scp dehydrated-backup-*.tar.gz backup-server:/backups/

# Or upload to Amazon S3
aws s3 cp dehydrated-backup-*.tar.gz s3://my-backup-bucket/

Automated daily backup script (saves 30 days of history):

#!/bin/bash

BACKUP_DIR="/backup/ssl"
RETENTION_DAYS=30

mkdir -p "$BACKUP_DIR"

# Create today's backup
tar -czf "${BACKUP_DIR}/dehydrated-$(date +%Y%m%d).tar.gz" /etc/dehydrated/

# Delete backups older than 30 days
find "$BACKUP_DIR" -name "dehydrated-*.tar.gz" -mtime +$RETENTION_DAYS -delete

echo "Backup saved to ${BACKUP_DIR}/dehydrated-$(date +%Y%m%d).tar.gz"

Section 7 — Troubleshooting Common Problems

Problem 1 — Hook Script Is Not Running

Symptom: Certificate issuance fails with a message about the hook or no DNS record being created.

Solution: Check that the hook script file has the execute permission and starts with the correct line.

# Grant execute permission
chmod +x /etc/dehydrated/hooks/cloudflare.sh

# Check the first line — it must say #!/usr/bin/env bash
head -n 1 /etc/dehydrated/hooks/cloudflare.sh

# Test the hook manually
bash -x /etc/dehydrated/hooks/cloudflare.sh \
deploy_challenge "test.com" "token_file" "token_value"

Problem 2 — DNS Record Takes Too Long to Spread

Symptom: The ACME server says the challenge failed because it could not find the DNS record.

Solution: Increase the wait time in your hook script so it gives DNS more time to propagate globally.

nano /etc/dehydrated/hooks/cloudflare.sh

# Inside wait_for_propagation, increase these values:
local max_attempts=6

Have any Questions

Call HTTPS

If you have any questions, feel free to call us