Here’s how to deploy and self-host a VuePress site on DigitalOcean without fuss. We’ll start from a blank Droplet, build the site, and serve it efficiently with Nginx. I’ll also add a quick production checklist at the end so you don’t cut corners.
You’ll need a DigitalOcean account to spin up a Droplet—obvious, but easy to forget when you’re in a hurry.
VuePress is a Vue-powered static site generator. You write in Markdown, VuePress generates pre-rendered HTML for each page, and once loaded in the browser it behaves like a SPA. In production, you serve plain static files. That means: no app server to baby-sit, minimal moving parts, and excellent performance.
What we’re building
- A small Ubuntu Droplet on DigitalOcean.
- Nginx serving your VuePress build (
.vuepress/dist
) directly. - Optional: Let’s Encrypt TLS.
- Git-based deploys (local build + rsync) or on-server build—your call.
Prerequisites
- A domain you control (point it to the Droplet later).
- A fresh Ubuntu 22.04 or 24.04 Droplet.
- SSH access as a sudo user.
If you’ve never installed Nginx on Ubuntu before, DigitalOcean’s docs are solid and current. I’ll fold the critical bits in below.
Create the Droplet and SSH in
Pick Ubuntu 22.04 or 24.04, give it a sensible hostname, and add your SSH key. From your machine:
ssh root@your_droplet_ip
adduser deploy
usermod -aG sudo deploy
rsync -avz ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
Reconnect as your non-root user:
ssh deploy@your_droplet_ip
Optional but wise—basic firewall:
sudo apt update
sudo apt install ufw -y
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status
Install Nginx
sudo apt update
sudo apt install -y nginx
sudo systemctl enable nginx
sudo systemctl status nginx
You should see the default Nginx welcome page at http://your_droplet_ip/
.
Create an A record for @
(and optionally www
) to your Droplet’s public IP. Once it propagates, you can swap server_name
to your domain.
Bootstrap your VuePress project (locally or on the server)
VuePress expects a source dir with a .vuepress
folder inside; the default build output is YOUR_SOURCE/.vuepress/dist
. You can build locally and upload the dist
directory, or build on the server. Either way, the toolchain is the same.
Option A — Build locally (recommended)
On your laptop/workstation:
# Create a new project directory
mkdir docs-site && cd docs-site
# Initialize package.json
npm init -y
# Install VuePress (v2) locally
npm install -D vuepress
# Scaffold basic docs
mkdir -p docs/.vuepress
printf '# Hello VuePress\n\nThis is my site.' > docs/README.md
# Add scripts
npm pkg set scripts.docs:dev="vuepress dev docs"
npm pkg set scripts.docs:build="vuepress build docs"
# Run dev server locally (optional)
npm run docs:dev
Build:
npm run docs:build
Your static site is now in docs/.vuepress/dist
. The CLI and default output behavior are documented here.
Option B — Build on the Droplet
If you prefer to build on the VPS, install Node LTS (via apt or NodeSource), clone your repo, and run the same scripts:
# On the server
sudo apt update
sudo apt install -y curl ca-certificates gnupg
# Install Node.js LTS quickly with NodeSource (example for 20.x)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs git
# Clone your project and build
git clone https://github.com/your/repo.git /var/www/docs-site
cd /var/www/docs-site
npm ci
npm run docs:build
VuePress’s own docs confirm the build command and output directory; we’re just doing it on the server.
Deploy the static files to the server
If you built locally, ship the dist
folder to the VPS. I like an rsync
push to a versioned release directory.
# On your local machine:
rsync -avz --delete docs/.vuepress/dist/ deploy@your_domain_or_ip:/var/www/docs-site/current/
Create the target path first:
# On the server
sudo mkdir -p /var/www/docs-site/current
sudo chown -R deploy:www-data /var/www/docs-site
sudo find /var/www/docs-site -type d -exec chmod 755 {} \;
sudo find /var/www/docs-site -type f -exec chmod 644 {} \;
Configure an Nginx server block for the site
Create a dedicated server block, enable it, and disable the default.
sudo nano /etc/nginx/sites-available/vuepress.conf
Paste this minimal, correct config:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/docs-site/current;
index index.html;
# Serve static files
location / {
try_files $uri $uri/ /index.html;
}
# Security & performance niceties
location ~* \.(?:jpg|jpeg|gif|png|svg|webp|ico|css|js|woff2?)$ {
access_log off;
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
}
Enable it:
sudo ln -s /etc/nginx/sites-available/vuepress.conf /etc/nginx/sites-enabled/vuepress.conf
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx
Why Nginx over a Node static server (like
pm2 serve
)? Nginx is purpose-built to serve static files fast, handle TLS, and scale with minimal memory. PM2’s static server is convenient for quick previews, but Nginx is the boring, reliable choice for production.
Add HTTPS (Let’s Encrypt)
If your DNS is pointed correctly, Certbot can fetch and install a certificate in minutes:
sudo apt update
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
Follow the prompts; Certbot will update the server block to redirect HTTP→HTTPS and install auto-renewal. DigitalOcean’s step-by-step Nginx guides link out to certbot usage if you want screenshots and options.
CI/CD (optional but worth it)
You have two clean deployment paths:
- A. Local build + rsync (simple): Keep building locally and push
dist
withrsync
. It’s fast and predictable. - B. GitHub Actions (hands-off): On push to
main
, build andrsync
thedist
folder to the Droplet over SSH.
Example GitHub Actions workflow:
name: Deploy VuePress
on:
push:
branches: [ "main" ]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install & Build
run: |
npm ci
npm run docs:build
- name: Deploy via rsync
uses: burnett01/rsync-deployments@6.0
with:
switches: -avzr --delete
path: docs/.vuepress/dist/
remote_path: /var/www/docs-site/current/
remote_host: ${{ secrets.DROPLET_HOST }}
remote_user: deploy
remote_key: ${{ secrets.DEPLOY_KEY }}
VuePress’s deployment docs mostly focus on third-party platforms, but the key assumption is always the same: ship .vuepress/dist
to where your web server can serve it. We’re doing exactly that with Nginx.
Common gotchas (and quick fixes)
- Blank page or 404s on nested routes: Your
try_files
rule must fallback to/index.html
to support SPA-style routing. See the Nginx block above. Serving thedist
directory directly is the correct approach for VuePress. - Wrong output path: By default, VuePress puts the build at
YOUR_SOURCE/.vuepress/dist
. Don’t point Nginx at the source directory; point it at the builtdist
. - Slow first byte / missing compression: Enable gzip (or brotli if you’re comfortable) in Nginx, and set long-cache headers for hashed assets (we did).
- Tempted to run
pm2 serve
forever: It works, but you’re reinventing a web server. Use Nginx in front; it’s simpler and sturdier. PM2’s own docs call it a convenience tool.
Production checklist (don’t skip this)
[ ] DNS points to Droplet; both root and www work
[ ] Nginx serves /var/www/docs-site/current correctly
[ ] HTTP→HTTPS redirect in place (certbot did it)
[ ] Cache headers set for static assets
[ ] 404 page exists in dist (optional but nice)
[ ] Automated deploy (CI or a simple script) documented in README
[ ] Regular OS patching (unattended-upgrades) enabled
Quick “all-in-one” commands you’ll use often
Rebuild locally and deploy:
npm run docs:build
rsync -avz --delete docs/.vuepress/dist/ deploy@example.com:/var/www/docs-site/current/
Swap releases safely (optional):
# On the server
mkdir -p /var/www/docs-site/releases
cp -a /var/www/docs-site/current /var/www/docs-site/releases/$(date +%F-%H%M%S)
rsync -avz --delete ~/upload/dist/ /var/www/docs-site/current/
sudo systemctl reload nginx
Renew certificates (cron handles this, but manual is fine):
sudo certbot renew --dry-run
Recap
You built your VuePress docs, shipped the static dist
to /var/www/docs-site/current
, and let Nginx do the serving. That’s the right, boring architecture for a static site: fast, cacheable, cheap to run, and easy to automate. VuePress’s defaults (and docs) align cleanly with Nginx on a plain Ubuntu Droplet, which is exactly what we want for long-term maintainability.