> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sevalla.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> These are the docs for Sevalla, a platform for deploying applications, databases, static sites, object storage and load balancers. The dashboard is at https://app.sevalla.com.
> Bold text in steps is an exact dashboard label.
> To act on a Sevalla account, prefer the Sevalla MCP server (https://mcp.sevalla.com/mcp, see https://docs.sevalla.com/ai-agents/overview.md), then the Sevalla CLI (https://docs.sevalla.com/developer-tools/cli.md), then the REST API (https://api-docs.sevalla.com, OpenAPI spec at https://api.sevalla.com/v3/openapi.json).
> Every page index is at https://docs.sevalla.com/llms.txt.

# Deploy EmDash

> Deploy the EmDash CMS to Sevalla from Sevalla's template: a Dockerfile application with PostgreSQL, object storage for media and edge caching purged on publish.

[EmDash](https://docs.emdashcms.com/) is a CMS built on Astro. It runs on Sevalla as an application built from a Dockerfile, keeps content, users and sessions in a Sevalla PostgreSQL database, stores uploaded media in object storage, and serves pages from Cloudflare with edge caching that the application purges through the Sevalla API when an editor publishes. This guide deploys Sevalla's EmDash template, which has all of this set up.

## Before you begin

* A Sevalla company with a payment method, and a GitHub, GitLab or Bitbucket account for the repository. See [Git providers](/git/overview).
* Node.js 22.16 or later on your computer, to generate the encryption key.

## Use the example repository

The [EmDash template for Sevalla](https://github.com/sevalla-templates/emdash-template) is EmDash's blog template with the hosting layer adapted to Sevalla:

| Part | What the template does |
| - | - |
| Database | Uses PostgreSQL instead of SQLite, reads the connection URL when the server starts, and searches posts with PostgreSQL full-text search. |
| Sessions | Stores Astro sessions in PostgreSQL, so editors stay signed in across deployments and instances. |
| Media | Uploads to object storage with presigned URLs and serves files from the bucket's public domain. |
| Caching | Sends `Cache-Control` headers for public pages and purges the application's edge cache after every content change. |
| Build | A multi-stage `Dockerfile` on Node.js 24 that needs no environment variables at build time. The server listens on `PORT`. |

Create an empty repository on your Git provider, such as `acme/storefront`, then copy the template into it and generate the key that encrypts EmDash's plugin secrets:

```bash theme={null}
git clone https://github.com/sevalla-templates/emdash-template.git storefront
cd storefront
git remote set-url origin https://github.com/acme/storefront.git
git push -u origin main
npm install
npx emdash secrets generate
```

Keep the generated key in a password manager: you add it as `EMDASH_ENCRYPTION_KEY` in the deployment steps, and EmDash can't read its stored secrets without it.

## Prepare the project for Sevalla

The template needs no code changes. Its parts depend on four Sevalla services, which you create in [Deploy to Sevalla](#deploy-to-sevalla) in the same location, because an internal connection only works within one location:

* A PostgreSQL database. The database user that Sevalla creates owns the database, which EmDash needs because it creates and alters its own tables.
* The application, built with the **Dockerfile** build strategy.
* An object storage bucket with public access and a CORS rule, so the admin panel can upload from the browser.
* An API key that can update the application, used to purge the edge cache.

## Deploy to Sevalla

<Steps>
  <Step title="Create the database">
    In the sidebar, click **Databases**, then click **Create database**. Under **Database**, choose **PostgreSQL** and version 16 or newer. Enter `orders-db` as the **Display name**, choose a **Data center** and a **Size**, then click **Create database**. See [Create a database](/databases/create) for every field.
  </Step>

  <Step title="Create the application">
    In the sidebar, click **Applications**, then click **Create** > **Application**. Under **Source**, click **Private repository**, then choose the **Git provider**, the **Repository** and the **Branch**. Under **Location**, choose the same **Data center** as the database. Keep the **Size** at **H1** or choose a larger one, then click **Create application**. The application opens on its **Overview** tab with the status **Not deployed yet**: creating an application doesn't deploy it.
  </Step>

  <Step title="Set the build strategy">
    Click the **Settings** tab, then click **Update build strategy**. Choose **Dockerfile**, keep the **Dockerfile path** at `Dockerfile` and the **Docker context** at `.`, and click **Update build strategy**. While you're on the **Settings** tab, copy the application's **ID**: it's the value of `SEVALLA_APP_ID`.
  </Step>

  <Step title="Connect the database">
    Click the **Networking** tab. Under **Connected services**, click **Add internal connection**. Choose `orders-db` as the **Service**, turn on **Add environment variables to the application**, and click **Add internal connection**. The template reads the `DB_URL` variable that Sevalla adds. See [Connect an application](/databases/connect#connect-an-application).
  </Step>

  <Step title="Create the bucket">
    In the sidebar, click **Object storage**, then click **Create object storage**. Enter a **Name**, choose the **Location** closest to the application, then click **Create object storage**. See [Create a bucket](/object-storage/create).
  </Step>

  <Step title="Open the bucket to the admin panel">
    On the bucket's **Settings** tab, under **Public access**, click **Enable public access** and confirm. Then, under **CORS policy**, click **Create rule**. Select `PUT`, `GET` and `HEAD` under **Allowed methods**, enter the application's address, such as `https://storefront-x7k2m.sevalla.app`, under **Allowed origins** and `content-type` under **Allowed headers**, then click **Create CORS rule**. See [Bucket settings](/object-storage/settings).
  </Step>

  <Step title="Create an API key">
    In the sidebar, under **Tools**, click **API keys**, then click **Create API key**. Choose **Custom capabilities** and allow only updating this application (the `APP:UPDATE` permission), then create the key and copy it. See [API keys](/developer-tools/api-keys).
  </Step>

  <Step title="Add the environment variables">
    On the application's **Environment variables** tab, click **Add environment variables**. Add these variables and click **Add environment variables**:

    | Variable | Value |
    | - | - |
    | `EMDASH_SITE_URL` | The application's address, such as `https://storefront-x7k2m.sevalla.app`. Set it before you run the setup wizard: passkeys only work on the address they were registered on. |
    | `EMDASH_ENCRYPTION_KEY` | The key that `npx emdash secrets generate` printed. |
    | `S3_ENDPOINT` | The bucket's **Endpoint**, from its **Settings** tab. |
    | `S3_BUCKET` | The bucket's **Bucket name**, not its display name. |
    | `S3_ACCESS_KEY_ID` | The bucket's **Access key**. |
    | `S3_SECRET_ACCESS_KEY` | The bucket's **Secret key**. |
    | `S3_REGION` | `auto` |
    | `S3_PUBLIC_URL` | `https://` followed by the bucket's **Public domain**. Without it, media is served through the application. |
    | `SEVALLA_APP_ID` | The application's **ID**, from its **Settings** tab. |
    | `SEVALLA_API_KEY` | The API key you created. Without it and `SEVALLA_APP_ID`, pages update only when their cache time runs out. |

    The template reads them when the server starts, so they don't need to be available during the build.
  </Step>

  <Step title="Turn on the CDN and edge caching">
    On the application's **Networking** tab, under **CDN & Edge caching**, click **Update settings**. Turn on **CDN** and **Edge caching**, then click **Done**. See [CDN and edge caching](/applications/caching).
  </Step>

  <Step title="Deploy">
    Click the **Overview** tab, then click **Deploy now** under **Recent deployments**. Check the **Branch** and click **Deploy**.
  </Step>

  <Step title="Turn on auto-deploy">
    To deploy every push to the branch automatically, click the **Settings** tab, then click **Update auto-deploy** in the **Auto-deploy** section. Turn on **Auto-deploy** and click **Update auto-deploy**. See [Deployments](/applications/deployments#deploy-automatically-on-push).
  </Step>
</Steps>

See [Connect with the S3 API](/object-storage/connect) for where each bucket value comes from.

## Verify the deployment

1. On the **Deployments** tab, wait for the deployment's status to change to **Deployed**. The application's status next to its name changes to **Running**.
2. Open the application's address followed by `/_emdash/admin`, for example `https://storefront-x7k2m.sevalla.app/_emdash/admin`. The setup wizard creates the site, can load the demo content, and registers your admin passkey. EmDash applies its database migrations on the first request.
3. Publish a post, then open the site's home page. The post shows without waiting for the cached page to expire, because publishing purges the edge cache.

If the deployment fails, open it on the **Deployments** tab and read its build log, then see [Troubleshoot builds and deployments](/applications/troubleshooting/builds-and-deployments). If the server stops with `DATABASE_URL is not set` in the [runtime logs](/applications/logs), the internal connection didn't add its variables: add the connection again with **Add environment variables to the application** turned on. If media uploads fail in the admin panel, check the bucket's CORS rule.

## Deploy changes

With **Auto-deploy** on, every push to the branch starts a deployment; otherwise, click **Deploy now** for each change. Each deployment builds the image, then replaces the web process's instances. See [Deployments](/applications/deployments).

## Add health checks

The template answers `/healthz` when the server runs and `/healthz?db=1` when it can also reach PostgreSQL. Use them as the web process's probes, so that Sevalla restarts an instance that stops responding and sends traffic only to instances that reach the database:

<Steps>
  <Step title="Open the web process">
    On the **Processes** tab, open the web process's menu and click **Update process**.
  </Step>

  <Step title="Turn on the probes">
    Under **Health checks**, turn on **Liveness probe** with the **Path** `/healthz`, and **Readiness probe** with the **Path** `/healthz?db=1`. Click **Update process**. The probes apply at the next deployment.
  </Step>
</Steps>

See [Processes](/applications/processes) for the other probe settings.

## Run migrations before each deployment

Without a job, the first request after a deployment applies EmDash's pending migrations. To apply them once, before new instances get traffic, add a [job](/applications/processes):

<Steps>
  <Step title="Create the job">
    On the **Processes** tab, click **Create process** and choose **Job**.
  </Step>

  <Step title="Set its command">
    Enter `Migrate` as the **Name** and `npm run migrate` as the **Custom start command**. Keep the **Start policy** at **Before deployment**, then click **Create process**.
  </Step>
</Steps>

The template's `migrate` script runs `emdash migrate` without prompts. If the job fails, the deployment fails and nothing changes for visitors.

## Add a custom domain

Custom domains need a web process larger than **H1**, so [scale](/applications/scaling) the web process first. After you add the domain on the **Domains** tab (see [Custom domains](/domains/overview)):

* Change `EMDASH_SITE_URL` to the new address and deploy again.
* Add the new address to the bucket's CORS rule. Rules can't be updated: delete the rule and create a new one with both origins.
* Register a passkey on the new domain before you remove the old one. Passkeys registered on the old address don't work on the new one.

## Scale the application

The template keeps no state in the instance, so you can run more than one instance or turn on autoscaling. Keep at least one instance running: EmDash's scheduler for scheduled posts runs inside the web process. See [Scaling](/applications/scaling).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.