Installing and configuring nginx on Debian and Ubuntu

Published on 18 min read

From the apt package to the first server block with PHP-FPM and HTTPS: which nginx version each distribution ships, what is different about the nginx.org repository, and how to get rid of the typical errors.

Installing nginx takes thirty seconds. The rest of the day goes into finding out why the server block never matches, why PHP throws a 502 or why Apache is already sitting on port 80. This article walks through exactly those places, separately for Debian 13, Debian 12, Ubuntu 24.04 and Ubuntu 22.04, because the four systems behave differently in several respects.

Which nginx: distribution package or official repository

The first decision comes before the first command. Each distribution ships a frozen version that only receives security patches. As of July 2026 the picture looks like this:

Systemnginx from the distribution package
Debian 13 (trixie)1.26.3
Debian 12 (bookworm)1.22.1
Ubuntu 24.04 LTS (noble)1.24.0
Ubuntu 22.04 LTS (jammy)1.18.0

nginx.org itself offers the 1.30.4 (stable) and 1.31.3 (mainline) branches in July 2026. That leaves Ubuntu 22.04 roughly six years of feature development behind.

Take the distribution package if you run classic websites or reverse proxy setups and want unattended-upgrades to do the work for you. Take the nginx.org repository if you need HTTP/3 and QUIC (not available at all in Ubuntu 22.04, and not in Debian 12 either), if you want ready-made dynamic modules such as nginx-module-brotli or the ACME module, or if you want to run exactly the same version across several distributions.

What many guides leave out: the two packages are not the same program in different versions, they are built differently and packaged differently. That is the most common reason why a copied guide does not work.

PropertyDebian/Ubuntu packagenginx.org package
Process userwww-datanginx
Default docroot/var/www/html/usr/share/nginx/html
sites-available and sites-enabledpresentdoes not exist
/etc/nginx/snippets/presentdoes not exist
ufw profiles (Nginx Full etc.)presentdoes not exist
Dynamic moduleslibnginx-mod-*nginx-module-*

Installing from the distribution package

Identical on all four systems:

apt update
apt install -y nginx
apt install -y curl

The nginx metapackage is enough. nginx-full and nginx-extras still exist in Debian 12 and 13, but they only pull in additional module packages. Install individual modules on purpose, for example apt install libnginx-mod-http-headers-more-filter.

curl is in that list deliberately. It is not a dependency of nginx and is missing on a freshly installed Debian or Ubuntu, even after nginx itself installed cleanly. Without this step the first verification command below stops with curl: command not found, and the same trap hits the host header test and the PHP test later on.

Now the part most guides skip: proving that it really runs. An apt install without an error message proves nothing at all.

nginx -v
systemctl is-enabled nginx
systemctl is-active nginx
curl -I http://127.0.0.1/

Expect enabled, active and an HTTP/1.1 200 OK with a Server header that names nginx (Debian 13 answers with Server: nginx, Ubuntu 24.04 with Server: nginx/1.24.0). Only then is the web server actually up. If you want to know which options the package was built with, use nginx -V (capital V), which also tells you whether --with-http_v3_module is included.

If ufw is active, the firewall rule is still missing. First a look at the package itself: on Ubuntu Server ufw is installed out of the box and merely inactive, on Debian it is not present at all. There the first call would answer with ufw: command not found. So install it as well, the package exists on all four systems:

apt install -y ufw
ufw app list
ufw allow 'Nginx Full'

The profiles come with the nginx-common package from the distribution: Nginx Full, Nginx HTTP and Nginx HTTPS, plus Nginx QUIC on Debian 13. Installed from the nginx.org repository, they never show up in ufw app list in the first place (see the table above).

Adding the official nginx repository

nginx.org supports bookworm, trixie, jammy and noble. apt-key is deprecated, so the key belongs in its own keyring and is referenced via signed-by.

apt install -y curl gnupg2 ca-certificates lsb-release debian-archive-keyring

On Ubuntu the last package is called ubuntu-keyring instead of debian-archive-keyring. After that:

