Free SSL Certificates with Let's Encrypt: Certbot Setup, Auto-Renewal and Mixed-Content Fixes

How do you remove the browser's 'Not secure' warning from your project site for free? Install a Let's Encrypt certificate with Certbot - about fifteen minutes of work on a VPS, with automatic renewal. This guide covers the Nginx/Apache plugin setup, HTTP-01 vs DNS-01 vs webroot challenges, verifying auto-renewal with --dry-run, fixing mixed-content warnings after migration, and the real error messages (rate limits, NXDOMAIN, expired certificates) with fixes.

Written by Projectech16 min readPublished
For B.E./B.Tech students deploying project websites on a VPS who need HTTPS Topics: Let's Encrypt, Certbot, TLS, Nginx, Apache, DNS
Illustration of a brass padlock securing a browser window with a green checkmark badge and encrypted data flowing between a server and a laptop.
Illustration generated for this guide.
In this guide

Browsers now label every HTTP site "Not secure" in the address bar. For a final-year project demo, that label sits next to your login page while the examiner types a password — it undermines the entire presentation before you say a word. The fix is free, takes about fifteen minutes, and renews itself: a Let's Encrypt certificate installed with Certbot. This guide covers the manual Certbot setup on a VPS, automatic renewal, the DNS and webroot challenge types, and the mixed-content cleanup that most tutorials skip — plus the real error messages you will hit and what they mean.

