Tutorial
Aug 13, 202620 views

How to Deploy HTML and CSS Projects on Netlify (Manual Upload)

Deploy an HTML and CSS project to Netlify by dragging a folder — plus fixes for the broken image paths, missing CSS, and subpage 404s that show up right after, and when to switch to a Git-connected deploy.

HTMLCSSNetlify
VS
Vikash SinghUpdated Nov 28, 2025
Likes0
Shares0
20 views · 20 YouTube viewsAug 13, 2026

TL;DR

Put index.html at the root of your folder, drag it onto Netlify's deploy area, and rename the site from its random subdomain. If styling breaks, check filename case — Netlify's servers are case-sensitive and your laptop is not. Switch to a Git deploy once you are updating regularly.

Walkthrough

Step-by-step.

You built something with HTML and CSS. Now you want a link you can send to someone.

Netlify's drag-and-drop deploy gets you a live URL in under a minute. No Git, no terminal, no build tools. Drag a folder onto a page and the site is online.

This guide covers the upload, plus the three things that go wrong immediately afterward: broken image paths, subpages returning 404, and a custom domain that will not connect.

What You Need

A folder containing your project files. HTML, CSS, images, whatever else the site uses.

A file named index.html at the top level of that folder. Not home.html. Not Index.html. Netlify looks for exactly index.html, lowercase, and serves it as your homepage. Getting this wrong is the most common reason a fresh deploy shows a blank page or a directory listing.

A Netlify account. The free tier is generous and no card is required.

This works for vanilla CSS, Bootstrap, and Tailwind — as long as Tailwind is loaded via CDN or you have already compiled your CSS to a plain file. If you are running a Tailwind build step, compile first and upload the output.

Step 1: Check Your Folder Structure

Before uploading, open the folder and confirm the layout looks like this:

1 2 3 4 5 6 7 8
my-project/
├── index.html
├── css/
│ └── style.css
├── js/
│ └── script.js
└── images/
└── logo.png

The critical detail is that index.html sits at the root of the folder you drag, not inside a subfolder. If your files are nested one level deeper, drag the inner folder instead.

Step 2: Deploy

Sign in at app.netlify.com.

On your team dashboard, find the drag-and-drop area — it usually reads "Drag and drop your site output folder here" or sits under Add new site, then Deploy manually.

Drag your project folder onto it. Upload takes a few seconds.

Netlify assigns a random subdomain such as spontaneous-halva-4f2a91.netlify.app. Your site is live at that URL. That is the whole deploy.

Step 3: Rename the Site

The random name is fine for a quick share and terrible for a portfolio.

Go to Site configuration, then Site details, then Change site name. Pick something readable — yourname-portfolio.netlify.app.

Do this before you share the link anywhere, because the old URL stops working once you change it.

Fixing Broken Images and Missing CSS

Your site loads but the styling is gone and images are missing. This happens to almost everyone on their first deploy, and there are two causes.

Case sensitivity. Windows and macOS treat Logo.png and logo.png as the same file. Netlify's servers do not. If your HTML references images/Logo.png and the actual file is logo.png, it worked locally and it will 404 in production.

Fix: make every filename lowercase, and make every reference in your HTML match exactly.

Absolute local paths. If your HTML contains something like C:/Users/You/Desktop/project/css/style.css, or a leading slash pointing somewhere that does not exist on the server, the browser cannot find the file.

Fix: use relative paths. css/style.css and images/logo.png, referenced from where index.html sits.

Open your browser's developer console on the live site. Every failed file shows up there as a 404 with the exact path it tried, which usually tells you the answer immediately.

Fixing 404s on Subpages

Your homepage works, but yoursite.netlify.app/about returns a 404.

For a static HTML site, Netlify serves files by path. /about only works if a file named about.html exists — Netlify will match the clean URL to it. If you have pages/about.html, then the URL is /pages/about.

If your site is a single-page app using client-side routing, you need a _redirects file at the root of the folder you upload, containing:

