Skip to main content
A Puppeteer service runs on Sevalla as an application: Ingot, Sevalla’s builder, sees puppeteer in your dependencies and installs Chrome for Testing and the Linux libraries it needs into the image, and the web process runs a Node.js server that drives Chrome on the port that Sevalla sets in the PORT environment variable. This guide builds a service that returns a screenshot of a page.

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 or later on your computer.

Create the project

Create a folder, install Puppeteer and download Chrome for Testing for local use:
npm doesn’t run Puppeteer’s install script, which downloads Chrome, until you approve it, so npx puppeteer browsers install chrome downloads the release your Puppeteer version expects. Create the server. It starts Chrome once, opens a new page for each request and returns a PNG of https://example.com:
server.js
Keep installed packages out of the repository:
.gitignore

Prepare the project for Sevalla

The npm pkg set command already set what Ingot, the default build strategy, reads from package.json: Because puppeteer is in dependencies, Ingot also installs Chrome, with no Dockerfile:
  • npm ci runs with PUPPETEER_SKIP_DOWNLOAD set, so the install doesn’t download Chrome into the build.
  • The final image runs puppeteer browsers install chrome --install-deps, which installs the Chrome release your Puppeteer version expects into /usr/local/share/puppeteer, with the Debian packages Chrome needs.
  • The image sets PUPPETEER_CACHE_DIR to /usr/local/share/puppeteer, so puppeteer.launch() finds that Chrome.
Ingot installs Chrome only for puppeteer in dependencies. In devDependencies, it treats Puppeteer as a test tool and installs no browser. The server reads its port from PORT, which Sevalla sets to the web process’s port (8080 by default), and server.listen() listens on all network interfaces. The Chrome flags matter in a container: Run npm start and open http://localhost:3000/screenshot to check the service locally. Then commit everything and push it 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. Choose S1 (1 GB of memory) or a larger Size: Chrome and every page it opens share the instance’s memory, and H1 has 0.3 GB. Click Create application. The application opens on its Overview tab with the status Not deployed yet: creating an application doesn’t deploy it.
2

