# Certbot "Could not bind TCP port 80": standalone plugin fix

> Certbot's standalone plugin cannot listen on port 80 because a web server already uses it, or Certbot is not root. Switch plugin or stop the server.

Source: https://ssl.sh/errors/certbot-could-not-bind-tcp-port-80/
Last updated: 2026-11-10

**"Could not bind TCP port 80" means Certbot's standalone plugin tried to start a temporary web server on port 80 for the HTTP-01 challenge and the operating system refused.** Usually your web server already listens on port 80; less often Certbot lacks root privileges. The fix is local; your domain and Let's Encrypt are fine.

## The two versions of the message

The message ends in one of two ways, and the ending tells you the cause:

```text
Could not bind TCP port 80 because it is already in use by another process on this
system (such as a web server). Please stop the program in question and then try again.
```

```text
Could not bind TCP port 80 because you don't have the appropriate permissions (for
example, you aren't running this program as root).
```

The first one comes with a Retry or Cancel prompt in interactive runs. Logs may also show the underlying `Problem binding to port 80: ...` error.

## Why port 80 at all

Standalone mode needs port 80 because Let's Encrypt performs HTTP-01 validation only on that port. With HTTP-01, the CA fetches `http://example.com/.well-known/acme-challenge/<token>`. The Let's Encrypt documentation states that the HTTP-01 challenge can only be done on port 80. The standalone plugin answers that request with its own server, and Certbot's guide notes that it needs to bind to port 80, "so you may need to stop your existing webserver".

## Where you meet it

The message appears in three situations: a first install with `--standalone`, an automatic renewal, or a container setup. During a first installation, someone follows a guide that uses `certbot certonly --standalone` on a server that already runs a web server. During automatic renewal, the systemd timer or cron job runs `certbot renew` with the standalone settings saved months earlier, the run fails quietly, and the problem surfaces only when the certificate is close to expiry; Let's Encrypt stopped sending expiry reminder emails on 2025-06-04, so nothing warns you. And in containers, Certbot runs in one container while a reverse proxy in another already publishes port 80 on the host.

## Common causes, most frequent first

### 1. A web server is already running

Nginx, Apache, Caddy or a control panel's web server holds port 80. Find it:

```bash
sudo ss -ltnp 'sport = :80'
```

### 2. Renewal saved the standalone plugin

The first certificate was issued before a web server existed. Certbot stores the authenticator in `/etc/letsencrypt/renewal/example.com.conf`; when the renewal timer runs months later, port 80 is busy. Check:

```bash
sudo grep -E 'authenticator|pre_hook|post_hook' /etc/letsencrypt/renewal/example.com.conf
```

### 3. A container publishes port 80

`docker ps` shows `0.0.0.0:80->80/tcp` for a reverse proxy container. Certbot on the host cannot bind the same port. See [Docker, Nginx and Certbot](https://ssl.sh/guides/docker-nginx-certbot/) for a webroot-based setup.

### 4. Certbot runs without root

Running as a normal user, or in a container without the capability to bind low ports, triggers the permission variant. Use `sudo`, or run Certbot where it has those privileges.

### 5. A stuck earlier run

An interrupted Certbot process may still hold the port. `ss` shows a `certbot` or `python` process; stop it and retry.

## How to fix it

Switching to the webroot, Nginx or Apache plugin is the cleanest fix; stopping the web server briefly or using DNS-01 also works.

**Option A: use the web server instead of standalone (recommended).**

With Nginx or Apache installed, let Certbot use them:

```bash
sudo certbot certonly --nginx -d example.com -d www.example.com
```

Or keep the web server running and use webroot, serving challenge files from a directory:

```bash
sudo certbot certonly --webroot -w /var/www/example.com -d example.com -d www.example.com
```

To switch an existing certificate without reissuing it by hand, Certbot 2.3.0 and newer provide `reconfigure`, which runs a test renewal against the staging server and saves the new options only if it succeeds:

```bash
sudo certbot reconfigure --cert-name example.com --webroot -w /var/www/example.com
```

**Option B: keep standalone and stop the web server briefly.**

```bash
sudo certbot renew --pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx"
```

Certbot's guide explains that these hooks run before and after each renewal attempt. Executables in `/etc/letsencrypt/renewal-hooks/pre` and `/post` run automatically as well; see [Certbot renewal hooks](https://ssl.sh/guides/certbot-renewal-hooks/).

**Option C: use DNS-01.** No inbound port is needed, and wildcards become possible.

**Permission variant:** run with `sudo`. Do not change port 80 permissions system-wide.

## What not to do

- **Do not kill whatever holds port 80 without checking.** On a shared server or hosting panel, that process may serve other sites; stop it through its service manager so it comes back cleanly.
- **Do not move the challenge to another port** with `--http-01-port` unless external port 80 is forwarded to it; the CA will still connect to port 80.
- **Do not grant the Python interpreter the right to bind low ports** to avoid running Certbot as root; that capability would apply to every script using the same interpreter.
- **Do not disable automatic renewal** to silence failing runs. Fix the plugin choice, then confirm with a dry run.

## How to verify the fix

1. Test the full flow against the staging environment without touching your real certificate:

   ```bash
   sudo certbot renew --dry-run
   ```

2. `sudo certbot certificates` lists the certificate with its new expiry date after a real run.
3. Check that the web server is running again (`systemctl status nginx`) and that the site serves the new certificate with the [SSL checker](https://ssl.sh/tools/ssl-checker/).
