Build and Deploy a Static Site with Hugo and Caddy on a VPS

Build a fast static site with Hugo, ship the generated files to a VPS, and let Caddy serve the site with automatic HTTPS instead of turning every edit into a risky production shell adventure.

Hugo Caddy Static site
Illustrated guide cover for Build and Deploy a Static Site with Hugo and Caddy on a VPS
Hugo • Caddy • Static site deployment
What you learn

How to install Hugo, generate a site, copy the public build to a VPS, point Caddy at the right directory, and verify HTTPS cleanly.

Best for

Small brochure sites, docs portals, personal projects, and landing pages where a database and app runtime would be needless baggage.

Risk to watch

The common failure mode is editing generated files directly on the server and slowly losing track of what is source and what is build output.

Before you begin

  • A VPS you can reach over SSH.
  • A domain or subdomain already pointed at the VPS public IP.
  • Docker is optional here. This guide uses Hugo as a build tool and Caddy as the web server directly on the host.
  • A local workstation where you can edit files and run Hugo.

Hugo's official Linux docs still recommend the standard edition unless you specifically need extra features, and on Debian or Ubuntu the simplest package path is sudo apt install hugo. Caddy's static-file quick start is intentionally minimal: point a site block at the right root directory and turn on file_server.

Expected outcome: You will finish with a generated static site in a predictable directory like /var/www/example.com, served by Caddy over HTTPS with a repeatable deployment routine.

Step 1: Install Hugo and create a site

On a Debian or Ubuntu workstation or build box, install Hugo:

sudo apt update
sudo apt install -y hugo

Create a new project:

hugo new site mysite
cd mysite

Hugo separates source content from generated output. That distinction is the whole point of this workflow. You edit content and templates in the project. You deploy only the compiled files from public/.

If you want a theme, add it now using the theme's own install instructions before you start writing content.

Step 2: Build and preview locally

Add a first page or post, then preview the site locally:

hugo new content content/posts/hello-world.md
hugo server -D

Once the local preview looks right, generate the deployable output:

hugo

The generated files should now exist in public/. That is the directory you deploy. Do not point Caddy at the Hugo project root.

Warning: Keep generated output out of source-control debates unless your workflow truly requires it. The safer pattern is source in Git, generated files rebuilt on demand, and server deploys treated as artifacts.

Step 3: Prepare the VPS and site directory

On the VPS, create a dedicated web root:

sudo mkdir -p /var/www/example.com
sudo chown -R $USER:$USER /var/www/example.com

Install Caddy if it is not already present. The exact package path depends on your distro and how current you want the binary to be, but once Caddy is installed you only need a site root and a small Caddyfile.

Before you touch Caddy, confirm DNS already points at the VPS. Automatic HTTPS only works cleanly when the hostname resolves to the machine that is requesting the certificate.

Step 4: Deploy the generated files

From the Hugo project directory on your workstation, copy only the built output:

rsync -avz --delete public/ user@example.com:/var/www/example.com/

The trailing slash matters. It syncs the contents of public/ into the site root instead of nesting one extra public directory.

Verify the files landed where you expect on the VPS:

ssh user@example.com
find /var/www/example.com -maxdepth 2 -type f | head

If your build includes an index.html at the root of public/, you are in good shape for a first deploy.

Step 5: Configure Caddy and HTTPS

Create or update a Caddyfile entry on the VPS:

example.com {
  root * /var/www/example.com
  file_server
}

This follows Caddy's static-files pattern: set the site label, point root at the deployed directory, and enable file_server.

Validate and reload Caddy:

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

Then test in a browser or with curl:

curl -I https://example.com

On the first reload, Caddy may need a moment to finish certificate issuance. A short delay is normal. A long delay usually means DNS, firewall, or port 80/443 reachability is wrong.

Update and rollback notes

The clean update loop is: edit source, run hugo, sync public/, then reload Caddy only if configuration changed. Ordinary content-only updates do not require a Caddy reload.

A simple rollback pattern is to keep timestamped deploy directories and switch a symlink:

/var/www/releases/2026-07-11-1600
/var/www/releases/2026-07-12-0915
/var/www/example.com -> /var/www/releases/2026-07-12-0915

That is optional for a tiny site, but it becomes valuable once production updates matter.

Troubleshooting

The site works locally with hugo server but not on the VPS.
Check that you deployed the generated public/ files rather than the project source tree.

Caddy serves a directory listing or 404 instead of the homepage.
Confirm the root directory contains index.html at the top level and that the root path in the Caddyfile is correct.

HTTPS is not issuing.
Verify the domain resolves to the VPS and that inbound ports 80 and 443 are reachable from the public internet.

New deploys leave stale files behind.
Use rsync --delete so removed pages disappear from the server instead of lingering forever.