EmDash works on Sevalla, but its starter templates need a few adjustments first: the Node.js adapter, the PostgreSQL package, and the S3 storage packages. For the best experience, we created a Sevalla-flavored template that already includes these changes, along with sessions in PostgreSQL, edge cache purging, PostgreSQL full-text search, health checks, and a production-ready Dockerfile. Take a look at the EmDash template for Sevalla.
Sevalla template
The EmDash template for Sevalla is EmDash’s official blog template (posts, pages, categories, tags, search, RSS, comments, and dark mode) with the hosting layer adapted for Sevalla. It uses the following Sevalla products:
The template’s README walks through creating the database, bucket, and application step by step. If you start from your own EmDash project instead, the rest of this guide explains the changes you need to make.
Application Hosting
Required adjustments
EmDash’s starter templates target either Cloudflare Workers (with D1 and R2 bindings) or a single Node.js server (with SQLite and media files on the local disk). Neither works on Sevalla without changes:- Node.js adapter - Sevalla runs your application as a Node.js server, so you need
@astrojs/nodein standalone mode instead of the Cloudflare adapter. - PostgreSQL - SQLite stores the database in a file on the local disk. Application containers on Sevalla use ephemeral storage, so the database is lost on every deploy and can’t be shared between instances. Use a Sevalla PostgreSQL database instead, which requires the
pgpackage. - S3-compatible storage - Uploaded media must live outside the container for the same reason. Use Sevalla Object Storage through EmDash’s
s3()adapter, which requires the AWS SDK packages.
Although you can attach persistent storage to keep SQLite and local uploads, this limits your application to a single instance. We recommend PostgreSQL and Object Storage so you can scale horizontally and deploy without downtime.
Configuration
We recommend the following best practices for configuring EmDash on Sevalla:- Use
output: "server"with the Node.js adapter instandalonemode. - Connect the application to PostgreSQL through an internal connection in the same region for lower latency and better security.
- Read database and storage credentials from runtime environment variables, and never commit them to your repository or bake them into the image.
- Store Astro sessions outside the container, for example in PostgreSQL or Redis, so editors stay signed in across deploys and instances.
- Set
EMDASH_SITE_URLto your public URL before running the setup wizard, because passkeys are tied to this origin. - Generate an
EMDASH_ENCRYPTION_KEYwithnpx emdash secrets generateand keep a copy in a password manager. - Set
trustedProxyHeadersso EmDash uses the real client IP for rate limiting. - Use
toolbar: "client"so public HTML is the same for every visitor and can be cached at the edge.
astro.config.mjs file for deploying EmDash on Sevalla:
Environment variables
Add the following environment variables to your application:
You can find the endpoint, bucket name, and keys on your bucket’s Settings page.
Object Storage
The admin panel uploads media directly from the browser to the bucket with presigned URLs. To allow this, configure your bucket as follows:- Under Settings > CORS Policy, create a rule with your application URL as the allowed origin,
PUT,GET, andHEADas the allowed methods, andcontent-typeas the allowed header. Add your custom domains to the rule later. - Optionally, enable Public access to serve media directly from the bucket’s
sevalla.storagedomain, and set it asS3_PUBLIC_URL.
Containerization
Dockerfile
The build for Dockerfiles is fully customizable. The following is an example Dockerfile for EmDash:Nixpacks
You can customize the Nixpacks build process by defining anixpacks.toml file and using Nixpacks-specific environment variables.
The following is an example nixpacks.toml configuration:
engines field in your package.json or with the NIXPACKS_NODE_VERSION environment variable.
Buildpacks
If you’re using Buildpacks, you cannot modify the underlying build phases directly. Buildpacks run thebuild script in your package.json and start the application with the start script:
Database migrations
By default, EmDash applies pending core migrations on the first request after a deploy, and seeds a fresh database with your seed file. To run migrations once per deploy before new instances receive traffic, add a job process with the Start policy set to Before deployment and a start command that runsnpx emdash migrate.
emdash migrate refuses to apply migrations outside an interactive terminal unless you pass the fingerprint of the target database. The Sevalla template includes a migrate script that reads the target database fingerprint and applies migrations non-interactively, so you can use npm run migrate as the job’s start command.
EmDash creates and alters its own tables as you change the content model, so the database user must own the database. The default user Sevalla creates for a PostgreSQL database meets this requirement.
CDN
Sevalla provides a premium, Cloudflare-powered CDN for Application Hosting at no additional cost. To get the most out of Sevalla’s CDN when deploying your EmDash site, we recommend the following best practices:- Enable the CDN for all production applications.
- Leverage Astro’s fingerprinted assets in the
_astro/directory for safe long-term caching. - Serve uploaded media from your bucket’s public domain instead of through the application.
- Upload web-sized images, because EmDash on Node.js serves original uploads without resizing.
- Purge the CDN cache after deploying critical updates to avoid serving stale content.
Edge caching
Edge caching stores rendered pages on Cloudflare’s 260+ global data centers, delivering responses from the location nearest to each visitor. A CMS is a good fit for edge caching because most pages change only when an editor publishes. To use it with EmDash, we recommend the following best practices:- Only cache public pages. Never cache the admin panel, API, and authentication routes under
/_emdash/, previews, or any response for a signed-in user. - Use
toolbar: "client"so the HTML of public pages is the same for every visitor. - Purge the edge cache whenever content changes, so visitors see new content immediately.
- Monitor cache efficiency using the
cf-cache-statusheader returned by Cloudflare.
Cache-Control
With Sevalla’s Cloudflare integration, Cache-Control headers are respected at the edge. The following directives are useful for EmDash sites:
public, max-age=0, s-maxage=3600- Caches the page on the edge for 1 hour, while browsers revalidate on every request.public, max-age=31536000, immutable- For fingerprinted assets in/_astro/.private, no-store- For the admin panel, API, previews, and personalized responses.
Purge the edge cache on publish
Sevalla purges both caches after every deploy, but content changes in EmDash don’t trigger a deploy. To show new content right away, call the Sevalla API to clear the edge cache when an editor publishes, updates, or deletes content:src/sevalla/edge-cache.ts and src/sevalla/purge.ts.
Health checks
Ensure your application remains available during deployments by implementing health checks:- Always implement health checks for production applications.
- Keep checks lightweight; responses should complete in under 1 second.
- Use a basic check for the liveness probe and a check that also verifies PostgreSQL for the readiness probe.
- Return 503 only for critical failures that require pod restarts.
- Never cache health check responses.
/healthz for the liveness probe and /healthz?db=1 for the readiness probe.
Scaling
EmDash can run on multiple instances when all state lives outside the container. Before you increase the instance count or enable autoscaling, make sure that:- Content is stored in PostgreSQL and media in Object Storage.
- Astro sessions are stored in a shared store. On Node.js, Astro stores sessions on the local filesystem by default, so editors are signed out on every deploy and when requests reach a different instance.
- At least one instance is always running, because EmDash’s scheduler for scheduled publishing and plugin tasks runs inside the Node.js process.
Troubleshooting
Build fails with Rollup failed to resolve import "@aws-sdk/client-s3"
- EmDash’s
s3()adapter doesn’t bundle the AWS SDK. Install@aws-sdk/client-s3and@aws-sdk/s3-request-presigneras dependencies.
Media uploads fail in the admin panel
- Check that the bucket’s CORS policy allows your application’s origin with
PUT,GET, andHEAD, and thecontent-typeheader. - Add every custom domain you use for the admin panel to the CORS policy.
- Set
AWS_REQUEST_CHECKSUM_CALCULATIONandAWS_RESPONSE_CHECKSUM_VALIDATIONtoWHEN_REQUIRED.
Passkeys don’t work after adding a custom domain
- Passkeys are tied to the origin they were registered on. Update
EMDASH_SITE_URLto the new domain, redeploy, and register a new passkey on the new domain before removing the old one.
Search returns no results
- EmDash’s built-in search uses SQLite full-text search and returns no results on PostgreSQL. Use PostgreSQL full-text search instead, as the Sevalla template does in
src/sevalla/search.ts.
Migrations fail with must be owner of table
- EmDash needs to own its tables. Connect with the same database user that created them, and don’t switch to a different user in
DATABASE_URL.