1
/* /index.html 200

That tells Netlify to serve index.html for every path and let your JavaScript handle routing. For a plain HTML and CSS site, you do not need this.

Adding a Custom Domain

Go to Domain management, then Add a domain, and enter your domain.

You have two paths.

Netlify DNS. Netlify gives you four nameservers to enter at your domain registrar. This is the simpler option and it handles the SSL certificate automatically.

External DNS. Keep your current DNS provider and add records manually. Point the apex domain at Netlify's load balancer IP, and add a CNAME for www pointing to your Netlify subdomain. Netlify shows you the exact values.

Either way, DNS propagation takes anywhere from a few minutes to 48 hours. Netlify provisions a free Let's Encrypt certificate automatically once the domain resolves, so leave it a while before assuming something is broken.

If HTTPS does not appear after a day, check Domain management for a certificate error. It is nearly always a DNS record that has not propagated or a conflicting record left over from a previous host.

Updating a Manually Deployed Site

Here is the tradeoff nobody mentions in the drag-and-drop tutorials.

Every change means dragging the entire folder again. There is no version history you can meaningfully work with, no rollback to a specific commit, and no record of what changed between deploys.

For a one-off share or a class assignment, that is fine.

For anything you will update more than a handful of times, connect a Git repository instead. Push to your branch and Netlify rebuilds automatically. You get deploy previews on pull requests, a full history, and one-click rollback to any previous deploy.

The manual upload is the right way to start. It is the wrong way to maintain something.

When to Move Beyond Manual Deploys

Move to a Git-connected deploy when any of these become true.

You are updating the site regularly. Dragging a folder gets old fast.

You need a build step. Tailwind compilation, Sass, a bundler, any framework — Netlify runs the build for you on a Git deploy.

More than one person touches the site. Manual uploads have no merge story at all.

You need environment variables or serverless functions.

Connecting a repo takes about two minutes: Add new site, then Import an existing project, then authorise GitHub and pick the repository.

Netlify's Free Tier Limits

Worth knowing before you build something that outgrows it.

The free tier covers a substantial amount of bandwidth and build minutes per month, and supports custom domains with free SSL. Concurrent builds are limited to one, and some team features are paid.

The specific numbers change. Check Netlify's current pricing page rather than trusting any figure in a tutorial, including this one.

Where This Fits in a Real Project

Static hosting handles the frontend. The moment you need authentication, a database, payments, or an API, the architecture question changes — and that decision is easier to make early than to retrofit.

Our team builds and ships production frontends in React and Next.js, along with the backend infrastructure that supports them. You can see the stack we work across on our technologies page and browse shipped work in our projects portfolio.

If you are setting up a local environment, the NVM on Windows tutorial covers Node version management. For self-hosted deployments, the SSL certificate guide handles TLS on Nginx.

More walkthroughs are on the tutorials hub, and longer technical writing lives on the blog.

Answers

Frequently asked.

Why is my CSS not loading after deploying to Netlify?+

Almost always filename case. Netlify's servers are case-sensitive; Windows and macOS are not. If your HTML references Style.css and the file is style.css, it works locally and 404s in production. Check the browser console for the exact failing path.

Do I need index.html to deploy on Netlify?+

Yes. Netlify serves index.html from the root of your uploaded folder as the homepage. It must be lowercase and at the top level of the folder you drag, not inside a subfolder.

Can I deploy a Tailwind CSS project by dragging it to Netlify?+

Yes, if Tailwind is loaded from a CDN or you have already compiled your CSS to a plain file. If your project has a Tailwind build step, run the build first and upload the output folder — manual deploys do not run build commands.

How do I update a site I deployed manually on Netlify?+

Drag the whole folder onto the deploy area again. There is no partial update. If you are updating often, connect a Git repository instead so pushes deploy automatically.

Is Netlify free for HTML and CSS projects?+

The free tier covers static site hosting, custom domains, and automatic SSL, with monthly bandwidth and build limits. Check Netlify's current pricing page for exact figures, as they change.

Why do my subpages return 404 on Netlify?+

For a static site, check the file actually exists at the path you are requesting — /about needs about.html at the root. For a single-page app with client-side routing, add a _redirects file containing /* /index.html 200.

Craxinno Service

Have a project in mind?

Our team has shipped production software for client apps — from initial integration to scaling, caching and cost optimisation. We’d love to help.

Was this tutorial helpful?Your feedback helps us prioritise what to publish next.

Continue with Tutorials.

View all tutorials
How to Send Twilio SMS Using Your Brand Name (Sender ID Setup)
Twilio

How to Send Twilio SMS Using Your Brand Name (Sender ID Setup)

Getting an SMS from "CRAXINNO" instead of a random number looks more legitimate and gets opened more often. Twilio calls this an Alphanumeric Sender ID, and in supported countries it takes about ten minutes to set up. The catch is the phrase "in supported countries." Where you are sending decides almost everything about how this works, and one very large market does not support it at all. Read This First: Where Alphanumeric Sender IDs Do Not Work Alphanumeric Sender IDs are not available for the United States or Canada. Not with an application, not with a business account, not with a workaround. The carriers do not accept them. For US traffic, you register a phone number through A2P 10DLC, or use a toll-free number or short code. Your brand identity comes from carrier-level brand registration rather than from the sender string. This catches out a lot of teams who build the integration first and discover the restriction when messages start failing. If your primary market is North America, stop here and read Twilio's A2P 10DLC documentation instead. Where it does work: most of Europe, the UK, Australia, India, much of Asia, Africa, and the Middle East. Country support and requirements change regularly, so check Twilio's country guidelines for your specific destinations before you build. Three Levels of Country Requirement Supported countries fall into three groups, and knowing which one you are sending to determines your timeline. No registration needed. Set the sender string and send. The UK, Australia, and most of Western Europe work this way. This is what the video demonstrates. Pre-registration required. You submit the sender ID to Twilio, who registers it with local carriers. Approval takes days to weeks. India, Vietnam, the Philippines, Saudi Arabia, the UAE, and Qatar all sit here. Sending an unregistered ID to these countries means silent failure or substitution with a random number. Not supported. The US and Canada, as covered above. India deserves a specific mention because it is the strictest. You need DLT registration with an Indian telecom operator, covering your entity, your sender ID header, and every message template you intend to send. Unregistered templates are blocked. Budget two to three weeks. The Limitation Nobody Mentions Until It Bites Alphanumeric Sender IDs are one-way only. Recipients cannot reply. There is no inbound path. If someone tries, the message goes nowhere and they get no error. This makes them right for one-time passcodes, delivery notifications, appointment reminders, and transactional alerts. It makes them wrong for anything conversational — support threads, two-way confirmations, or any flow where a customer might reasonably respond. If you need replies, use a phone number. Some teams run both: a sender ID for outbound notifications and a number for conversations. Sender ID Format Rules Between 1 and 11 characters. Letters A-Z and a-z, digits 0-9, and spaces. At least one letter is required — an all-numeric string is not valid. No leading or trailing spaces. Two practical notes. Casing is not guaranteed to survive; some carriers normalise everything to uppercase. And avoid characters that look alike across fonts, since a sender ID exists to be recognised at a glance. Pick something short and obviously yours. "CRAXINNO" reads better than "CraxinnoTech" truncated to eleven characters. Step 1: Get Your Credentials Sign in to the Twilio Console. Your Account SID and Auth Token are on the dashboard. The Auth Token is a password. Do not commit it, do not paste it into a client-side file, and do not put it in a support ticket. Store both in environment variables. For anything beyond a test, use an API Key and Secret instead of the Auth Token directly. API keys can be revoked individually without rotating your account credentials, which matters when someone leaves or a laptop goes missing. Step 2: Install the SDK npm install twilio Then create a .env file: 1 2 3 TWILIO_ACCOUNT_SID = your_account_sid TWILIO_AUTH_TOKEN = your_auth_token TWILIO_SENDER_ID = CRAXINNO Add .env to .gitignore before you write anything into it. Step 3: Send the Message 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 require ( 'dotenv' ) . config ( ) ; const twilio = require ( 'twilio' ) ; const client = twilio ( process . env . TWILIO_ACCOUNT_SID , process . env . TWILIO_AUTH_TOKEN ) ; async function sendSMS ( to , body ) { try { const message = await client . messages . create ( { body , from : process . env . TWILIO_SENDER_ID , to } ) ; console . log ( 'Sent:' , message . sid ) ; return message ; } catch ( error ) { console . error ( 'Failed:' , error . code , error . message ) ; throw error ; } } sendSMS ( '+447700900000' , 'Your verification code is 482913' ) ; The only difference from sending via a phone number is the from value. Instead of a number in E.164 format, you pass your sender string. The to number must be in E.164 format — a plus sign, country code, then the number, with no spaces or dashes. Step 4: Verify Delivery A successful API call means Twilio accepted the message, not that it arrived. Check the actual status: 1 2 const sent = await client . messages ( message . sid ) . fetch ( ) ; console . log ( sent . status , sent . errorCode ) ; Status moves through queued , sent , and ideally delivered . A status of undelivered or failed comes with an error code that tells you why. For anything in production, configure a status callback webhook rather than polling. Twilio posts delivery updates to your endpoint as they happen. Using a Messaging Service Instead For production traffic, create a Messaging Service in the Twilio Console rather than hardcoding the sender. Add both your alphanumeric sender ID and a phone number to the sender pool. Twilio then picks the right sender for each destination automatically — the sender ID where it is supported, the number where it is not. In code you pass messagingServiceSid instead of from . That one change means a message to a US number does not simply fail; it falls back to your registered number. This is the difference between a demo and something you can run a business on. Common Errors 21612 — cannot route to this number. Usually an unsupported destination country for alphanumeric senders. This is what a US number returns. 21606 — the From number is not a valid, SMS-capable inbound number. Your sender string breaks a format rule, or the destination country requires pre-registration you have not completed. Message shows as delivered but the sender is a random number. The carrier substituted it. Common where registration is required and has not been done. 30007 — message filtered. The carrier blocked it, typically for content resembling spam or, in India, an unregistered template. Works in testing, fails in one country. Almost always a country-specific registration requirement. Check Twilio's country guidelines for that destination. 21608 — unverified number on a trial account. Trial accounts can only send to numbers you have verified in the console. Upgrade or verify the recipient. Before You Go Live Test with a real handset in each destination country. Emulators and virtual numbers do not reflect carrier behaviour. Keep a fallback sender configured. A Messaging Service handles this; hardcoded from values do not. Log the message SID for every send. When a customer says they never received a code, the SID is the only way to find out what actually happened. Respect local rules on timing and consent. Several jurisdictions restrict marketing SMS to certain hours and require documented opt-in. The penalties are real. Monitor your delivery rate per country. A drop in one market usually means a carrier policy change rather than a bug in your code. Where This Fits in a Real Project SMS is rarely the whole feature. It sits inside authentication, order flows, or a notification system, alongside retry logic, rate limiting, and a record of what was sent to whom. Our team builds and ships production integrations like this for client software — messaging, payments, and third-party APIs, with the operational layer around them. You can see the stack we work across on our technologies page and browse shipped work in our projects portfolio. If you are setting up the environment around this, the NVM on Windows tutorial covers Node version management, and the SSL certificate guide handles securing the server your webhook endpoint runs on. More walkthroughs are on the tutorials hub, and longer technical writing lives on the blog .

Posted 28.11.2025
How to Create and Run a React Project with TypeScript
React

How to Create and Run a React Project with TypeScript

Setting up a React project with TypeScript takes about two minutes with the right tool. The hard part is knowing which tool to use, because the answer changed. Create React App was the default for years. It was deprecated in February 2025 and is no longer recommended for new projects. If a tutorial tells you to run npx create-react-app , it is out of date. This guide uses Vite, which is what the React team now points people toward for single-page apps. What You Need First Node.js version 20 or higher. Check with: node -v If that command is not recognised or the number is below 20, install Node first. On Windows, the NVM tutorial covers installing it in a way that lets you switch versions per project. You also need npm, which ships with Node. Confirm with npm -v . A code editor. VS Code has the strongest TypeScript support out of the box. Why Vite Instead of Create React App Three reasons, briefly. Speed. Vite starts a dev server in under a second because it serves native ES modules rather than bundling everything upfront. CRA's Webpack setup takes tens of seconds on a large project. Maintenance. CRA has not received meaningful updates in years and its dependency tree carries known vulnerabilities that cannot be resolved without ejecting. Direction. The React documentation now recommends either a framework such as Next.js, or Vite for a plain single-page app. If you are building something with routing, data fetching, and SEO requirements, consider Next.js instead. Vite is the right answer for a client-side app, a dashboard behind a login, or anything you are learning on. Step 1: Create the Project Run this in the folder where you keep your projects: npm create vite@latest my-app -- --template react-ts The react-ts template is the important part. It scaffolds React with TypeScript already configured — no separate setup step. If you prefer the interactive prompts, run npm create vite@latest on its own and select React, then TypeScript, when asked. Step 2: Install Dependencies cd my-app npm install This pulls down React, TypeScript, Vite, and the type definitions. It takes a few seconds. Step 3: Run It npm run dev Vite prints a local URL, usually http://localhost:5173 . Open it and you have a working React app with hot module replacement — save a file and the browser updates without a full reload, preserving component state. Note the command. Vite uses npm run dev , not npm start . That difference catches out everyone coming from CRA. Understanding the Project Structure 1 2 3 4 5 6 7 8 9 10 11 my - app / ├── public / ├── src / │ ├── App . tsx │ ├── main . tsx │ ├── index . css │ └── vite - env . d . ts ├── index . html ├── package . json ├── tsconfig . json └── vite . config . ts A few things differ from what you may expect. index.html sits at the project root, not inside public/ . Vite treats it as the entry point and processes it directly. main.tsx is where React mounts to the DOM. App.tsx is your root component. .tsx is the extension for files containing JSX. Plain TypeScript files without JSX use .ts . vite-env.d.ts gives TypeScript the type definitions for Vite's environment variables. Leave it alone. Writing Your First Typed Component Delete the contents of App.tsx and replace with: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type GreetingProps = { name : string ; count ? : number ; } ; function Greeting ( { name , count = 0 } : GreetingProps ) { return ( < div > < h1 > Hello , { name } < / h1 > < p > Clicked { count } times < / p > < / div > ) ; } export default function App ( ) { return < Greeting name = "Nimish" / > ; } Two things worth understanding here. The type block defines the shape of the props. TypeScript now checks every use of Greeting against it. Pass a number where a string is expected and your editor flags it before you run anything. The ? on count marks it optional. Without the ? , TypeScript requires it on every usage. Try removing name="Nimish" and watch the error appear in your editor. That immediate feedback is the entire value proposition of TypeScript. Typing State and Events Two patterns you will use constantly. State usually infers its own type: 1 const [ count , setCount ] = useState ( 0 ) ; TypeScript works out that count is a number from the initial value. You only need an explicit type when the initial value does not tell the full story: 1 const [ user , setUser ] = useState < User | null > ( null ) ; Event handlers need their event typed: 1 2 3 function handleChange ( e : React . ChangeEvent < HTMLInputElement > ) { setName ( e . target . value ) ; } The element type inside the angle brackets matters. HTMLInputElement for inputs, HTMLTextAreaElement for textareas, HTMLSelectElement for selects. Getting it wrong means e.target.value will not be typed correctly. Building for Production npm run build This type-checks the whole project, then compiles and bundles into a dist folder. The build fails on type errors. That is intentional and it is the point — a type error caught at build time is a bug that never reaches a user. Preview the production build locally before deploying: npm run preview The dist folder is what you deploy. It is static output, so it works on Netlify, Vercel, S3, or any static host. The Netlify deployment guide covers that process, though for a Vite app you would normally connect Git rather than dragging the folder. Common Errors and Fixes Cannot find module './App' or its corresponding type declarations — usually a case mismatch in the filename, or a missing .tsx extension on the file itself. JSX element implicitly has type 'any' — the React type definitions are missing. Run npm install -D @types/react @types/react-dom . Property does not exist on type — you are accessing a field that is not in your type definition. Add it to the type rather than reaching for any . Port 5173 already in use — another Vite server is running. Either stop it, or start this one with npm run dev -- --port 3000 . Blank page after building and deploying — the base path is wrong. If you are deploying to a subdirectory, set base: '/subdirectory/' in vite.config.ts . Environment variables are undefined — Vite only exposes variables prefixed with VITE_ . Rename API_URL to VITE_API_URL and access it via import.meta.env.VITE_API_URL , not process.env . Migrating an Existing CRA Project If you have a Create React App project you want to move over, the outline is: Install Vite and the React plugin, remove react-scripts , move index.html from public/ to the root and add a script tag pointing at src/main.tsx , rename any .js files containing JSX to .jsx or .tsx , swap process.env.REACT_APP_* for import.meta.env.VITE_* , and update your package scripts to dev , build , and preview . Budget an afternoon for a mid-sized app. Most of the time goes on environment variables and any Webpack-specific configuration you had customised. Where This Fits in a Real Project A scaffolded project is the starting point. What follows is routing, state management, a testing setup, and a CI pipeline — and those decisions are cheaper to make now than to retrofit later. Our team builds production React and Next.js applications in TypeScript for client software. You can see the stack we work across on our technologies page and browse shipped work in our projects portfolio . If you are still setting up locally, the NVM on Windows tutorial handles Node version management. For deployment, the Netlify guide covers static hosting and the SSL certificate guide covers self-hosted TLS. More walkthroughs are on the tutorials hub, and longer technical writing lives on the blog .

Posted 02.12.2025
How to Set Up an SSL Certificate in Nginx (Step-by-Step Guide)
Nginx

How to Set Up an SSL Certificate in Nginx (Step-by-Step Guide)

Serving a site over plain HTTP in 2026 means browsers flag it as insecure before anyone reads a word of it. Setting up SSL on Nginx takes about fifteen minutes. There are two paths. Certbot with Let's Encrypt is free, automated, and renews itself — this is the right choice for almost everyone. A purchased commercial certificate involves manual file handling and is only necessary in specific cases. This guide covers both. Before You Start You need a domain with an A record pointing at your server's public IP. DNS must have propagated. Certificate issuance verifies domain ownership, and it will fail if DNS is not resolving yet. Ports 80 and 443 must be open in your firewall and any cloud security group. Port 80 is required for the initial validation even though your site will end up on 443. You need root or sudo access, and Nginx already installed and running. Which Certificate Do You Actually Need? Let's Encrypt is free, issues in seconds, and auto-renews every 90 days. It provides domain validation, which is exactly the same encryption strength as any paid certificate. For the vast majority of sites, this is the correct answer. A commercial certificate is worth paying for in three situations. You need Organisation Validation or Extended Validation, where the certificate authority verifies your legal entity. You need a warranty and paid support for compliance reasons. Or you need a multi-year wildcard managed outside your server. Encryption is identical across all of them. What you pay for is the validation process and the paperwork, not stronger security. Path A: Let's Encrypt with Certbot Step 1: Install Certbot On Ubuntu or Debian, the snap package is what Let's Encrypt recommends: sudo snap install --classic certbot sudo ln -s /snap/bin/certbot /usr/bin/certbot The apt package exists but tends to lag behind. Step 2: Run Certbot sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com Certbot verifies domain ownership, obtains the certificate, edits your Nginx config to use it, and sets up the HTTP-to-HTTPS redirect. All of it. When prompted about redirecting HTTP traffic, choose redirect. Step 3: Confirm Auto-Renewal sudo certbot renew --dry-run Certificates last 90 days. The installer adds a systemd timer that renews at around 60 days. The dry run confirms it works. If you skip this check and renewal is broken, you find out when the certificate expires. Verify the timer is active: sudo systemctl list-timers | grep certbot That is the whole process for most sites. Everything below is for people using a purchased certificate. Path B: Installing a Purchased Certificate Step 1: Generate a CSR and Private Key On your server: sudo mkdir -p /etc/nginx/ssl cd /etc/nginx/ssl sudo openssl req -new -newkey rsa:2048 -nodes -keyout yourdomain.key -out yourdomain.csr Fill in the prompts. Common Name must be your exact domain. For a wildcard, use *.yourdomain.com . The private key never leaves your server. Any vendor asking you to upload it is doing something wrong. Step 2: Submit the CSR and Collect Your Files Paste the contents of the .csr file into your certificate authority's order form. Complete their validation. You will receive back your domain certificate and one or more intermediate certificates, sometimes called a CA bundle. Step 3: Build the Certificate Chain Nginx needs the domain certificate and the intermediates in a single file, in the correct order. Your certificate first, then the intermediates: sudo cat yourdomain.crt intermediate.crt root.crt > yourdomain-chained.crt Order matters. Reversed, browsers will report an incomplete chain — and desktop Chrome often hides the problem while mobile browsers and API clients fail hard. This is the single most common mistake with manual installs. Step 4: Set Permissions sudo chmod 600 /etc/nginx/ssl/yourdomain.key sudo chown root:root /etc/nginx/ssl/yourdomain.key A world-readable private key means anyone with shell access can impersonate your site. Configuring the Nginx Server Block Open your site config, typically at /etc/nginx/sites-available/yourdomain.com : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 server { listen 443 ssl ; http2 on ; server_name yourdomain . com www . yourdomain . com ; ssl_certificate / etc / nginx / ssl / yourdomain - chained . crt ; ssl_certificate_key / etc / nginx / ssl / yourdomain . key ; ssl_protocols TLSv1 . 2 TLSv1 . 3 ; ssl_prefer_server_ciphers off ; ssl_session_cache shared : SSL : 10m ; ssl_session_timeout 1d ; ssl_session_tickets off ; add_header Strict - Transport - Security "max-age=63072000" always ; root / var / www / yourdomain ; index index . html ; } server { listen 80 ; server_name yourdomain . com www . yourdomain . com ; return 301 https : / / $host$request_uri ; } A few notes on that config. The listen 443 ssl http2 one-liner is deprecated in Nginx 1.25 and later. Use the separate http2 on; directive shown above. Plenty of older tutorials still show the old form and it now produces a warning. TLS 1.0 and 1.1 are omitted deliberately. Both are deprecated and will fail a PCI compliance scan. ssl_prefer_server_ciphers off is correct for modern setups. TLS 1.3 clients pick sensibly on their own. Start HSTS with a short max-age while testing. Once a browser has seen the header, it refuses plain HTTP for that duration and there is no way to undo it from the server side. Only add includeSubDomains and preload once you are certain every subdomain serves HTTPS. Testing and Reloading Always test before reloading: sudo nginx -t If it reports the syntax is ok and the test is successful: sudo systemctl reload nginx Use reload rather than restart. Reload applies the new config without dropping active connections. Verifying the Installation Check the chain from the command line: openssl s_client -connect yourdomain.com:443 -servername yourdomain.com Look for "Verify return code: 0 (ok)" at the end of the output. Anything else means a chain problem. Then run your domain through SSL Labs' server test. Aim for an A. If you score lower, the report tells you which directive is responsible. Check your site in a browser too, on mobile as well as desktop. Mobile browsers are stricter about incomplete chains and will surface problems desktop Chrome quietly tolerates. Common Nginx SSL Errors SSL_ERROR_RX_RECORD_TOO_LONG — you are serving plain HTTP on port 443. The ssl parameter is missing from your listen directive. ERR_CERT_AUTHORITY_INVALID — the intermediate certificates are missing or in the wrong order. Rebuild the chained file. nginx: [emerg] cannot load certificate ... PEM_read_bio_X509 — the file path is wrong, or the file has Windows line endings from being edited on a desktop. Run dos2unix on it. ERR_SSL_KEY_MISMATCH — the certificate does not match the private key. Compare the modulus hashes of both files with openssl x509 -noout -modulus and openssl rsa -noout -modulus . They must match. Certbot fails with "Timeout during connect" — port 80 is blocked. Check both your server firewall and your cloud provider's security group. Padlock shows but some resources are blocked — mixed content. Something on the page is still loading over HTTP. Check the browser console. Keeping It Working Set a calendar reminder before expiry even with auto-renewal enabled. Renewals fail silently when firewall rules change or a config edit breaks the challenge path. Monitor the certificate externally. Uptime tools with certificate expiry checks catch what your server will not tell you. Redirect HTTP to HTTPS at the server level rather than in application code. It is faster and it cannot be bypassed by a routing bug. If you sit behind Cloudflare or a load balancer, know which layer terminates TLS. Origin certificate misconfigurations behind a proxy are a common and confusing failure mode. Where This Fits in a Real Project TLS is one layer. Security headers, rate limiting, and keeping the server patched matter alongside it. Our team configures and maintains production infrastructure for client software, from Nginx and TLS through to CI and observability. You can see the stack we work across on our technologies page and browse shipped work in our projects portfolio . If you are setting up the rest of the environment, the NVM on Windows tutorial covers Node version management, and the S3 backup guide handles data protection on AWS. More walkthroughs are on the tutorials hub , and longer technical writing lives on the blog.

Posted 02.12.2025
Connect With Us

Have something in mind?

We take on a handful of new custom-software engagements every quarter. If your problem is interesting and your timeline is real — let’s talk.

Let’s ConnectAvg. response · under 4 hours
01
Ideate · 1 weekWorkshops, scoping, success metrics agreed.
02
Design + Build · 8–14 weeksBi-weekly demos. Production code from week one.
03
Ship + Support · ongoingDeployment, observability, and a long-tail retainer.