Skip to content

Deploy Astro

Astro builds to static files by default, so a stock Astro project needs no changes at all. Connect the repository in the Quick Start and it deploys.

This page covers the settings that do need your attention: the ones Astro gets from its config rather than from us, and, if some pages must render per request, the deploybase adapter that runs them.

Setting Value
Build command npm run build
Output directory dist
Image node:24-alpine

Detection is automatic when your repository has an astro.config.* file. Every value above is editable per project, and the full defaults table covers the other frameworks.

Astro needs the production URL to generate absolute links, and it has no way to learn it from us:

astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
site: 'https://example.com',
});

Leave it out and Astro.site is undefined, which silently breaks canonical tags, RSS feeds and @astrojs/sitemap. The pages still build, so nothing fails loudly. Set it to your custom domain if you have one, otherwise to your .sites.deploybase.eu URL.

An adapter written for another host, such as @astrojs/node, @astrojs/vercel or @astrojs/netlify, produces output deploybase cannot run. A leftover one either fails the build or produces a dist/ full of server entrypoints that publishes as an empty site.

  • If every page can be built ahead of time, delete the adapter line and output: 'server', and uninstall the package.
  • If some pages must render per request, swap it for @deploybase/adapter-astro, as the next section shows.

@deploybase/adapter-astro runs the parts of your site that render per request, on a deploybase function in Paris (Node 22): on-demand pages, API routes, actions and server islands. Prerendered pages and assets are still served from the CDN.

Two conditions apply before you start:

  • Your team needs HTTP functions, which come with the Starter, Pro and Scale plans. On Free, the build still succeeds and the site goes live as a static site: the prerendered pages work, the on-demand routes answer 404, and the build log says why:

    ⚠️ This build includes server-rendered routes, which need a paid plan. Only the static pages were deployed; server routes will answer 404

    After upgrading, deploy again: the server is set up only when a deployment is built.

  • On-demand routes render on every address of the site: the project’s deploybase address, branch previews and custom domains, so https://example-site.sites.deploybase.eu, https://staging-example-site.previews.deploybase.eu and https://example.com all work.

Terminal window
npm install @deploybase/adapter-astro
astro.config.mjs
import { defineConfig } from 'astro/config';
import deploybase from '@deploybase/adapter-astro';
export default defineConfig({
output: 'server', // every page renders on demand unless it opts into prerendering
adapter: deploybase(),
});

Keep the project’s build defaults: build command npm run build, output directory dist. Nothing else needs setting on deploybase.

Pick the default that matches most of your site, and mark the exceptions:

  • Mostly dynamic: output: 'server', and export const prerender = true on the pages that can be built ahead of time.
  • Mostly static: leave output at its default 'static', and put export const prerender = false on the pages that must render on demand.

Prerender src/pages/404.astro with export const prerender = true. An on-demand 404 page means every missing path wakes the server just to say “not found”. The prerendered 404 and 500 pages are also bundled with the server, so it can answer with them straight away.

If nothing renders on demand (no on-demand pages, no actions, no server islands), the adapter writes a plain static site and no server is deployed at all.

Variables with Runtime ticked on the project’s Environment page reach your server through getSecret() from astro:env/server, and through process.env:

src/pages/api/quote.ts
import { getSecret } from 'astro:env/server';
export const prerender = false;
export async function GET() {
const key = getSecret('QUOTES_API_KEY');
// ...
}

A production deployment gets the production values and a branch preview the preview values, each captured when the deployment is built. To change what a running server sees, redeploy after saving the variable. Rolling back to an older deployment brings back the values that deployment was built with.

astro build writes everything deploybase needs into dist/.deploybase/v1/:

Path What it is
config.json Which paths the server answers, and the 404 page
static/ Client assets, prerendered pages and public/. Published to the CDN
server/ The server, bundled into self-contained files with entry.mjs as its entry. Never public

If you change Astro’s outDir, change the project’s output directory to match.

  1. A request for a file that exists in static/ is served from the CDN. A static file always wins.
  2. A GET or HEAD that matches no file goes to your server only when its path matches one of your on-demand routes. Anything else gets the 404 page straight from the CDN, without starting the server.
  3. Every other method (POST, PUT, PATCH, DELETE, OPTIONS) goes to the server, on any path.

The adapter writes one route pattern per on-demand route, plus Astro’s own /_actions/, /_server-islands/ and /_image. A src/pages/[...slug].astro that renders on demand sends every missing path to the server.

deploybase({
external: ['example-optional-telemetry'],
});

Packages listed in external are left out of the server bundle and are not installed on deploybase, so list only one whose import is optional or lazy. sharp is always left out.

What does not work with on-demand rendering yet

Section titled “What does not work with on-demand rendering yet”
  • Promoting a preview. A preview deployment’s server was built with the preview variables, so promoting it to production is refused with 409. Deploy the commit on the production branch instead. Rolling back to an older production deployment works.
  • Cold starts. The server scales to zero when it goes quiet, sooner on a branch preview than in production, and the first request after that waits several seconds. Prerender every page that can be built ahead of time. The functions reference has the idle windows.
  • 50 second deadline. A response must start within 50 seconds, and a streamed response must also finish within that time.
  • Request body size. A body over the HTTPS request limit is refused with 413.
  • No WebSockets. Server-sent events and streamed responses work.
  • HEAD is rendered as GET. The page or endpoint runs in full and the body is dropped.
  • No edge caching of server responses. Every on-demand response is sent with Cache-Control: private, no-store, whatever your code sets. Prerender what can be cached.
  • No native modules. The server is bundled into plain JavaScript and uploaded without node_modules, so a package with a native binary cannot run. The build fails if the bundle still imports anything from node_modules, and names the package.
  • No on-demand image transformation. Images are optimised with sharp at build time as usual, but /_image does not work at request time. Use <Image> and <Picture> on prerendered pages, or images that need no transforming.
  • Sessions need a remote driver. Instances are short-lived and share no disk, so the build fails if session uses the filesystem or memory driver, or has none. Use a Redis, Upstash or database driver from sessionDrivers in astro/config.
  • i18n.domains is not supported.
  • Up to 200 on-demand route patterns. Past that, every missing path is sent to the server instead, and the build log says so.

Environment variables with Build ticked are injected during the build, so they are available to astro.config.mjs, to content loaders, and to anything running at build time. Astro then exposes only the PUBLIC_-prefixed ones to client code, which is Astro’s rule rather than ours.

const key = import.meta.env.PUBLIC_ANALYTICS_ID; // in the browser bundle
const secret = import.meta.env.API_SECRET; // build time only

A PUBLIC_ variable is baked into the shipped JavaScript, so treat it as published. Anything you would not paste into a public repository does not belong in one.

The site deploys but every page is blank, or assets 404. Your outDir is not dist. Astro writes wherever the config says, and we upload the directory the project is configured with. Match them: either remove outDir from astro.config.mjs, or set the project’s output directory to the same path.

Links and images work locally but 404 in production. A base set in astro.config.mjs prefixes every generated URL. You need it only when the site is served from a subpath, which is not the case on a deploybase domain. Remove it.

Content collections fail on a schema error. Collections are validated at build time, so a frontmatter field that does not match the Zod schema fails the whole build rather than one page. The build log names the file and the field.

The build cannot reach an API. Builds have outbound HTTPS, but private networks and cloud metadata endpoints are blocked, so a loader that fetches from an internal host has no route to it. Publish the data or fetch it at runtime from the browser instead.

Live a8b7eb1d built