PORT environment variable. With the web process’s protocol set to gRPC, Cloudflare and Sevalla’s proxy terminate TLS, so clients call the service on the application’s domain on port 443. Other applications in the same location call it over an internal connection, without TLS.
This guide builds catalog, a gRPC service with one method, and storefront, a JSON API that calls it over the internal connection. Both come from one Go module and one repository, and run as two applications.
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.
- Go 1.27 on your computer.
-
The Protocol Buffers compiler,
protoc, for example withbrew install protobuf, and the two Go plugins for it:go installputs them in$(go env GOPATH)/bin, which must be on yourPATH. -
grpcurl, to call the service from a terminal:
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest, orbrew install grpcurl.
Create the project
Create a folder and a Go module, and add grpc-go and the Protocol Buffers runtime. The last part of the module path,catalog, becomes the name of the server’s binary:
go get installs grpc-go 1.85 and protobuf 1.36. Describe the service in a .proto file:
catalogpb:
protoc writes catalogpb/catalog.pb.go and catalogpb/catalog_grpc.pb.go. Commit them: Ingot builds the Go code in the repository and doesn’t run protoc. Run the command again after every change to the .proto file.
Create the server at the root of the module:
grpc.NewServer()andsrv.Serve(lis)serve gRPC over cleartext HTTP/2 onPORT, which Sevalla sets to the web process’s port (8080by default). On your computer, the server uses port 8080.- A method returns a gRPC status for errors, such as
codes.NotFound, and the client receives it as is. - The health server and reflection serve clients and tools such as grpcurl. See Check health and use reflection.
- The HTTP server on port
8081answers the readiness probe with the health server’s status. - On
SIGTERM, the server stops gracefully. See Shut down gracefully.
storefront, the JSON API that calls catalog, in cmd/storefront:
storefront connects without TLS (insecure.NewCredentials()): traffic over an internal connection stays on Sevalla’s private network, where the web process serves cleartext HTTP/2. A NotFound from catalog becomes a 404, and any other error a 503.
Record the dependencies, and keep the binaries that go build writes out of the repository:
Prepare the project for Sevalla
The project needs no build configuration:go build -trimpath -ldflags="-s -w" -o /usr/local/bin/ . ./cmd/storefront, with CGO_ENABLED=0, because nothing in the module imports "C".
Run go run . in one terminal, then call the server from another with grpcurl:
-plaintext connects without TLS, as the server listens on your computer. 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.Create the application
Switch the protocol to gRPC
Turn on the readiness probe
/healthz and the Port to 8081, then click Update process.Deploy
Turn on auto-deploy
Verify the deployment
-
On the Deployments tab, click the deployment. Its Build log shows Ingot’s build plan: Go 1.27 from the
goline ofgo.mod, and the start command from themainpackage at the root.Build log -
Wait for the deployment’s status to change to Deployed. The application’s status next to its name changes to Running, and its runtime logs show
Listening on port 8080. -
Call the service on the application’s domain, for example
catalog-x7k2m.sevalla.app, on port 443. grpcurl connects with TLS by default:A product that doesn’t exist returns the gRPC status the method sent, and grpcurl exits with code69:
status with Get deployment (getDeploymentV3) and its build log with Get application deployment logs (getApplicationDeploymentLogsV3), as in Deploy with the API.
Check health and use reflection
The server registers two standard gRPC services next toCatalogService:
- Health checking (
grpc.health.v1.Health):health.NewServer()reportsSERVINGfor the whole server, the empty service name. Load balancers, clients and grpcurl read it withCheck, or follow it with the streamingWatchmethod. - Reflection (
grpc.reflection.v1.ServerReflection): clients ask the server for its services and message types, so grpcurl needs no.protofile. Reflection publishes your API’s schema to anyone who can reach the domain; leave outreflection.Registerif you don’t want that.
GET requests, and a gRPC port doesn’t answer them: a gRPC server reads the request as a broken HTTP/2 connection and closes it, and curl reports Received HTTP/0.9 when not allowed. So the server answers the probe on a second port, 8081, with the health server’s status: 200 and ok while it’s SERVING, 503 once the server shuts down. The probe’s Port can be any port the process listens on; it doesn’t need a private port. Each new instance gets traffic only after it answers the probe.
Get process (getProcessV3) returns the probe and the protocol:
What works through the proxy
Every request to the application’s domain passes through Cloudflare and Sevalla’s proxy before it reaches the web process. Tested with grpc-go 1.85 and grpcurl, with grpc-go’s test service for the streaming rows:server: cloudflare and cf-ray. See Request headers.
Call the service from another application
Another application in the same location callscatalog on its internal hostname, on the web process’s port, over an internal connection. The traffic stays on Sevalla’s private network and doesn’t pass through Cloudflare, so the client connects without TLS. The web process’s protocol doesn’t matter for internal calls: they work with HTTP or gRPC, and with HTTP traffic turned off.
storefront is in the same repository. Its image has both binaries, and a custom start command runs storefront:
Copy the internal hostname
catalog, click the Networking tab. In Internal hostnames, copy the web process’s hostname, for example catalog-x7k2m-web.catalog-x7k2m.svc.cluster.local.Create the second application
catalog, and name it storefront. Keep the Size at H1.Set the start command
storefront, click the Processes tab, click ⋯ on the Web process, then click Update process. Enter exec /usr/local/bin/storefront as the Custom start command, then click Update process. exec makes the binary the container’s main process, so it receives SIGTERM.Connect the applications
catalog as the Service, turn off Add environment variables to the application, then click Add internal connection.Add the address
CATALOG_ADDR with the hostname you copied, followed by :8080, for example catalog-x7k2m-web.catalog-x7k2m.svc.cluster.local:8080, and click Add environment variables.Deploy
storefront answers on its own domain with the product from catalog:
catalog off the internet and reachable only by your applications, turn its HTTP traffic off: on its Networking tab, in Expose gRPC traffic, click Disable, then Disable gRPC. Its domain then answers 404, and storefront keeps working. See Turn HTTP traffic off.
Use a private port
Over an internal connection, the web process’s port needs no setup. Any other port a process listens on, such as a second gRPC listener next to an HTTP server on the web process’s port, must be a private port first. Without it, every call fails withconnection error: desc = "error reading server preface: connection reset by peer".
To serve the same gRPC server on a second port, add this before the go func() that calls srv.Serve(lis) in main.go, and set the GRPC_PRIVATE_PORT environment variable to the port, such as 50051:
catalog’s Networking tab, in Private ports, click Expose private port, choose the web process as the Process, enter 50051 as the Port, and click Expose port. Sevalla replaces the process’s instances to apply it, without a deployment, and the port answers about 20 seconds later. Point CATALOG_ADDR in storefront at the new port, for example catalog-x7k2m-web.catalog-x7k2m.svc.cluster.local:50051, and deploy storefront.
With the API, Create private port (createApplicationPrivatePortV3):
sevalla apps private-ports create $APP_ID --process-id $WEB_ID --port 50051. MCP: sevalla_api_create with createApplicationPrivatePortV3.
Shut down gracefully
When Sevalla stops an instance, during a deployment, a restart or a scale-down, it waits 5 seconds, sendsSIGTERM, and stops the instance if it’s still running 30 seconds after the shutdown began. See How processes run.
On SIGTERM, catalog sets its health status to NOT_SERVING, then srv.GracefulStop() stops accepting connections and waits for the calls in progress. If they take longer than 20 seconds, srv.Stop() closes them. storefront stops its HTTP server the same way. On every deployment, the old instance’s runtime logs end with Shutting down and Server stopped. Ingot’s image runs catalog under tini, which passes the signal on; for storefront, the exec in the start command does the same.
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. Both applications build from the sameDockerfile and .dockerignore at the root of the repository:
Dockerfile, and it adds the patterns in .dockerignore to its own, so you can keep both files in a repository that builds with Ingot. Then switch each 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 the deployment’s Dockerfile tab shows your file. The grpcurl calls in Verify the deployment and the storefront checks return the same results. See Dockerfile.
With the API, send {"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."} to Update application (updateApplicationV3) for each application, then deploy each one.
Deploy changes
With Auto-deploy on, every push to the branch starts a deployment of each application; otherwise, click Deploy now in each one. When you changeproto/catalog.proto, run protoc again and commit the generated files with the code. Deploy catalog before a storefront that calls a new method. See Deployments.
Use environment variables
Add configuration on an application’s Environment variables tab and read it withos.Getenv, as the servers read PORT and CATALOG_ADDR. Variables apply at the next deployment. See Environment variables.
Add a custom domain
Custom domains reach the web process with the same protocol as the application’ssevalla.app domain, so gRPC clients can call catalog.example.com:443. 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. See Custom domains.
Troubleshoot
Deploy with the API
These calls make the same deployment as Deploy to Sevalla and Call the service from another application, 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 an application checks it: when Sevalla can’t read the repository, the call returns
400withCannot access repository https://github.com/acme/catalog: Repository not found or you don't have access., followed by where to grant access. curl,jqand grpcurl.
Choose the location and the size
listClustersV3) returns the locations your company can use, with the region ID in location; both applications must be in the same one. List process resource types (listProcessResourceTypesV3) gives the ID of H1:sevalla resources clusters and sevalla resources process-types. MCP: get_platform_options.Create the catalog application
createApplicationV3) creates it with a web process and doesn’t deploy it. List application domains (listApplicationDomainsV3) returns its domain a few seconds later:sevalla apps create --name catalog --cluster $CLUSTER_ID --source privateGit --git-type github --repository $REPO_URL --branch $BRANCH. MCP: create_application with deploy: false.Set the protocol, the size and the readiness probe
listProcessesV3) finds the web process, and Update process (updateProcessV3) sets ingress_protocol to grpc, the size to H1 (the API creates it at S1) and the readiness probe on port 8081. Send timeoutSeconds: a probe without it gets 120 seconds, where the dashboard sets 1. The response includes the web process’s internal hostname, for storefront:sevalla apps processes update $WEB_ID --app-id $APP_ID --resource-type $H1_ID sets the size; the CLI can’t set the protocol or probes. MCP: sevalla_api_update with updateProcessV3 and the same body.Deploy the catalog
triggerDeploymentV3), then poll Get deployment (getDeploymentV3) until the status is final, for up to 30 minutes:failed, read the build log with Get application deployment logs (getApplicationDeploymentLogsV3): curl -s "$API/applications/$APP_ID/deployments/$DEPLOYMENT_ID/logs?limit=5000" -H "$H" | jq -r '.[].message'. CLI: sevalla apps deployments trigger $APP_ID --branch $BRANCH, sevalla apps deployments get $DEPLOYMENT_ID --app-id $APP_ID and sevalla apps deployments logs $DEPLOYMENT_ID --app-id $APP_ID. MCP: trigger_deployment, get_deployment and get_deployment_logs.Call the catalog
Create the storefront application
storefront from the same repository. Update process (updateProcessV3) sets its web process’s size and its start command, entrypoint:sevalla apps create --name storefront ... as for catalog, then sevalla apps processes update $STORE_WEB_ID --app-id $STORE_ID --resource-type $H1_ID --command "exec /usr/local/bin/storefront". MCP: create_application with deploy: false, then sevalla_api_update with updateProcessV3.Connect storefront to the catalog
createApplicationInternalConnectionV3) with target_type app, then Create environment variable (createEnvironmentVariableV3) adds CATALOG_ADDR, for runtime only:CATALOG_ADDR. CLI: sevalla apps internal-connections create $STORE_ID --target-id $APP_ID --target-type app, and sevalla apps env-vars create $STORE_ID --key CATALOG_ADDR --value $CATALOG_HOST:8080, which makes the variable available during the build too. MCP: sevalla_api_create with createApplicationInternalConnectionV3, and set_environment_variables.Deploy storefront
sevalla apps deployments trigger $STORE_ID --branch $BRANCH. MCP: trigger_deployment and get_deployment.Check storefront
{"auto_deploy": true} to Update application (updateApplicationV3) for each application. Then run the other checks in Verify the deployment and Check health and use reflection, with $APP_HOST as the domain.