If your site is on shared hosting with cPanel, or on a free tier like Netlify or Vercel, you likely do not need this guide: those platforms provision certificates automatically (check the control panel's SSL section first — ten seconds that saves fifteen minutes). This guide is for the case where you control the server: a VPS running Nginx or Apache, which is exactly where students deploying Node, Django, or PHP apps themselves end up. Pair it with the cheap web hosting guide if you are still choosing where to host.

What a certificate actually does

HTTPS is HTTP wrapped in TLS encryption. The certificate has two jobs: it proves to the browser that the server really is yourproject.in (not an imposter), and it carries the public key the browser uses to negotiate an encrypted connection. Let's Encrypt is a nonprofit certificate authority that issues these certificates free, automatically, for 90 days at a time. The 90-day lifetime is deliberate: it forces automation, so certificates get renewed by software instead of by a human remembering once a year. Certbot is the standard software that talks to Let's Encrypt, proves you control the domain, installs the certificate into your web server, and sets up renewal.

Before you start: the prerequisites checklist

Certbot needs three things to be true, and every failed issuance is one of these being false:

  1. A domain name pointing at your server. An A record for yourproject.in (and usually www.yourproject.in) resolving to your VPS IP. Verify with a DNS lookup from your laptop — if the domain does not resolve to the server yet, wait for propagation before running Certbot.
  2. Port 80 reachable from the internet. Let's Encrypt's servers must connect to your server on port 80 to verify control. Check your cloud firewall (AWS security groups, Hostinger firewall, UFW on the machine itself): sudo ufw allow 80 and sudo ufw allow 443 if you run UFW.
  3. A working site on HTTP first. Get the site loading over plain HTTP before adding TLS. Debugging a broken site and a broken certificate at the same time is how afternoons disappear.

The standard path: Certbot with the Nginx or Apache plugin

On Ubuntu/Debian, install Certbot with the plugin matching your web server:

sudo apt update
sudo apt install certbot python3-certbot-nginx

(Use python3-certbot-apache for Apache.) Then run it against your domain:

sudo certbot --nginx -d yourproject.in -d www.yourproject.in

What happens next: Certbot asks for an email (used for expiry warnings and account recovery — give a real one), asks you to agree to the terms, then proves domain control, obtains the certificate, edits your Nginx config to use it, and asks whether to redirect all HTTP traffic to HTTPS (say yes — you want the redirect). Test with https://yourproject.in in a browser: the padlock should appear with no warnings.

For Apache the command is identical with --apache. The plugin handles the server-specific configuration; the certificate files land in the same place either way: /etc/letsencrypt/live/yourproject.in/ containing fullchain.pem (the certificate chain) and privkey.pem (the private key — readable only by root, which is correct).

Challenge types: how Let's Encrypt verifies you

The --nginx plugin uses the HTTP-01 challenge: Let's Encrypt asks Certbot to place a token file at http://yourproject.in/.well-known/acme-challenge/<token>, then fetches it. If it matches, you control the domain. Two alternatives matter for students:

Challenge How it proves control When you need it
HTTP-01 (default) Serves a token file over port 80 Standard VPS with a web server — use this
DNS-01 Creates a TXT record in your DNS Wildcard certificates (*.yourproject.in), or servers unreachable on port 80
Webroot Places the token in an existing web root directory Your app server is not Nginx/Apache (e.g. a Node app behind a reverse proxy) — point Certbot at the static files directory

The webroot variant looks like this:

sudo certbot certonly --webroot -w /var/www/yourproject -d yourproject.in -d www.yourproject.in

certonly means "get the certificate but do not touch my server config" — you then point your app or reverse proxy at the files in /etc/letsencrypt/live/ yourself. This is the right mode when Nginx is only a reverse proxy in front of a Node/Django app: Certbot handles issuance, your proxy config handles TLS termination.

Wildcard certificates (*.yourproject.in, covering every subdomain) require DNS-01 and a Certbot DNS plugin for your provider. Most student projects do not need wildcards — list each subdomain with -d instead; it is simpler and HTTP-01 works fine.

Auto-renewal: the part that actually matters

A 90-day certificate that you renew by hand is a certificate that expires during your exams. Certbot installs automatic renewal when you install it via apt: a systemd timer (or cron job on some systems) runs certbot renew twice daily. That command renews any certificate expiring within 30 days and reloads the web server. Verify it works before you forget about it:

sudo certbot renew --dry-run

--dry-run talks to Let's Encrypt's staging server (no rate limits, certificates not trusted — purely a test) and exercises the whole renewal path including the challenge. If it succeeds, production renewal will succeed. Two gotchas that break renewal silently:

  • Firewall changes. If you later lock down port 80, renewal fails. HTTP-01 needs port 80 at renewal time, every time.
  • The webroot moved. If you restructured the site after issuance, the challenge path Certbot remembers may no longer exist. --dry-run catches this; run it after any restructuring.

Check renewal status any time with sudo certbot certificates — it lists every certificate, its domains, and its expiry date. Put a calendar reminder for a month before your viva to glance at it. Thirty seconds, once.

Manual configuration: what the plugin actually wrote

The --nginx plugin edits your server config for you, but you should know what a correct TLS server block looks like — partly so you can verify it, partly because the certonly + reverse-proxy setup (Node/Django behind Nginx) requires writing it yourself:

server {
    listen 443 ssl;
    server_name yourproject.in www.yourproject.in;

    ssl_certificate /etc/letsencrypt/live/yourproject.in/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourproject.in/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen 80;
    server_name yourproject.in www.yourproject.in;
    return 301 https://$host$request_uri;
}

Three details worth understanding: fullchain.pem (certificate plus the intermediate chain — browsers need the whole chain, not just your certificate), privkey.pem (never leaves the server, never committed to Git, never pasted into a chat), and the port-80 block that exists solely to redirect everything to HTTPS (and to answer the HTTP-01 challenge at renewal time). The X-Forwarded-Proto header is the line that tells your Node/Django app "the original request was HTTPS" — this is what fixes frameworks generating http:// URLs behind a proxy, the mixed-content cause from the earlier section. After editing, sudo nginx -t validates the config before you reload — never reload Nginx without testing first.

Wildcards via DNS-01: when you actually need one

If your project genuinely needs arbitrary subdomains (tenant1.yourproject.in, tenant2.yourproject.in — a multi-tenant project, for example), list-every-subdomain stops scaling and you want *.yourproject.in. That requires the DNS-01 challenge and a Certbot DNS plugin for your provider (most Indian hosts and Cloudflare have one):

sudo apt install python3-certbot-dns-cloudflare
sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /root/.cloudflare.ini \
  -d yourproject.in -d "*.yourproject.in"

Certbot creates (and cleans up) the _acme-challenge TXT record automatically via the provider's API. The credentials file holds an API token — chmod 600 it, keep it out of backups that leave the server. Note the quoted "*.yourproject.in": the shell would otherwise try to expand the asterisk. Renewal works the same automated way, since the DNS plugin handles the challenge each time. For everyone else — one domain, maybe www — skip this section; HTTP-01 with explicit -d flags is simpler and has fewer moving parts.

When Certbot is not an option

Two common student situations where the Certbot-on-VPS path does not apply:

Shared hosting without shell access. Use the control panel: cPanel's "SSL/TLS Status" (or the host's SSL installer) typically offers free AutoSSL/Let's Encrypt certificates in one click. The host handles issuance and renewal; your job is just the mixed-content cleanup afterwards. If your host charges extra for what Let's Encrypt gives free, that is a signal about the host, not about SSL.

