You’ve got a Svelte or SvelteKit app ready to go. Now you want it live on DigitalOcean without yak-shaving. Here’s the practical path, based on what actually works — and what keeps working.
We’ll cover the two big deployment shapes:
- Static site (pure Svelte or SvelteKit with
adapter-static
): host it as a Static Site on DigitalOcean App Platform (free tier available). - Server-rendered (SSR) or API (SvelteKit with
adapter-node
): run it as a Node service on App Platform or on a Droplet behind Nginx + HTTPS.
Along the way, I’ll call out gotchas and why a step matters, so when something inevitably changes, you’ll still know what to tweak.
Quick decision guide
- Your app is static (no SSR, no server API)? Use SvelteKit
adapter-static
→ App Platform Static Site. It’s simple, globally cached, and on the free tier you can host up to 3 static sites per account with automatic HTTPS and CDN. - You need SSR, endpoints, auth callbacks, or websockets? Use SvelteKit
adapter-node
→ run as a web service on App Platform or a Droplet with PM2 + Nginx.
Path A — Static Svelte/SvelteKit on App Platform (free, fast, dead-simple)
1) Prepare your project
For classic Svelte (Vite) you already output dist/
. For SvelteKit, switch to the static adapter.
npm i -D @sveltejs/adapter-static
// svelte.config.js
import adapter from '@sveltejs/adapter-static';
export default {
kit: {
adapter: adapter({
// optionally: fallback: '200.html' for SPA-style routing
})
}
};
Build locally to confirm it emits build/
(SvelteKit) or dist/
(Vite Svelte):
npm run build
SvelteKit’s adapter-static
prerenders your site into static files; by default it outputs to build/
. If a page can’t be prerendered, fix routing or set a SPA fallback.
2) Push to GitHub/GitLab
App Platform deploys straight from your repo. Keep node
pinned in package.json
so builds are predictable:
{
"engines": { "node": "22.x" }
}
(DO’s Node buildpack respects the engines.node
field; current default LTS on the platform tracks Node 22.)
3) Create an App Platform → Static Site
During creation, point DO to your repo and select Static Site. Set:
- Build command (examples)
SvelteKit:npm ci && npm run build
Svelte (Vite):npm ci && npm run build
- Output directory
- SvelteKit
adapter-static
:build
- Svelte (Vite):
dist
- SvelteKit
You can set or change the output directory in the component settings (Output Directory) if DO doesn’t auto-detect.
Environment variables at build time (e.g., VITE_*
for public env): add them in App Settings → Environment Variables and set scope: BUILD_TIME
when you need them during the build.
That’s it. App Platform wires up HTTPS, CDN, and deploy-on-push. The free tier covers up to 3 static apps per account with 1 GiB egress/mo each.
Why this holds up over time: You’re not tied to any Svelte-specific hosting gimmick; you’re just serving static files behind DO’s CDN with automatic TLS.
Path B — SvelteKit SSR on App Platform (Node service)
When you need server rendering, endpoints, or sockets, use adapter-node
. It generates a Node server you run with node build
.
1) Switch to Node adapter
npm i -D @sveltejs/adapter-node
// svelte.config.js
import adapter from '@sveltejs/adapter-node';
export default {
kit: {
adapter: adapter({
// out: 'build', // default is 'build'
// precompress: true // optional: generate .br/.gz
})
}
};
npm run build
This produces build/index.js
(entry) and build/handler.js
(if you want to embed in your own server). For production you run node build
. Don’t use vite preview
in prod — it’s not the real server.
2) App Platform → Web Service
Create a new Web Service from your repo. Set:
- Build command
npm ci && npm run build
- Run command
node build
- Environment
- Pin Node in
package.json
engines.node
(same advice as above). - Add your secrets (JWT keys, API keys) as runtime env vars.
- Pin Node in
App Platform handles HTTPS, scaling, rolling deploys, and logs; you don’t need to manage the OS. If you need build- or run-command changes later, edit them in Settings → Commands.
Gotcha to avoid: If your build or Tailwind isn’t picked up, ensure the build command is correct and that any build-time env is defined with BUILD_TIME
scope. Misplaced envs are the #1 cause of “works locally, fails on App Platform.”
Path C — SvelteKit SSR on a Droplet (DIY, full control)
If you want root control or to squeeze costs, deploy on a small Ubuntu Droplet. The hardened pattern hasn’t changed in years:
- Provision Droplet (Ubuntu LTS), SSH in, update packages.
- Install Node LTS, PM2 to supervise the app.
- Build your app and run
node build
under PM2. - Nginx as a reverse proxy on
:80/:443
→ your Node app:3000
. - Let’s Encrypt via Certbot for TLS.
Example commands (condensed so you can paste and go):
# On the Droplet (Ubuntu)
sudo apt update && sudo apt -y upgrade
# Node (use NodeSource or nvm; here’s NodeSource 22.x):
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
# PM2
sudo npm i -g pm2
# App directory
sudo mkdir -p /var/www/myapp && sudo chown $USER:$USER /var/www/myapp
cd /var/www/myapp
git clone https://github.com/you/your-repo.git .
npm ci
npm run build
# Start with PM2 (SvelteKit adapter-node)
pm2 start "node build" --name sveltekit
pm2 save
pm2 startup systemd -u $USER --hp $HOME
# Nginx + Certbot
sudo apt install -y nginx
sudo ufw allow 'Nginx Full'
# /etc/nginx/sites-available/myapp
# (proxy to Node on 127.0.0.1:3000)
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/myapp
sudo nginx -t && sudo systemctl reload nginx
# HTTPS
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
If you’ve done any Node deployment before, this feels familiar for a reason — it’s the same proven Nginx reverse proxy pattern.
CI/CD options you’ll actually use (and keep)
- App Platform auto-deploy on push: turn it on in the app’s Settings; it rebuilds from your repo.
- Spec-as-code: define the app in app spec YAML and apply it via
doctl
/API/Terraform/Pulumi. This keeps environment, build/run commands, and components versioned.
Minimal static site spec (YAML) to illustrate:
name: my-svelte-static
services: []
static_sites:
- name: web
github:
repo: yourorg/yourrepo
branch: main
deploy_on_push: true
build_command: "npm ci && npm run build"
output_dir: "build"
envs:
- key: PUBLIC_API_BASE
value: "https://api.example.com"
scope: BUILD_TIME
App Spec fields and the BUILD_TIME
scope are documented and stable. If DigitalOcean changes defaults, your spec still captures what you intend.
Production-proof settings and fixes (learned the hard way)
- Pin Node in
package.json
engines.node
. App Platform updates defaults over time (today: Node 22). Pinning avoids surprise build breaks. - Don’t run
vite preview
in prod. Withadapter-node
, production isnode build
. - Static output dir:
- SvelteKit
adapter-static
:build/
- Svelte (Vite):
dist/
If App Platform guesses wrong, set Output Directory explicitly.
- SvelteKit
- Build-time env vs runtime env: if a value must be embedded into the JS at build (like
PUBLIC_*
), mark itBUILD_TIME
. Otherwise keep secrets runtime-only. - Nginx reverse proxy: on Droplets, keep Node private (
127.0.0.1:3000
). Nginx terminates TLS and forwards. This is the supported, durable pattern.
Bonus: Docker the Node build (works on App Platform & Droplets)
When you want total reproducibility, ship a container. App Platform can deploy a Dockerfile as a web service.
# Dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/build ./build
EXPOSE 3000
CMD ["node", "build"]
Push to GHCR/Docker Hub, point App Platform at the container registry, and you’re done. (Same run command, same ports.)
What not to do (saves you time)
- Don’t host full static sites directly from Spaces — it’s for object storage/CDN, not a turnkey static site host. Use App Platform Static Sites and pull assets from Spaces if you must.
- Don’t rely on preview servers or dev servers in production. They behave differently and aren’t meant for it.
Wrap-up
The durable playbook hasn’t changed: static → App Platform Static Site; SSR/API → Node adapter, either App Platform Web Service or Droplet with Nginx+PM2. Pin Node, separate build-time vs runtime env, and you’ll avoid 90% of deployment facepalms.