How to deploy an Astro application on DigitalOcean

How to deploy an Astro application on DigitalOcean

It's the framework of choice for fast and snappy sites.

Astro is a modern web framework that ships less JavaScript by default and lets you mix UI islands from React, Svelte, Vue, and others. It excels at static sites, but also supports server-side rendering (SSR) for dynamic routes. In this guide, you’ll deploy Astro on DigitalOcean in two common ways: as a static site on App Platform (fastest path) and as an SSR Node.js service on App Platform (for dynamic features). A third section shows how to serve a static build from a Droplet with Nginx if you prefer full control. You’ll need a DigitalOcean account and a GitHub/GitLab repo for the smoothest flow; we’ll assume you have both ready.

Astro’s default output is a static site in dist/, which App Platform can publish directly behind its global CDN. If you enable SSR with the official Node adapter, App Platform runs your built Node server and exposes PORT automatically. App Platform’s Node runtime now defaults to Node 22.x (pin it in package.json for reproducible builds), so you can rely on current language features.

1) Create (or upgrade) your Astro project

If you’re starting fresh, generate the latest Astro:

# Create a new project in ./my-astro
npm create astro@latest my-astro
cd my-astro
npm install
npm run dev

For an existing project, update to the latest Astro and integrations:

npm i astro@latest @astrojs/node@latest

Astro’s static build goes to dist/ by default; SSR requires adding the Node adapter (next section).

2) Fast path: deploy a static Astro site on App Platform

This path is ideal for marketing sites, blogs, docs, and any project that doesn’t require per-request server logic.

Prepare package.json

Pin a current Node.js engine to match App Platform’s default (Node 22). This prevents surprises during upgrades:

{
  "name": "my-astro",
  "private": true,
  "engines": { "node": "22.x" },
  "scripts": {
    "dev": "astro dev",
    "build": "astro build",
    "preview": "astro preview"
  }
}

Push your code

Commit and push to your repo:

git add -A
git commit -m "Initial Astro site"
git branch -M main
git remote add origin <your-repo-url>
git push -u origin main

Create the App on DigitalOcean

  1. In the DigitalOcean dashboard, create a new App and select your repo/branch.
  2. When prompted, choose Static Site for the component that contains your Astro project.
  3. Build command: npm run build
  4. Output directory: dist (App Platform also auto-detects dist for static sites).
  5. Deploy. Subsequent pushes to main will auto-deploy.

If you prefer configuration-as-code, you can supply an App Spec file. App Platform recognizes dist as the output directory for static sites and uses buildpacks to run your build.

Optional: app spec for a static site (.do/app.yaml)

name: my-astro-static
static_sites:
  - name: web
    github:
      repo: yourname/yourrepo
      branch: main
      deploy_on_push: true
    build_command: "npm ci && npm run build"
    output_dir: "dist"
    envs:
      - key: NODE_ENV
        value: "production"

Why this works: Astro’s static build emits plain HTML/CSS/JS to dist/. App Platform serves this via its CDN edge, so you get fast global delivery with minimal configuration.

3) Dynamic path: deploy Astro with SSR on App Platform (Node)

Use this when you need server actions, sessions, or on-demand rendering.

Add and configure the Node adapter

# Add the official Node adapter
npx astro add @astrojs/node

Update astro.config.mjs to use the adapter in SSR mode:

import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  output: 'server', // enables SSR output
  adapter: node({
    mode: 'standalone' // bundles deps for simpler deploys
  })
});

The Node adapter builds a server entry that listens on process.env.PORT, which App Platform injects for you. After npm run build, you’ll start the server with Node.

Update package.json scripts and engines

{
  "name": "my-astro-ssr",
  "private": true,
  "engines": { "node": "22.x" },
  "scripts": {
    "dev": "astro dev",
    "build": "astro build",
    "start": "node ./dist/server/entry.mjs"
  }
}

App Platform configuration (service component)

When you create the app from your repo:

  • Choose Web Service (not Static Site).
  • Build command: npm ci && npm run build
  • Run command: npm run start
  • HTTP Port: leave default; App Platform sets PORT and routes traffic.

Optional: app spec for Node SSR

name: my-astro-ssr
services:
  - name: web
    github:
      repo: yourname/yourrepo
      branch: main
      deploy_on_push: true
    environment_slug: "node-js"
    instance_size_slug: "basic-xxs"
    http_port: 8080
    routes:
      - path: /
    build_command: "npm ci && npm run build"
    run_command: "npm run start"
    envs:
      - key: NODE_ENV
        value: "production"
      - key: PORT
        value: "8080"

Why this works: App Platform uses a Node buildpack and runs your server with the PORT you define. Pinning Node 22 ensures parity with the platform’s default and avoids runtime mismatches.

