Skip to content

Complete Guide to Enabling HTTPS with acme.sh and Nginx

Overview

A practical HTTPS setup guide based on a real stellhub.top rollout, covering acme.sh, Let's Encrypt, Nginx, HTTP-01 validation, certificate installation, automatic renewal, and common troubleshooting.

This article is based on a real HTTPS rollout for stellhub.top. It covers preparation from zero, certificate issuance, Nginx HTTPS integration, automatic renewal, and common error FAQ.


1. Solution Summary

Using acme.sh + Let's Encrypt + Nginx to enable HTTPS for a self-hosted website is fully feasible.

This solution is:

  • Free
  • Trusted by browsers
  • Automatically renewable
  • Suitable for personal websites, blogs, SaaS, self-hosted APIs, and microservice gateways
  • Stable enough in engineering practice without purchasing commercial certificates

One point must be emphasized:

acme.sh is not a CA. It is only an ACME client.

The actual certificate issuers are:

  • Let's Encrypt
  • ZeroSSL
  • Google Trust Services

As long as the certificate comes from these public CAs, mainstream browsers such as Chrome, Edge, Safari, and Firefox will trust it.


2. Final Target Architecture

The final access path is:

text
Browser
  ↓ HTTPS :443
Nginx
  ↓ HTTP :8080
Application

That is:

  • The HTTPS certificate is configured on Nginx
  • Nginx performs TLS termination
  • The backend application continues listening on a local HTTP port, such as 127.0.0.1:8080

This is the most common and most correct deployment approach.


3. Prerequisites

Using the domain stellhub.top as an example, the following conditions must be satisfied first.

3.1 Domain DNS Resolution Is Correct

Your domain needs to resolve to the server's public IP.

Check:

bash
nslookup stellhub.top
nslookup www.stellhub.top

Or:

bash
ping stellhub.top

Confirm that the resolved result is your server's public IP.


3.2 Cloud Server Security Group Allows Ports

The following ports must be open:

PortPurpose
80ACME HTTP-01 validation
443HTTPS access

If you use Alibaba Cloud, Tencent Cloud, Huawei Cloud, AWS, or Google Cloud, allow these rules in the security group or firewall rules:

text
TCP 80   0.0.0.0/0
TCP 443  0.0.0.0/0

3.3 Local Server Firewall Allows Traffic

If firewalld is enabled:

bash
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload

If it is not enabled, you can ignore this step.


4. Install Nginx

CentOS / Rocky / AlmaLinux:

bash
yum install -y nginx

Start Nginx and enable it on boot:

bash
systemctl enable nginx
systemctl start nginx

Check status:

bash
systemctl status nginx

Check listening ports:

bash
ss -lntp | grep nginx

5. Install acme.sh

Install:

bash
curl https://get.acme.sh | sh

Load environment variables:

bash
source ~/.bashrc

Verify:

bash
acme.sh --version

Set the default CA to Let's Encrypt:

bash
acme.sh --set-default-ca --server letsencrypt

6. Step One: Configure Nginx for HTTP Validation First

Before issuing the certificate, Let's Encrypt needs to access your server through HTTP to verify that you really control the domain.

It will access a URL similar to:

text
http://stellhub.top/.well-known/acme-challenge/xxxxx

Therefore, Nginx must first provide normal service on port 80.


7. HTTP Validation Configuration That Can Directly Replace /etc/nginx/nginx.conf

This is the minimum usable configuration for the certificate issuance phase.

nginx
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    sendfile on;
    keepalive_timeout 65;

    server {
        listen 80;
        server_name stellhub.top www.stellhub.top;

        root /usr/share/nginx/html;

        location /.well-known/acme-challenge/ {
            root /usr/share/nginx/html;
            try_files $uri =404;
        }

        location / {
            return 200 "acme ok\n";
        }
    }
}

After saving, check:

bash
nginx -t

If there is no problem, start or restart Nginx:

bash
systemctl restart nginx

Test HTTP:

