Skip to content

Local Development

This document is a conceptual overview of how Tye behaves when using tye run for local development. For reference docs on the tye run command see here.

Tye acts as a local orchestrator for development by building and launching multiple services, managing and monitoring services, and enabling network communication between services.

This document will describe how these features work, as well as the implications and behaviors of various optional settings.

Executing tye run

When executing tye run for local development, Tye goes through the following steps:

  • Load the application (tye.yaml)
  • Build all projects
  • Pull all container images
  • Assign ports and bindings
  • Launch and monitor all services

Loading the application

A Tye application is specified using tye.yaml. You can find more information about the Tye application format here.

In the event that tye run is without a tye.yaml then the tool will populate the list of services using the provided solution file (multiple projects) or project file (single project).

Tye will ignore non-runnable projects when populating services based on a solution or project file, by excluding projects without a launchSettings.json.


Tye categorizes each service as either:

  • A .NET Project
  • A container image
  • An executable
  • (non-runnable) External

An example:

yaml
name: service-kinds
services:

# A project service
- name: myproject
  project: myproject/myproject.csproj

# A container image service
- name: myimage
  image: redis

# An executable service
- name: myexecutable
  executable: notepad.exe

# An external service
- name: myproject
  external: true

After building the list of services, Tye will use MSBuild in-process to read additional details for any .NET projects added as services. Using MSBuild allows Tye to get information from projects in the same way as tools like IDEs and dotnet build.

The information gathered from projects includes details like the target framework, and binary output location, as well as settings that affect local development like RunArguments.

If the project uses ASP.NET Core, then Tye will assign some default bindings, an unnamed http binding and an https binding (appropriately named https).

For example if frontend and backend are ASP.NET Core projects, then the following:

yaml
name: frontend-backend
services:
- name: backend
  project: backend/backend.csproj
- name: frontend
  project: frontend/frontend.csproj

Will behave as-if the following had been written:

yaml
name: frontend-backend
services:
- name: backend
  project: backend/backend.csproj
  bindings:
  - protocol: http
  - name: https
    protocol: https
- name: frontend
  project: frontend/frontend.csproj
  bindings:
  - protocol: http
  - name: https
    protocol: https

This binding inference saves a lot of typing in a very common case.


For other service types (container image, executable, external) no binding inference will take place, since it's not possible for Tye to introspect those service kinds. Add bindings manually for all of the network services that each service provides. See the service discovery for more information and recommendations.

Building projects

Tye will build all .NET projects (unless build: false has been specified) before running. Builds currently take place sequentially to avoid concurrency issues that can arise when executing MSBuild multiple times concurrently for the same project.

Pulling Container Images

Tye will use the Docker cli to pull any needed container images before launching any services. This is done as a separate step to avoid failures when the network is very slow or an image is very large.

Determining Docker base images and tags

Tye will use the TFM of the project to set the base image and tags of the container images to use when creating the associated Dockerfile.

Should you choose to use a custom base image and/or tag or variant you can do so by specifying the base image and tag in a <PropertyGroup> in any of the projects.

Example - custom image

xml
<PropertyGroup>
   <ContainerBaseImage>myregistry/dotnetbaseimage</ContainerBaseImage>
   <ContainerBaseTag>v1.1.0</ContainerBaseTag>
</PropertyGroup>

Example - Debian buster tag

xml
<PropertyGroup>
   <ContainerBaseImage>mcr.microsoft.com/dotnet/core/sdk</ContainerBaseImage>
   <ContainerBaseTag>3.1-buster</ContainerBaseTag>
</PropertyGroup>

Computing bindings

In order to help services communicate, Tye manages details like listening ports and hostnames. This listening information is specified to services using environment variables in development. This section will summarize some of the key information for local development, but the best guide for the topic is the service discovery documentation.

For each binding provided by a service, Tye assigns a unique network port (if the port is not specified in config). This port information will be used to generate the environment variables provided to services for service discovery. This port information is how other services will communicate with the service that defines the binding.

Consider an example:

yaml
name: frontend-backend
services:
- name: backend
  project: backend/backend.csproj
- name: frontend
  project: frontend/frontend.csproj
- name: redis
  image: redis
  bindings:
    - port: 6379

In this example, backend and frontend each have two bindings which will have auto-assigned ports. redis provides a single binding with a predetermined port.

The assigned ports might look like:

Servicefrontend (http)frontend (https)backend (http)backend (https)redis
Port575405754157542575436379

Since the binding for redis supplies a hardcoded port (6379) then the value will always be 6379. It's the user's responsibility to avoid conflicts when choosing a hardcoded port. Tye will not override a hardcoded port if there is a conflict, it will simply fail. ]

For all of the other bindings in this example, the ports are autogenerated based on what's available, and they will be unguessable. This is a basic scenario. Some other features of Tye change the details of how this works.

💡 Avoid relying on hardcoded ports where possible. Hardcoding ports might be something you're used to, but it also ignores Tye's features that give you more flexibility. See the service discovery docs for more information.

Specifying a container port

In contrast to a .NET project or executable, a container image runs in a different namespace from where the user is working. Many containers also have a preferred port that they will listen on by default. Fortunately Docker provides features that allow mapping of an external port (on the user's system) to a different container port (inside the container).

From our previous example:

yaml
name: frontend-backend
services:
- name: backend
  project: backend/backend.csproj
- name: frontend
  project: frontend/frontend.csproj
- name: redis
  image: redis
  bindings:
    - port: 6379 # redis specifies a hardcoded port

To use this feature, specify containerPort: 6379 instead of port: 6379. Tye will then assign a random port for the external port, and tell Docker to map it to 6379 inside the container.

Now our table from above might look like:

Servicefrontend (http)frontend (https)backend (http)backend (https)redis
External Port5754057541575425754357544
Listening Port575405754157542575436379

So in this case the Redis service running inside the container is listening on port 6379, but the service is accessible outside the container on port 57544 (auto-assigned). The auto-assigned port 57544 for redis is used to populate the environment variables given to frontend and backend as part of service discovery.

More TODO

  • Replicas

    • how replicas require/work with proxies
  • Hostnames inside containers

    • how networking works inside containers vs not
  • Running containers

    • volumes
    • secrets
    • logs
  • Running .NET applications

    • env-vars passed to .net applications
    • logs
    • event-pipe/metrics
  • Running .NET services in containers