Web server with HTTPS: nginx or Apache and Let's Encrypt
Installing nginx or Apache, setting up a site for your domain and getting a free Let's Encrypt certificate with certbot.
On this page
This guide shows you how to publish a site on your BLACKBOX server: install the nginx or Apache web server, set up the site for your domain and get a free HTTPS certificate from Let's Encrypt with certbot. You work in the server's terminal, connected over SSH as root (Connecting to your server over SSH (Windows, macOS, Linux)). The app.blackbox.md panel has no section for web servers or certificates: all you need from it is the server's IP address, shown on the server card on the “My Servers” page (app.blackbox.md/my-servers).
Available to: anyone with root access over SSH to a BLACKBOX server running Ubuntu 24.04, 22.04 or 20.04, Debian 12 or 11, AlmaLinux 9 or CentOS Stream 9.
In the commands, replace DOMAIN with your domain without www (for example example.com). Choose only one web server, nginx or Apache: both use ports 80 and 443 and can't run at the same time.
If you have a “VPS hosting” plan with a control panel, don't install another web server on top of the panel: you set up sites and certificates in the control panel. If you don't know which panel you have or how to sign in to it, write to support. See “VPS hosting” plans with a control panel.
Before you start
| Requirement | Details |
|---|---|
| The domain points to the server | the A records for DOMAIN and www.DOMAIN have the value SERVER_IP, and there is no AAAA record left over from your old hosting; see How to point your domain to the server |
| Ports 80 and 443 are open | on a new server, BLACKBOX network filtering allows SSH (22), HTTP (80), HTTPS (443) and ping; the operating system's firewall (ufw, firewalld) is not enabled |
| Ports 80 and 443 are free | no other program (such as another web server) is listening on them |
On the server, check which programs are listening on ports 80 and 443. On a new server, the command shows nothing:
ss -tlnp | grep -E ':80|:443'If you have enabled the operating system's firewall yourself, allow HTTP and HTTPS. On Ubuntu and Debian (ufw):
ufw allow 80,443/tcpOn AlmaLinux and CentOS Stream (firewalld):
firewall-cmd --permanent --add-service=http --add-service=https
firewall-cmd --reloadDon't enable the firewall just for this guide. If you want to enable it, allow SSH first and keep a second SSH session open so you don't lose access: Firewall: network filtering and ufw/firewalld on the server.
Option 1: nginx
Ubuntu and Debian
Install nginx. The service starts automatically after installation.
apt update apt install -y nginxCreate the site's directory and a test page:
mkdir -p /var/www/DOMAIN echo "DOMAIN OK" > /var/www/DOMAIN/index.htmlCreate the site's configuration file:
nano /etc/nginx/sites-available/DOMAINPaste the content below, save with Ctrl+O and Enter, then exit with Ctrl+X:
server { listen 80; server_name DOMAIN www.DOMAIN; root /var/www/DOMAIN; index index.html index.htm; location / { try_files $uri $uri/ =404; } }Enable the site:
ln -s /etc/nginx/sites-available/DOMAIN /etc/nginx/sites-enabled/Check the configuration and reload nginx.
nginx -tmust show “syntax is ok” and “test is successful”; if it shows an error, fix the file before you reload.nginx -t systemctl reload nginx
AlmaLinux and CentOS Stream
Install and start nginx:
dnf install -y nginx systemctl enable --now nginxCreate the site's directory and the test page, then apply the SELinux label for web content: files without it return “403 Forbidden”. Run
restoreconagain whenever you move (mv) files into the directory from another location.mkdir -p /var/www/DOMAIN echo "DOMAIN OK" > /var/www/DOMAIN/index.html restorecon -Rv /var/wwwCreate the configuration file (if
nanois missing, the first command installs it):dnf install -y nano nano /etc/nginx/conf.d/DOMAIN.confPaste the same
server { … }block as for Ubuntu and Debian, save and exit.Check the configuration and reload nginx:
nginx -t systemctl reload nginx
Option 2: Apache
Ubuntu and Debian
Install Apache. The service starts automatically after installation.
apt update apt install -y apache2Create the site's directory and the test page with the commands from step 2 of the nginx option.
Create the site's configuration file:
nano /etc/apache2/sites-available/DOMAIN.confPaste the content below, save and exit:
<VirtualHost *:80> ServerName DOMAIN ServerAlias www.DOMAIN DocumentRoot /var/www/DOMAIN ErrorLog ${APACHE_LOG_DIR}/DOMAIN-error.log CustomLog ${APACHE_LOG_DIR}/DOMAIN-access.log combined </VirtualHost>Enable the site, check the configuration and reload Apache.
apachectl configtestmust show “Syntax OK”.a2ensite DOMAIN.conf apachectl configtest systemctl reload apache2
AlmaLinux and CentOS Stream
Install Apache (
httpd) and the HTTPS module (mod_ssl, which certbot needs), then start the service:dnf install -y httpd mod_ssl systemctl enable --now httpdCreate the site's directory and the test page with the commands from step 2 of the nginx option, including
restorecon.Create the site's configuration file (if
nanois missing, install it as in step 3 of the nginx option):nano /etc/httpd/conf.d/DOMAIN.confPaste the
<VirtualHost *:80>block from the Ubuntu and Debian steps, with different log paths:/var/log/httpd/DOMAIN-error.logand/var/log/httpd/DOMAIN-access.log. Save and exit.Check the configuration and reload Apache:
apachectl configtest systemctl reload httpd
Checking the site over HTTP
On your computer, open http://DOMAIN in a browser: you should see the text “DOMAIN OK”. Or run the command below (on Windows, in PowerShell, type curl.exe instead of curl); the response must start with HTTP/1.1 200 OK. If the site doesn't respond, fix that first: certbot needs a site that is reachable over HTTP.
curl -I http://DOMAINLet's Encrypt certificate with certbot
Installing certbot
Ubuntu and Debian. For Apache, install python3-certbot-apache instead of python3-certbot-nginx.
apt install -y certbot python3-certbot-nginxAlmaLinux and CentOS Stream. Certbot comes from the EPEL repository. The last command starts the renewal timer, which isn't turned on automatically on these systems. For Apache, install python3-certbot-apache.
dnf install -y epel-release
dnf install -y certbot python3-certbot-nginx
systemctl enable --now certbot-renew.timerOn Ubuntu and Debian, you can also use the snap package, which the certbot developers recommend: it is usually a newer version and includes the nginx and Apache plugins. Use either apt or snap, not both. If you have already installed certbot with apt, remove it:
apt remove -y certbotOn Debian, install snapd first:
apt install -y snapd
snap install coreThen install certbot from snap:
snap install --classic certbot
ln -s /snap/bin/certbot /usr/bin/certbotGetting the certificate
Run certbot with the plugin for your web server. For nginx:
certbot --nginx -d DOMAIN -d www.DOMAINFor Apache:
certbot --apache -d DOMAIN -d www.DOMAINOn the first run, certbot asks for an email address and for you to accept the Let's Encrypt terms: enter the address and answer
Y. When asked about the EFF newsletter, you can answerN.If certbot asks whether to redirect HTTP to HTTPS (older versions do), choose “2: Redirect”. Newer versions add the redirect without asking.
At the end, you see a message that starts with “Congratulations!”. The certificate and key are in
/etc/letsencrypt/live/DOMAIN/, and certbot has added the HTTPS block (port 443) and the redirect from HTTP to the site's configuration.Open
https://DOMAINin your browser and check for the padlock. The command below shows your certificates, their domains and the expiry date (“Expiry Date”):certbot certificates
Automatic renewal
The certificate is valid for 90 days. The certbot timer checks the certificates twice a day and renews them on its own before they expire. With --nginx and --apache, certbot also reloads the web server after renewal.
| How you installed certbot | Renewal timer | Started automatically |
|---|---|---|
| apt (Ubuntu, Debian) | certbot.timer |
yes |
| snap | snap.certbot.renew.timer |
yes |
| EPEL (AlmaLinux, CentOS Stream) | certbot-renew.timer |
no: start it with systemctl enable --now certbot-renew.timer |
Check the timer and simulate a renewal. --dry-run doesn't issue a new certificate and ends with “Congratulations, all simulated renewals succeeded” (in older versions, “Congratulations, all renewals succeeded”).
systemctl list-timers | grep -i certbot
certbot renew --dry-runNote Let's Encrypt no longer sends emails before certificates expire, and it has announced that it will gradually shorten how long certificates are valid. Don't schedule manual renewals by the calendar: let the certbot timer do the work, and check the expiry date from time to time with
certbot certificates.
Wildcard certificate (*.DOMAIN) with DNS validation
A wildcard certificate covers all subdomains one level down (www.DOMAIN, api.DOMAIN and so on), and you can only get one with DNS validation: certbot temporarily creates the TXT record _acme-challenge.DOMAIN. To keep renewal automatic, certbot needs a plugin for your DNS provider. The example below is for Cloudflare; for other providers, see the list of DNS plugins in the certbot documentation. Without a plugin (--manual), the certificate is no longer renewed automatically. If you need a wildcard, follow the steps below instead of the ones in “Getting the certificate”.
In your Cloudflare account, create a token just for certbot: “My Profile” → “API Tokens” → “Create Token” → the “Edit zone DNS” template. Under “Zone Resources”, select only the
DOMAINzone, create the token and copy it. Don't reuse the token connected on the panel's “DNS” page: you can revoke a separate token without affecting the panel.Install the Cloudflare plugin. Ubuntu and Debian:
apt install -y python3-certbot-dns-cloudflareAlmaLinux and CentOS Stream:
dnf install -y python3-certbot-dns-cloudflareIf you use certbot from snap:
snap set certbot trust-plugin-with-root=ok snap install certbot-dns-cloudflareOn Ubuntu 20.04, the apt plugin is too old and doesn't support API tokens: use certbot and the plugin from snap.
Save the token in a file:
mkdir -p /root/.secrets nano /root/.secrets/cloudflare.iniThe file's content, with your token in place of
CLOUDFLARE_TOKEN:dns_cloudflare_api_token = CLOUDFLARE_TOKENMake the file accessible only to root:
chmod 600 /root/.secrets/cloudflare.iniGet the certificate.
--deploy-hookreloads the web server after each renewal; for Apache, writesystemctl reload apache2(Ubuntu, Debian) orsystemctl reload httpd(AlmaLinux, CentOS Stream).certbot certonly --dns-cloudflare --dns-cloudflare-credentials /root/.secrets/cloudflare.ini -d DOMAIN -d "*.DOMAIN" --deploy-hook "systemctl reload nginx"certonlydoesn't change the web server configuration: add an HTTPS block to the site's file yourself. For nginx, add the block below, then runnginx -tand reload nginx. For a subdomain, copy the block and changeserver_nameandroot; the certificate lines stay the same.server { listen 443 ssl; server_name DOMAIN www.DOMAIN; root /var/www/DOMAIN; ssl_certificate /etc/letsencrypt/live/DOMAIN/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/DOMAIN/privkey.pem; }To redirect HTTP to HTTPS, in the block with
listen 80, replace therootandindexlines and thelocationblock withreturn 301 https://$host$request_uri;.For Apache, add the block below to the site's file. To redirect HTTP to HTTPS, add the line
Redirect permanent / https://DOMAIN/to the<VirtualHost *:80>block.<VirtualHost *:443> ServerName DOMAIN ServerAlias www.DOMAIN DocumentRoot /var/www/DOMAIN SSLEngine on SSLCertificateFile /etc/letsencrypt/live/DOMAIN/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/DOMAIN/privkey.pem </VirtualHost>On Ubuntu and Debian, enable the SSL module, check the configuration and restart Apache:
a2enmod ssl apachectl configtest systemctl restart apache2On AlmaLinux and CentOS Stream,
mod_sslis already installed (step 1 of the Apache option):apachectl configtest systemctl reload httpd
Common problems
| Message or symptom | Cause | What to do |
|---|---|---|
| “DNS problem: NXDOMAIN looking up A for DOMAIN” | the A record is missing or hasn't propagated yet | create the record, wait and check with dig +short DOMAIN A |
| “Timeout during connect (likely firewall problem)” | port 80 is blocked or the domain points to a different address | allow 80 and 443 in ufw/firewalld; check the A and AAAA records; if it still doesn't work, write to support |
| “Connection refused” | the web server isn't running | systemctl status nginx (or apache2, httpd) and ss -tlnp |
| “Invalid response from http://DOMAIN/.well-known/acme-challenge/…” with 404 | the request reaches a different server: an old A or AAAA record, or the Cloudflare proxy | fix the DNS records; turn off the proxy temporarily |
| “Could not automatically find a matching server block for DOMAIN” | there is no server_name with this domain |
add server_name, run nginx -t, reload and run certbot again |
| “bind() to 0.0.0.0:80 failed (98: Address already in use)” | another web server is using port 80 | stop or remove it; keep only one web server |
| “too many failed authorizations” or “too many certificates” | Let's Encrypt limits | wait (see below); test with --dry-run |
| “403 Forbidden” on AlmaLinux or CentOS Stream | the SELinux label on the files | restorecon -Rv /var/www |
“502 Bad Gateway” on AlmaLinux or CentOS Stream, with “(13: Permission denied)” in /var/log/nginx/error.log |
SELinux doesn't let nginx connect to the application behind it | setsebool -P httpd_can_network_connect 1; see Node.js, Python and other services: nginx as a reverse proxy |
ERR_TOO_MANY_REDIRECTS through Cloudflare |
the “Flexible” SSL mode in Cloudflare | choose the “Full (strict)” mode |
The full certbot log is in /var/log/letsencrypt/letsencrypt.log.
After you fix the cause, test issuance against the Let's Encrypt staging server, without getting a real certificate. --dry-run only works with certonly and renew; for Apache, write --apache.
certbot certonly --nginx --dry-run -d DOMAIN -d www.DOMAINWarning Let's Encrypt limits attempts: after 5 failed validations for the same name within an hour, new attempts are temporarily refused, and at most 5 certificates can be issued for exactly the same set of names in 7 days. Don't run certbot over and over: test with
--dry-run, which doesn't count against these limits. Details: Let's Encrypt rate limits.
The domain goes through the Cloudflare proxy
- For the first issuance, keep the
DOMAINandwwwrecords on “DNS only” (on the panel's “DNS” page, the “Proxied” checkbox unchecked), or use the DNS validation described above. - Once the site works over HTTPS directly on the server, turn on the proxy.
- In your Cloudflare account, under “SSL/TLS”, choose the “Full (strict)” encryption mode. The “Flexible” mode causes redirect loops, because the server redirects HTTP to HTTPS.
More about the proxy: How to point your domain to the server.
Next steps
- Install a WordPress site on this server: WordPress on LEMP (nginx, MariaDB, PHP).
- Publish a Node.js or Python application through nginx: Node.js, Python and other services: nginx as a reverse proxy.
- Upload your site's files to
/var/www/DOMAIN: Transferring files to the server (scp, rsync, SFTP). - Keep an eye on renewal: after about two months,
certbot certificatesshould show a new expiry date. If the date doesn't change, check the timer and the certbot log.
Frequently asked questions
Can I host several sites on the same server?
Yes. For each domain, create a separate directory and configuration file (a server block in nginx, a VirtualHost in Apache), then run certbot with that domain's names.
How do I add a subdomain to an existing certificate?
Create the A record for the subdomain, add it to server_name (or ServerAlias), reload the web server and run certbot with all the names, the existing ones and the new one. For Apache, write --apache instead of --nginx:
certbot --nginx --expand -d DOMAIN -d www.DOMAIN -d app.DOMAINWhat happens if renewal fails?
The current certificate stays valid until its expiry date, and the timer tries again twice a day. The cause is in /var/log/letsencrypt/letsencrypt.log; once you fix it, confirm with certbot renew --dry-run.
Can the AI assistant do these steps for me?
Yes, if its token has the “Citește lista și statusul serverelor mele” (Read the list and status of my servers) (servers:read) and “Rulează orice comandă ca root pe serverele mele” (Run any command as root on my servers) (servers:exec) permissions. The domain must already point to the server. See Example tasks for the AI assistant and the permissions they need.
I followed the steps, but the site still doesn't open. Go through the checklist in The site does not open. If the problem persists, write to us: How to contact BLACKBOX support.