How to deploy and self-host a Tailwind site on DigitalOcean

How to deploy & self-host a Tailwind site on DigitalOcean

We may earn an affiliate commission through purchases made from our guides and tutorials.

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.

Was this helpful?

Thanks for your feedback!
Alex is the resident editor and oversees all of the guides published. His past work and experience include Colorlib, Stack Diary, Hostvix, and working with a number of editorial publications. He has been wrangling code and publishing his findings about it since the early 2000s.

Leave a comment

Your email address will not be published. Required fields are marked *