How to deploy a Svelte application on DigitalOcean

How to deploy a Svelte application on DigitalOcean

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

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-staticApp 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

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.

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:

  1. Provision Droplet (Ubuntu LTS), SSH in, update packages.
  2. Install Node LTS, PM2 to supervise the app.
  3. Build your app and run node build under PM2.
  4. Nginx as a reverse proxy on :80/:443 → your Node app :3000.
  5. 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. With adapter-node, production is node build.
  • Static output dir:
    • SvelteKit adapter-static: build/
    • Svelte (Vite): dist/
      If App Platform guesses wrong, set Output Directory explicitly.
  • Build-time env vs runtime env: if a value must be embedded into the JS at build (like PUBLIC_*), mark it BUILD_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.

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 *