Back to Blog
Tutorial6 min read2026-07-13

How to Deploy SolidStart to Production (with PostgreSQL)

Ship a SolidStart app: the node-server preset, the self-contained .output build, VITE_ env var rules, and Drizzle migrations done safely.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

SolidStart is the full-stack framework for SolidJS, and it inherits an interesting deployment property from its server engine: the production build is genuinely self-contained. No node_modules in the final image, no npm install at runtime. That makes for tiny containers — but there are a few sharp edges around presets, environment variables, and where migrations run. Here's the full path from npm run build to a live URL with a real database behind it.

How SolidStart builds

A new project comes from the official scaffolder:

npm create solid@latest my-app

Pick a SolidStart template and you get the standard three scripts in package.jsondev, build, and start. SolidStart 1.x builds through Vinxi, which orchestrates Vite for bundling and Nitro for the server output (the project has been consolidating toward plain Vite, but the shape of the deployable output described here is the Nitro one, and that's what you target in production).

The part that matters for deployment is the server preset. Nitro can emit output for a dozen targets — serverless functions, edge runtimes, plain Node. For a container platform you want the plain Node server, and it's worth pinning explicitly in app.config.ts rather than relying on defaults:

import { defineConfig } from '@solidjs/start/config';

export default defineConfig({
  server: {
    preset: 'node-server'
  }
});

Now build:

npm run build

The output lands in .output/:

  • .output/server/index.mjs — the entire server, with its dependencies bundled alongside it
  • .output/public/ — static assets, precompressed and fingerprinted

And this is the nice part — you start it with plain Node, no framework CLI required:

node .output/server/index.mjs

The server listens on port 3000 by default and respects the PORT and HOST environment variables, so it slots into any container platform that injects a port without extra glue.

Environment variables: the VITE_ boundary

SolidStart follows Vite's env conventions, and the rule is worth internalizing because getting it backwards is either a broken app or a leaked secret:

  • Client-side code can only see variables prefixed with VITE_, via import.meta.env.VITE_WHATEVER — and these are inlined at build time. Change one, and you need a rebuild.
  • Server-side code (API routes, server functions, anything behind "use server") reads process.env at runtime, like any Node process.

Two practical consequences. First, never put a secret in a VITE_ variable — it gets compiled into the JavaScript you ship to browsers. Second, infrastructure values like DATABASE_URL belong in plain process.env, read at runtime, which means the platform can inject them into the container without touching your build.

Wiring up PostgreSQL with Drizzle

Drizzle is a natural fit here. Install it plus the postgres driver:

npm i drizzle-orm postgres
npm i -D drizzle-kit

Keep the database module in a server-only location so it never gets pulled into a client bundle:

// src/server/db.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';

const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client);

Then use it from an API route or a server function:

// src/routes/api/todos.ts
import { db } from '~/server/db';
import { todos } from '~/server/schema';

export async function GET() {
  const rows = await db.select().from(todos);
  return Response.json(rows);
}

Migrations follow the standard Drizzle two-step — generate produces SQL files from your schema diff, migrate applies them:

npx drizzle-kit generate
npx drizzle-kit migrate

Here's the SolidStart-specific gotcha: because the production image contains only .output/, your Drizzle config and migration files don't exist inside the runtime container. Don't try to migrate from application startup code. Run drizzle-kit migrate as a release step from the repo checkout — during the build pipeline or as a one-off command — before the new server version starts taking traffic. That also protects you from the classic failure where two replicas boot at once and race on the same migration.

A minimal production Dockerfile

The self-contained output makes this one of the cleanest Dockerfiles you'll write:

FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/.output ./.output
USER node
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

No npm ci --omit=dev in the runtime stage, no package.json copied over — everything the server needs was bundled into .output at build time. The main exception to be aware of: native modules (a C++ addon like better-sqlite3, say) can't be bundled and would need to be installed in the final stage. With the pure-JS postgres driver above, you don't hit this.

Deploying on PandaStack

With the preset pinned and env handling understood, going live is mostly pushing code:

  1. 1Create a managed PostgreSQL instance from the dashboard — 14.x and 16.x are available, with daily scheduled backups out of the box.
  2. 2Connect your SolidStart repo as a container app. With the Dockerfile above it's used directly; without one, the Node buildpack auto-detects the framework, install command (npm, yarn, pnpm, or bun), and build step.
  3. 3Attach the database to the app. DATABASE_URL is injected into the container automatically at runtime — which is exactly the pattern the process.env.DATABASE_URL code above expects. No credentials pasted into a dashboard form.
  4. 4Add any other env vars in the app settings. Remember the boundary: VITE_-prefixed values need to be present at build time; everything else is runtime.
  5. 5Push. Each git push kicks off a build — rootless BuildKit in an ephemeral Kubernetes job pod, no shared Docker daemon — and the build logs stream live so you're not staring at a spinner. Run your drizzle-kit migrate release step, and the app is serving.

Worth knowing before you rely on it: free-tier apps scale to zero when idle and cold-start on the next request, and free databases come with a small storage volume — great for a side project, not sized for heavy production. Paid tiers keep instances warm on stable nodes.

SolidStart's build model — one preset flag, one self-contained directory, one node command — is about as deployment-friendly as full-stack frameworks get. Connect a repo at https://pandastack.io and see how far a single push takes it.

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also