Skip to content

Deploy to Node.js

EmDash runs on any Node.js 22+ hosting platform. This guide uses SQLite with local or S3-compatible storage; libSQL and PostgreSQL work the same way on Node.js — see Database Options.

  • Node.js v22.12.0 or higher
  • A Node.js hosting provider or VPS

Configure EmDash for Node.js deployment:

astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
import emdash, { local, s3 } from "emdash/astro";
import { sqlite } from "emdash/db";
export default defineConfig({
output: "server",
adapter: node({ mode: "standalone" }),
integrations: [
emdash({
database: sqlite({ url: "file:./data/emdash.db" }),
storage: local({
directory: "./data/uploads",
baseUrl: "/_emdash/api/media/file",
}),
}),
],
});
  1. Build the project:

    Terminal window
    npm run build
  2. Start the server:

    Terminal window
    node ./dist/server/entry.mjs

The server runs on http://localhost:4321 by default. Migrations are applied on the first request. If the database is empty and setup hasn’t been completed, your seed file (or the built-in default if you don’t have one) is also applied on that first request.

For production, use S3-compatible storage instead of local filesystem:

astro.config.mjs
import emdash, { s3 } from "emdash/astro";
export default defineConfig({
integrations: [
emdash({
database: sqlite({ url: `file:${process.env.DATABASE_PATH}` }),
storage: s3({
endpoint: process.env.S3_ENDPOINT,
bucket: process.env.S3_BUCKET,
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
publicUrl: process.env.S3_PUBLIC_URL, // Optional CDN URL
}),
}),
],
});

Add a .dockerignore to keep the build context small:

.dockerignore
node_modules
dist
.git

Create a Dockerfile:

Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
RUN mkdir -p data
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD ["node", "./dist/server/entry.mjs"]

The seed file is read at build time and inlined into the bundle, so it does not need to be copied into the runtime image. Migrations run on the first request after a deploy; the seed applies only when the database has no collections and setup hasn’t been completed — existing data is never overwritten.

Build the image and run the container:

Terminal window
docker build -t my-emdash-site .
docker run -p 4321:4321 -v emdash-data:/app/data my-emdash-site

A Docker Compose file manages the same container with a named volume:

compose.yaml
services:
emdash:
build: .
ports:
- "4321:4321"
volumes:
- emdash-data:/app/data
restart: unless-stopped
volumes:
emdash-data:

Start the stack in the background:

Terminal window
docker compose up -d

EMDASH_ENCRYPTION_KEY is the key for encrypting plugin secrets at rest. The key is validated on startup; plugin secret encryption uses it once enabled. Set it on every deployment so secrets are protected without a later config change.

Generate a key and add the result to your environment:

Terminal window
npx emdash secrets generate # add the result to your environment

The key is provided by you and never stored in the database; only encrypted ciphertext is. Back it up somewhere durable (a password manager, KMS, or your team’s secret store) — losing it means losing every secret encrypted with it.

EmDash auto-generates the preview HMAC secret and commenter-IP hash salt and persists them in the database on first use. The env vars below pin them to a value you control — useful when a separate process needs to share a secret with your main site.

VariableDescription
EMDASH_PREVIEW_SECRETOverride for the auto-generated preview HMAC secret.
EMDASH_IP_SALTOverride for the auto-generated commenter-IP hash salt.
EMDASH_AUTH_SECRETOptional. If set, used as the IP-salt source (unless EMDASH_IP_SALT is also set, which takes precedence), keeping commenter-IP hashes stable for installs that already rely on it. Leave it unset for a new deployment.
VariableDescriptionExample
DATABASE_PATHPath to SQLite database/data/emdash.db
HOSTServer host0.0.0.0
PORTServer port4321
S3_ENDPOINTS3 endpoint URLhttps://xxx.r2.cloudflarestorage.com
S3_BUCKETS3 bucket namemy-media-bucket
S3_ACCESS_KEY_IDS3 access keyAKIA...
S3_SECRET_ACCESS_KEYS3 secret key...
S3_PUBLIC_URLPublic URL for mediahttps://cdn.example.com

SQLite requires persistent disk storage. Ensure your hosting platform provides:

  • A mounted volume or persistent disk
  • Write access to the database directory
  • Backup mechanisms for the database file

Add a health check endpoint for load balancers:

src/pages/health.ts
export const GET = () => {
return new Response("OK", { status: 200 });
};

Configure your platform to check /health for liveness probes.