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-appPick a SolidStart template and you get the standard three scripts in package.json — dev, 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 buildThe 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.mjsThe 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_, viaimport.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") readsprocess.envat 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-kitKeep 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 migrateHere'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:
- 1Create a managed PostgreSQL instance from the dashboard — 14.x and 16.x are available, with daily scheduled backups out of the box.
- 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.
- 3Attach the database to the app.
DATABASE_URLis injected into the container automatically at runtime — which is exactly the pattern theprocess.env.DATABASE_URLcode above expects. No credentials pasted into a dashboard form. - 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. - 5Push. Each
git pushkicks 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 yourdrizzle-kit migraterelease 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.