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
sites-enabled/is not auto-included. The nginx.org package only includes/etc/nginx/conf.d/*.conf. If your vhosts live insites-enabled/, add this to thehttp{}block innginx.conf:
include /etc/nginx/sites-enabled/*;
Or symlink each vhost into conf.d/.
- Deprecated
listen ... http2directive. nginx 1.25+ throws warnings. Migrate to:
listen 443 ssl;
http2 on;
ssl_staplingwarnings for certs lacking an OCSP responder (some Let's Encrypt issuance paths). Either disable stapling per-vhost or ignore the warning.- systemd unit changed. Run
systemctl daemon-reloadif you see a "unit file changed on disk" notice. server_tokensdefaults differ. The nginx.org package does not enableserver_tokens off;by default. Add it explicitly to thehttp{}block.- Log paths and user account stay the same (
/var/log/nginx/,www-datauser) — no migration needed there.
Rollback Procedure
If anything breaks, restore in this order:
- Stop nginx:
systemctl stop nginx
- 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
- Restore configs:
rm -rf /etc/nginx && cp -a /root/nginx-backup/nginx /etc/nginx
- Hold the package so
apt upgradedoes not re-upgrade it:
apt-mark hold nginx
- 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
- 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