Vue is easy to deploy if you pick the right path for your app. On DigitalOcean you’ve got three solid options, each with different trade-offs:
- App Platform (PaaS): push your repo, DO builds the site, auto CDN/SSL. Fastest path for static Vue (Vite) builds.
- Droplet (VM) + Nginx: full control, great for simple static hosting or as a reverse proxy in front of Node/SSR.
- Docker (on a Droplet or App Platform): reproducible builds; handy when you want an image pipeline.
Below I’ll walk through each option. I’ll also call out the classic Vue router “history mode” pitfall, environment variables, and caching quirks so you don’t trip later.
Most Vue apps today are built with Vite and ship static assets (HTML/CSS/JS) in the dist/
folder. That’s perfect for static hosting or a CDN. If you’re using Nuxt or your own server-side rendering (SSR) with Node, treat it like a Node app behind Nginx/PM2 instead of “static files only.” Vue Router’s history mode needs a server fallback to index.html
. The official docs literally say: add a catch-all that serves your SPA when the URL doesn’t match a real file.
Option A — DigitalOcean App Platform (static Vue via Vite)
Why: zero-maintenance deploys, global CDN, automatic SSL, and quick rollbacks. Great default for SPAs.
1) Prepare the project
From your Vue project root:
npm ci
npm run build
That should create dist/
(Vite’s default). Vite’s “static deploy” guidance expects exactly this.
2) Connect the repo & set build/output
In the DO dashboard → Create App, connect GitHub/GitLab and select your repo/branch. For a Static Site component set:
- Build command:
npm ci && npm run build
- Output directory:
dist
You can also do this by spec/CLI if you prefer infra-as-config.
3) Fix SPA routing (history mode)
Two ways to make deep links work (no 404 on refresh):
- URL Rewrites: add a rewrite that sends
/*
to/index.html
with status 200. - Or Catch-all document / “Custom Pages” setting with
index.html
.
Both accomplish the same SPA fallback.
4) Environment variables (build vs run)
If your Vue build needs env at build time (e.g., import.meta.env.VITE_API_URL
), mark those as BUILD_TIME in App settings/spec. Runtime variables are RUN_TIME—but note a purely static SPA can’t “read” new server env after build unless you inject them some other way.
Dockerfile builds are stricter: build-time env must be passed as
--build-arg
/ARG
. Don’t assume normal envs are available duringdocker build
.
5) Caching & CDN behavior
Static sites on App Platform are automatically cached at the edge (24h at CDN, ~10s in browsers by default). Redeploys invalidate the cache. Know this if you’re debugging “why didn’t my change show up instantly?”
You can’t fully disable the static CDN cache on the starter domain; custom domain is required for certain cache controls.
That’s it—click Deploy. If you want to automate, store a spec and run doctl apps update … --spec your-app.yaml
.
Option B — Droplet + Nginx (host static dist/
like a brick-house)
Why: maximum control, minimal moving parts. You pay with a bit of ops work, but it’s classic, stable, fast.
1) Provision an Ubuntu Droplet & install Nginx
SSH in, then:
sudo apt update
sudo apt install -y nginx
Use any current Ubuntu (22.04/24.04) guide; process is the same.
2) Build locally and upload dist/
npm ci
npm run build
scp -r dist/ root@your_server:/var/www/your-app
3) Nginx server block with SPA fallback
Create /etc/nginx/sites-available/your-app
:
server {
server_name yourdomain.com www.yourdomain.com;
root /var/www/your-app;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(?:css|js|jpg|jpeg|gif|png|svg|ico|webp|woff2?)$ {
access_log off;
expires 7d;
add_header Cache-Control "public, max-age=604800";
try_files $uri =404;
}
}
That try_files
line is the history-mode fix—serve index.html
whenever the path isn’t a real file. (router.vuejs.org)
Enable and reload:
ln -s /etc/nginx/sites-available/your-app /etc/nginx/sites-enabled/your-app
nginx -t
systemctl reload nginx
4) HTTPS with Let’s Encrypt
(See our full guide on Let’s Encrypt here.)
apt install -y certbot python3-certbot-nginx
certbot --nginx -d yourdomain.com -d www.yourdomain.com
Certbot will write SSL blocks, set up renewals, and can add HTTP→HTTPS redirects. Use the current Ubuntu/Nginx + Certbot guide if you want the long form.
Optional: turn on HSTS once you’ve confirmed HTTPS everywhere.
Option C — Docker (Nginx image serving your built Vue)
Why: you want a repeatable image and easy CI. Works great on a Droplet or as an App Platform container.
1) Dockerfile (multi-stage build)
# build stage
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# serve stage
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
# SPA history-mode fallback
RUN printf "server {\n listen 80;\n server_name _;\n root /usr/share/nginx/html;\n location / {\n try_files \$uri \$uri/ /index.html;\n }\n}\n" > /etc/nginx/conf.d/default.conf
Build and run locally:
docker build -t your-vue-app .
docker run -p 8080:80 your-vue-app
For server install/use, follow the standard Docker-on-Ubuntu steps.
If you deploy this on App Platform as a Docker component and need build-time values, pass them as
--build-arg
/ARG
. Don’t rely on plain env at build in Docker mode.
SSR note (Nuxt or custom Node server)
If you’re not shipping static dist
(because you need SSR, auth cookies at render time, etc.), treat the app like any Node service: run it with PM2 and proxy through Nginx. There’s a canonical production pattern (install Node, run your server, PM2 keeps it alive, Nginx terminates TLS and reverse-proxies).
A typical Nginx proxy block looks like this:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
}
And PM2:
npm ci
npm run build
pm2 start "npm run start" --name my-vue-ssr
pm2 save
pm2 startup
Then add HTTPS with Certbot as shown earlier.
Common gotchas (and how to not step in them)
“History mode 404s on refresh.” You forgot the fallback. On App Platform: add a rewrite or catch-all. On Nginx: try_files $uri $uri/ /index.html;
. On any host: same idea.
“My .env didn’t show up in build.” Distinguish build-time vs run-time env; Vite inlines VITE_*
at build. App Platform scopes are explicit; Dockerfile builds need --build-arg
.
“Changes don’t show instantly on App Platform.” The edge caches static files (24h). Redeploy invalidates, but browser caches can linger briefly. Plan cache headers or versioned assets.
“I want more CDN control.” App Platform’s static CDN defaults are opinionated; you can’t fully disable on starter domains. Use a custom domain (and, if needed, serve via a service component or your own Droplet+Nginx) for fine-grained cache-control.
Copy-paste snippets you’ll actually use
Vite production build (package.json):
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview --port 4173"
}
}
App Platform URL rewrite (conceptual):/* -> /index.html (200)
(configure in App settings → Rewrites/Redirects).
Minimal Nginx static SPA block:
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
root /var/www/your-app;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
Certbot one-liner for Nginx:
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Multi-stage Dockerfile (build + serve static):
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
RUN printf "server {\n listen 80;\n server_name _;\n root /usr/share/nginx/html;\n location / {\n try_files \$uri \$uri/ /index.html;\n }\n}\n" > /etc/nginx/conf.d/default.conf
If you follow Option A, you’ll be live in minutes, with the platform handling CDN and SSL for you. If you go Option B/C, you get iron-grip control—rock-solid and predictable. Either way, the evergreen bits don’t change: build to dist
, serve with an SPA fallback, secure with TLS, and be deliberate about env and caching. That’s the whole game.