Here’s a clean way to deploy and self-host a Jekyll site on a DigitalOcean Droplet with Nginx and HTTPS. I’ll keep it practical, stick to commands that won’t age badly, and explain why you’re doing each step so it actually makes sense when you come back to this later.
You’ll need a DigitalOcean account to spin up a Droplet. I’m assuming Ubuntu 24.04 (or 22.04) on a fresh box, a domain you control (e.g., example.com
), and basic SSH access.
Jekyll is a Ruby-based static site generator: you write Markdown, it renders a folder of plain HTML/CSS/JS, and a web server (Nginx) serves those files. No app server, no database, no moving parts on production—just static assets. That’s why it’s fast, cheap, and frankly hard to break. If you’ve used GitHub Pages, you’ve already touched Jekyll; we’re just taking the wheel and hosting it ourselves.
Create a user, lock down SSH, and enable the firewall
SSH into your new Droplet as root
, then add a normal sudo user and turn on UFW. This is DigitalOcean’s bread-and-butter first step for Ubuntu; do it once, do it right.
adduser deploy
usermod -aG sudo deploy
rsync -a ~/.ssh /home/deploy/ && chown -R deploy:deploy /home/deploy/.ssh
# (optional) if you're using passwordless sudo for deployment workflows:
# echo "deploy ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/90-deploy
# Firewall: allow SSH and (soon) HTTP/HTTPS
ufw allow OpenSSH
ufw allow 'Nginx Full' # creates rules for 80/443 when Nginx is installed
ufw --force enable
ufw status
Log out and back in as deploy
from here on:
ssh deploy@your_droplet_ip
Install Nginx and get a clean server block ready
Nginx will serve the generated _site/
directory. Keep the default config untouched and add your own server block for the domain.
sudo apt update
sudo apt install -y nginx
sudo systemctl enable --now nginx
Create the web root and a basic structure:
sudo mkdir -p /var/www/example.com/site
sudo chown -R deploy:www-data /var/www/example.com
Now create the Nginx server block:
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/site; # we'll publish Jekyll's _site here
index index.html;
# Serve static files directly; if a directory is requested, load index.html
location / {
try_files $uri $uri/ /index.html;
}
# Sensible cache headers for static assets (tweak as you like)
location ~* \.(?:css|js|jpg|jpeg|gif|png|svg|ico|webp|avif|woff2?)$ {
expires 7d;
add_header Cache-Control "public";
}
}
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
That try_files
line is the key for static sites—Nginx will serve the file if it exists, fall back to directory index, then finally to /index.html
. Exactly what you want for SPA-ish behavior or clean URLs on static sites.
Install Ruby the right way (rbenv), then Jekyll
Yes, Ubuntu has ruby-full
, but using rbenv keeps your Ruby clean, upgradeable, and out of the system’s way. This is the least painful long-term approach.
# Build tools and headers Jekyll/Ruby will need:
sudo apt update
sudo apt install -y git build-essential libssl-dev zlib1g-dev libreadline-dev libffi-dev
# Install rbenv and ruby-build
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init - bash)"' >> ~/.bashrc
exec $SHELL
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
# Install a stable Ruby (adjust version over time with `rbenv install -l`)
rbenv install 3.3.4
rbenv global 3.3.4
# Gems: install Bundler + Jekyll
gem install bundler jekyll
rbenv rehash
jekyll -v
If you prefer the distro packages, Jekyll’s Ubuntu page outlines that route (handy on minimal boxes). I still recommend rbenv for clean upgrades.
Scaffold your Jekyll site and build it
Create a new site near your home, version-control it, and build to _site/
:
cd ~
jekyll new mysite
cd mysite
# local build
bundle exec jekyll build
You’ll find the generated files in ~/mysite/_site
. That’s what we’ll publish to /var/www/example.com/site
.
Wire up a simple deploy: publish _site
to Nginx’s web root
For a quick manual deploy, rsync the built site to your web root:
# build
cd ~/mysite
bundle exec jekyll build
# publish to web root
rsync -avh --delete _site/ /var/www/example.com/site/
Reload Nginx (not strictly required for pure static updates, but good habit):
sudo systemctl reload nginx
Visit http://example.com
—you should see your site.
Optional, but smart: GitHub Actions for push-to-deploy
Don’t SSH every time you post. Have CI build the site and push only the _site/
artifacts to your server with rsync over SSH.
- On the server, make a limited deploy key:
ssh-keygen -t ed25519 -C "jekyll-ci" -f ~/.ssh/jekyll_ci -N ""
cat ~/.ssh/jekyll_ci.pub >> ~/.ssh/authorized_keys
- Add the private key (
~/.ssh/jekyll_ci
) to your GitHub repo as an encrypted secretDEPLOY_KEY
. - In your repo, create
.github/workflows/deploy.yml
:
name: Build & Deploy Jekyll
on:
push:
branches: [ main ]
jobs:
build-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
- name: Build site
run: bundle exec jekyll build --trace
- name: Deploy via rsync
env:
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh -o StrictHostKeyChecking=no deploy@example.com 'mkdir -p /var/www/example.com/site'
rsync -avz --delete _site/ deploy@example.com:/var/www/example.com/site/
That’s it: push to main
, CI builds, rsyncs HTML/CSS/JS to your Droplet.
Turn on HTTPS with Let’s Encrypt (Certbot)
Once the site answers on HTTP, get a certificate and auto-renewal with Certbot.
sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
# This will obtain a cert and update your Nginx server block automatically.
sudo certbot --nginx -d example.com -d www.example.com
# Test renewal (dry run)
sudo certbot renew --dry-run
Production-grade tweaks you won’t regret
Set the site URL in Jekyll config. It affects canonical URLs, feeds, and sitemaps.
# in ~/mysite/_config.yml
url: "https://example.com"
Nginx cache headers. You already added a 7-day static cache. Consider 30d or 1y for hashed assets.
Directory ownership and permissions. Keep /var/www/example.com
owned by your deploy user and group www-data
. Nginx only needs read-access:
sudo chown -R deploy:www-data /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 755 {} \;
sudo find /var/www/example.com -type f -exec chmod 644 {} \;
UFW sanity. Ensure HTTP/HTTPS open, SSH allowed, everything else default-deny.
Keep Ruby manageable. When a new Ruby release ships, upgrade with:
rbenv install -l # see latest
rbenv install 3.3.x
rbenv global 3.3.x
gem install bundler jekyll
rbenv rehash
(Why rbenv? It keeps your runtime separate from the OS and easy to upgrade.)
Two alternative deployment patterns (pick your poison)
- Pure static hosting with Nginx (what we did). Build on CI or locally; publish the
_site
directory. It’s the simplest and most robust path for 99% of sites. (Nginx static-content serving doc backs this approach.) - Containerized build & serve. You can also bake the built site into a tiny container and deploy with a tool like Kamal or plain Docker—overkill for many, but neat for uniform infra.
Quick health checklist (so you don’t chase ghosts)
curl -I http://example.com
returns200 OK
and showsServer: nginx
.sudo nginx -t
is clean after every config change.sudo systemctl status nginx
isactive (running)
.https://example.com
loads with a valid certificate (lock icon), andsudo certbot renew --dry-run
passes.
Why this setup stands the test of time
- Static by design. No app server to babysit, just files. That’s as future-proof as it gets.
- Upgrades are painless. rbenv keeps Ruby modern without fighting the OS. Jekyll builds on CI keep production clean.
- Nginx is excellent at what it does. Serving static content is its home turf. Keep the config lean, and it’ll keep running.