At a glance
Environment variables
The template reads every variable when the server starts, so none has to be available during the build.Before you begin
- A Sevalla company with a payment method, and a GitHub, GitLab or Bitbucket account for the repository. See Git providers.
- Node.js 22.16 or later on your computer, to generate the encryption key.
Use the example repository
The EmDash template for Sevalla is EmDash’s blog template with the hosting layer adapted to Sevalla:acme/storefront, then copy the template into it and generate the key that encrypts EmDash’s plugin secrets:
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 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. Ingot, the default build strategy, detects Astro with the Node adapter and starts the server with the
startscript,node ./server.mjs. The template also has aDockerfile, which Ingot doesn’t use: it generates its own, and adds the patterns in the template’s.dockerignoreto it. To build from a Dockerfile instead, see Deploy with a Dockerfile. - 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
These steps build with Ingot. To build from your own Dockerfile instead, see Deploy with a Dockerfile.Create the database
orders-db as the Display name, choose a Data center and a Size, then click Create database. See Create a database for every field.Create the application
Connect the database
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.Create the bucket
Open the bucket to the admin panel
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.Create an API key
storefront-cache-purge, and click Custom capabilities. Under Capabilities, select Update in the App row. Under Resource scope, choose Specific app resources and select the application. Click Create API key and copy the key. Purging the edge cache needs the APP:UPDATE permission, and this key has it for this application only. See API keys.Add the environment variables
Turn on the CDN and edge caching
Deploy
Turn on auto-deploy
Verify the deployment
-
On the Deployments tab, click the deployment. Its Build log shows Ingot’s build plan: Node.js 24, which satisfies the template’s
engines.node, npm frompackage-lock.json, and the start command from thestartscript.Ingot prints theBuild log!warning for every EmDash site. The Add the environment variables step in Deploy to Sevalla setsEMDASH_ENCRYPTION_KEY, so there’s nothing to do. - Wait for the deployment’s status to change to Deployed. The application’s status next to its name changes to Running.
-
Check from a terminal that the server runs and reaches PostgreSQL:
-
Open the application’s address followed by
/_emdash/admin, for examplehttps://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. -
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: the application’s runtime logs show
[sevalla] Edge cache purge requesteda few seconds after you publish.
getDeploymentV3) returns the deployment’s status, success once it’s deployed, and Get application deployment logs (getApplicationDeploymentLogsV3) returns its build log, which ends with Web has been deployed successfully. Get application runtime logs (getApplicationRuntimeLogsV3) with search finds the purge:
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.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:
Open the web process
Turn on the probes
/healthz, and Readiness probe with the Path /healthz?db=1. Click Update process. The probes apply at the next deployment.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:Create the job
Set its command
Migrate as the Name and npm run migrate as the Custom start command. Keep the Start policy at Before deployment, then click Create process.migrate script runs emdash migrate without prompts. It’s the job Ingot recommends for the template: Ingot finds migrate in package.json and lists npm run migrate among the processes it recommends, but doesn’t create it. If the job fails, the deployment fails and nothing changes for visitors. In the next deployment’s build log, the job runs after the build, as ⏩ Running Migrate (timeout: 600s)..., and ends with ✅ Migrate has been successful before the web process rolls out.
Deploy with a Dockerfile
Ingot needs no Dockerfile. Write your own when you want full control of the image: the base image, the system packages and every layer. Replace the template’sDockerfile with this one:
.dockerignore, which keeps node_modules, dist, .git, local .env files and SQLite databases out of the build.
Commit and push the Dockerfile. Ingot doesn’t use it, so you can keep it in a repository that builds with Ingot. Then switch the application to the Dockerfile build strategy:
Choose the Dockerfile build strategy
Dockerfile and the Docker context ., then click Update build strategy.Deploy


