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.
Prerequisites
Section titled “Prerequisites”- Node.js v22.12.0 or higher
- A Node.js hosting provider or VPS
Configuration
Section titled “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
Section titled “Build and Run”-
Build the project:
Terminal window npm run build -
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.
Production Storage
Section titled “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
Section titled “Docker”Add a .dockerignore to keep the build context small:
node_modulesdist.gitCreate a Dockerfile:
FROM node:22-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build
FROM node:22-alpineWORKDIR /appCOPY --from=builder /app/dist ./distCOPY --from=builder /app/node_modules ./node_modulesCOPY --from=builder /app/package.json ./
RUN mkdir -p data
ENV HOST=0.0.0.0ENV PORT=4321
EXPOSE 4321CMD ["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-siteA 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 -dEnvironment Variables
Section titled “Environment Variables”Recommended: encryption key
Section titled “Recommended: encryption key”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 environmentThe 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
Section titled “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.
| Variable | Description |
|---|---|
EMDASH_PREVIEW_SECRET | Override for the auto-generated preview HMAC secret. |
EMDASH_IP_SALT | Override for the auto-generated commenter-IP hash salt. |
EMDASH_AUTH_SECRET | Optional. 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
Section titled “Database and Storage”| Variable | Description | Example |
|---|---|---|
DATABASE_PATH | Path to SQLite database | /data/emdash.db |
HOST | Server host | 0.0.0.0 |
PORT | Server port | 4321 |
S3_ENDPOINT | S3 endpoint URL | https://xxx.r2.cloudflarestorage.com |
S3_BUCKET | S3 bucket name | my-media-bucket |
S3_ACCESS_KEY_ID | S3 access key | AKIA... |
S3_SECRET_ACCESS_KEY | S3 secret key | ... |
S3_PUBLIC_URL | Public URL for media | https://cdn.example.com |
Persistent Storage
Section titled “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
Section titled “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.