Skip to main content
An Angular application with server-side rendering runs on Sevalla as an application: Ingot, Sevalla’s builder, detects Angular and @angular/ssr in your Git repository, runs ng build, and the web process runs the Express server that Angular builds, node dist/storefront/server/server.mjs. An Angular app without server-side rendering builds to static files only, and you can publish it as a static site instead.

At a glance

Environment variables

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 24.15 or later on your computer. Angular 22 needs Node.js 22.22.3 or later in the 22 line, 24.15 or later in the 24 line, or 26.

Create the project

Create a workspace with server-side rendering:
--ssr adds @angular/ssr and an Express server in src/server.ts. The other flags, and NG_CLI_ANALYTICS=false for the question about sharing usage data, answer the CLI’s questions, so the command runs without prompts and installs the dependencies with npm. The workspace comes with a .gitignore and a Git repository with a first commit.

Prepare the project for Sevalla

Render each request on the server

ng new sets every route to prerender: ng build renders the pages once, and the server sends the same HTML to everyone. To render each request on the server instead, set the render mode to RenderMode.Server in src/app/app.routes.server.ts:
src/app/app.routes.server.ts
Keep RenderMode.Prerender for pages that are the same for every visitor; the server sends them as built.

Stop the server gracefully

When Sevalla replaces an instance, during a deployment for example, it sends SIGTERM. Angular’s server doesn’t handle it, so requests in progress would be cut off. In src/server.ts, keep the server that app.listen() returns and close it on SIGTERM. Replace the block that starts the server with this:
src/server.ts
server.close() stops accepting connections and lets the requests in progress finish, then Node.js exits.

Pin the Node.js version

Ingot needs no start command: the start script runs ng serve, Angular’s development server, so Ingot ignores it and starts the server that ng build writes. It removes the devDependencies after the build; the server doesn’t need them, because ng build bundles Express and Angular into server.mjs.

Allow the domains and the proxy headers

Angular’s server answers only requests for host names it knows, to protect server-side requests from forged Host headers. Any other host gets status 400 and Header "host" with value "<host>" is not allowed. You give it the list in the NG_ALLOWED_HOSTS environment variable. It also ignores the X-Forwarded-For and X-Forwarded-Proto headers that Sevalla’s proxy adds to every request, and logs Received "x-forwarded-proto" header but "trustProxyHeaders" was not set up to allow it. for each one, unless you list them in NG_TRUST_PROXY_HEADERS. Trusting X-Forwarded-Proto also tells the server that the request came over HTTPS. The deployment steps set both variables on Sevalla. To check the production build locally, set NG_ALLOWED_HOSTS to localhost:
Open http://localhost:4000. The Angular starter page shows Hello, storefront. Then push the repository that ng new created to your Git provider:

Deploy to Sevalla

These steps build with Ingot. To build from your own Dockerfile instead, see Deploy with a Dockerfile.
1

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 a Data center. 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.
2

Add the environment variables

Click the Environment variables tab, then click Add environment variables. Add these variables and click Add environment variables:
3

Deploy

Click the Overview tab, then click Deploy now under Recent deployments. Check the Branch and click Deploy.
4

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.

Verify the deployment

  1. On the Deployments tab, click the deployment. Its Build log shows Ingot’s build plan: Node.js 24 from engines.node, npm at the version in packageManager, Angular with server-side rendering, and the server it starts.
    Build log
    The plan’s warning about NG_ALLOWED_HOSTS shows on every build: Ingot reads angular.json, not the application’s environment variables.
  2. Wait for the deployment’s status to change to Deployed. The application’s status next to its name changes to Running.
  3. Click the domain under the application’s name, for example storefront-x7k2m.sevalla.app. The Angular starter page opens with Hello, storefront. In the page source, the <h1> already holds Hello, storefront and <app-root> has ng-server-context="ssr": the server rendered the page for this request.
The application header with its name, Running status, repository, branch and the domain that opens the applicationThe application header with its name, Running status, repository, branch and the domain that opens the application
To check from a terminal, read the deployment’s status with Get deployment and its build log with Get application deployment logs, as the last steps of Deploy with the API do, then request the domain:
If the deployment fails, read its build log, then see Troubleshoot Ingot builds. If the domain answers Header "host" with value "storefront-x7k2m.sevalla.app" is not allowed., the domain isn’t in NG_ALLOWED_HOSTS: fix the value, then deploy again, because variables apply at the next deployment.

Deploy with a Dockerfile