mkdir -p /root/.gnupg && chmod 700 /root/.gnupg
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor | tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null
gpg --dry-run --quiet --no-keyring --import --import-options import-show /usr/share/keyrings/nginx-archive-keyring.gpg

The mkdir in the first line is not filler. On a freshly installed server /root/.gnupg does not exist yet, and gpg 2.4.7 from Debian 13 does not create the directory itself in this exact combination of calls. The verification command then stops with gpg: Fatal: /root/.gnupg: directory does not exist!, and it does so in the middle of the output, that is, before the fingerprints appear. If you want to see them, create the directory beforehand, or simply run gpg --list-keys once.

The last command is not decoration either, it is the actual counter check. It shows three keys, which is intended and no reason to stop: the current signing key 8540 A6F1 8833 A80E 9C16 53A4 2FD2 1310 B49F 6B46 (signing-key-2@nginx.com), plus 573B FD6B 3D8F BC64 1079 A6AB ABF5 BD82 7BD9 BF62 and 9E9B E90E ACBC DE69 FE9B 204C BCDC D8A3 8D88 A2B3. If different fingerprints show up there, the download delivered something other than expected, and you stop right here.

Now the package source. Watch the path segment after /packages/, because nginx.org maintains two separate directory trees for Debian and Ubuntu. Under /packages/debian/dists/ there is neither noble nor jammy, the Ubuntu packages live exclusively under /packages/ubuntu/. The version below sets that segment itself and therefore runs unchanged on both families:

OS=$(. /etc/os-release; echo $ID)
echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] http://nginx.org/packages/$OS $(lsb_release -cs) nginx" | tee /etc/apt/sources.list.d/nginx.list

$ID is debian on Debian and ubuntu on Ubuntu, so the path matches the distribution automatically. If you hard-code packages/debian instead, because you are working through a Debian guide on an Ubuntu machine, echo writes the line without complaint and the error only turns up at the next apt update:

Err: http://nginx.org/packages/debian noble Release
E: The repository 'http://nginx.org/packages/debian noble Release' does not have a Release file.

On Ubuntu 22.04 the message says jammy instead of noble, the cause is the same. For the mainline branch, put mainline/ in front of the distribution name. For apt to actually prefer the nginx.org package over the distribution package, you need pinning, otherwise the wrong one wins depending on the version number:

printf 'Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n' | tee /etc/apt/preferences.d/99nginx
apt update
apt-cache policy nginx
apt install -y nginx

apt-cache policy nginx is the check before installing: the candidate has to be the nginx.org version (something like 1.30.4-1~noble) and the priority has to be the 900 from the pin file. If the distribution version is still listed there, the pinning is not taking effect and you would install the wrong package straight away.

Switching over from an existing distribution install

If the distribution package is already running, the upgrade fails. The message reads roughly like this:

dpkg: error processing archive /var/cache/apt/archives/nginx_1.30.4-1~bookworm_amd64.deb (--unpack):
 trying to overwrite '/etc/nginx/mime.types', which is also in package nginx-common 1.22.1-9+deb12u9

The nginx.org package knows nothing about nginx-common, so the files collide. The clean way: back up the configuration first, then remove the distribution package, then install fresh.

tar czf /root/nginx-config-backup.tar.gz /etc/nginx
systemctl stop nginx
apt purge -y nginx nginx-common
apt install -y nginx

The fact that tar prints tar: Removing leading '/' from member names is normal and not an error. Afterwards /etc/nginx/sites-available is gone and your old vhosts only exist inside the tarball. Copy them to /etc/nginx/conf.d/ and give them the extension .conf, otherwise they are not loaded. Watch two things while doing that: include snippets/fastcgi-php.conf; does not exist in the nginx.org package, and the process user is now called nginx, which affects file permissions and PHP-FPM sockets.

Understanding sites-available and sites-enabled properly

The two-directory model is purely a Debian invention, nginx itself knows nothing about it. /etc/nginx/sites-available/ holds all configuration files, /etc/nginx/sites-enabled/ holds symlinks to the active ones. Only what is listed in nginx.conf via include gets loaded. When in doubt, check it yourself:

grep include /etc/nginx/nginx.conf