A college or department server you do not administer. You cannot install Certbot without root. Options: ask the admin (many will do it — it is fifteen minutes of their time and improves the department's security posture), or put Cloudflare in front of the site: point the domain's DNS at Cloudflare, enable its proxy, and Cloudflare terminates TLS with its own free certificate while the origin stays HTTP behind it. Be honest about what this means: the visitor-to-Cloudflare leg is encrypted, the Cloudflare-to-server leg is not — acceptable for a demo, not for anything handling real user data. Document the limitation in your report rather than pretending it is end-to-end HTTPS.

Mixed content: the warnings after migration

The certificate is installed, the padlock appears — and then some pages show "Not secure" again, or styles break. This is mixed content: your HTML loads some resources over http:// — an image, a stylesheet, a script, a font — and the browser blocks or warns on them because the page is HTTPS but the resource is not. The fix is systematic, not mysterious:

  1. Find them. Browser DevTools → Console lists every blocked mixed-content URL. The Security tab shows the page's overall state.
  2. Fix the source, not the symptom. Replace hardcoded http:// URLs in your templates, database content, and config with https:// (or protocol-relative URLs where appropriate). The usual suspects: image URLs pasted from tutorials, CDN links copied years ago, API base URLs in JavaScript config, and http:// links stored in the database (WordPress's wp_posts is notorious — a search-replace on the database fixes it).
  3. Check what the server sends. Redirect chains (http:// → https://www → https://) are fine; what breaks is a page served over HTTPS that references HTTP resources. Also confirm your app generates https:// links itself — frameworks behind a reverse proxy sometimes think the request was HTTP and emit HTTP URLs. The fix is telling the framework it is behind a TLS-terminating proxy (e.g. setting the forwarded-proto handling in Express/Django, and the X-Forwarded-Proto header from the Nginx section above), not disabling anything.
  4. HSTS (optional, do it). Once everything is clean, add the Strict-Transport-Security header (e.g. max-age=31536000) so browsers always use HTTPS for your domain, even if a user types http://. Only enable it after mixed content is fully fixed — HSTS with broken HTTPS is worse than no HSTS.

For an e-commerce project like an e-commerce website with payment gateway, this step is non-negotiable: payment providers and their webhooks increasingly refuse to talk to non-HTTPS endpoints, and Razorpay/Stripe dashboards will flag the integration until the whole flow is clean.

Real error messages and what they mean

Error Meaning Fix
Failed to authenticate... connection refused Let's Encrypt could not reach port 80 Open port 80 in the cloud firewall and UFW; confirm the A record points at this server
DNS problem: NXDOMAIN The domain does not resolve The A record is missing or has not propagated — wait, then verify with a DNS checker
Too many certificates already issued Rate limit: 50 certificates per registered domain per week Stop re-issuing; use --dry-run / staging while testing; wait a week. This limit is why you test with staging first
Certbot failed to find a virtual host (nginx/apache plugin) No server block matches the domain Create the virtual host for the domain on port 80 first, then run Certbot
Renewal failed: missing challenge file Webroot path changed since issuance Update the webroot in /etc/letsencrypt/renewal/yourproject.in.conf or reissue with the correct -w path
Browser: NET::ERR_CERT_COMMON_NAME_INVALID Certificate is for a different name (often missing www) Reissue including both names with -d yourproject.in -d www.yourproject.in
Browser: certificate expired Auto-renewal broke silently Run certbot renew --dry-run to find the cause; fix port 80 / webroot; renew manually once with certbot renew --force-renewal

The rate-limit row deserves emphasis: students hit it by repeatedly issuing real certificates while debugging. The staging server exists for exactly this — --dry-run and --test-cert never count against limits. Debug on staging, issue production once.

What about paid certificates?

A paid certificate (typically Rs 500–2,000/year from a commercial CA) buys you nothing technical for a student project: the encryption is identical, browsers treat Let's Encrypt certificates exactly the same, and no examiner has ever asked which CA issued a project certificate. The historical reasons to pay — wildcard support, long lifetimes, the green company-name bar — are gone or irrelevant: Let's Encrypt issues wildcards free, 90-day auto-renewed lifetimes are operationally safer than annual manual renewals, and extended-validation indicators were removed from browsers years ago. Save the money.

Putting it together

The fifteen-minute version: point the domain at the server, open ports 80 and 443, get the site working on HTTP, run certbot --nginx -d yourproject.in -d www.yourproject.in, accept the HTTPS redirect, verify with --dry-run that renewal works, then hunt down mixed content in DevTools. Do that, and your project demo opens with a padlock instead of a warning — which is one less thing for the examiner to ask about, and one more thing that looks finished. Projects handling logins or payments — the online auction platform or a freelance services marketplace — should treat HTTPS as part of the feature, not decoration.

Reading the certificate you installed

It is worth knowing how to inspect what Certbot created — partly for the viva ("how does HTTPS actually work in your deployment?"), partly for debugging:

openssl x509 -in /etc/letsencrypt/live/yourproject.in/fullchain.pem -noout -subject -issuer -dates

This prints the subject (your domain), the issuer (Let's Encrypt), and the validity dates. The dates line is the one you care about operationally: notAfter should be ~90 days out, and after a renewal it should move. If notAfter is in the past, renewal broke — which takes you back to the --dry-run debugging in the renewal section.

Check what the world sees with an external test: SSL Labs' SSL Test (ssllabs.com/ssltest) grades your server's TLS configuration — protocol versions, cipher suites, certificate chain, HSTS. Aim for an A. The common deductions for a default Certbot+Nginx setup: none, honestly — modern Certbot configures TLS 1.2+ with sane ciphers out of the box. If you score below A, the report names the exact weakness (usually an old protocol left enabled by a distro default). Quote the grade in your report; it is a concrete, verifiable security claim, unlike "our site is secure".

Renewal on Docker and non-systemd setups

The automatic renewal described earlier assumes Certbot installed via apt on a systemd machine. Two setups students actually use need a tweak:

Docker deployments. If Nginx/Certbot run in containers, the host's systemd timer cannot reach them. The standard pattern is a dedicated Certbot container sharing the webroot volume with the Nginx container, plus a cron entry on the host (or a tiny scheduler container) running docker compose run --rm certbot renew twice daily, followed by reloading Nginx (docker compose exec nginx nginx -s reload). The certificates live in a named volume both containers mount. Test the full cycle with --dry-run inside the Certbot container before trusting it.

Bitnami / panel-managed stacks. Some one-click VPS images (Bitnami, CloudPanel, aaPanel) manage certificates through their own tooling rather than stock Certbot. Use the panel's Let's Encrypt integration instead of fighting it with manual Certbot — two certificate managers on one machine will overwrite each other's configs. The concepts in this guide (challenges, renewal, mixed content) all still apply; only the button you press changes.

Certificate transparency and the public record

One thing that surprises students: every Let's Encrypt certificate is published to public Certificate Transparency logs within minutes of issuance. Anyone can look up that yourproject.in got a certificate and when. This is by design — it is how the web detects mis-issued certificates — and it has two practical consequences. First, there is no "stealth" HTTPS: the moment you issue, the domain's existence is public record, so do not use certificate issuance as your launch announcement timing. Second, expiry-monitoring services watch these logs and will email you before a certificate expires — a free second layer of defence behind Certbot's own renewal. For a viva, it is also a neat verifiable fact: the examiner can independently confirm your site's certificate history.

TLS and the report: what to write

Examiners reward specific, verifiable security statements over vague ones. In your report's deployment chapter, write: the CA (Let's Encrypt), the validation method (HTTP-01 via Certbot), the renewal mechanism (systemd timer running certbot renew twice daily, verified with --dry-run), the TLS versions enabled (1.2 and 1.3), the HSTS policy if set, and the SSL Labs grade. That paragraph takes ten minutes to assemble from the commands in this guide and reads as genuine engineering rather than decoration — because it is.

Quick reference: the commands on one card

Taped to the monitor during deployment week: sudo certbot --nginx -d yourproject.in -d www.yourproject.in (issue), sudo certbot renew --dry-run (test renewal), sudo certbot certificates (list and expiry), sudo nginx -t && sudo systemctl reload nginx (safe config reload), openssl x509 -in /etc/letsencrypt/live/yourproject.in/fullchain.pem -noout -dates (check expiry). Five commands cover the entire certificate lifecycle.

Keep this card until the viva is over; after that, the automated renewal handles everything quietly in the background.

More project guides

More in Web Development