bash
curl http://stellhub.top
curl http://www.stellhub.top

Expected response:

text
acme ok

8. Test the ACME Validation Directory

Create a test file:

bash
mkdir -p /usr/share/nginx/html/.well-known/acme-challenge

echo test > /usr/share/nginx/html/.well-known/acme-challenge/test.txt

Access it:

bash
curl http://stellhub.top/.well-known/acme-challenge/test.txt
curl http://www.stellhub.top/.well-known/acme-challenge/test.txt

Expected response:

text
test

Only after this step succeeds does acme.sh --issue --webroot make sense.


9. Issue the Certificate

Run:

bash
acme.sh --issue -d stellhub.top -d www.stellhub.top \
  --webroot /usr/share/nginx/html

On success, you will see output similar to:

text
Verifying: stellhub.top
Success
Verifying: www.stellhub.top
Success
Cert success.

The certificate is generated by default under:

text
/root/.acme.sh/stellhub.top_ecc/

The directory usually contains:

FilePurpose
stellhub.top.keyPrivate key
stellhub.top.cerDomain certificate
ca.cerIntermediate CA certificate
fullchain.cerFull certificate chain

Nginx should ultimately use:

text
fullchain.cer
stellhub.top.key

10. Do You Need to Save the Certificate Content Manually?

No.

The PEM content printed by acme.sh:

text
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----

does not need to be manually copied and saved.

The correct approach is to use acme.sh --install-cert to install the certificate into an Nginx-specific directory.

Do not let Nginx directly depend on /root/.acme.sh/....

Reasons:

  • /root permissions are not suitable for direct Nginx reads
  • The path is not suitable as a service runtime dependency
  • It is inconvenient for unified management
  • --install-cert can bind a reload action after automatic renewal

11. Install the Certificate into the Nginx Directory

Create the certificate directory:

bash
mkdir -p /etc/nginx/ssl

Install the certificate:

bash
acme.sh --install-cert -d stellhub.top \
  --key-file       /etc/nginx/ssl/stellhub.key \
  --fullchain-file /etc/nginx/ssl/stellhub.cer \
  --reloadcmd     "systemctl reload nginx"

Here, /etc/nginx/ssl/stellhub.cer is actually fullchain.cer.

When the certificate is automatically renewed later, acme.sh will automatically update these files and run:

bash
systemctl reload nginx

12. Final HTTPS Nginx Configuration

After the certificate is installed, you can update /etc/nginx/nginx.conf to the final version below.