Deploy

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

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 from package-lock.json and the start command from the start script.
    Build log
    Further down, the step that installs Chrome prints the release and its path, such as chrome@156.0.8078.47 /usr/local/share/puppeteer/chrome/linux-156.0.8078.47/chrome-linux64/chrome.
  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, and add /screenshot to the address. The page shows a screenshot of https://example.com.
  4. Check the same address from a terminal:
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
With the API, Get deployment (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. See Deploy with the API. If the deployment fails, read its build log, then see Troubleshoot Ingot builds. If the instance restarts or /screenshot fails, read the application’s runtime logs:
  • Could not find Chrome means the image has no Chrome: check that puppeteer is in dependencies, not devDependencies.
  • A process that stops without an error usually ran out of memory: scale the web process to a larger size.

Add fonts for other languages

The image has only the Liberation fonts that Chrome’s packages install, so text in scripts such as Arabic, Chinese, Japanese or Korean shows up as boxes in screenshots and PDFs. Install more fonts with Ingot’s INGOT_RUN_PACKAGES variable, which adds Debian packages to the final image:
1

Add the variable

Click the application’s Environment variables tab, then click Add environment variables. Enter INGOT_RUN_PACKAGES as the Key and fonts-noto-core fonts-noto-cjk as the Value. Keep Available during buildtime selected, because Ingot reads the variable during the build, and click Add environment variables.
2

Deploy

Click the Overview tab, then click Deploy now under Recent deployments. Check the Branch and click Deploy.
After the deployment, /screenshot shows the Arabic and Chinese text on https://example.com in Noto fonts. To keep the packages in the repository instead, set packages.run in an ingot.json file. See Configure the build. With your own Dockerfile, add fonts-noto-core fonts-noto-cjk to the apt-get install line in Deploy with a Dockerfile instead. To add the variable with the API, see Add fonts with the API.

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. This one installs Debian’s Chromium package instead of Chrome for Testing. Add a Dockerfile and a .dockerignore to the root of the repository:
Dockerfile
server.js launches Chrome with the same --no-sandbox and --disable-dev-shm-usage flags on both build strategies.
.dockerignore
Commit and push both files. Ingot doesn’t use the Dockerfile, so you can keep it in a repository that builds with Ingot, but it adds the .dockerignore patterns to its own, and its build plan lists them in an Ignored row. 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 Update build strategy sheet with Dockerfile selected, the Dockerfile path, the Docker context and the Docker registry credentialThe Update build strategy sheet with Dockerfile selected, the Dockerfile path, the Docker context and the Docker registry credential
The deployment’s Build log shows 📦 Building and pushing docker image using Dockerfile... instead of Ingot’s build plan, then installs chromium. Its Dockerfile tab shows your Dockerfile. When the deployment is Deployed, /screenshot shows a screenshot of https://example.com again, and the two curl commands in Verify the deployment print the same lines. See Dockerfile for build arguments and private base images, and Deploy with the API to switch the build strategy with the API.

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.

Run browser jobs in the background

Screenshots and PDFs of long pages can take longer than a visitor waits. Run them in a background worker process that takes work from a queue in a Sevalla Redis database, and store the results in object storage instead of the instance’s disk. The worker runs from the same image as the web process, so it has Chrome too.

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. See Custom domains.

Deploy with the API

These calls make the same deployment as Deploy to Sevalla, 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, 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 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 400 with Cannot access repository https://github.com/acme/storefront: Repository not found or you don't have access., followed by where to grant access.
  • curl and jq.
1

Choose the location

List clusters (listClustersV3) returns the locations your company can use, each with its id, its display_name and its region ID in location (see Locations). Keep the ID of the one you want, here Frankfurt, Germany:
CLI: sevalla resources clusters. MCP: get_platform_options.
2

Create the application

Create application (createApplicationV3) creates the application with a web process at S1, the size this guide needs, and auto-deploy off. It builds with Ingot (build_type is ingot) and isn’t deployed yet:
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, because it deploys by default.
3

Turn on auto-deploy

Update application (updateApplicationV3) turns on automatic deployments for every push to the branch:
CLI: sevalla apps update $APP_ID --data '{"auto_deploy":true}'. MCP: sevalla_api_update with updateApplicationV3.
4

Deploy

Trigger deployment (triggerDeploymentV3) deploys the latest commit on the application’s branch:
CLI: sevalla apps deployments trigger $APP_ID. MCP: trigger_deployment.
5

Wait for the deployment

Get 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:
CLI: sevalla apps deployments get $DEPLOYMENT_ID --app-id $APP_ID. MCP: get_deployment.
6

Read the build log

Get application deployment logs (getApplicationDeploymentLogsV3) returns the build log, oldest line first: the build plan from Verify the deployment, the build 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.
CLI: sevalla apps deployments logs $DEPLOYMENT_ID --app-id $APP_ID. MCP: get_deployment_logs.
7

Check the application

List application domains (listApplicationDomainsV3) returns the application’s domain. Request the home page and a screenshot from it:
CLI: sevalla apps domains list --app-id $APP_ID. MCP: get_application.

Build from the Dockerfile with the API

After you push the Dockerfile 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:
CLI: sevalla apps update $APP_ID --data '{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."}'. MCP: sevalla_api_update with updateApplicationV3.

Add fonts with the API

To add fonts for other languages, Create environment variable (createEnvironmentVariableV3) adds INGOT_RUN_PACKAGES. Without is_runtime and is_buildtime, the variable is available during the build and at runtime, and Ingot reads it during the build. Then repeat the Deploy, Wait for the deployment and Check the application steps:
CLI: sevalla apps env-vars create $APP_ID --key INGOT_RUN_PACKAGES --value "fonts-noto-core fonts-noto-cjk". MCP: set_environment_variables. The build plan doesn’t list the packages. The new deployment’s build log shows Debian installing them: