You’re building a small site, want it fast, and don’t need a full framework—perfect territory for Tailwind CSS. Tailwind is a utility-first CSS toolkit: you compose interfaces by stacking small classes (flex
, gap-4
, text-slate-700
) and let the build step purge unused styles so the final CSS stays tiny. We’ll create a minimal Tailwind project locally, build optimized assets, and serve them from Nginx on a DigitalOcean Droplet with HTTPS. You’ll need a DigitalOcean account and a domain you control so we can point DNS and generate certificates; with that in place, the rest is just careful, repeatable steps. We’ll go from a blank server to a secure, cached, gzip/Brotli-friendly static site, and you can extend from there.
What you’ll set up
You’ll provision an Ubuntu Droplet, install Nginx, and lock down the firewall. You’ll deploy static files produced by Tailwind’s build to /var/www/<your-domain>
and configure an Nginx server block for your domain. You’ll obtain and auto-renew an HTTPS certificate with Let’s Encrypt’s Certbot, which integrates cleanly with Nginx. These choices are intentionally boring and well-documented, which is exactly what you want for a site that should run unattended. If you later move to Next.js or another framework, the Nginx and TLS foundation stays the same.
1) Create a minimal Tailwind project locally
If you prefer a bundler, Tailwind’s docs show a Vite setup that “just works.” The modern path is: install tailwindcss
, add the Tailwind Vite plugin, import tailwindcss
in your CSS, and let the build step output a small file for production. We’ll make a simple HTML+CSS project now; swap Vite or your framework of choice later without changing the server side. The point is to produce static assets (index.html
, style.css
, images) that Nginx can serve directly.
Create a folder and initialize npm:
mkdir tailwind-site && cd tailwind-site
npm init -y
npm install -D tailwindcss
npx tailwindcss init
Wire up Tailwind in tailwind.config.js
so it can purge unused styles from your HTML:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./*.html", "./src/**/*.{js,ts}"],
theme: { extend: {} },
plugins: [],
};
Import Tailwind layers in src/styles.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
Add a simple index.html
at project root to prove everything renders:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="preload" href="/styles.css" as="style" />
<link rel="stylesheet" href="/styles.css" />
<title>Tailwind on DigitalOcean</title>
</head>
<body class="min-h-screen bg-slate-50 text-slate-800">
<main class="mx-auto max-w-3xl p-6">
<h1 class="text-3xl font-semibold tracking-tight">Hello, DigitalOcean</h1>
<p class="mt-3 text-base">
This page is built with Tailwind and will be served by Nginx.
</p>
</main>
</body>
</html>
Add build scripts to package.json
so you can produce a minified production CSS:
{
"scripts": {
"dev": "npx tailwindcss -i ./src/styles.css -o ./styles.css --watch",
"build": "npx tailwindcss -i ./src/styles.css -o ./styles.css --minify"
}
}
Build once:
npm run build
At this point your site is just two files (index.html
and styles.css
) plus any assets. Tailwind’s production guidance is to minify and compress; we’ll handle HTTP-level compression at Nginx, so the CLI --minify
flag suffices here.
2) Point DNS to your Droplet
Create a Droplet (Ubuntu LTS is fine), then in your domain registrar or DigitalOcean DNS, add an A
record from @
and www
to the Droplet’s public IPv4. Give it a few minutes to propagate. We’ll verify it when we fetch the TLS certificate. This is the only DNS step, and you rarely need to touch it again.
3) Harden the server base: packages, firewall, Nginx
SSH into the Droplet as root
or your sudo user and update packages:
sudo apt update && sudo apt upgrade -y
Install Nginx from Ubuntu’s repositories and enable it at boot:
sudo apt install -y nginx
sudo systemctl enable --now nginx
sudo systemctl status nginx --no-pager
Allow only the essentials through the firewall. With UFW, open SSH and web ports, then enable the firewall:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status
4) Stage your site on the server
Create a web root and set permissions:
sudo mkdir -p /var/www/example.com
sudo chown -R $USER:$USER /var/www/example.com
sudo chmod -R 755 /var/www/example.com
Copy your local build up with rsync
(run this from the project folder on your machine, replace the host and path):
rsync -avz --delete \
./index.html ./styles.css ./assets/ \
user@your_droplet_ip:/var/www/example.com/
You can repeat that command on every deploy; it syncs only changed files. If you prefer GitHub Actions later, you can keep the same destination path and skip reconfiguring Nginx.
5) Configure Nginx to serve your domain
Create a server block file referencing your domain and web root:
sudo tee /etc/nginx/sites-available/example.com >/dev/null <<'NGINX'
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
# Basic performance toggles; safe defaults for static sites
gzip on;
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
gzip_min_length 512;
# If nginx is built with brotli dynamic modules on your image, you can enable them later.
# brotli on;
# brotli_types text/plain text/css application/javascript application/json image/svg+xml;
}
NGINX
Enable it and test:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
If you plan to proxy an app server later (Node, Python, etc.), the same site file can add a location /api
block and pass to an upstream. For a static Tailwind site, try_files
is enough.
6) Add HTTPS with Let’s Encrypt (Certbot)
Install Certbot with the Nginx plugin, obtain a certificate, and set up auto-renewal. The plugin edits your server block safely and adds a redirect:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com --agree-tos -m you@example.com --redirect
sudo systemctl status certbot.timer --no-pager
sudo certbot renew --dry-run
Certbot integrates with Nginx on Ubuntu and configures a timer so renewals run unattended. If you can’t open port 80/443 or the domain isn’t pointing to the Droplet, issuance will fail—fix DNS first, then rerun.
7) Deploy updates cleanly
On your workstation, rebuild, then rsync
again to the same path. Reload Nginx if you’ve changed only static files (no need to unless you modified the config):
npm run build
rsync -avz --delete ./index.html ./styles.css ./assets/ user@your_droplet_ip:/var/www/example.com/
If you ever edit the Nginx config, validate and reload:
sudo nginx -t && sudo systemctl reload nginx
For CI, the simplest GitHub Actions job builds with Node, then uses appleboy/scp-action
or rsync
over SSH to push to /var/www/example.com
. The server-side does not change, which keeps your runtime surface small.
8) Optional hardening and polish
Set stricter UFW defaults (deny incoming, allow outgoing) and verify only OpenSSH
and Nginx Full
are open. Consider enabling HTTP/2 and adding a conservative CSP meta tag if you inline scripts; both are simple edits to the Nginx file. Our UFW guide is a handy reference if you later add services and need to scope by port or interface. None of this changes your deploy routine, which remains “build locally → sync to web root.”
Quick sanity checklist
Your domain should load over HTTPS with a valid certificate and a small CSS file (Tailwind’s purge tends to keep it lean). Your Nginx access log should show 200
responses for /
and /styles.css
. sudo certbot renew --dry-run
should pass without errors, confirming renewals will run on schedule. If something misbehaves, test locally with python3 -m http.server
to ensure index.html
references styles.css
correctly, then re-sync and check Nginx’s error log for typos. From here, you can add a CI job or swap the local build for a framework without touching the server.
Appendix: common commands you’ll reuse
Check service health:
systemctl status nginx --no-pager
journalctl -u nginx -e --no-pager
Inspect firewall rules:
sudo ufw status verbose
Renew TLS on demand and verify timer:
sudo systemctl list-timers | grep certbot
sudo certbot renew --dry-run
Rebuild and deploy (local):
npm run build
rsync -avz --delete ./ user@your_droplet_ip:/var/www/example.com/ \
--exclude node_modules --exclude .git
Why this approach ages well
Nginx serving static files is stable, fast, and easy to reason about. Tailwind’s build pipeline outputs plain CSS, and the CLI or Vite integration stays consistent even as the ecosystem moves. Certbot’s Nginx plugin keeps certificates current without custom cron jobs, and UFW gives you a small, readable firewall surface. When you eventually add a backend or migrate to an SSR framework, you can keep the same Droplet, firewall, and TLS, and only swap the Nginx location
rules.
If you want me to tailor this for App Platform or add a GitHub Actions workflow file that deploys on push, let me know in the comments which repo layout you’re using and I’ll give you a ready-to-run workflow.