Upgrading nginx from Ubuntu Repo (1.18) to nginx.org Mainline (1.31.0)

Share
Upgrading nginx from Ubuntu Repo (1.18) to nginx.org Mainline (1.31.0)

With nginx's recent disclosure of multiple CVEs in the 1.27.x–1.30.x line (HTTP/2 request injection, buffer overflows in rewrite/SCGI/uWSGI/charset modules, HTTP/3 address spoofing, and a use-after-free in OCSP resolver), a lot of admins are stuck on Ubuntu's repo version (1.18) with no straightforward path to patch — apt upgrade simply doesn't see anything newer because the distro repo is frozen at that release. This guide walks through the clean way to migrate off the Ubuntu package and onto the official nginx.org repo, including version pinning, rollback, and the gotchas you'll hit on a live server.

Table of Contents

  • Context
  • Version Selection
  • Step 1 — Backup
  • Step 2 — Install Prerequisites
  • Step 3 — Import nginx.org Signing Key
  • Step 4 — Add the Repository
  • Step 5 — Pin nginx.org Over Distro
  • Step 6 — Verify Candidate Version
  • Step 7 — Pre-Install Module Check
  • Step 8 — Install
  • Step 9 — Post-Install Verification
  • Troubleshooting
  • Rollback Procedure
  • Post-Upgrade Hardening

Context

Ubuntu ships nginx 1.18 in noble/jammy packages. nginx.org ships 1.30.x (stable) and 1.31.x (mainline). Reasons to move:

  • CVE coverage — 1.18 is 5+ years old and EOL
  • Modern directives: http2 on;, HTTP/3 (QUIC), Early Hints, ECH
  • Faster CVE response cycle

⚠️ Production-impacting upgrade. Schedule a maintenance window even though the install itself takes <30 seconds.


Version Selection

Branch Latest Use when
Mainline 1.31.0 You want newest features (HTTP/3, ECH, HTTP forward proxy, least_time LB)
Stable 1.30.1 You want only critical bugfixes, no new features mid-cycle

Recommendation: Mainline for caching/reverse-proxy hosts (cache5, etc.) — nginx mainline is production-grade despite the name. Stable only if you have strict change-control requirements.


Step 1 — Backup

mkdir -p /root/nginx-backup && cp -a /etc/nginx /root/nginx-backup/ && cp -a /etc/letsencrypt /root/nginx-backup/

Record current version for rollback:

dpkg -l nginx | awk '/^ii/ {print $3}' > /root/nginx-backup/version.txt

Step 2 — Install Prerequisites

apt install -y curl gnupg2 ca-certificates lsb-release ubuntu-keyring

Step 3 — Import nginx.org Signing Key

curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor | tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null

Verify the fingerprint:

gpg --dry-run --quiet --no-keyring --import --import-options import-show /usr/share/keyrings/nginx-archive-keyring.gpg

Expected fingerprint: 573B FD6B 3D8F BC64 1079 A6AB ABF5 BD82 7BD9 BF62

⚠️ If the fingerprint does not match, abort. Do not proceed with a mismatched key.


Step 4 — Add the Repository

Mainline (1.31.0):

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

Stable (1.30.1):

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

lsb_release -cs auto-fills the codename (noble, jammy, focal, etc.). Only one of these two repo lines should exist — switching branches later means editing this file, not adding a second one.


Step 5 — Pin nginx.org Over Distro

printf 'Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n' > /etc/apt/preferences.d/99nginx

Priority 900 beats the default 500 of the Ubuntu repo, so apt always prefers nginx.org for the nginx package.


Step 6 — Verify Candidate Version

apt update && apt-cache policy nginx

Expected output should show Candidate: 1.31.0-1~<codename> (or 1.30.1-1~<codename> for stable) with nginx.org as the source. Do not proceed if it still points to ubuntu.com.


Step 7 — Pre-Install Module Check

nginx -V 2>&1 | tr ' ' '\n' | grep -E 'module|add'

⚠️ If you see third-party modules (ngx_headers_more, ngx_http_geoip2, nchan, etc.), the nginx.org binary will not include them. Either:

  • Install matching nginx-module-* packages from nginx.org (apt search nginx-module)
  • Build dynamic modules against the new nginx version after install
  • Skip the upgrade and stay on the Ubuntu package

Step 8 — Install

apt install -y nginx

⚠️ dpkg will prompt about config file conflicts on /etc/nginx/nginx.conf and possibly /etc/nginx/conf.d/default.conf. Press N (keep your version) — the nginx.org defaults will wipe your tuning otherwise.


Step 9 — Post-Install Verification

nginx -t && nginx -v && systemctl status nginx --no-pager && ss -tlnp | grep nginx

Sanity-check a real domain over the wire:

curl -sI -o /dev/null -w "%{http_code} %{http_version}\n" https://yourdomain.com

Should return 200 2 for HTTP/2.


Troubleshooting

  1. sites-enabled/ is not auto-included. The nginx.org package only includes /etc/nginx/conf.d/*.conf. If your vhosts live in sites-enabled/, add this to the http{} block in nginx.conf:
   include /etc/nginx/sites-enabled/*;

Or symlink each vhost into conf.d/.

  1. Deprecated listen ... http2 directive. nginx 1.25+ throws warnings. Migrate to:
   listen 443 ssl;
   http2 on;
  1. ssl_stapling warnings for certs lacking an OCSP responder (some Let's Encrypt issuance paths). Either disable stapling per-vhost or ignore the warning.
  2. systemd unit changed. Run systemctl daemon-reload if you see a "unit file changed on disk" notice.
  3. server_tokens defaults differ. The nginx.org package does not enable server_tokens off; by default. Add it explicitly to the http{} block.
  4. Log paths and user account stay the same (/var/log/nginx/, www-data user) — no migration needed there.

Rollback Procedure

If anything breaks, restore in this order:

  1. Stop nginx:
   systemctl stop nginx
  1. Downgrade the package (replace version with the contents of /root/nginx-backup/version.txt):
   apt install -y --allow-downgrades nginx=1.18.0-6ubuntu14.8
  1. Restore configs:
   rm -rf /etc/nginx && cp -a /root/nginx-backup/nginx /etc/nginx
  1. Hold the package so apt upgrade does not re-upgrade it:
   apt-mark hold nginx
  1. Remove the nginx.org repo and pin to prevent accidental re-upgrade:
   rm -f /etc/apt/sources.list.d/nginx.list /etc/apt/preferences.d/99nginx && apt update
  1. Start nginx:
   systemctl start nginx && systemctl status nginx --no-pager

Post-Upgrade Hardening

Add to the http{} block in /etc/nginx/nginx.conf:

server_tokens off;

Modern TLS baseline per server{} block:

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;

Drop the deprecated listen 443 ssl http2; syntax in favor of:

listen 443 ssl;
http2 on;

Verify final state externally:

  • SSL Labs: https://www.ssllabs.com/ssltest/analyze.html?d=yourdomain.com
  • Security Headers: https://securityheaders.com/?q=yourdomain.com

Re-pin the package once you're satisfied to prevent surprise upgrades during routine apt upgrade:

apt-mark hold nginx

Unhold when you want to upgrade again:

apt-mark unhold nginx && apt update && apt upgrade nginx