In the Debian and Ubuntu package there are two lines there, include /etc/nginx/conf.d/*.conf; and include /etc/nginx/sites-enabled/*;. In the nginx.org package only the first one is present. That is exactly where the classic problem comes from: somebody follows an Ubuntu guide on an nginx.org package, creates /etc/nginx/sites-available/my-site, sets the symlink, gets a clean syntax is ok from nginx -t, and still nothing happens. There is no error message, because nobody is looking at that directory in the first place.

The counter test that always tells the truth is nginx -T with a capital T. It prints the complete resolved configuration, that is, exactly what nginx really sees:

nginx -T | grep -n "server_name\|listen\|root"

If your server_name does not appear there, the file is not being read. Full stop. Any further debugging of the configuration itself is a waste of time.

The second common mistake is a symlink pointing nowhere, after a typo for instance, or because the target file was renamed:

nginx: [emerg] open() "/etc/nginx/sites-enabled/my-site" failed (2: No such file or directory) in /etc/nginx/nginx.conf:62

You find broken symlinks with find /etc/nginx/sites-enabled/ -xtype l. And a site is never disabled by deleting it in sites-available, but by removing the symlink with unlink /etc/nginx/sites-enabled/my-site.

Your first own server block

Let us set up a static site. First the directory and a test file:

mkdir -p /var/www/example/html
echo '<h1>KernelHost test page</h1>' > /var/www/example/html/index.html
chown -R www-data:www-data /var/www/example

With the nginx.org package the owner is nginx:nginx. Now the configuration, and really as a separate step: the file has to exist before the symlink points at it. Create it in an editor or write it directly with a heredoc; with the nginx.org package it goes to /etc/nginx/conf.d/example.conf instead:

cat > /etc/nginx/sites-available/example <<'EOF'
server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;
    root /var/www/example/html;
    index index.html;

    access_log /var/log/nginx/example.access.log;
    error_log  /var/log/nginx/example.error.log;

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

The single quotes around 'EOF' matter, otherwise the shell replaces $uri with nothing and the block is already broken the moment you save it. Only after that: enable, test, reload.

ln -s /etc/nginx/sites-available/example /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx

The order is not arbitrary. ln creates the symlink even when the target file does not exist at all, without saying a word about it. It only comes to light during the test:

nginx: [emerg] open() "/etc/nginx/sites-enabled/example" failed (2: No such file or directory) in /etc/nginx/nginx.conf:61

The line number differs per system (61 on Debian 13, 60 on Debian 12 and on both Ubuntu releases), the message is identical. A reload would be rejected in this state, so the running configuration stays untouched. This is exactly the case that find /etc/nginx/sites-enabled/ -xtype l catches as well.

Three more errors keep showing up at this point.

Duplicate default server. If you copy default_server along from a guide while the Debian default site is still active, you get:

nginx: [emerg] a duplicate default server for 0.0.0.0:80 in /etc/nginx/sites-enabled/example:2

Either leave the keyword out or switch off the default site with unlink /etc/nginx/sites-enabled/default.

Duplicate server name. If the same name appears in two blocks, the one loaded first wins without comment, and there is only a warning:

nginx: [warn] conflicting server name "example.com" on 0.0.0.0:80, ignored

Server name too long. With long domains or many subdomains:

nginx: [emerg] could not build server_names_hash, you should increase server_names_hash_bucket_size: 32

The remedy is server_names_hash_bucket_size 64; in the http block of nginx.conf.

You can prove it works without DNS by going through the host header:

curl -H 'Host: example.com' -sS http://127.0.0.1/

If the Debian default page comes back instead of your test page, your block is not matching and you end up in the default server. If your page comes back, everything is fine.

Wiring up PHP-FPM

This is where the nastiest trap of the whole guide waits, and it costs plenty of people an hour. The php metapackage depends, through php8.x, on the alternative libapache2-mod-php8.x | php8.x-fpm | php8.x-cgi. apt always picks the first one, so apt install php reliably installs Apache along with it, and Apache then occupies port 80. Install the specific packages instead:

apt install -y php-fpm php-mysql php-xml php-curl php-mbstring php-zip

Which PHP version you get and what the socket is called depends on the distribution:

SystemPHPSocketService
Debian 138.4/run/php/php8.4-fpm.sockphp8.4-fpm
Debian 128.2/run/php/php8.2-fpm.sockphp8.2-fpm
Ubuntu 24.048.3/run/php/php8.3-fpm.sockphp8.3-fpm
Ubuntu 22.048.1/run/php/php8.1-fpm.sockphp8.1-fpm

Do not guess the path, read it off:

ls -l /run/php/

The PHP block inside the server (Debian 12 as the example, adjust the path):

    index index.php index.html;

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }

On Debian and Ubuntu you can replace the first lines with include snippets/fastcgi-php.conf;. The nginx.org package does not have that snippet, so there you need the spelled out version. The try_files $uri =404; is not a stylistic detail, it prevents uploaded files with .php appended in the path from being executed.

One step is still missing though, and it gets skipped almost every time. The test call coming up runs through the default vhost that ships with the package, and in /etc/nginx/sites-available/default the location ~ \.php$ section is completely commented out by default. As long as that stays the case, nginx hands .php files through unchanged: you get a 200 OK with Content-Type: application/octet-stream and the PHP source code in plain text. That is not a cosmetic flaw. In a real file this is where database credentials or API keys sit, and everyone who knows the URL gets to read them.

So arm the block first. In /etc/nginx/sites-available/default, remove the comment characters in front of it or enter it spelled out:

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    }

The socket name depends on the distribution, the table above lists it: php8.4 on Debian 13, php8.3 on Ubuntu 24.04, php8.2 on Debian 12, php8.1 on Ubuntu 22.04. Then test and reload:

nginx -t
systemctl reload nginx

Only now does the proof carry any weight that PHP really runs through FPM and that the file is not just offered as a download:

printf '<?php echo "PHP ", PHP_VERSION, " via ", php_sapi_name(), "\n";' > /var/www/html/kh-check.php
curl -s http://127.0.0.1/kh-check.php
rm /var/www/html/kh-check.php

The correct output looks like PHP 8.4.23 via fpm-fcgi, or PHP 8.3.6 via fpm-fcgi on Ubuntu 24.04. If the source code comes back instead, location ~ \.php$ in the active vhost is not taking effect yet, so it is either still commented out or points at the wrong socket. Do not forget to delete the file, and do not use phpinfo() on a server that is reachable from outside.

Reading a 502 Bad Gateway properly

A 502 is not a diagnosis, the diagnosis is in /var/log/nginx/error.log. There are three typical lines:

connect() to unix:/run/php/php8.2-fpm.sock failed (2: No such file or directory) while connecting to upstream

Wrong path, or FPM is not running. Check with systemctl status php8.2-fpm and ls /run/php/. A common trigger is a distribution upgrade: after the jump from Debian 12 to 13 the old configuration still points at php8.2-fpm.sock, while what is installed is 8.4.

connect() to unix:/run/php/php8.2-fpm.sock failed (13: Permission denied) while connecting to upstream

This almost exclusively hits installations from the nginx.org repository. The FPM pool belongs to www-data and has mode 0660, but nginx runs as the user nginx there. Set listen.group = nginx in /etc/php/8.2/fpm/pool.d/www.conf and restart FPM.

FastCGI sent in stderr: "Primary script unknown" while reading response header from upstream

SCRIPT_FILENAME points nowhere. Usually root sits in the location block instead of the server block, or it is missing entirely.

HTTPS with Let's Encrypt

The certbot versions of the distributions are far apart: Debian 13 ships 4.0.0, Ubuntu 24.04 ships 2.9.0, Debian 12 ships 2.1.0 and Ubuntu 22.04 only 1.21.0. They all speak ACMEv2, but 1.21 is too old for newer features such as short-lived certificates. On Ubuntu 22.04 the detour via snap is worth it.

apt install -y certbot python3-certbot-nginx
certbot --version

Three conditions have to be met before issuing, otherwise validation fails without a usable message: the A record (and the AAAA record where applicable) points at the server, port 80 is reachable from outside, and a server block with a matching server_name exists. The last point is the decisive one, because the nginx plugin finds the block through server_name, not through the file name. Then:

certbot --nginx -d example.com -d www.example.com

certbot then writes into your configuration file: a second server block with listen 443 ssl, the paths to fullchain.pem and privkey.pem, an include /etc/letsencrypt/options-ssl-nginx.conf and a redirect from port 80. The fact that the file gets modified takes people by surprise time and again. If you overwrite it by hand afterwards, you lose HTTPS and get this at the next reload:

nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/example.com/fullchain.pem": BIO_new_file() failed

Automatic renewal runs through a systemd timer, not through a cron job. Check both:

systemctl list-timers | grep certbot
certbot renew --dry-run

The dry run is the only real proof that renewal will work in ninety days. If you use the nginx.org repository, nginx 1.29 and later also offer nginx-module-acme, a variant entirely without certbot in which nginx requests and renews the certificates itself.

nginx -t, reload and restart

The rule is simple and still gets broken constantly: never reload without running nginx -t first. On a syntax error nginx does refuse to adopt the broken configuration, but with a restart the old process has already been stopped and the site is offline.

nginx -t

This is exactly what you should see:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

reload starts new workers with the new configuration and lets the old ones finish serving their open connections. Not a single request is lost. You only need restart when something about the process itself changes, for example after a package update, with a changed user or when loading load_module directives.

When the start fails, the systemd output is almost useless:

Job for nginx.service failed because the control process exited with error code.
See "systemctl status nginx.service" and "journalctl -xeu nginx.service" for details.

The usable text is one level deeper:

journalctl -xeu nginx.service --no-pager -n 30

Solving the port conflict with Apache on port 80

The best known nginx error of them all:

nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)

Note that nginx -t does not catch this error, the test binds no ports. Only the start fails. Find out who is holding the port first instead of guessing:

apt install -y iproute2
ss -tlnp | grep ':80'

The output names the process in plain text, typically users:(("apache2",pid=612,fd=4)). In nine out of ten cases Apache came onto the system through apt install php or through a hosting control panel. There are three clean ways out.

First, switch Apache off. If you do not need it, disabling it is enough so that it does not come back after the next reboot:

systemctl disable --now apache2
systemctl start nginx

Second, remove Apache. Careful: if libapache2-mod-php depends on it, apt purge apache2 may take PHP with it. Check the list that apt shows before executing, and install php-fpm afterwards.

Third, run both in parallel. That makes sense when existing Apache vhosts with .htaccess are supposed to keep running and nginx works in front of them as a reverse proxy. For that, put a Listen 127.0.0.1:8080 into /etc/apache2/ports.conf, change <VirtualHost *:80> to <VirtualHost *:8080> in every vhost and let nginx forward:

    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;
    }

So that Apache no longer sees just 127.0.0.1 in its logs, enable the remoteip module there. Without this step, Fail2ban rules on Apache logs are worthless, because every request appears to come from localhost.

Two variants of the same error are easily overlooked. If the same listen entry appears twice across two of your own files, nginx reports Address already in use as well, even though no Apache is involved at all. And with bind() to [::]:80 failed only IPv6 is affected, usually because a second block also sets listen [::]:80 without ipv6only=on.

The sign-off: eight checks instead of gut feeling

Before you consider an installation finished, work through these points. Every single one of them has prevented a silent outage at some point.

  1. nginx -t reports test is successful.
  2. systemctl is-enabled nginx returns enabled, so the service comes back after a reboot.
  3. ss -tlnp | grep nginx shows port 80 and, if set up, port 443.
  4. nginx -T | grep server_name lists every domain that is supposed to run.
  5. curl -I http://127.0.0.1/ returns a 200 or an intended redirect.
  6. The PHP test file prints fpm-fcgi and has been deleted afterwards.
  7. certbot renew --dry-run completes without errors.
  8. /var/log/nginx/error.log contains no new entries after a test request.

On a freshly installed server the firewall belongs on top of that, see our guide on setting up ufw. Debian 10 and Ubuntu 20.04 have been out of support since June 2024 and May 2025 respectively and no longer receive nginx security updates, so an upgrade there is not a question of comfort, it is overdue.

What KernelHost adds

On the KVM root servers and dedicated servers from KernelHost you install Debian 13, Debian 12, Ubuntu 24.04 or Ubuntu 22.04 straight from the customer panel and then have full root access, so the steps described above run unchanged. The systems are located in the maincubes datacenter in Frankfurt am Main, TÜV certified to TIER3+, connected through our own network. DDoS protection takes effect in front of the server and not just inside nginx: 3.2 Tbps of Arbor real-time filtering directly on site, plus up to 17 Tbps of global filtering capacity in the Professional plans. Everything runs on a PrePaid basis, so no minimum term, no notice period and no setup fee.

Frequently asked questions

Should I install nginx from the distribution package or from the official nginx.org repository?
For classic websites and reverse proxy setups the distribution package is entirely sufficient, it receives security updates over the whole lifetime of the release without you doing anything. The nginx.org repository pays off if you need HTTP/3 and QUIC (not included in Debian 12 and Ubuntu 22.04), if you want ready-made dynamic modules such as Brotli or the ACME module, or if you want to run exactly the same version across several distributions. Keep in mind that the nginx.org package is built differently: it runs as the user nginx instead of www-data and knows neither sites-available nor the snippets directory.
Why is my server block in sites-available ignored completely even though nginx -t succeeds?
Either the symlink in sites-enabled is missing, or your nginx.conf does not load that directory at all. The latter is always the case with the package from nginx.org, where only include /etc/nginx/conf.d/*.conf exists. nginx -t checks only the files that are actually included, which is why the test stays green. The truth comes from nginx -T with a capital T: if your server_name does not show up in that output, the file is not being read. With the nginx.org package you move the configuration to /etc/nginx/conf.d/ and give it the extension .conf.
nginx does not start and reports bind() to 0.0.0.0:80 failed (98: Address already in use). What now?
Another process is holding port 80, usually Apache. Find it with ss -tlnp | grep ':80', the output names the process and the PID. Very often Apache came onto the system unnoticed through apt install php, because the PHP metapackage resolves libapache2-mod-php as the first alternative. After that you have three options: switch Apache off permanently with systemctl disable --now apache2, remove it with apt purge (while checking whether PHP gets taken along), or move it to 127.0.0.1:8080 in /etc/apache2/ports.conf and put nginx in front of it as a reverse proxy. Important: nginx -t does not catch this error, because the configuration test binds no ports.
Which PHP version and which FPM socket do I need on my distribution?
Debian 13 ships PHP 8.4 with /run/php/php8.4-fpm.sock, Debian 12 PHP 8.2 with php8.2-fpm.sock, Ubuntu 24.04 PHP 8.3 with php8.3-fpm.sock and Ubuntu 22.04 PHP 8.1 with php8.1-fpm.sock. Do not guess the path, read it off with ls -l /run/php/. Install php-fpm specifically and not the php metapackage, otherwise apt pulls Apache onto the system as well. After a distribution upgrade you have to adjust the socket path in your nginx configuration, otherwise you get a 502 with the log line connect() to unix:/run/php/... failed (2: No such file or directory).
How do I reliably tell that nginx really runs correctly and that the command did not just complete?
Check five things: nginx -t reports test is successful, systemctl is-enabled nginx returns enabled (otherwise the web server is gone after the next reboot), ss -tlnp | grep nginx shows the expected ports, nginx -T | grep server_name lists every domain that is supposed to run, and curl -I http://127.0.0.1/ returns a 200 (curl is not a dependency of nginx and is missing on a freshly installed system, fetch it beforehand with apt install -y curl). For PHP, create a short file with php_sapi_name(); the output has to read fpm-fcgi, and afterwards you delete it again. If the PHP source code comes back instead of that output, the PHP block in the active vhost is still commented out, exactly as it ships in /etc/nginx/sites-available/default. For HTTPS, certbot renew --dry-run is the only real proof that renewal works in ninety days.

nginx Debian Ubuntu Web server PHP-FPM HTTPS Let's Encrypt Linux administration