EmDash runs on Node.js 22.16 or later. This guide uses SQLite and local storage for one server. Use PostgreSQL or libSQL when several instances need one database, and S3-compatible storage when media must survive independently of the server disk.
Prerequisites
- Node.js v22.16.0 or higher
- A Node.js hosting provider or VPS
Configure the site
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
-
Build the project:
npm run build -
Start the server:
node ./dist/server/entry.mjs
The server runs on http://localhost:4321 by default. With the default auto migration mode, the first request applies pending core migrations. A fresh database also receives the embedded seed. Manage core database migrations explains how to migrate before restarting production traffic.
Scheduled tasks
The built-in scheduler runs only while a Node.js process is running. It handles scheduled publishing, plugin tasks, and general maintenance.
Keep at least one Node.js process running continuously in production. Scheduled tasks pause when every process stops or sleeps.
Plugin sandbox
Marketplace plugins and the plugins listed under sandboxed: [] need a sandbox runner. On Node.js, the runner is @emdash-cms/sandbox-workerd, which runs plugins in a workerd child process. Plugin Sandbox covers the installation, how the workerd process runs, and its failure modes.
Choose production data services
Use the following pattern when the database stays on a persistent volume and media moves to S3-compatible storage:
import emdash, { s3 } from "emdash/astro";
export default defineConfig({
integrations: [
emdash({
database: sqlite({ url: `file:${process.env.DATABASE_PATH}` }),
storage: s3(),
}),
],
});
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
Runtime environment
Read database and storage credentials from the process environment when the server starts. The following variables support the configuration above:
Encryption key validation
EMDASH_ENCRYPTION_KEY does not currently encrypt plugin secrets or any other stored data. If the variable is set, EmDash checks its format during startup, but plugin secret values remain plaintext in the database.
If you set the variable, generate a valid value and add the result to your environment:
npx emdash secrets generate # add the result to your environment
The value is operator-provided and is not stored in the database. Losing it has no current data-recovery impact because no stored data depends on it. Treat the database and its backups as sensitive because they contain plaintext plugin secrets.
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. |
See Secrets and key management for the key format, every supported secret, and the effects of rotation or loss.
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_REGION | S3 region | auto |
S3_PUBLIC_URL | Public URL for media | https://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
Back up both the SQLite file and the upload directory. Stop the process before replacing either during recovery. See Backups.
Health checks
Add a health check endpoint for load balancers:
export const GET = () => {
return new Response("OK", { status: 200 });
};
This endpoint proves that the Node.js process can serve Astro routes. It does not prove that the database, storage backend, migration state, or plugin sandbox is healthy. Verify those dependencies separately before sending traffic to a new release.
Verify before sending traffic
After starting a new build, verify the same runtime services that production requests use:
- Request
/healthand one public content page. Both must return a successful response. - Run
npx emdash migrate --checkfrom the built project. It must report no pending or unknown migrations for the configured database. - Sign in to
/_emdash/admin, create or edit a disposable draft, and publish it. Confirm that the public page shows the change. - Upload a disposable media file and open its returned URL. Delete the file after verifying it.
- If the site uses sandboxed plugins, invoke one plugin route or hook and confirm that the server log has no sandbox-unavailable or
workerdstartup error.
Keep the new instance out of the load balancer until every applicable check passes.