Ingot needs no Dockerfile. Write one when you want full control of the image: the base image, the system packages and every layer. Add a Dockerfile and a .dockerignore to the root of the repository:
Dockerfile
.dockerignore
Commit and push both files. Ingot doesn’t use the Dockerfile and adds your .dockerignore patterns to its own, so you can keep both files in a repository that builds with Ingot. Then switch the application to the Dockerfile build strategy:
1

Choose the Dockerfile build strategy

On the application’s Settings tab, in the Build strategy section, click Update build strategy. Click Dockerfile, keep the Dockerfile path Dockerfile and the Docker context ., then click Update build strategy.
2

Deploy

Click the Overview tab, then click Deploy now under Recent deployments. Check the Branch and click Deploy.
The deployment’s build log shows 📦 Building and pushing docker image using Dockerfile... instead of Ingot’s build plan, and its Dockerfile tab shows your file. NG_ALLOWED_HOSTS and NG_TRUST_PROXY_HEADERS still apply: the server reads them when it starts. When the status changes to Deployed, the domain opens the starter page with Hello, storefront, rendered by the server. See Dockerfile for build arguments and private base images.

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, and the old instances finish their requests before they stop. See Deployments.

Use environment variables

Add configuration on the application’s Environment variables tab. Code that runs on the server, such as src/server.ts, reads it with process.env when the instance starts; variables apply at the next deployment. Code that runs in the browser can’t read the server’s environment. See Environment variables.

Add a custom domain

To serve the application from your own domain, such as shop.example.com, add the domain on the application’s Domains tab and create the DNS records that Sevalla shows. Custom domains need a web process larger than H1, so scale the web process first. Then add the domain to NG_ALLOWED_HOSTS, separated by a comma, for example storefront-x7k2m.sevalla.app,shop.example.com, and deploy. See Custom domains.

Deploy an Angular app without server-side rendering

An Angular app created without --ssr builds to HTML, CSS and JavaScript files only, and Sevalla can publish it as a static site, free up to 100 GB of bandwidth a month:
ng build writes the app to dist/storefront/browser. Push the repository to your Git provider, then create the static site:
1

Choose the repository

In the sidebar, click Static sites, then click Create static site. Under Source, click Private repository, then choose the Git provider, the Repository and the Branch.
2

Set the build settings

Under Build settings, set these values:
3

Create the static site

Enter a Name under Details, then click Create static site. Sevalla starts the first deployment right away, and deploys every push to the branch while Auto-deploy is on.
The Error file answers every path that has no file with index.html, so the Angular router renders its routes when someone opens or reloads them. Latest LTS meets Angular 22’s Node.js requirement; static sites don’t read engines in package.json. When the deployment’s status changes to Deployed, click Visit site on the static site’s Overview tab: the Angular starter page opens on the site’s *.sevalla.page domain. From a terminal, any path returns the app’s index.html, which the browser renders:
See Build settings for every field, and Troubleshoot static sites if the deployment fails.

Deploy with the API

The Sevalla API makes the same deployment as Deploy to Sevalla without the dashboard. Run the steps in order in one shell: each step is one API call, and saves the IDs that later steps need in shell variables. Each step also names the CLI command and the MCP server tool that do the same. A variable that holds null means the call failed: run the curl command without its jq part to read the error. This section has two sequences: the application with server-side rendering, and the static site of Deploy an Angular app without server-side rendering. You need:
  • An API key with the Company developer role, or the APP:CREATE, APP:READ and APP:UPDATE permissions, in the SEVALLA_API_KEY environment variable. The CLI reads the key from SEVALLA_API_TOKEN.
  • The repository on GitHub, GitLab or Bitbucket, and a Git account that can read it connected to your Sevalla user, which only the dashboard can do. See Git providers. Creating the application or the static site checks the access: if Sevalla can’t read the repository, the call returns 400 with Cannot access repository https://github.com/acme/storefront: Repository not found or you don't have access., followed by where to grant the access.
  • curl and jq.

Deploy the application

1

Find the location

List clusters (listClustersV3) returns every location with its id, its name in display_name and its region ID in location. Pick one by its region ID, here eu-frankfurt-1 for Frankfurt, Germany. Locations lists them all.
CLI: sevalla resources clusters. MCP: get_platform_options.
2

Create the application

Create application (createApplicationV3) creates the application with the Ingot build strategy and a web process, and doesn’t deploy it.
CLI: 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.
3

Find the web process

List processes (listProcessesV3) returns the web process that Create application added:
CLI: sevalla apps processes list --app-id "$APP_ID". MCP: get_application, in processes.
4

Find the H1 size

The API creates the web process with the S1 size, where the dashboard picks H1. List process resource types (listProcessResourceTypesV3) returns the ID of each size:
CLI: sevalla resources process-types. MCP: get_platform_options.
5