📦 Building and pushing docker image using Dockerfile... instead of Ingot’s build plan, and its Dockerfile tab shows your Dockerfile. When the deployment is Deployed, curl -s "https://storefront-x7k2m.sevalla.app/healthz?db=1" prints {"status":"ok","db":"ok"}, and publishing a post purges the edge cache as before. See Dockerfile for build arguments and private base images, and Deploy with the API to switch the build strategy with the API.
Add a custom domain
Custom domains need a web process larger than H1, so scale the web process first. After you add the domain on the Domains tab (see Custom domains), changeEMDASH_SITE_URL to the new address, add the new address to the bucket’s CORS rule, and deploy again. CORS rules can’t be updated: delete the rule and create a new one with both origins.
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.Deploy with the API
These calls make the same deployment as Deploy to Sevalla, Add health checks and Run migrations before each deployment, in an order that a script or an AI agent runs as it is. Each step names the Sevalla API operation, then the CLI command and the MCP server tool that do the same. You need:- An API key with the Company developer role in the
SEVALLA_API_KEYenvironment variable: the steps create a database, a bucket and an API key as well as the application. The CLI reads the key fromSEVALLA_API_TOKEN. - The repository from Use the example repository on a Git provider that’s connected to your Sevalla account. See Git providers. Creating the application checks it: when Sevalla can’t read the repository, the call returns
400withCannot access repository https://github.com/acme/storefront: Repository not found or you don't have access., followed by where to grant access. - The encryption key in
EMDASH_ENCRYPTION_KEY.npx emdash secrets generateprints only the key to standard output, so in the project folder, afternpm install, runEMDASH_ENCRYPTION_KEY=$(npx emdash secrets generate). curl,jqandopenssl.
Choose the location
listClustersV3) returns the locations your company can use, each with its id, its display_name and its region ID in location (see Locations). The database and the application must be in the same one. Keep the ID of the one you want, here Frankfurt, Germany:sevalla resources clusters. MCP: get_platform_options.Find the database size
listDatabaseResourceTypesV3) returns the database sizes by name. db1, the smallest, is enough for a blog:sevalla resources db-types. MCP: get_platform_options with kind set to database.Create the database
createDatabaseV3) creates PostgreSQL 18 with a database and a user named storefront, which owns the database. The API doesn’t return the password later, so generate it first and keep it in DB_PASSWORD. A hex password needs no escaping in DB_URL:sevalla databases create --name orders-db --type postgresql --db-version 18 --cluster $CLUSTER_ID --resource-type $DB_SIZE_ID --db-name storefront --db-user storefront --db-password $DB_PASSWORD. MCP: create_database, which generates the password itself and never returns it. Given connect_to_application_id once the application exists, it also connects the database and stores a postgresql:// URL in DATABASE_URL, which the template reads before DB_URL: you then skip Connect the database and the DB_ variables.Find the H1 size
listProcessResourceTypesV3) returns the process sizes by name, in lowercase:sevalla resources process-types. MCP: get_platform_options.Create the application
createApplicationV3) creates the application with a web process and auto-deploy off. It builds with Ingot (build_type is ingot) and isn’t deployed yet:sevalla apps create --name storefront --cluster $CLUSTER_ID --source privateGit --git-type github --repository https://github.com/acme/storefront --branch main. MCP: create_application with deploy set to false, because it deploys by default.Find the web process
listProcessesV3) returns the web process that the application was created with:sevalla apps processes list --app-id $APP_ID. MCP: get_application.Set the size and the health checks
updateProcessV3) sets H1 and adds the probes from Add health checks, with the dashboard’s default timings. Send timeoutSeconds: a probe without it gets 120 seconds, where the dashboard sets 1.sevalla apps processes update $WEB_ID --app-id $APP_ID --resource-type $H1_ID sets the size; the CLI can’t set probes. MCP: scale_process with size_id and apply_now set to false for the size, and sevalla_api_update with updateProcessV3 and the same body for the probes.Create the migration job
createProcessV3) adds the Migrate job from Run migrations before each deployment. A job needs a size, a scaling strategy and a start policy:sevalla apps processes create $APP_ID --name Migrate --type job --command "npm run migrate" --job-start-policy beforeDeployment --resource-type $H1_ID. MCP: sevalla_api_create with createProcessV3 and the same body.Wait for the database
getDatabaseV3) returns the database’s status, creating until it’s ready, and its internal hostname and port. Poll it every 15 seconds, for up to 30 minutes, until the status is ready or error, then keep the connection values:sevalla databases get $DB_ID. MCP: get_database.Connect the database
createApplicationInternalConnectionV3) connects the application to the database. Unlike the dashboard, it doesn’t add environment variables: the Add the environment variables step adds them.sevalla apps internal-connections create $APP_ID --target-type database --target-id $DB_ID. MCP: sevalla_api_create with createApplicationInternalConnectionV3.Find the domain
listApplicationDomainsV3) returns the application’s domain, which exists before the first deployment. EMDASH_SITE_URL and the bucket’s CORS rule need it:sevalla apps domains list --app-id $APP_ID. MCP: get_application.Create the bucket
createObjectStorageV3) creates the bucket with public access, near Frankfurt (weur, Western Europe). Its response is the only one that has the bucket’s access key and secret key, so keep the whole response in BUCKET:sevalla object-storage create --name storefront-media --location weur --json, then sevalla object-storage cdn-domain enable $BUCKET_ID for public access. MCP: sevalla_api_create with createObjectStorageV3.Add the CORS rule
createObjectStorageCorsPolicyV3) lets the admin panel upload from the application’s address:sevalla object-storage cors-policies create $BUCKET_ID --methods PUT,GET,HEAD --origins https://$HOST --headers content-type. MCP: sevalla_api_create with createObjectStorageCorsPolicyV3.Create the cache purge key
createApiKeyV3) creates the key the application purges its edge cache with: APP:UPDATE for this application only. The response is the only one that has the key’s token:sevalla api-keys create --name storefront-cache-purge --capabilities APP:UPDATE --json creates the key for every application: the CLI can’t limit a permission to one application. MCP: sevalla_api_create with createApiKeyV3.Add the environment variables
createEnvironmentVariableV3) adds one variable per call. The DB_ variables have the names and formats that the dashboard’s internal connection adds; every variable is available at runtime only. SEVALLA_API_KEY in the application is the purge key, not your own key:sevalla apps env-vars create $APP_ID --key DB_URL --value "...", one call per variable; the CLI makes every variable available during the build and at runtime. MCP: set_environment_variables with runtime set to true and buildtime set to false.Turn on the CDN
toggleApplicationCdnV3) switches the CDN on when it’s off, and off when it’s on, then returns the new state. Check that it’s true:sevalla apps cdn-toggle $APP_ID. MCP: sevalla_api_create with toggleApplicationCdnV3.Turn on edge caching
toggleApplicationEdgeCacheV3) works the same way for edge caching:sevalla apps edge-cache-toggle $APP_ID. MCP: sevalla_api_create with toggleApplicationEdgeCacheV3.Turn on auto-deploy
updateApplicationV3) turns on automatic deployments for every push to the branch:sevalla apps update $APP_ID --data '{"auto_deploy":true}'. MCP: sevalla_api_update with updateApplicationV3.Deploy
triggerDeploymentV3) deploys the latest commit on the application’s branch:sevalla apps deployments trigger $APP_ID. MCP: trigger_deployment.Wait for the deployment
getDeploymentV3) returns the deployment’s status: waiting and inProgress while it runs, then success or failed. Poll it every 15 seconds, for up to 30 minutes, until the status is final:sevalla apps deployments get $DEPLOYMENT_ID --app-id $APP_ID. MCP: get_deployment.Read the build log
getApplicationDeploymentLogsV3) returns the build log, oldest line first: the build plan from Verify the deployment, the build, the Migrate job and the rollout, which ends with Web has been deployed successfully. When the status is failed, the last lines say why. Without limit, the call returns the latest 1,000 lines, so a longer log loses its start; 5000 is the most it allows.sevalla apps deployments logs $DEPLOYMENT_ID --app-id $APP_ID. MCP: get_deployment_logs.Check the application
https://$HOST/_emdash/admin in a browser to run the setup wizard, as in Verify the deployment: registering a passkey needs one.Build from the Dockerfile with the API
After you push theDockerfile from Deploy with a Dockerfile, Update application (updateApplicationV3) switches the build strategy. Then repeat the Deploy, Wait for the deployment and Check the application steps:
sevalla apps update $APP_ID --data '{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."}'. MCP: sevalla_api_update with updateApplicationV3.