4) Alternative: serve a static Astro build from a Droplet with Nginx

If you prefer your own VM, build locally and copy dist/ to the server, then serve with Nginx.

Provision and secure your Droplet

Create an Ubuntu 22.04+ Droplet, add SSH keys, and harden it per standard practice. Install Nginx:

sudo apt update
sudo apt install -y nginx
sudo systemctl enable --now nginx

Our in-house guide covers managing the service and server blocks.

Build locally and deploy artifacts

npm ci
npm run build
rsync -avz --delete dist/ user@your_server:/var/www/my-astro/

Minimal Nginx server block

server {
    listen 80;
    server_name dropletdrift.com www.dropletdrift.com;

    root /var/www/my-astro;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    # Basic static asset caching
    location ~* \.(?:css|js|mjs|png|jpg|jpeg|gif|svg|ico|webp|woff2?)$ {
        expires 7d;
        add_header Cache-Control "public, max-age=604800";
        try_files $uri =404;
    }

    # Optional: enable gzip (or brotli if you add the module)
    gzip on;
    gzip_types text/css application/javascript application/json image/svg+xml;
}

Reload Nginx:

sudo nginx -t
sudo systemctl reload nginx

Deeper performance tuning (gzip, cache) and config structure is entirely up to you; the pattern above is a safe default for static sites.

5) Environment variables, secrets, and previews

  • Env vars/secrets: In App Platform, add them on the component’s Environment Variables tab (or in your app spec). Variables marked secret are encrypted.
  • Preview deployments: Enable “Deploy to preview” on pull requests to test changes before merging to main.
  • CDN/edge: Static sites automatically sit behind App Platform’s CDN; web services can use edge controls and cache settings as needed.

6) CI/CD hygiene for smooth deploys

  • Use npm ci in build commands for reproducible installs.
  • Pin Node in engines and commit your lockfile (package-lock.json, pnpm-lock.yaml, or yarn.lock). App Platform prefers a lockfile and detects the package manager accordingly.

Example build command (both static and SSR):

npm ci && npm run build

7) Troubleshooting checklist

  • Build succeeds locally but fails on App Platform: Ensure the same Node version (pin 22.x) and use the same package manager/lockfile in CI. App Platform’s Node buildpack supports Node 22 and uses it by default for new apps; mismatches cause subtle errors.
  • White screen on static deploy: Verify Output directory is dist and your build actually produced files there. App Platform auto-detects dist, but an override can point it elsewhere accidentally.
  • SSR app binds to wrong port: Your start script must run the Node entry (node ./dist/server/entry.mjs) and bind to process.env.PORT. Don’t hardcode ports.
  • Slow first request on SSR: Warm up the app post-deploy or add simple health probes that hit critical routes.
  • Static assets 404s on a Droplet: Double-check your Nginx root and try_files directives; try_files $uri $uri/ /index.html; ensures client-side routing works for SPA-like islands. DigitalOcean’s Nginx guides show the file structure and context rules.

8) Sample repositories and commands to copy-paste

Static site: minimal package.json

{
  "name": "astro-static",
  "private": true,
  "engines": { "node": "22.x" },
  "scripts": {
    "build": "astro build",
    "dev": "astro dev",
    "preview": "astro preview"
  },
  "dependencies": {
    "astro": "^4.0.0"
  }
}

SSR site: minimal package.json

{
  "name": "astro-ssr",
  "private": true,
  "engines": { "node": "22.x" },
  "scripts": {
    "build": "astro build",
    "start": "node ./dist/server/entry.mjs",
    "dev": "astro dev"
  },
  "dependencies": {
    "astro": "^4.0.0",
    "@astrojs/node": "^9.0.0"
  }
}

Tip: the exact semver ranges for Astro and the Node adapter can be bumped as new majors release; the structure stays the same.

9) Choosing the right path

  • Pick Static on App Platform if your site is content-heavy and mostly static. You’ll get a CDN out of the box, the simplest deploys, and minimal moving parts.
  • Pick SSR on App Platform if you need sessions, dynamic routes, or server actions. You’ll trade a bit of complexity for flexibility while staying fully managed.
  • Pick a Droplet + Nginx if you want root access, custom modules, or non-standard networking. You’ll own updates, security patches, and scale behaviors.

Either way, you can start static and move to SSR later without rewriting your entire site, because Astro treats SSR as an adapter choice rather than a separate framework. From here, add your domain, set up HTTPS in App Platform or with Nginx on your Droplet, and push a change to watch your pipeline deliver continuously.

Was this helpful?

Thanks for your feedback!

Leave a comment

Your email address will not be published. Required fields are marked *