Set the web process size

Update process (updateProcessV3) sets the size, which applies at the first deployment:
CLI: sevalla apps processes update "$WEB_ID" --app-id "$APP_ID" --resource-type "$H1_ID". MCP: scale_process with size_id, and apply_now set to false.
6

Find the domain

List application domains (listApplicationDomainsV3) returns the application’s domains. NG_ALLOWED_HOSTS needs the primary one. Sevalla adds the domain a few seconds after it creates the application, so the loop asks until the domain is there:
CLI: sevalla apps domains list --app-id "$APP_ID". MCP: get_application, in domains.
7

Add the environment variables

Create environment variable (createEnvironmentVariableV3) adds one variable per call. The server reads both variables when it starts, so they need to be available at runtime only. The add_var function sends each one, with jq building the JSON body:
CLI: sevalla apps env-vars create "$APP_ID" --key <key> --value <value> for each variable, which makes it available during both the build and runtime. MCP: set_environment_variables with all the variables in one call, runtime set to true and buildtime set to false.
8

Turn on auto-deploy

Update application (updateApplicationV3) with auto_deploy deploys every push to the branch:
CLI: sevalla apps update "$APP_ID" --data '{"auto_deploy":true}'. MCP: sevalla_api_update with updateApplicationV3, the application ID as path_params.id and the same body.
9

Choose the Dockerfile build strategy

Skip this step to build with Ingot. To build from the Dockerfile instead, Update application (updateApplicationV3) sets the build strategy:
CLI: sevalla apps update "$APP_ID" --data '{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."}'. MCP: sevalla_api_update with updateApplicationV3 and the same body.
10

Deploy

Trigger deployment (triggerDeploymentV3) deploys the latest commit of the branch:
CLI: sevalla apps deployments trigger "$APP_ID". MCP: trigger_deployment.
11

Wait for the deployment

Get deployment (getDeploymentV3) returns the deployment’s status: waiting, inProgress, then success, failed, cancelled or skipped. Poll it until the deployment ends:
The loop asks every 15 seconds, for up to 30 minutes, and keeps asking through an empty or failed reply. success means the application is deployed. CLI: sevalla apps deployments get "$DEPLOYMENT_ID" --app-id "$APP_ID". MCP: get_deployment.
12

Read the build log

Get application deployment logs (getApplicationDeploymentLogsV3) returns the build log, one line per entry. With Ingot, the Start row of the build plan shows the start command:
With a Dockerfile, the log shows 📦 Building and pushing docker image using Dockerfile... in place of the build plan. When a deployment fails, the end of this log says why. CLI: sevalla apps deployments logs "$DEPLOYMENT_ID" --app-id "$APP_ID". MCP: get_deployment_logs.
13

Check the application

Request the application on its domain. The HTML holds the heading and the ng-server-context="ssr" attribute that the server adds when it renders the page:

Deploy the static site

The static site needs an API key with the STATIC_SITE:CREATE, STATIC_SITE:READ and STATIC_SITE:UPDATE permissions, or the Company developer role.
1

Create the static site

Create static site (createStaticSiteV3) creates the static site with the build settings of the dashboard steps. node_version lts is the dashboard’s Latest LTS. A static site has no location.
CLI: sevalla static-sites create --name storefront --repository https://github.com/acme/storefront --branch main --git-type github --build-command "npm run build" --node-version lts --publish-directory dist/storefront/browser. The CLI has no options for the error file and auto-deploy, so set error_file and auto_deploy with the API. MCP: create_static_site with the same fields.
2

Deploy the static site

Deploy static site (deployStaticSiteV3) builds and publishes the latest commit of the branch. Unlike the dashboard, the API doesn’t start the first deployment when it creates the site.
CLI: sevalla static-sites deployments trigger "$SITE_ID". MCP: trigger_deployment with service_type set to static_site.
3

Wait for the static site deployment

Get static site deployment (getStaticSiteDeploymentV3) returns the deployment’s status. Poll it until the deployment ends:
success means the site is live. If it’s failed, Get static site deployment logs (getStaticSiteDeploymentLogsV3) returns the build log. CLI: sevalla static-sites deployments get "$DEPLOYMENT_ID" --site-id "$SITE_ID". MCP: get_deployment, and get_deployment_logs for the log.
4

Check the static site

Get static site (getStaticSiteV3) returns the site’s domain in hostname, such as storefront-x7k2m.sevalla.page. The home page is the empty <app-root> element that the browser renders, and a path with no file gets the same index.html, the error file, so the router can render it:
CLI: sevalla static-sites get "$SITE_ID". MCP: get_static_site.