---
title: "Ship your first service"
description: "Define a protobuf contract, build a Go service from a pushed commit and deploy it to Cloud Run with the alis CLI."
url: "https://docs.alisx.com/tutorials/first-build-service/"
---
# Ship your first service: Define, Build, Deploy

In this lesson you take a service from nothing to running in a cloud environment with Alis Build DBD, from the CLI: you write a protobuf contract, publish it, implement it in Go, build a version from a pushed commit and deploy that version with Terraform. It is for a builder who has the CLI installed and is signed in.

You will end with a gRPC service called `hello` running on Cloud Run in your product's development environment, reachable by your team and visible in the console.

## Before you start

- The CLI is installed and you are signed in. If not, follow [Install the CLI](https://docs.alisx.com/tutorials/install-the-cli.md) first.
- You are a builder on a product. This lesson uses the build environment `acme` and the product `sh`; replace them with your own ids. If you have no product yet, see [Create a product](https://docs.alisx.com/how-to/alis-build/create-a-product.md).
- Go 1.26 or later and git are installed.

## 1. Clone the product's repositories

Every product has two repositories: the build environment's definitions repo (protobuf contracts) and the product's build repo (implementations). One command clones both and configures git credentials.

```bash title="Terminal"
alis clone acme.sh
```

The command reports where each repository landed. You now have:

```text
~/alis.build/acme/define        the definitions repo
~/alis.build/acme/build/sh      the product's build repo
```

## 2. Create the service

A service is addressed by its package id: `<build environment>.<product>.<service>.<version>`.

```bash title="Terminal"
alis service new acme.sh.hello.v1 --json
```

```json title="Output"
{
  "packageId": "acme.sh.hello.v1",
  "name": "organisations/acme/products/sh/neurons/hello-v1",
  "organisation": "acme",
  "product": "sh",
  "serviceId": "hello-v1",
  "created": true,
  "buildFolder": "/Users/you/alis.build/acme/build/sh/hello/v1",
  "defineFolder": "/Users/you/alis.build/acme/define/acme/sh/hello/v1",
  "next": ["..."]
}
```

The service is registered on the platform and two empty folders are created: `hello/v1` (with an `infra/` subfolder) in the build repo, and `acme/sh/hello/v1` in the definitions repo. No files are generated.

## 3. Write the contract

Create `hello.proto` in the define folder.

```protobuf title="~/alis.build/acme/define/acme/sh/hello/v1/hello.proto"
syntax = "proto3";

package acme.sh.hello.v1;

// HelloService greets people.
service HelloService {
  // Returns a greeting for the given name.
  rpc SayHello(SayHelloRequest) returns (SayHelloResponse) {}
}

message SayHelloRequest {
  // The name to greet.
  string name = 1;
}

message SayHelloResponse {
  // The greeting.
  string message = 1;
}
```

The `package` line must match the service's folder: `acme.sh.hello.v1` for `acme/sh/hello/v1`.

## 4. Check that it compiles

A dry run compiles your local files, uncommitted edits included, with the same compiler a real define uses. Nothing is published.

```bash title="Terminal"
alis define acme.sh.hello.v1 --dry-run --json
```

```json title="Output"
{
  "package": "acme.sh.hello.v1",
  "folder": "/Users/you/alis.build/acme/define/acme/sh/hello/v1",
  "files": ["hello.proto"],
  "valid": true,
  "compiled": true,
  "diagnostics": [],
  "git": {
    "uncommittedFiles": ["hello.proto"],
    "unpushedCommits": 0,
    "defineCommit": "..."
  },
  "next": "alis define acme.sh.hello.v1 --json"
}
```

`valid` is `true` and `diagnostics` is empty. If a diagnostic appears, it names the file, line and column to fix.

## 5. Commit and push the contract

A define always compiles a pushed commit, so push first.

```bash title="Terminal"
cd ~/alis.build/acme/define
git add acme/sh/hello/v1/hello.proto
git commit -m "feat(sh.hello.v1): add HelloService"
git push
```

## 6. Write the server

Move to the build folder and create a Go module named after the package id, then the server. The generated Go package for your contract is imported as `alis.build/acme/sh/hello`.

```bash title="Terminal"
cd ~/alis.build/acme/build/sh/hello/v1
go mod init acme.sh.hello.v1
```

```go title="~/alis.build/acme/build/sh/hello/v1/main.go"
package main

import (
	"context"
	"log"
	"net"
	"os"

	pb "alis.build/acme/sh/hello"
	"google.golang.org/grpc"
)

type server struct {
	pb.UnimplementedHelloServiceServer
}

func (s *server) SayHello(ctx context.Context, req *pb.SayHelloRequest) (*pb.SayHelloResponse, error) {
	return &pb.SayHelloResponse{Message: "Hello, " + req.GetName()}, nil
}

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	listener, err := net.Listen("tcp", ":"+port)
	if err != nil {
		log.Fatalf("listen: %v", err)
	}
	srv := grpc.NewServer()
	pb.RegisterHelloServiceServer(srv, &server{})
	log.Printf("hello-v1 listening on :%s", port)
	if err := srv.Serve(listener); err != nil {
		log.Fatal(err)
	}
}
```

## 7. Define the contract and install the generated package

This publishes version `1.0.0` of the contract, generates language packages, and then runs the package install for Go so `go.mod` picks up `alis.build/acme/sh/hello` and gRPC.

```bash title="Terminal"
alis define acme.sh.hello.v1 --install --install-language go --json
```

Progress streams on stderr. The final result on stdout is the operation:

```json title="Output"
{
  "schemaVersion": 1,
  "name": "operations/...",
  "done": true,
  "status": "succeeded",
  "version": "1.0.0",
  "notes": "..."
}
```

`go.mod` and `go.sum` now list the generated package. Do not run `go get` for it yourself; the CLI refreshes the private registry credentials.

## 8. Write the Dockerfile

Build detects a `Dockerfile` in the service folder and builds it into the image `neurons/hello-v1`. The Go build inside the container must reach your product's private Go registry, so it authenticates first. Replace `<region>` with your product's default region and `<product-project-id>` with the `projectId` that `alis product view acme.sh --json` prints.

```dockerfile title="~/alis.build/acme/build/sh/hello/v1/Dockerfile"
FROM golang:1.26 AS builder
WORKDIR /app

# Authenticate Go module downloads against the product's private registry.
ENV GOPROXY=proxy.golang.org
RUN go run github.com/GoogleCloudPlatform/artifact-registry-go-tools/cmd/auth@v0.4.0 add-locations --locations=<region>

COPY . ./

RUN go run github.com/GoogleCloudPlatform/artifact-registry-go-tools/cmd/auth@v0.4.0 refresh
ENV GOPROXY=https://<region>-go.pkg.dev/<product-project-id>/define-go,https://proxy.golang.org,direct
ENV GONOSUMDB=alis.build

RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -o /server .

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
```

## 9. Write the infrastructure

Deploy runs the Terraform in `infra/`. The platform injects `ALIS_*` variables with the deployment environment's project, region and the commit that was built; you declare the ones you use.

```hcl title="infra/main.tf"
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = ">= 6.0.0"
    }
  }
  backend "gcs" {
    bucket = "provided_at_runtime_by_alis"
    prefix = "provided_at_runtime_by_alis"
  }
}

provider "google" {
  project = var.ALIS_OS_PROJECT
}
```

```hcl title="infra/variables.tf"
# Injected by Alis Build on every deploy.
variable "ALIS_OS_PROJECT" {}
variable "ALIS_OS_PRODUCT_PROJECT" {}
variable "ALIS_OS_NEURON" {}
variable "ALIS_OS_NEURON_VERSION_COMMIT_SHA" {}
variable "ALIS_REGION" {}
variable "ALIS_PRODUCT_REGION" {}
```

```hcl title="infra/cloudrun.tf"
resource "google_project_service" "run" {
  service            = "run.googleapis.com"
  disable_on_destroy = false
}

resource "google_cloud_run_v2_service" "hello" {
  name                = var.ALIS_OS_NEURON
  location            = var.ALIS_REGION
  deletion_protection = false

  template {
    containers {
      image = "${var.ALIS_PRODUCT_REGION}-docker.pkg.dev/${var.ALIS_OS_PRODUCT_PROJECT}/neurons/${var.ALIS_OS_NEURON}:${var.ALIS_OS_NEURON_VERSION_COMMIT_SHA}"
      # gRPC needs end-to-end HTTP/2.
      ports {
        name           = "h2c"
        container_port = 8080
      }
    }
    service_account = "alis-build@${var.ALIS_OS_PROJECT}.iam.gserviceaccount.com"
  }
}
```

## 10. Commit and push the implementation

```bash title="Terminal"
cd ~/alis.build/acme/build/sh
git add hello/v1
git commit -m "feat(sh.hello.v1): first server"
git push
```

## 11. Find the deployment environment id

Deploys target a deployment environment by id. Read it from the resolved context rather than guessing.

```bash title="Terminal"
alis --cwd ~/alis.build/acme/build/sh/hello/v1 context view --json
```

```json title="Output"
{
  "organisation": "acme",
  "product": "sh",
  "packageId": "acme.sh.hello.v1",
  "serviceFolder": "/Users/you/alis.build/acme/build/sh/hello/v1",
  "defineFolder": "/Users/you/alis.build/acme/define/acme/sh/hello/v1",
  "environments": [
    {
      "id": "<env-id>",
      "displayName": "Development",
      "status": "ACTIVE",
      "production": false,
      "projectId": "..."
    }
  ]
}
```

Copy the `id` of the Development environment.

## 12. Build and deploy

Start the build with a deploy attached, then wait for the operation. Inside the service folder the package id can be omitted.

```bash title="Terminal"
cd ~/alis.build/acme/build/sh/hello/v1
alis build --deploy -e <env-id> --async --json
```

```json title="Output"
{
  "name": "operations/...",
  "done": false,
  "status": "running",
  "next": "alis operations wait operations/... --json"
}
```

Run the `next` command to stream progress until the build and deploy finish:

```bash title="Terminal"
alis operations wait operations/... --json
```

```json title="Output"
{
  "schemaVersion": 1,
  "name": "operations/...",
  "done": true,
  "status": "succeeded",
  "version": "1.0.1",
  "images": [{ "path": "." }],
  "deployments": [
    {
      "name": "organisations/acme/products/sh/environments/<env-id>/deployments/hello-v1",
      "state": "RUNNING",
      "logsUri": "..."
    }
  ],
  "logsUri": "..."
}
```

`status` is `succeeded`, the version was built from your pushed commit, and the deployment is `RUNNING`.

## 13. Verify

Read the service's Cloud Run logs in the deployment environment:

```bash title="Terminal"
alis logs runtime -e <env-id> --since 30m
```

You see the startup line from your server, `hello-v1 listening on :8080`. The console shows the same result: open **Environments**, select `acme`, then the product `sh`, then **Services**. The `hello-v1` row shows version `1.0.1` deployed to Development.

## What you did

You defined a contract and published it as a versioned package, implemented it against the generated code, built an immutable version from a pushed commit and deployed that version with Terraform into a real Google Cloud project. Every later change follows the same loop.

## Where next

- [Define, Build, Deploy](https://docs.alisx.com/explanation/define-build-deploy.md) explains why the loop has three steps and what each one guarantees.
- [Organisations, products and services](https://docs.alisx.com/explanation/organisations-products-services.md) explains the hierarchy you worked in and why the contract lives apart from the implementation.
- [Deployment environments and versions](https://docs.alisx.com/explanation/environments-and-versions.md) explains deployment environments, designated branches and the production gate.
- The [Build how-to guides](https://docs.alisx.com/how-to/alis-build/index.md) cover each step in depth.
