How to deploy & self-host Jekyll on DigitalOcean

How to deploy & self-host Jekyll on DigitalOcean

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

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.

  1. 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
  1. Add the private key (~/.ssh/jekyll_ci) to your GitHub repo as an encrypted secret DEPLOY_KEY.
  2. 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 returns 200 OK and shows Server: nginx.
  • sudo nginx -t is clean after every config change.
  • sudo systemctl status nginx is active (running).
  • https://example.com loads with a valid certificate (lock icon), and sudo 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.

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 *