nginx
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;

    sendfile on;
    keepalive_timeout 65;

    server {
        listen 80;
        server_name stellhub.top www.stellhub.top;

        location /.well-known/acme-challenge/ {
            root /usr/share/nginx/html;
            try_files $uri =404;
        }

        location / {
            return 301 https://$host$request_uri;
        }
    }

    server {
        listen 443 ssl http2;
        server_name stellhub.top www.stellhub.top;

        ssl_certificate     /etc/nginx/ssl/stellhub.cer;
        ssl_certificate_key /etc/nginx/ssl/stellhub.key;

        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;

        location / {
            proxy_pass http://127.0.0.1:8080;

            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

If your backend is not on 8080, change this line to the real port:

nginx
proxy_pass http://127.0.0.1:8080;

If you temporarily do not have a backend application, you can first change it to:

nginx
location / {
    return 200 "https ok\n";
}

13. Apply the Final Configuration

Check the Nginx configuration:

bash
nginx -t

Restart Nginx:

bash
systemctl restart nginx

Check ports:

bash
ss -lntp | grep nginx

You should see:

text
:80
:443

Test HTTPS:

bash
curl -I https://stellhub.top
curl -I https://www.stellhub.top

Open in a browser:

text
https://stellhub.top

Seeing the security lock means HTTPS has truly been enabled.


14. Automatic Renewal

Let's Encrypt certificates are usually valid for 90 days.

After installation, acme.sh automatically configures cron.

Check:

bash
crontab -l

You will usually see:

bash
~/.acme.sh/acme.sh --cron --home ~/.acme.sh

Simulate renewal manually:

bash
acme.sh --cron --force

List certificates:

bash
acme.sh --list

15. FAQ: Issues Encountered in This Rollout

FAQ 1: acme.sh --issue Reports Connection refused

Symptom

text
Fetching http://stellhub.top/.well-known/acme-challenge/xxx: Connection refused

Cause

Let's Encrypt failed to access your port 80.

This is not a certificate problem, nor an acme.sh command problem. It means the public HTTP service is not reachable.

Common causes:

  • Nginx is not started
  • Nginx is not listening on port 80
  • The cloud server security group does not allow port 80
  • The local firewall does not allow port 80
  • The domain resolves to the wrong IP

Troubleshooting Commands

bash
ss -lntp | grep ':80'
systemctl status nginx
curl http://127.0.0.1
curl http://stellhub.top

Solution

Ensure that:

  • Nginx is running normally
  • Port 80 is being listened on
  • TCP 80 is open in the security group
  • curl http://stellhub.top returns content

FAQ 2: nginx -t Reports "server" directive is not allowed here

Symptom

text
nginx: [emerg] "server" directive is not allowed here in /etc/nginx/nginx.conf:1

Cause

You placed:

nginx
server {
}

directly at the top level of /etc/nginx/nginx.conf.

This is incorrect.

Nginx configuration has hierarchy:

text
main
 ├── events
 └── http
      └── server
           └── location

server {} must be written inside http {}.

Correct Form

nginx
http {
    server {
        listen 80;
    }
}

FAQ 3: nginx -s reload Reports /run/nginx.pid Does Not Exist

Symptom

text
nginx: [error] open() "/run/nginx.pid" failed (2: No such file or directory)

Cause

Nginx is not running at all.

reload requires an existing Nginx master process.

Wrong Operation

bash
nginx -s reload

when Nginx is not started.

Correct Operation

Check configuration first:

bash
nginx -t

Then start:

bash
systemctl start nginx

After configuration changes, use:

bash
systemctl reload nginx

It is recommended to use systemctl consistently instead of mixing too many commands.


FAQ 4: Why Is the Browser Still Not HTTPS after Certificate Issuance Succeeds?

Cause

Successful certificate issuance only means the files have been generated.

You still need to:

  • Install the certificate into the Nginx directory
  • Configure listen 443 ssl
  • Configure ssl_certificate
  • Configure ssl_certificate_key
  • Restart Nginx

Correct Process

bash
acme.sh --install-cert -d stellhub.top \
  --key-file       /etc/nginx/ssl/stellhub.key \
  --fullchain-file /etc/nginx/ssl/stellhub.cer \
  --reloadcmd     "systemctl reload nginx"

Then configure:

nginx
server {
    listen 443 ssl http2;
    server_name stellhub.top www.stellhub.top;

    ssl_certificate     /etc/nginx/ssl/stellhub.cer;
    ssl_certificate_key /etc/nginx/ssl/stellhub.key;
}

FAQ 5: Should the Certificate Content Printed by acme.sh Be Saved Manually?

No.

Do not manually copy PEM text.

The certificate has already been saved to:

text
/root/.acme.sh/stellhub.top_ecc/

For production use, install it with --install-cert into:

text
/etc/nginx/ssl/

FAQ 6: Which Certificate File Should Nginx Use?

Use the fullchain.

In this solution:

nginx
ssl_certificate     /etc/nginx/ssl/stellhub.cer;
ssl_certificate_key /etc/nginx/ssl/stellhub.key;

where:

text
/etc/nginx/ssl/stellhub.cer

comes from:

text
fullchain.cer

Do not configure only the standalone domain certificate, or some clients may fail because the intermediate certificate chain is missing.


FAQ 7: Should HTTP Still Be Kept?

Yes.

Port 80 is recommended for two purposes:

  1. Redirect HTTP to HTTPS automatically
  2. Keep the validation entry for future ACME renewals

Recommended configuration:

nginx
server {
    listen 80;
    server_name stellhub.top www.stellhub.top;

    location /.well-known/acme-challenge/ {
        root /usr/share/nginx/html;
        try_files $uri =404;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

FAQ 8: Do You Need to Buy a Certificate?

In most cases, no.

For:

  • Personal websites
  • Technical blogs
  • SaaS
  • Self-hosted admin consoles
  • API gateways

Let's Encrypt is enough.

Scenarios that truly require commercial certificates are usually:

  • Financial institutions
  • Mandatory requirements from government or enterprise customers
  • Special compliance requirements
  • Enterprise brand-oriented OV / EV certificate needs

Otherwise, buying a certificate has low cost-effectiveness.


FAQ 9: Can You Issue a Certificate for an IP Address?

Usually no.

Public HTTPS certificates are generally issued for domain names.

Use:

text
stellhub.top
www.stellhub.top

instead of:

text
47.84.192.24

FAQ 10: How Do You Issue a Wildcard Certificate?

If you later want to support:

text
*.stellhub.top

you need DNS-01 validation instead of HTTP-01.

Example:

bash
acme.sh --issue -d stellhub.top -d '*.stellhub.top' --dns dns_cf

The exact command depends on the DNS provider, such as:

  • Cloudflare
  • Alibaba Cloud DNS
  • Tencent Cloud DNSPod
  • Route53

Wildcard certificates are more suitable for multi-subdomain scenarios, such as:

text
api.stellhub.top
grafana.stellhub.top
prometheus.stellhub.top
blog.stellhub.top

FAQ 11: Does www.stellhub.top Need Separate Configuration?

Yes.

If the certificate request includes:

bash
-d stellhub.top -d www.stellhub.top

then Nginx server_name should also include:

nginx
server_name stellhub.top www.stellhub.top;

Otherwise, one domain may work while the other behaves unexpectedly.


Because it is acme.sh's internal working directory and is not suitable as a service runtime configuration path.

A more reasonable path is:

text
/etc/nginx/ssl/

Benefits:

  • Clear semantics
  • More controllable permissions
  • Easier backup
  • Easier troubleshooting
  • Suitable for multi-site management

FAQ 13: How Do You Confirm Nginx Is Really Listening on 443?

Run:

bash
ss -lntp | grep ':443'

If there is no output, the HTTPS server has not taken effect.

Continue checking:

bash
nginx -t
systemctl status nginx
journalctl -u nginx -n 100 --no-pager

FAQ 14: Does a Stopped Backend Service Affect HTTPS?

It affects page access, but not the TLS handshake.

If Nginx is configured with:

nginx
proxy_pass http://127.0.0.1:8080;

but no service is running on 8080, HTTPS access may return:

text
502 Bad Gateway

This means the HTTPS layer is working, but the backend application is not running.

For temporary testing, you can change it to:

nginx
location / {
    return 200 "https ok\n";
}

The full process can be condensed into the following commands.

bash
# Install acme.sh
curl https://get.acme.sh | sh
source ~/.bashrc

# Use Let's Encrypt
acme.sh --set-default-ca --server letsencrypt

# Check nginx config
nginx -t
systemctl restart nginx

# Issue cert
acme.sh --issue -d stellhub.top -d www.stellhub.top \
  --webroot /usr/share/nginx/html

# Install cert for nginx
mkdir -p /etc/nginx/ssl

acme.sh --install-cert -d stellhub.top \
  --key-file       /etc/nginx/ssl/stellhub.key \
  --fullchain-file /etc/nginx/ssl/stellhub.cer \
  --reloadcmd     "systemctl reload nginx"

# Reload nginx
nginx -t
systemctl restart nginx

# Verify
curl -I https://stellhub.top
curl -I https://www.stellhub.top

Chinese Reference

GitHub Discussions

Join the discussion

Comments are synchronized with GitHub Discussions in stellhub/stell-web.

Powered by VitePress and GitHub Discussions.