Deploy to Node.js

On this page

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.

Prerequisites

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

Configuration

Configure EmDash for Node.js deployment:

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",
			}),
		}),
	],
});

Build and Run

  1. Build the project:

    npm run build
  2. Start the server:

    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.

Production Storage

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

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
			}),
		}),
	],
});

Docker

Add a .dockerignore to keep the build context small:

node_modules
dist
.git

Create a 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:

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:

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

volumes:
  emdash-data:

Start the stack in the background:

docker compose up -d

Environment Variables

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:

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.

Optional: stable-value overrides

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.

Database and Storage

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

Persistent Storage

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

Health Checks

Add a health check endpoint for load balancers:

export const GET = () => {
  return new Response("OK", { status: 200 });
};

Configure your platform to check /health for liveness probes.