playwright in your dependencies and installs Chromium, the Linux libraries it needs and a set of fonts into the image, and the web process runs a Node.js server that drives Chromium on the port that Sevalla sets in the PORT environment variable. This guide builds a service that returns a screenshot or a PDF 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 Playwright and download Chromium for local use:playwright package doesn’t download browsers when you install it, so npx playwright install chromium downloads the Chromium release your Playwright version drives.
Create the server. It starts Chromium once, opens a new page for each request and returns a PNG or a PDF of https://example.com:
server.js
.gitignore
Prepare the project for Sevalla
Thenpm pkg set command already set what Ingot, the default build strategy, reads from package.json:
Because
playwright is in dependencies, Ingot also installs the browser, with no Dockerfile:
- The final image runs
playwright install --with-deps chromium, which installs the Chromium release that your locked Playwright version drives into/usr/local/share/playwright, with the Debian packages Chromium needs and a set of fonts. - The image sets
PLAYWRIGHT_BROWSERS_PATHto/usr/local/share/playwright, sochromium.launch()finds that Chromium. - Playwright 1.55.0 and later install Debian 13’s packages, which the Node.js 24 images are built on. With an older Playwright in
package-lock.json, Ingot builds on the Node.js 24 images for Debian 12 (node:24-bookworm-slim) instead, and its build plan adds aDebianrow that says why.
playwright or @playwright/test in dependencies only. In devDependencies, it treats Playwright 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. Chromium needs no flags in a container: chromium.launch() already passes --no-sandbox, because Chromium’s sandbox needs kernel privileges that containers don’t get, and --disable-dev-shm-usage, which keeps Chromium’s shared memory out of the small /dev/shm. Without the sandbox, open only pages you trust.
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: Chromium and every page it opens share the instance’s memory, and H1 has 0.3 GB. See Choose a size. 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
-
On the Deployments tab, click the deployment. Its Build log shows Ingot’s build plan: Node.js 24 from
engines.node, npm frompackage-lock.jsonand the start command from thestartscript.Further down, the step that installs the browser prints the release and its path, such asBuild logChrome Headless Shell 156.0.8078.4 (playwright chromium-headless-shell v1248) downloaded to /usr/local/share/playwright/chromium_headless_shell-1248. - Wait for the deployment’s status to change to Deployed. The application’s status next to its name changes to Running.
-
Click the domain under the application’s name, for example
storefront-x7k2m.sevalla.app, and add/screenshotto the address. The page shows a screenshot ofhttps://example.com. With/pdf, the browser opens a PDF of the same page. -
Check the same addresses from a terminal:


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 and see Troubleshoot.
Choose a size
Chromium and every page it has open share the instance’s memory. When they need more than the size has, the instance restarts, and every request in progress fails. These are the peaks of one instance of this service, measured on Sevalla with pages ofhttps://example.com:
H1 (0.3 GB) handled 5 requests at once and restarted at 10. S1 (1 GB) handled 10 with room to spare. Real pages with images, fonts and scripts need more memory per page than
https://example.com, so measure with your own: send the requests you expect at once, then run cat /sys/fs/cgroup/memory.peak in the application’s web terminal, which prints the instance’s peak memory in bytes since it started. Get process exit codes (getProcessExitCodesV3) returns 137 with OOMKilled (SIGKILL). The process was killed because it exceeded its memory limit. for each restart that ran out of memory.
When an instance runs out of memory, scale the web process to a larger size, or run more instances so that each one renders fewer pages at once.
Add fonts
The image has the fonts that Playwright installs with Chromium on Debian: Liberation, FreeFont and Unifont for Latin, Greek, Cyrillic, Arabic, Hebrew, Devanagari, Bengali and Tamil, IPA Gothic for Japanese, WenQuanYi Zen Hei for Chinese and Korean, Loma for Thai, and Noto Color Emoji. Text in these scripts renders without boxes. To render with another font family, such as Noto, install it with Ingot’sINGOT_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 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.
Noto Sans and Noto Serif in their CSS. 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 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 only Chromium’s headless shell, the build thatchromium.launch() runs, which keeps the image smaller than Ingot’s. Add a Dockerfile and a .dockerignore to the root of the repository:
Dockerfile
.dockerignore
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.


📦 Building and pushing docker image using Dockerfile... instead of Ingot’s build plan, then installs the headless shell. Its Dockerfile tab shows your Dockerfile. When the deployment is Deployed, /screenshot and /pdf work again, and the 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: each old instance getsSIGTERM, and its runtime log shows SIGTERM received, closing the browser. 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 Chromium too.Add a custom domain
To serve the application from your own domain, such asshop.example.com, add the domain on the application’s Domains tab and create the DNS records that Sevalla shows. See Custom domains.
Troubleshoot
For more causes, see Troubleshoot Ingot builds and Troubleshoot builds and deployments.
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:READandAPP:UPDATEpermissions, in theSEVALLA_API_KEYenvironment variable. The CLI reads the key fromSEVALLA_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
400withCannot access repository https://github.com/acme/storefront: Repository not found or you don't have access., followed by where to grant access. curlandjq.
1
Choose the location
List clusters (CLI:
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:sevalla resources clusters. MCP: get_platform_options.2
Create the application
Create application (CLI:
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: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 (CLI:
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.4
Deploy
Trigger deployment (CLI:
triggerDeploymentV3) deploys the latest commit on the application’s branch:sevalla apps deployments trigger $APP_ID. MCP: trigger_deployment.5
Wait for the deployment
Get deployment (CLI:
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.6
Read the build log
Get application deployment logs (CLI:
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, and installing the browser alone prints more, so pass 5000, the most it allows.sevalla apps deployments logs $DEPLOYMENT_ID --app-id $APP_ID. MCP: get_deployment_logs.7
Check the application
List application domains (CLI:
listApplicationDomainsV3) returns the application’s domain. Request the home page, a screenshot and a PDF from it:sevalla apps domains list --app-id $APP_ID. MCP: get_application.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.
Add fonts with the API
To add fonts to the Ingot build, 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:
sevalla apps env-vars create $APP_ID --key INGOT_RUN_PACKAGES --value fonts-noto-core. MCP: set_environment_variables.
The build plan doesn’t list the package. The new deployment’s build log shows Debian installing it: