eustasy / bubbly Star on GitHub
BASH · MIT · v3.0 · beta

Better SSL in Nginx,
in ten minutes.

Drop-in configuration files and setup scripts for Certbot. An A+ on SSL Labs and an A on SecurityHeaders.io — the same day you ship.

https://

We'll template example.com through every command below.

Why

SSL is fiddly. This isn't.

Compare

vs. doing it by hand.

Certbot and acme.sh are great. They just don't ship the Nginx config you actually want. Bubbly does — plus the headers, and a default server that pins your security floor.

Bubbly Certbot, raw acme.sh Caddy
Issues Let's Encrypt certs
Auto-renews on a timer systemd cron
Nginx config that scores A+ bring your own bring your own own server
Session tickets optional
Security headers pack by default manual
Stays vanilla — no daemon replaces Nginx
Read the source in 5 min bash + conf Python Go binary

Bubbly composes with Certbot — it doesn't replace it. The scripts wrap certbot certonly --webroot and rsync battle-tested config blocks into /etc/nginx/.

Setup · ~10 min

From cold server to A+, in six steps.

You'll need basic Nginx familiarity. You won't need to know Certbot, Let's Encrypt, the ACME spec, or how SSL works underneath.

Bubbly 3 wants Nginx 1.25.1+ for http2 on and OpenSSL 3.5+ for X25519MLKEM768. Check with nginx -V, which reports the OpenSSL Nginx was built against, not the one on your $PATH. Ubuntu 26.04 LTS and Debian 13 clear both; Ubuntu 24.04 and Debian 12 clear neither, and want Bubbly 2.2.0 instead. The config is written for Debian's layout — Nginx here already includes sites-enabled/, so the symlinks below work as written.
  1. Step 1

    Install Certbot. Clone Bubbly.

    Drop into $HOME, grab the dependencies, pull the repo.

    bash
    Distribution packages only — no third-party repositories. nginx.org's builds carry no Brotli module, and the module packages are compiled against one exact Nginx ABI.
  2. Step 2

    Copy the config blocks.

    Mirrors the repo's nginx-config/ into /etc/nginx/conf.d/, directive/, location/, groups/, and sites-available/ all land in place. Run it again whenever you pull a newer Bubbly.

    bash
    Nginx picks up conf.d/bubbly_ssl.conf by itself: the shared TLS session cache and the OCSP resolver. Protocols, groups and ciphers live in directive/ instead — see the next step for why.
  3. Step 3

    Enable the default server.

    Once per server, before any site. It answers whatever no site claims: an unknown Host, a connection with no SNI, a probe at your bare IP. The rm drops the distribution's own default, which also claims default_server — with two, Nginx refuses to start.

    bash
    This matters more than it looks. A handshake begins before SNI picks a server, so the default server's protocol list and key exchange groups govern every site on the machine. Without one of your own the role falls to whichever sites-enabled/ file sorts first — and a new site sorting earlier would move them underneath you.
  4. Step 4

    Configure verification.

    Place the ACME challenge site so Let's Encrypt can find you.

    bash
    In nano: Ctrl + \ to search-and-replace example.com with your domain. Migrating a site that is already serving? Leave it running and add include location/bubbly_well-known-passthrough.conf; to it instead.
  5. Symlink it live, lint, reload.

    bash
  6. Step 5

    Fetch certificates.

    It will ask for the root password and an email. Then it's done in a few seconds.

    bash
    Certbot installs a systemd timer running certbot renew twice daily. The deploy hook reloads Nginx after every successful renewal. No cron, no babysitting — which is why running the script by hand always passes --force-renew. Don't loop it: Let's Encrypt allows five certificates per identical set of names per week, and that limit cannot be raised. Add --dry-run to rehearse against staging.
  7. Step 6

    Swap in the live config.

    Add the HTTPS site alongside the HTTP one you already have. Keep example.com_http.conf symlinked — it answers ACME renewal challenges forever. Read the [OPTION]s and [WARNING]s; point the cert paths at your domain.

    bash
  8. Optional

    Share one ticket key across a fleet.

    Skip this on a single server. Nginx 1.23.2+ generates ticket keys itself and rotates them in the shared session cache, which beats one static key. You only need your own when several Nginx instances behind a load balancer have to resume each other's tickets.

    bash
    Run it again to rotate: the current key moves to ticket.old.key and a fresh one is written, so tickets already issued keep working. Nginx encrypts with the first key listed and decrypts with any of them — keep the newest on top. Both are 600, since the key decrypts captured sessions.

That's it. The next time Let's Encrypt rotates your cert, Nginx will reload itself. The next time you run nginx -t, it will pass. Everything after this is optional — Brotli, per-site logs, a CSP, extra PHP versions, limits behind a CDN — and each one is a line to uncomment, listed in the README's Configuration table. Go test it ↓

Results

Goes straight to the top of the class.

Tested against SSL Labs and SecurityHeaders.io with the defaults shipped in the repo — no extra tuning.

A+
Qualys SSL Labs
ssllabs.com/ssltest
  • Certificate100
  • Protocol Support100
  • Key Exchange90
  • Cipher Strength90
Qualys SSL Labs · screenshot
A
SecurityHeaders.io
securityheaders.io
  • Strict-Transport-Security
  • Content-Security-Policy opt-in
  • X-Frame-Options
  • X-Content-Type-Options
  • Referrer-Policy
  • Permissions-Policy
SecurityHeaders.io · screenshot