From 7a5c6540014675c6ebfecfed9b4e96e679931f01 Mon Sep 17 00:00:00 2001 From: David Pine Date: Tue, 29 Sep 2026 10:02:10 -0500 Subject: [PATCH 1/2] Onboard first-party Java and Rust integrations Rewrite the Java and Rust integration docs for the first-party Aspire.Hosting.Java and Aspire.Hosting.Rust packages in Aspire 13.6, replacing the Community Toolkit content with the official three-article integration format: - Get started with the Java/Rust integration - Set up Java/Rust apps in the AppHost - Connect to Java/Rust apps (new) The pages use ApiReference for API mentions, twoslash-checked TypeScript AppHost samples (TypeScript tab first), and a concise "Migrate from the Community Toolkit" section on each host page. Also add the connect pages to the integrations sidebar, and update the AppHost overview's Java pivot and shared sample to use Aspire.Hosting.Java and AddSpringBootApp. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../config/sidebar/integrations.topics.ts | 8 + .../components/SimpleAppHostCode.shared.ts | 3 +- .../src/content/docs/get-started/app-host.mdx | 13 +- .../frameworks/java/java-connect.mdx | 357 ++++++++++ .../frameworks/java/java-get-started.mdx | 114 +-- .../frameworks/java/java-host.mdx | 671 ++++++++++++++---- .../frameworks/rust/rust-connect.mdx | 386 ++++++++++ .../frameworks/rust/rust-get-started.mdx | 124 ++-- .../frameworks/rust/rust-host.mdx | 493 +++++++++++-- .../content/docs/ja/get-started/app-host.mdx | 13 +- 10 files changed, 1827 insertions(+), 355 deletions(-) create mode 100644 src/frontend/src/content/docs/integrations/frameworks/java/java-connect.mdx create mode 100644 src/frontend/src/content/docs/integrations/frameworks/rust/rust-connect.mdx diff --git a/src/frontend/config/sidebar/integrations.topics.ts b/src/frontend/config/sidebar/integrations.topics.ts index 8fe87bb1e..391630f18 100644 --- a/src/frontend/config/sidebar/integrations.topics.ts +++ b/src/frontend/config/sidebar/integrations.topics.ts @@ -1414,6 +1414,10 @@ export const integrationTopics: StarlightSidebarTopicsUserConfig = { label: 'Set up Java apps in the AppHost', slug: 'integrations/frameworks/java/java-host', }, + { + label: 'Connect to Java apps', + slug: 'integrations/frameworks/java/java-connect', + }, ], }, { @@ -1500,6 +1504,10 @@ export const integrationTopics: StarlightSidebarTopicsUserConfig = { label: 'Set up Rust apps in the AppHost', slug: 'integrations/frameworks/rust/rust-host', }, + { + label: 'Connect to Rust apps', + slug: 'integrations/frameworks/rust/rust-connect', + }, ], }, ], diff --git a/src/frontend/src/components/SimpleAppHostCode.shared.ts b/src/frontend/src/components/SimpleAppHostCode.shared.ts index e50a97b20..c7a0650de 100644 --- a/src/frontend/src/components/SimpleAppHostCode.shared.ts +++ b/src/frontend/src/components/SimpleAppHostCode.shared.ts @@ -170,8 +170,7 @@ var postgres = builder.AddPostgres("db") .WithDataVolume(); // Add API service and reference the database -var api = builder.AddSpringApp("api", "../api", "otel.jar") - .WithHttpEndpoint(port: 8080) +var api = builder.AddSpringBootApp("api", "../api") .WithReference(postgres) .WaitFor(postgres); diff --git a/src/frontend/src/content/docs/get-started/app-host.mdx b/src/frontend/src/content/docs/get-started/app-host.mdx index 88a38f876..08238c42e 100644 --- a/src/frontend/src/content/docs/get-started/app-host.mdx +++ b/src/frontend/src/content/docs/get-started/app-host.mdx @@ -138,7 +138,7 @@ architecture-beta This architecture demonstrates a **Java API** (using Spring Boot) connecting to a **PostgreSQL database**, with a **React frontend** consuming the API. The Java API uses Spring Boot with Spring Data JPA and connects to PostgreSQL using JDBC or Spring Data. The React frontend is built with Vite and communicates with the API over HTTP. @@ -172,7 +172,7 @@ You can represent that architecture in an AppHost like this: -

Uses AddSpringApp() for Spring Boot applications.

+

Uses AddSpringBootApp() for Spring Boot applications.

@@ -346,7 +346,7 @@ With the builder ready, define resources and services. The snippet below shows h - +

How this works:

    @@ -448,12 +448,11 @@ Next, register the API service and wire it to the PostgreSQL resource with - +

    What this does:

      -
    • AddSpringApp("api", "../api", "otel.jar") registers a Spring Boot app named api.
    • -
    • WithHttpEndpoint(port: 8080) exposes the Spring Boot app on port 8080.
    • +
    • AddSpringBootApp("api", "../api") registers a Spring Boot app named api and declares an HTTP endpoint whose port Aspire passes to the app through the SERVER_PORT environment variable.
    • WithReference(postgres) injects connection details into the API configuration.
    • WaitFor(postgres) delays the API startup until PostgreSQL is healthy.
    @@ -544,7 +543,7 @@ This example uses a Node.js (React) frontend, but Aspire treats frontends as exe - +

    Key points:

      diff --git a/src/frontend/src/content/docs/integrations/frameworks/java/java-connect.mdx b/src/frontend/src/content/docs/integrations/frameworks/java/java-connect.mdx new file mode 100644 index 000000000..bc3382ab1 --- /dev/null +++ b/src/frontend/src/content/docs/integrations/frameworks/java/java-connect.mdx @@ -0,0 +1,357 @@ +--- +title: Connect to Java apps +seoTitle: 'Connect to Java apps in Aspire: URLs and configuration' +description: Learn how other apps call your Java apps through Aspire, and how Spring Boot, Quarkus, and plain Java apps read the URLs and connection details that Aspire injects. +next: false +--- + +import { Image } from 'astro:assets'; +import { Tabs, TabItem } from '@astrojs/starlight/components'; +import ApiReference from '@components/ApiReference.astro'; +import javaIcon from '@assets/icons/java-icon.png'; + +Java logo + +This article describes how the Java apps in your Aspire solution connect to other resources, in both directions. When another resource references a Java app, Aspire injects the Java app's URL into that resource, so apps written in C#, Go, Python, TypeScript, or Java can call it without hard-coded addresses. When a Java app references other resources — such as a database, a cache, or another service — Aspire injects their URLs and connection details into the JVM as environment variables, which Spring Boot, Quarkus, and plain Java apps read through their standard configuration mechanisms. It also describes how Java apps send telemetry to the Aspire dashboard and trust the development certificate. + +For the AppHost APIs that add and configure Java apps, see [Set up Java apps in the AppHost](/integrations/frameworks/java/java-host/). If you're new to the Java integration, start with [Get started with the Java integration](/integrations/frameworks/java/java-get-started/). + +## Connect from your AppHost + +The examples in this article use the following AppHost. The `orders` Spring Boot app references a PostgreSQL database and the `catalog` Spring Boot app, and a Node.js `storefront` app references `orders`: + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const postgres = await builder.addPostgres('postgres'); +const ordersDb = await postgres.addDatabase('ordersdb'); + +const catalog = await builder.addSpringBootApp('catalog', '../catalog'); + +const orders = await builder.addSpringBootApp('orders', '../orders'); +await orders.withReference(ordersDb); +await orders.withReference(catalog); +await orders.waitFor(ordersDb); +await orders.waitFor(catalog); + +const storefront = await builder.addNodeApp('storefront', '../storefront', 'server.js'); +await storefront.withReference(orders); +await storefront.waitFor(orders); + +await builder.build().run(); +``` + + + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +var postgres = builder.AddPostgres("postgres"); +var ordersDb = postgres.AddDatabase("ordersdb"); + +var catalog = builder.AddSpringBootApp("catalog", "../catalog"); + +var orders = builder.AddSpringBootApp("orders", "../orders") + .WithReference(ordersDb) + .WithReference(catalog) + .WaitFor(ordersDb) + .WaitFor(catalog); + +builder.AddNodeApp("storefront", "../storefront", "server.js") + .WithReference(orders) + .WaitFor(orders); + +builder.Build().Run(); +``` + + + + + injects the referenced resource's connection information into the referencing resource, and delays the referencing resource until the referenced one is running — or healthy, when it has a health check. A Java app can be on either side of a reference, and the environment variables are the same whatever language the other app uses. + +## Connection properties + +Aspire passes connection information to apps as environment variables. A Java app is on both sides of that exchange: it exposes endpoints that other apps call, and it receives variables of its own. + +### Variables for apps that reference a Java app + +When a resource references a Java app, Aspire injects the URL of each of the Java app's endpoints. In the preceding AppHost, `storefront` receives the following variables for the `orders` app: + +| Environment variable | Description | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ORDERS_HTTP` | The URL of the `orders` app's `http` endpoint, named `{RESOURCE}_{ENDPOINT}`. Read this variable from apps that don't use .NET service discovery. | +| `services__orders__http__0` | The same URL, in the format that [.NET service discovery](/fundamentals/service-discovery/) reads. | + +`AddSpringBootApp` and `AddQuarkusApp` name their endpoint `http`. When you add an endpoint with a different name to a Java app, the variables use that name instead — for example, an endpoint named `admin` produces `ORDERS_ADMIN`. For the naming rules, see [Endpoint URLs](/fundamentals/environment-variables/#endpoint-urls) and [Service discovery variables](/fundamentals/environment-variables/#service-discovery-variables). + +### Variables that a Java app receives + +A Java app receives the variables of every resource that it references. In the preceding AppHost, `orders` receives `CATALOG_HTTP` and `services__catalog__http__0` for the `catalog` app, and the connection properties of the `ordersdb` database, which include: + +| Environment variable | Description | +| ------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `ORDERSDB_JDBCCONNECTIONSTRING` | The JDBC URL, in the format `jdbc:postgresql://{Host}:{Port}/{DatabaseName}`. It doesn't include the credentials. | +| `ORDERSDB_USERNAME` | The user name for authentication. | +| `ORDERSDB_PASSWORD` | The password for authentication. | +| `ORDERSDB_HOST` | The host name of the PostgreSQL server. | +| `ORDERSDB_PORT` | The port of the PostgreSQL server. | +| `ORDERSDB_DATABASENAME` | The name of the database. | + +Each resource type documents its own connection properties, such as those in [Connect to PostgreSQL](/integrations/databases/postgres/postgres-connect/#connection-properties). For the naming rules, see [Resource properties](/fundamentals/environment-variables/#resource-properties). + +The Java integration also sets the following variables on the Java app itself: + +| Environment variable | Set on | Description | +| ----------------------------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `SERVER_PORT` | Spring Boot apps | The port that the app must listen on for its `http` endpoint. Spring Boot reads it as the `server.port` setting. | +| `QUARKUS_HTTP_PORT` | Quarkus apps | The port that the app must listen on for its `http` endpoint. Quarkus reads it as the `quarkus.http.port` setting. | +| `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_SERVICE_NAME`, and the other `OTEL_*` variables | All Java resources | Where and how to export telemetry. For details, see [OpenTelemetry environment variables](/fundamentals/telemetry/#opentelemetry-environment-variables). | +| `QUARKUS_OTEL_EXPORTER_OTLP_ENDPOINT`, `QUARKUS_OTEL_SERVICE_NAME`, and similar variables | Quarkus apps, when you run the AppHost | Copies of the OpenTelemetry settings under the names that the Quarkus OpenTelemetry extension reads. | +| `QUARKUS_PROFILE`, `QUARKUS_HTTP_HOST`, and `QUARKUS_OBSERVABILITY_ENABLED` | Quarkus apps, when you run the AppHost | Dev mode settings. For details, see [Add a Quarkus app](/integrations/frameworks/java/java-host/#add-a-quarkus-app). | +| `JAVA_TOOL_OPTIONS` | Java resources that need JVM options | The options from , the `-javaagent` option for the OpenTelemetry Java agent, and the development certificate trust store. | + +For an app that you add with `AddJavaApp`, the port variable is the one that you name with the `env` parameter of `WithHttpEndpoint`. + +## Call a Java app from other apps + +Apps call a Java app at the URL that Aspire injects. Each example calls the `orders` app from the `storefront` app in the preceding AppHost. Only the language differs. + + + + +In a project that uses the Aspire service defaults, .NET service discovery resolves the `orders` name from the `services__orders__http__0` variable: + +```csharp title="Program.cs" +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.Services.AddHttpClient("orders", client => +{ + client.BaseAddress = new Uri("https+http://orders"); +}); + +var app = builder.Build(); + +app.MapGet("/orders", async (IHttpClientFactory factory) => +{ + var client = factory.CreateClient("orders"); + + return await client.GetStringAsync("/orders"); +}); + +app.Run(); +``` + +The `https+http` scheme prefers an HTTPS endpoint and falls back to HTTP. Without service discovery, read the URL from the `ORDERS_HTTP` configuration value instead. + + + + +```go title="main.go" +package main + +import ( + "fmt" + "io" + "net/http" + "os" +) + +func main() { + ordersURL := os.Getenv("ORDERS_HTTP") + + response, err := http.Get(ordersURL + "/orders") + if err != nil { + panic(err) + } + defer response.Body.Close() + + body, err := io.ReadAll(response.Body) + if err != nil { + panic(err) + } + + fmt.Println(string(body)) +} +``` + + + + +```python title="app.py" +import os +from urllib.request import urlopen + +orders_url = os.environ["ORDERS_HTTP"] + +with urlopen(f"{orders_url}/orders") as response: + print(response.read().decode()) +``` + + + + +```typescript title="server.ts" +const ordersUrl = process.env.ORDERS_HTTP; + +const response = await fetch(`${ordersUrl}/orders`); + +console.log(await response.text()); +``` + + + + +## Read Aspire configuration in your Java app + +There's no Aspire client package for Java. A Java app reads the variables that Aspire injects through its framework's standard configuration mechanism. Each example configures the `orders` app from the preceding AppHost to call the `catalog` app and to use the `ordersdb` database. Only the framework differs. + + + + +Spring Boot resolves environment variables in `${...}` placeholders, so map the database variables to the standard datasource properties: + +```properties title="src/main/resources/application.properties" +spring.datasource.url=${ORDERSDB_JDBCCONNECTIONSTRING} +spring.datasource.username=${ORDERSDB_USERNAME} +spring.datasource.password=${ORDERSDB_PASSWORD} +``` + +Inject the `catalog` app's URL wherever you create a client: + +```java title="src/main/java/com/example/orders/CatalogClient.java" +package com.example.orders; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +@Component +public class CatalogClient { + + private final RestClient restClient; + + public CatalogClient( + RestClient.Builder builder, + @Value("${CATALOG_HTTP:http://localhost:8080}") String catalogUrl) { + this.restClient = builder.baseUrl(catalogUrl).build(); + } + + public String products() { + return restClient.get() + .uri("/products") + .retrieve() + .body(String.class); + } +} +``` + + + + +Quarkus expands environment variables in `${...}` expressions, so map the variables to the REST client and datasource properties: + +```properties title="src/main/resources/application.properties" +quarkus.rest-client.catalog-api.url=${CATALOG_HTTP:http://localhost:8080} + +quarkus.datasource.jdbc.url=${ORDERSDB_JDBCCONNECTIONSTRING} +quarkus.datasource.username=${ORDERSDB_USERNAME} +quarkus.datasource.password=${ORDERSDB_PASSWORD} +``` + +The REST client picks up its URL through its configuration key: + +```java title="src/main/java/com/example/orders/CatalogClient.java" +package com.example.orders; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterRestClient(configKey = "catalog-api") +@Path("/products") +public interface CatalogClient { + + @GET + String products(); +} +``` + +Because the datasource URL is configured, Quarkus connects to the Aspire database instead of starting a [Dev Services](https://quarkus.io/guides/dev-services) container for it. + + + + +Read the variables with `System.getenv`: + +```java title="src/main/java/com/example/orders/Main.java" +package com.example.orders; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.sql.Connection; +import java.sql.DriverManager; + +public class Main { + + public static void main(String[] args) throws Exception { + var catalogUrl = System.getenv("CATALOG_HTTP"); + + var client = HttpClient.newHttpClient(); + var request = HttpRequest.newBuilder(URI.create(catalogUrl + "/products")).build(); + var products = client.send(request, HttpResponse.BodyHandlers.ofString()).body(); + + try (Connection connection = DriverManager.getConnection( + System.getenv("ORDERSDB_JDBCCONNECTIONSTRING"), + System.getenv("ORDERSDB_USERNAME"), + System.getenv("ORDERSDB_PASSWORD"))) { + // Use the connection. + } + } +} +``` + + + + +A default after the colon in a placeholder, such as `http://localhost:8080` for `CATALOG_HTTP`, keeps the app runnable outside Aspire. Placeholders without a default make the app fail to start when the variable isn't set. + +## Send telemetry to the dashboard + +Every Java resource receives the standard OpenTelemetry environment variables, but a JVM doesn't export telemetry by itself. Use one of the following: + +- **The OpenTelemetry Java agent.** The agent reads the `OTEL_*` variables directly and instruments common libraries and frameworks without code changes. To attach it, see [Attach the OpenTelemetry Java agent](/integrations/frameworks/java/java-host/#attach-the-opentelemetry-java-agent). +- **The Quarkus OpenTelemetry extension.** When you run the AppHost, a Quarkus app added with `AddQuarkusApp` receives copies of the settings under the `QUARKUS_OTEL_*` names that the extension reads. A published app needs a mapping in `application.properties`, as described in [Add a Quarkus app](/integrations/frameworks/java/java-host/#add-a-quarkus-app). + +An app that uses neither doesn't send telemetry to the dashboard, but its console output still appears in the dashboard's console logs. + +## Trust the development certificate + +Aspire points each Java resource's JVM at a trust store that contains the development certificate and the system's root certificates, through `JAVA_TOOL_OPTIONS`. HTTP clients and database drivers that use the JVM's default trust settings, such as `java.net.http.HttpClient`, therefore trust HTTPS endpoints that use the development certificate without any code changes. Code that builds its own `SSLContext` from a specific trust store doesn't use Aspire's trust store. For details, see [Configure certificate trust](/integrations/frameworks/java/java-host/#configure-certificate-trust). + +## See also + +- [Set up Java apps in the AppHost](/integrations/frameworks/java/java-host/) +- [Get started with the Java integration](/integrations/frameworks/java/java-get-started/) +- [Environment variables in Aspire](/fundamentals/environment-variables/) +- [Service discovery](/fundamentals/service-discovery/) +- [Spring Boot externalized configuration](https://docs.spring.io/spring-boot/reference/features/external-config.html) +- [Quarkus configuration reference](https://quarkus.io/guides/config-reference) diff --git a/src/frontend/src/content/docs/integrations/frameworks/java/java-get-started.mdx b/src/frontend/src/content/docs/integrations/frameworks/java/java-get-started.mdx index a7fa53072..e4e90737c 100644 --- a/src/frontend/src/content/docs/integrations/frameworks/java/java-get-started.mdx +++ b/src/frontend/src/content/docs/integrations/frameworks/java/java-get-started.mdx @@ -1,12 +1,13 @@ --- -title: Get started with Java and Aspire +title: Get started with the Java integration +seoTitle: 'Get started with Java in Aspire: Spring Boot and Quarkus' +description: Run Spring Boot, Quarkus, and other Java apps from your Aspire AppHost with Maven or Gradle builds, OpenTelemetry, debugging, and generated Dockerfiles. category: quickstart -description: Run Java applications with Aspire, model executable and container resources, and configure Maven or Gradle development workflows. +prev: false --- -import { LinkButton, TabItem, Tabs } from '@astrojs/starlight/components'; -import InstallPackage from '@components/InstallPackage.astro'; import { Image } from 'astro:assets'; +import { LinkButton, Steps } from '@astrojs/starlight/components'; import javaIcon from '@assets/icons/java-icon.png'; -The first-party `Aspire.Hosting.Java` integration adds Java applications to your [AppHost](/get-started/app-host/). It supports Spring Boot, Quarkus, Maven and Gradle wrappers, existing JARs, and prebuilt container images. +[Java](https://dev.java/) is a general-purpose, object-oriented language whose apps run on the Java Virtual Machine (JVM). It powers popular frameworks such as [Spring Boot](https://spring.io/projects/spring-boot) and [Quarkus](https://quarkus.io/). The Aspire Java hosting integration lets you model Java apps as first-class resources in your AppHost — whether they run through a [Maven](https://maven.apache.org/) or [Gradle](https://gradle.org/) wrapper, from a prebuilt JAR, or from a container image built elsewhere — and connect them to the rest of your app, regardless of the language each part is written in. :::note[New in Aspire 13.6] -This package replaces `CommunityToolkit.Aspire.Hosting.Java` for Aspire 13.6 and later. If you use the Toolkit package, follow the [migration guidance](/integrations/frameworks/java/java-host/#migrate-from-community-toolkit) before changing the package reference. +[📦 Aspire.Hosting.Java](https://www.nuget.org/packages/Aspire.Hosting.Java) is the official Java hosting integration. It replaces [📦 CommunityToolkit.Aspire.Hosting.Java](https://www.nuget.org/packages/CommunityToolkit.Aspire.Hosting.Java) for Aspire 13.6 and later. If you use the Community Toolkit package, see [Migrate from the Community Toolkit](/integrations/frameworks/java/java-host/#migrate-from-the-community-toolkit). ::: -## How the integration works +## Why use Java with Aspire -Install the integration in the AppHost, then describe how each Java application runs. `AddSpringBootApp` / `addSpringBootApp` and `AddQuarkusApp` / `addQuarkusApp` detect Maven or Gradle and configure the framework's launch mode and listening-port environment variable. Use `AddJavaApp` / `addJavaApp` for other Java applications or `AddJavaContainer` / `addJavaContainer` for an image built elsewhere. +Adding Java apps through Aspire — rather than starting each JVM by hand and wiring up ports, URLs, and credentials yourself — gives you: -Aspire configures telemetry export, certificate trust, service discovery, and VS Code debugging. For publishing, it uses your application's Dockerfile or generates a multi-stage Dockerfile. JVM options and optional Java-agent attachment use `JAVA_TOOL_OPTIONS`. +- **Framework-aware defaults.** Spring Boot and Quarkus apps are added with a single call. Aspire detects Maven or Gradle from the project's build file, launches the framework's own run goal, and delivers the app's port through the environment variable the framework already reads. +- **Builds that run for you.** Aspire launches and builds apps with the Maven or Gradle wrapper checked into each project — the same pinned tool version that your CI and your published images use — and runs a build first when an app needs one, such as a JAR that must exist before it starts. +- **Telemetry in the dashboard.** Every Java resource receives the standard OpenTelemetry exporter settings. Attach the OpenTelemetry Java agent for automatic instrumentation, or use the Quarkus OpenTelemetry extension, which Aspire points at the dashboard during local runs. +- **Service discovery and connection information.** Reference a Java app from another resource — or reference databases, caches, and other services from a Java app — and Aspire injects URLs and connection details as environment variables that your framework's configuration system can read. +- **Trusted development certificates.** Aspire gives each JVM a trust store that contains the development certificate along with the system root certificates, so HTTPS calls between local resources work without turning off certificate validation. +- **Debugging and publishing.** Debug Java apps in VS Code with the standard Aspire debugging flow, and publish them as container images built from your own Dockerfile or from one that Aspire generates. ## Prerequisites -- Install a compatible [JDK](https://adoptium.net/) and ensure `java` is available on your `PATH` for executable resources. -- Include the Maven or Gradle wrapper in your Java project and ensure its script is executable when you use those launch modes. -- Install the [Aspire CLI](/get-started/install-cli/) and meet the [Aspire prerequisites](/get-started/prerequisites/). -- Download the [OpenTelemetry Java agent](https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases) if you plan to configure an agent path. +- A JDK that matches the Java version your app targets, with `java` on your `PATH`, for each Java app that the AppHost runs locally. Aspire doesn't install a JDK; one option is [Eclipse Temurin](https://adoptium.net/). A Java app that runs from a container image uses the JDK inside that image. +- For apps built or launched with Maven or Gradle, the project's wrapper (`mvnw` or `gradlew`), checked into the app directory or into the root of its multi-module build. Aspire doesn't fall back to a globally installed `mvn` or `gradle`. To publish an app with a Dockerfile that Aspire generates, its wrapper must be in the app directory. An app that runs a prebuilt JAR doesn't need a wrapper. +- To debug in VS Code, the [Language Support for Java](https://marketplace.visualstudio.com/items?itemName=redhat.java) and [Debugger for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-debug) extensions. +- The [Aspire CLI](/get-started/install-cli/), along with the other [Aspire prerequisites](/get-started/prerequisites/). -The JDK must match your Java application's requirements. Hosting a Java service from a C# or TypeScript AppHost doesn't require JDK 25; writing the AppHost itself in Java is a separate scenario that requires JDK 25 or later. - -## Add a Java app - -Install the package in your AppHost: - - +:::note +A C# or TypeScript AppHost can host Java apps that target any JDK version. Writing the AppHost itself in Java is a separate scenario that requires JDK 25 or later. +::: -The following example assumes an existing Spring Boot project in `../java-api`, relative to your AppHost. Include its `pom.xml` or Gradle build file and the corresponding wrapper in that directory. +## How the pieces fit together - - +The Java integration is a **hosting integration**. You install it in your AppHost and add a resource for each Java app. When a Java app starts, Aspire runs it through the project's wrapper or the `java` launcher, supplies its configuration as environment variables, and shows its logs, status, and telemetry in the dashboard. -```typescript title="apphost.mts" -import { createBuilder } from './.aspire/modules/aspire.mjs'; +```mermaid +architecture-beta -const builder = await createBuilder(); + group apphost(server)[AppHost] + group javaapp(server)[Java app] -const api = await builder.addSpringBootApp('java-api', '../java-api'); + service hosting(server)[Java hosting integration] in apphost + service resource(logos:java)[Java app resource] in apphost + service launcher(server)[Wrapper or java launcher] in javaapp + service app(logos:java)[JVM process] in javaapp -await builder.build().run(); + hosting:R --> L:resource + resource:R --> L:launcher + launcher:R --> L:app ``` - - +There's no Aspire client package for Java. Your app reads the environment variables Aspire injects through its framework's standard configuration system — Spring Boot properties, Quarkus configuration, or plain `System.getenv` calls — and exports telemetry with the OpenTelemetry Java agent or the Quarkus OpenTelemetry extension. -```csharp title="AppHost.cs" -var builder = DistributedApplication.CreateBuilder(args); +Getting there is a two-step process: model your Java apps in the AppHost, then connect them to — and from — the other resources in your app. -var api = builder.AddSpringBootApp("java-api", "../java-api"); + -builder.Build().Run(); -``` +1. ### Model Java apps in your AppHost - - + Add the Java hosting integration to your AppHost, then declare a resource for each Java app. The [Set up Java apps in the AppHost](/integrations/frameworks/java/java-host/) article walks through every capability — Spring Boot and Quarkus apps, Maven goals and Gradle tasks, prebuilt JARs and containers, JVM options, the OpenTelemetry Java agent, debugging, and publishing — with side-by-side TypeScript and C# examples. -Run the AppHost: + + Set up Java apps in the AppHost + -```bash title="Run the Java application with Aspire" -aspire run -``` +2. ### Connect to and from your Java apps + + When you reference a Java app from another resource, Aspire injects the app's URLs into that resource. When you reference other resources from a Java app, Aspire injects their connection information into the JVM. See [Connect to Java apps](/integrations/frameworks/java/java-connect/) for the environment variable reference, examples in C#, Go, Python, and TypeScript, and how Spring Boot, Quarkus, and plain Java apps read Aspire configuration. -The Spring Boot helper configures `SERVER_PORT` and skips tests in its default build arguments. It doesn't add a health check: add one for `/actuator/health` only if your application includes Spring Boot Actuator. For a Quarkus application, use the [Quarkus helper](/integrations/frameworks/java/java-host/#spring-boot-and-quarkus) instead. + + Connect to Java apps + - - Set up Java apps in the AppHost - + ## See also -- [Java documentation](https://dev.java/learn/) -- [Java AppHost API reference](/integrations/frameworks/java/java-host/) +- [Learn Java](https://dev.java/learn/) +- [Spring Boot documentation](https://docs.spring.io/spring-boot/) +- [Quarkus guides](https://quarkus.io/guides/) +- [OpenTelemetry Java agent](https://opentelemetry.io/docs/zero-code/java/agent/) - [Aspire integrations overview](/integrations/overview/) diff --git a/src/frontend/src/content/docs/integrations/frameworks/java/java-host.mdx b/src/frontend/src/content/docs/integrations/frameworks/java/java-host.mdx index bfbfb07bb..06cbfab09 100644 --- a/src/frontend/src/content/docs/integrations/frameworks/java/java-host.mdx +++ b/src/frontend/src/content/docs/integrations/frameworks/java/java-host.mdx @@ -1,12 +1,13 @@ --- title: Set up Java apps in the AppHost -description: Configure Java executable and container resources, Maven and Gradle commands, JVM settings, endpoints, health checks, and publishing behavior. +seoTitle: 'Set up Java apps in the Aspire AppHost: hosting integration' +description: Learn how to use the Aspire Java hosting integration to model Spring Boot, Quarkus, and other Java apps, prebuilt JARs, and container images in your AppHost. --- -import { TabItem, Tabs } from '@astrojs/starlight/components'; -import InstallPackage from '@components/InstallPackage.astro'; -import LearnMore from '@components/LearnMore.astro'; import { Image } from 'astro:assets'; +import { Tabs, TabItem } from '@astrojs/starlight/components'; +import ApiReference from '@components/ApiReference.astro'; +import LearnMore from '@components/LearnMore.astro'; import javaIcon from '@assets/icons/java-icon.png'; -This reference covers the first-party `Aspire.Hosting.Java` integration in Aspire 13.6 and later. For an introduction, see [Get started with the Java integration](/integrations/frameworks/java/java-get-started/). Existing Toolkit users should follow [Migrate from Community Toolkit](#migrate-from-community-toolkit). +This article is the reference for the Aspire Java hosting integration. It enumerates the AppHost APIs — with examples for both `apphost.mts` and `AppHost.cs` — that you use to model Java apps in your [AppHost](/get-started/app-host/): Spring Boot and Quarkus apps with framework-aware defaults, apps that launch through a Maven goal or a Gradle task, prebuilt JARs, and container images that were built elsewhere. It also describes how Aspire builds those apps, passes them JVM options, attaches the OpenTelemetry Java agent, configures certificate trust, debugs them, and packages them into container images when you publish. + +If you're new to the Java integration, start with [Get started with the Java integration](/integrations/frameworks/java/java-get-started/). For how other resources call your Java apps — and how your Java apps read the configuration that Aspire injects — see [Connect to Java apps](/integrations/frameworks/java/java-connect/). If you're moving from the Community Toolkit package, see [Migrate from the Community Toolkit](#migrate-from-the-community-toolkit). + +:::note[Prerequisites] +Each Java app that the AppHost runs on your machine needs a JDK for the Java version that the app targets, with `java` on your `PATH`. Apps that Aspire builds or launches with Maven or Gradle also need the project's [Maven Wrapper](https://maven.apache.org/tools/wrapper/) or [Gradle Wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html). Apps that run a prebuilt JAR don't need a wrapper, and apps that run from a container image use the JDK inside the image. To debug Java apps in VS Code, install the [Language Support for Java](https://marketplace.visualstudio.com/items?itemName=redhat.java) and [Debugger for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-debug) extensions. +::: ## Installation -Install the hosting package in your AppHost: +To start modeling Java apps in your AppHost, install the [📦 Aspire.Hosting.Java](https://www.nuget.org/packages/Aspire.Hosting.Java) NuGet package: + + + + +```bash title="Terminal" +aspire add java +``` + + + Learn more about [`aspire add`](/reference/cli/commands/aspire-add/) in the command reference. + + +This updates your `aspire.config.json` with the Java hosting integration package: + +```json title="aspire.config.json" ins={3} +{ + "packages": { + "Aspire.Hosting.Java": "%ASPIRE_VERSION_PREVIEW%" + } +} +``` + + + + +```bash title="Terminal" +aspire add java +``` + + + Learn more about [`aspire add`](/reference/cli/commands/aspire-add/) in the command reference. + - +Or, choose a manual installation approach: + +```csharp title="AppHost.cs" +#:package Aspire.Hosting.Java@%ASPIRE_VERSION_PREVIEW% +``` + +```xml title="AppHost.csproj" + +``` -Executable Java resources require a JDK compatible with the application on `PATH`. Wrapper-based builds and launches require a checked-in Maven or Gradle wrapper; Aspire doesn't fall back to a machine-global `mvn` or `gradle`. Running a prebuilt JAR needs no wrapper, and a prebuilt container uses the JDK inside its image. + + -## Spring Boot and Quarkus +## Add a Spring Boot app -Use the framework helper when your app directory contains its Maven or Gradle build file. Aspire detects the build tool, selects the framework's launch goal, and configures an HTTP endpoint with the port environment variable that framework reads. +Use to add a [Spring Boot](https://spring.io/projects/spring-boot) app whose Maven or Gradle build file is in the app directory. The app directory is resolved relative to the AppHost directory. It's the app's working directory and, when you publish, the build context of its container image. -```typescript title="apphost.mts" +```typescript title="apphost.mts" twoslash import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); const catalog = await builder.addSpringBootApp('catalog', '../catalog'); +await catalog.withHttpHealthCheck({ path: '/actuator/health' }); + +const orders = await builder.addSpringBootApp('orders', '../orders'); +await orders.withReference(catalog); +await orders.waitFor(catalog); + +await builder.build().run(); +``` + + + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +var catalog = builder.AddSpringBootApp("catalog", "../catalog") + .WithHttpHealthCheck("/actuator/health"); + +builder.AddSpringBootApp("orders", "../orders") + .WithReference(catalog) + .WaitFor(catalog); + +builder.Build().Run(); +``` + + + + +In the preceding example, makes Aspire poll the `catalog` app's `/actuator/health` endpoint. The `orders` app uses to receive the `catalog` app's URL, and to start only after `catalog` is healthy. + +When a Spring Boot app starts, Aspire: + +- Detects the build tool from the app directory: `pom.xml` selects Maven, and `build.gradle`, `build.gradle.kts`, `settings.gradle`, or `settings.gradle.kts` selects Gradle. If the directory contains neither — or both — the app fails to start with an error. For apps that are laid out differently, see [Add a Java app](#add-a-java-app). +- Launches the app through the project's wrapper with the Spring Boot plugin's run goal: `spring-boot:run` for Maven, or `bootRun` for Gradle. +- Adds an HTTP endpoint named `http` and passes its port to the app in the `SERVER_PORT` environment variable, which Spring Boot uses as its `server.port` setting. No fixed port is set, so several Spring Boot apps can run side by side. + +The helper doesn't add a health check, because `/actuator/health` responds only when the app depends on `spring-boot-starter-actuator`. A health check against a missing endpoint would leave the app permanently unhealthy and block every resource that waits for it. + +When you publish, the app is packaged inside its container image with `-B -ntp -DskipTests package` for Maven or `build -x test` for Gradle. To change those arguments, see [Build before running](#build-before-running). + +## Add a Quarkus app + +Use to add a [Quarkus](https://quarkus.io/) app. The build tool is detected the same way as for Spring Boot apps. The app runs in Quarkus dev mode — `quarkus:dev` for Maven or `quarkusDev` for Gradle — so live coding keeps working while the AppHost runs, and its HTTP endpoint's port reaches the app in the `QUARKUS_HTTP_PORT` environment variable. + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const pricing = await builder.addQuarkusApp('pricing', '../pricing'); +await pricing.withHttpHealthCheck({ path: '/q/health' }); + const inventory = await builder.addQuarkusApp('inventory', '../inventory'); +await inventory.withReference(pricing); +await inventory.waitFor(pricing); await builder.build().run(); ``` @@ -53,8 +158,12 @@ await builder.build().run(); ```csharp title="AppHost.cs" var builder = DistributedApplication.CreateBuilder(args); -var catalog = builder.AddSpringBootApp("catalog", "../catalog"); -var inventory = builder.AddQuarkusApp("inventory", "../inventory"); +var pricing = builder.AddQuarkusApp("pricing", "../pricing") + .WithHttpHealthCheck("/q/health"); + +builder.AddQuarkusApp("inventory", "../inventory") + .WithReference(pricing) + .WaitFor(pricing); builder.Build().Run(); ``` @@ -62,58 +171,74 @@ builder.Build().Run(); -Spring Boot uses `SERVER_PORT`; Quarkus uses `QUARKUS_HTTP_PORT` and runs in dev mode locally. The default build arguments skip tests. Configure `WithMavenBuild` / `withMavenBuild` or `WithGradleBuild` / `withGradleBuild` to override them. +When you run the AppHost, the helper also sets the following environment variables on the app: -Neither helper assumes a health extension is installed. Add a health check for `/actuator/health` only with Spring Boot Actuator, or `/q/health` only with Quarkus SmallRye Health. +| Environment variable | Value | Purpose | +| ------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `QUARKUS_PROFILE` | `dev` | Selects the `dev` profile, including when a debugger launches the packaged app instead of dev mode. | +| `QUARKUS_HTTP_HOST` | `0.0.0.0` | Binds all interfaces. When Quarkus binds a localhost name, its `Host` header validation rejects requests to the endpoint URL that the Aspire dashboard shows with `400 Bad Request`. | +| `QUARKUS_OBSERVABILITY_ENABLED` | `false` | Turns off the observability Dev Service, which would otherwise pull and start a `grafana/otel-lgtm` container and send the app's telemetry there instead of to the Aspire dashboard. | -For Quarkus applications using `quarkus-opentelemetry`, the helper maps Aspire's telemetry configuration to `QUARKUS_OTEL_*` and disables the observability Dev Service so it doesn't redirect telemetry to a separate container. Don't also attach the OpenTelemetry Java agent to that application. +The [Quarkus OpenTelemetry extension](https://quarkus.io/guides/opentelemetry) reads its own `quarkus.otel.*` configuration rather than the standard `OTEL_*` environment variables that Aspire sets. So that the app's telemetry reaches the dashboard, the helper copies Aspire's OpenTelemetry settings to the matching `QUARKUS_OTEL_*` variables, such as `QUARKUS_OTEL_EXPORTER_OTLP_ENDPOINT` and `QUARKUS_OTEL_SERVICE_NAME`. A Quarkus app that uses the extension doesn't need the [OpenTelemetry Java agent](#attach-the-opentelemetry-java-agent), so don't attach both. -For a published Quarkus container using the extension, add these expressions to the Java application's `application.properties` so it reads deployment-supplied telemetry configuration at startup: +The helper copies these settings only when you run the AppHost. A published app receives the standard `OTEL_*` variables from its deployment environment, so map them in the app's `application.properties`: ```properties title="application.properties" quarkus.otel.exporter.otlp.endpoint=${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4317} quarkus.otel.exporter.otlp.protocol=${OTEL_EXPORTER_OTLP_PROTOCOL:grpc} +quarkus.otel.service.name=${OTEL_SERVICE_NAME:inventory} ``` -## Add an executable Java app +Quarkus expands these expressions when the app starts. The endpoint and protocol defaults match the extension's own, and the service name falls back to the app's name, so the app also runs outside Aspire. Keep the expressions in `application.properties` rather than passing them as environment variables, because Docker Compose interprets `${...}` in environment variables itself and rejects this syntax. + +Other [Quarkus Dev Services](https://quarkus.io/guides/dev-services) stay enabled. A Dev Service starts its own container only when the configuration it provides is missing. When your app references an Aspire resource, such as a database, map the connection information that Aspire injects to the matching Quarkus property — for example, `quarkus.datasource.jdbc.url` — and Quarkus uses the Aspire resource instead. For examples, see [Connect to Java apps](/integrations/frameworks/java/java-connect/#read-aspire-configuration-in-your-java-app). -`AddJavaApp` / `addJavaApp` creates a Java executable resource with a working directory relative to the AppHost directory. Configure it with a Maven or Gradle task, or add a JAR path. +As with Spring Boot, no health check is added, because `/q/health` responds only when the app depends on the `quarkus-smallrye-health` extension. When you publish, Quarkus apps are packaged with the same default arguments as Spring Boot apps. -### Run a Maven or Gradle task +## Add a Java app -`WithMavenGoal` / `withMavenGoal` and `WithGradleTask` / `withGradleTask` launch through the respective wrapper in run mode. Maven defaults to `mvnw` (`mvnw.cmd` on Windows), and Gradle defaults to `gradlew` (`gradlew.bat` on Windows). Build-tool arguments belong to these methods; application arguments belong to the JAR overload or `WithArgs` / `withArgs`. +Use for any other Java app — one that's built on another framework, one that needs a different launch goal, or one whose build files aren't in the app directory. Configure exactly one of the following launch modes: + +| Launch mode | Configure it with | What Aspire runs | +| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| Maven goal | | The Maven wrapper with the goal and its arguments | +| Gradle task | | The Gradle wrapper with the task and its arguments | +| Prebuilt JAR | with a JAR path | `java -jar` with the JAR path and the app's arguments | + +An app without a launch mode fails to start. Mixing launch modes — a JAR with a Maven goal or Gradle task, or Maven with Gradle — throws an exception when you configure it. Calling the same method again replaces the earlier goal or task. -```typescript title="apphost.mts" +```typescript title="apphost.mts" twoslash import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); -const mavenApp = await builder - .addJavaApp('maven-api', '../maven-api') - .withMavenGoal('spring-boot:run', ['-Dspring-boot.run.profiles=dev']); +const catalog = await builder.addJavaApp('catalog', '../catalog'); +await catalog.withMavenGoal('spring-boot:run', ['-Dspring-boot.run.profiles=dev']); +await catalog.withHttpEndpoint({ env: 'SERVER_PORT' }); -const gradleApp = await builder - .addJavaApp('gradle-api', '../gradle-api') - .withGradleTask('bootRun', ['--args=--spring.profiles.active=dev']); +const orders = await builder.addJavaApp('orders', '../orders'); +await orders.withGradleTask('bootRun', ['--args=--spring.profiles.active=dev']); +await orders.withHttpEndpoint({ env: 'SERVER_PORT' }); await builder.build().run(); ``` - ```csharp title="AppHost.cs" var builder = DistributedApplication.CreateBuilder(args); -var mavenApp = builder.AddJavaApp("maven-api", "../maven-api") - .WithMavenGoal("spring-boot:run", "-Dspring-boot.run.profiles=dev"); +builder.AddJavaApp("catalog", "../catalog") + .WithMavenGoal("spring-boot:run", "-Dspring-boot.run.profiles=dev") + .WithHttpEndpoint(env: "SERVER_PORT"); -var gradleApp = builder.AddJavaApp("gradle-api", "../gradle-api") - .WithGradleTask("bootRun", "--args=--spring.profiles.active=dev"); +builder.AddJavaApp("orders", "../orders") + .WithGradleTask("bootRun", "--args=--spring.profiles.active=dev") + .WithHttpEndpoint(env: "SERVER_PORT"); builder.Build().Run(); ``` @@ -121,42 +246,43 @@ builder.Build().Run(); -Do not combine a Maven goal or Gradle task with a JAR path; the integration rejects that configuration. +Arguments that you pass with the goal or task go to the build tool. In TypeScript, the arguments array is required, so pass `[]` when there are none. Arguments that you add with are also placed after the goal or task, so in these launch modes they reach the build tool rather than your app. To pass arguments to the app itself, use the build plugin's option for it, such as `-Dspring-boot.run.arguments=...` for the Spring Boot Maven plugin or `--args=...` for the Spring Boot Gradle plugin. -### Run a JAR +Unlike the framework helpers, `AddJavaApp` doesn't declare an endpoint. Add one with , and use its `env` parameter to pass the port that Aspire allocates in the environment variable that your app reads, as the preceding example does with `SERVER_PORT`. Leave the target port unset: Java apps run as processes on your machine, so two apps with the same fixed port would collide. -Provide a JAR path relative to the resource working directory. The resource runs `java -jar ` followed by the application arguments. +### Run a prebuilt JAR + +Pass a JAR path to the JAR overload of `AddJavaApp` to run the app with `java -jar`. The JAR path is relative to the app directory, and the remaining arguments are the app's own arguments, which Aspire passes after the JAR path. In TypeScript, this overload is named `addJavaAppWithJar`, and its arguments array is required. -```typescript title="apphost.mts" +```typescript title="apphost.mts" twoslash import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); -const app = await builder.addJavaAppWithJar( - 'java-api', - '../java-api', - 'target/java-api.jar', - ['--spring.main.banner-mode=off'] +await builder.addJavaAppWithJar( + 'worker', + '../worker', + 'target/worker.jar', + ['--interval-seconds', '10'] ); await builder.build().run(); ``` - ```csharp title="AppHost.cs" var builder = DistributedApplication.CreateBuilder(args); -var app = builder.AddJavaApp( - name: "java-api", - appDirectory: "../java-api", - jarPath: "target/java-api.jar", - args: ["--spring.main.banner-mode=off"]); +builder.AddJavaApp( + "worker", + "../worker", + "target/worker.jar", + "--interval-seconds", "10"); builder.Build().Run(); ``` @@ -164,45 +290,40 @@ builder.Build().Run(); -The TypeScript export is named `addJavaAppWithJar`; pass an empty array when there are no application arguments. To select a different JAR for a generated container build, use `WithJarArtifact` / `withJarArtifact`. That publishing choice doesn't replace the local launch JAR. +The JAR must exist before the app starts. When the project's own build produces it, have Aspire run that build first, as described in [Build before running](#build-before-running). When you publish, the JAR path selects which of the build's JARs the image runs. For more information, see [Select the published JAR](#select-the-published-jar). ## Build before running -`WithMavenBuild` / `withMavenBuild` and `WithGradleBuild` / `withGradleBuild` configure the build before a JAR launch and the build inside a generated container image. Maven runs `clean package` by default and Gradle runs `clean build` by default; passing arguments replaces those defaults, so include the packaging goal. - -For JAR launches, a child build resource runs before the app starts. A Maven goal or Gradle task normally performs its own local compilation, so Aspire avoids an extra build step unless one is needed, such as to produce an OpenTelemetry agent before launch. +Use or to build an app with its wrapper before it starts — typically an app that runs a JAR its own build produces. When you run the AppHost, Aspire adds a child resource named `-maven-build` or `-gradle-build` that runs the build, and the app waits for that resource to finish successfully. When you publish, the same arguments build the app inside its generated container image, so they should produce the deployable JAR. -```typescript title="apphost.mts" +```typescript title="apphost.mts" twoslash import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); -const mavenApp = await builder - .addJavaAppWithJar('maven-api', '../maven-api', 'target/api.jar', []) - .withMavenBuild(['clean', 'package', '-DskipTests']); +const worker = await builder.addJavaAppWithJar('worker', '../worker', 'target/worker.jar', []); +await worker.withMavenBuild(['-B', '-ntp', '-DskipTests', 'package']); -const gradleApp = await builder - .addJavaAppWithJar('gradle-api', '../gradle-api', 'build/libs/api.jar', []) - .withGradleBuild(['clean', 'build', '-x', 'test', '--parallel']); +const reports = await builder.addJavaAppWithJar('reports', '../reports', 'build/libs/reports.jar', []); +await reports.withGradleBuild(['build', '-x', 'test']); await builder.build().run(); ``` - ```csharp title="AppHost.cs" var builder = DistributedApplication.CreateBuilder(args); -var mavenApp = builder.AddJavaApp("maven-api", "../maven-api", "target/api.jar") - .WithMavenBuild("clean", "package", "-DskipTests"); +builder.AddJavaApp("worker", "../worker", "target/worker.jar") + .WithMavenBuild("-B", "-ntp", "-DskipTests", "package"); -var gradleApp = builder.AddJavaApp("gradle-api", "../gradle-api", "build/libs/api.jar") - .WithGradleBuild("clean", "build", "-x", "test", "--parallel"); +builder.AddJavaApp("reports", "../reports", "build/libs/reports.jar") + .WithGradleBuild("build", "-x", "test"); builder.Build().Run(); ``` @@ -210,32 +331,40 @@ builder.Build().Run(); -Use `WithWrapperPath` / `withWrapperPath` to select a custom wrapper before or after configuring the build tool. Keep it inside the app directory when publishing, because that directory is the container build context. +By default, Maven builds with `clean package` and Gradle builds with `clean build`. Arguments that you pass replace the defaults, so include the packaging goal or task. An app is built with one tool only: combining the Maven and Gradle methods on the same app throws an exception. + +A Maven goal or Gradle task compiles the app as it launches. For apps that use one — including apps added with `AddSpringBootApp` or `AddQuarkusApp` — Aspire keeps the build arguments for publishing without adding a build resource. Two cases still build first: an app whose [OpenTelemetry agent the build produces](#attach-the-opentelemetry-java-agent), and a Quarkus app that [VS Code launches for debugging](#debug-java-apps). On a Spring Boot or Quarkus app, calling `WithMavenBuild` or `WithGradleBuild` replaces the default packaging arguments and keeps the framework's launch goal. + +### Use a custom wrapper + +Aspire builds and launches apps only through the project's wrapper — `mvnw` or `gradlew`, or `mvnw.cmd` or `gradlew.bat` on Windows — so that the AppHost, your CI, and your published images all use the tool version that the repository pins. It doesn't fall back to a globally installed `mvn` or `gradle`. + +Aspire looks for the wrapper in the app directory first. If the wrapper isn't there, Aspire checks each parent directory up to the repository root, and uses a wrapper that sits next to the root build file of a multi-module build: a Maven `pom.xml`, or a Gradle `settings.gradle` or `settings.gradle.kts`. If there's no wrapper, the app fails to start with an error that suggests generating one with `mvn -N wrapper:wrapper` or `gradle wrapper`. + +To use a wrapper in another location or with another name, call . The path is absolute, or relative to the app's working directory — the app directory, unless you change it. You can call the method before or after the one that selects the build tool. -```typescript title="apphost.mts" +```typescript title="apphost.mts" twoslash import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); -const app = await builder - .addJavaApp('java-api', '../java-api') - .withWrapperPath('tools/mvnw') - .withMavenGoal('spring-boot:run', []); +const catalog = await builder.addJavaApp('catalog', '../catalog'); +await catalog.withWrapperPath('tools/mvnw'); +await catalog.withMavenGoal('spring-boot:run', []); await builder.build().run(); ``` - ```csharp title="AppHost.cs" var builder = DistributedApplication.CreateBuilder(args); -var app = builder.AddJavaApp("java-api", "../java-api") +builder.AddJavaApp("catalog", "../catalog") .WithWrapperPath("tools/mvnw") .WithMavenGoal("spring-boot:run"); @@ -245,38 +374,34 @@ builder.Build().Run(); -## Configure JVM and OpenTelemetry options +When Aspire generates the Dockerfile to publish the app, the wrapper must be inside the app directory, because that directory is the container build context. This applies even when a local run finds the wrapper in a parent directory. For more information, see [Publish Java apps](#publish-java-apps). + +## Configure JVM options -`WithJvmArgs` / `withJvmArgs` appends JVM options to `JAVA_TOOL_OPTIONS`, which works with JAR, Maven, Gradle, and container launches. The Java resource already configures the OTLP exporter. Use `WithOtelAgent` / `withOtelAgent` when you also need Java-agent automatic instrumentation. +Use to pass options to the JVM, such as heap sizes or system properties. Aspire appends them to the `JAVA_TOOL_OPTIONS` environment variable, and quotes values that contain spaces. Every JVM reads that variable when it starts — including the build tool's own JVM and the app JVM it starts in a Maven goal or Gradle task — so the options apply in every launch mode, and to [Java containers](#add-a-java-container) as well. -```typescript title="apphost.mts" +```typescript title="apphost.mts" twoslash import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); -const app = await builder - .addJavaApp('java-api', '../java-api') - .withMavenGoal('spring-boot:run', []) - .withJvmArgs(['-Xms256m', '-Xmx512m']) - .withOtelAgent('agents/opentelemetry-javaagent.jar'); +const catalog = await builder.addSpringBootApp('catalog', '../catalog'); +await catalog.withJvmArgs(['-Xms256m', '-Xmx512m', '-Dfile.encoding=UTF-8']); await builder.build().run(); ``` - ```csharp title="AppHost.cs" var builder = DistributedApplication.CreateBuilder(args); -var app = builder.AddJavaApp("java-api", "../java-api") - .WithMavenGoal("spring-boot:run") - .WithJvmArgs(["-Xms256m", "-Xmx512m"]) - .WithOtelAgent("agents/opentelemetry-javaagent.jar"); +builder.AddSpringBootApp("catalog", "../catalog") + .WithJvmArgs("-Xms256m", "-Xmx512m", "-Dfile.encoding=UTF-8"); builder.Build().Run(); ``` @@ -284,41 +409,37 @@ builder.Build().Run(); -Download the agent JAR or produce it as part of your build. Relative agent paths resolve inside the Java app directory. The parameterless C# `WithOtelAgent()` and TypeScript `withOtelAgentDefaultPath()` look for an agent under `target/agent/` for Maven or `build/agent/` for Gradle; they do attach an agent, rather than only configuring export. +Aspire also uses `JAVA_TOOL_OPTIONS` for the OpenTelemetry Java agent and the development certificate trust store, and adds your options alongside those. When a JVM reads the variable, it prints a `Picked up JAVA_TOOL_OPTIONS:` line to standard error, so that line appears in the app's console logs. + +## Attach the OpenTelemetry Java agent -## Add a Java container app +Aspire sets the standard `OTEL_*` environment variables on every Java resource, such as `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_SERVICE_NAME`, so that telemetry can reach the Aspire dashboard. A JVM doesn't export telemetry by itself, though. To produce traces, metrics, and logs without changing your code, attach the [OpenTelemetry Java agent](https://opentelemetry.io/docs/zero-code/java/agent/), which instruments common libraries and frameworks automatically and reads those variables. Aspire doesn't download the agent — your build provides it, or you point Aspire at a copy that already exists: -`AddJavaContainer` / `addJavaContainer` models an existing container image and configures the OTLP exporter. Aspire runs the image as-is, without rebuilding it or adding an endpoint. Configure the image's listening port explicitly. +- Without an argument, ']} /> loads the agent from where the build writes it by convention: `target/agent/opentelemetry-javaagent.jar` for Maven, or `build/agent/opentelemetry-javaagent.jar` for Gradle. Aspire needs to know the build tool, so use it on an app added with `AddSpringBootApp`, or on one that calls `WithMavenBuild` or `WithGradleBuild`. +- With a path, ', 'string']} /> loads the agent from that path. A relative path is resolved against the app directory and names a file that the build produces. An absolute path names a file that already exists, such as an agent installed on the machine, and doesn't need a build. -```typescript title="apphost.mts" +```typescript title="apphost.mts" twoslash import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); -const app = await builder - .addJavaContainer('java-api', 'ghcr.io/contoso/java-api', { tag: '1.0' }) - .withJvmArgs(['-Djava.awt.headless=true']) - .withHttpEndpoint({ targetPort: 8080, env: 'SERVER_PORT' }); +const catalog = await builder.addSpringBootApp('catalog', '../catalog'); +await catalog.withOtelAgentDefaultPath(); await builder.build().run(); ``` - ```csharp title="AppHost.cs" var builder = DistributedApplication.CreateBuilder(args); -var app = builder.AddJavaContainer( - "java-api", - "ghcr.io/contoso/java-api", - tag: "1.0") - .WithJvmArgs(["-Djava.awt.headless=true"]) - .WithHttpEndpoint(targetPort: 8080, env: "SERVER_PORT"); +builder.AddSpringBootApp("catalog", "../catalog") + .WithOtelAgent(); builder.Build().Run(); ``` @@ -326,48 +447,264 @@ builder.Build().Run(); -`WithOtelAgent` isn't supported on prebuilt Java containers. If the image contains an agent, enable it with a `-javaagent:` JVM argument that refers to the path inside that image. +When the build produces the agent, Aspire runs that build as a `-maven-build` or `-gradle-build` resource before the app starts — even for a Maven goal or Gradle task that would otherwise compile the app itself. The agent has to exist first, because `JAVA_TOOL_OPTIONS` loads it into the build tool's own JVM too. Calling the method again replaces the agent path. + +The following build configuration copies the agent to the conventional location. Replace the version with the latest [agent release](https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases): + + + + +Binding the copy to the `initialize` phase writes the agent at the start of every Maven build, including the build that Aspire runs before the app. + +```xml title="pom.xml" + + 2.24.0 + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-opentelemetry-javaagent + initialize + + copy + + + + + io.opentelemetry.javaagent + opentelemetry-javaagent + ${opentelemetry-javaagent.version} + ${project.build.directory}/agent + opentelemetry-javaagent.jar + + + true + + + + + + +``` + + + + +A separate configuration keeps the agent off the app's classpath, and the `bootRun` and `classes` tasks depend on the copy so that the agent exists before the app starts. + +```groovy title="build.gradle" +configurations { + opentelemetryAgent { + canBeConsumed = false + canBeResolved = true + } +} + +dependencies { + opentelemetryAgent 'io.opentelemetry.javaagent:opentelemetry-javaagent:2.24.0' +} + +tasks.register('copyOpenTelemetryAgent', Copy) { + from configurations.opentelemetryAgent + into layout.buildDirectory.dir('agent') + rename '.*', 'opentelemetry-javaagent.jar' +} + +tasks.named('bootRun') { + dependsOn 'copyOpenTelemetryAgent' +} + +tasks.named('classes') { + dependsOn 'copyOpenTelemetryAgent' +} +``` + + + + +When you publish with a generated Dockerfile, Aspire copies a build-produced agent into the image and points the JVM at that copy. An absolute agent path is used unchanged, so the agent must already exist at that path inside the image. If you author your own Dockerfile, copy the agent in that Dockerfile and pass its absolute path inside the image, because Aspire copies a build-produced agent only into Dockerfiles that it generates. -## Configure endpoints, environment, health checks, and references +## Add a Java container -Java executable and container resources support standard AppHost endpoints, environment variables, health checks, waits, and service discovery. Pass Spring Boot's listening port through an endpoint environment variable, add a health-check path, and reference the resource from consumers: +Use when a Java app ships as a container image that's built elsewhere, such as by a separate pipeline or another team. Aspire runs the image as-is and never rebuilds it, so the JAR, the JDK, and any agent all come from the image. The resource still receives the OpenTelemetry exporter settings and the development certificate trust store, and it supports `WithJvmArgs`. -```typescript title="apphost.mts" +```typescript title="apphost.mts" twoslash import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); -const api = await builder - .addJavaApp('java-api', '../java-api') - .withMavenGoal('spring-boot:run', []) - .withHttpEndpoint({ targetPort: 8080, env: 'SERVER_PORT' }) - .withEnvironment('SPRING_PROFILES_ACTIVE', 'development') - .withHttpHealthCheck({ path: '/actuator/health' }); +const catalog = await builder.addJavaContainer('catalog', 'mycompany/catalog', { tag: '1.4.0' }); +await catalog.withHttpEndpoint({ targetPort: 8080 }); +await catalog.withJvmArgs(['-Xmx512m']); + +await builder.build().run(); +``` + + + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddJavaContainer("catalog", "mycompany/catalog", "1.4.0") + .WithHttpEndpoint(targetPort: 8080) + .WithJvmArgs("-Xmx512m"); + +builder.Build().Run(); +``` + + + + +Aspire doesn't add an endpoint to a Java container, because the port that the app listens on is a property of the image. Declare it with `WithHttpEndpoint` and a target port — 8080 is the default for both Spring Boot and Quarkus. + +The OpenTelemetry agent methods aren't available on a Java container, because there's no build to take the agent from. If the image already includes the agent, enable it with a JVM option that points to its location inside the image, such as `-javaagent:/app/opentelemetry-javaagent.jar`. -const web = await builder.addProject('web', '../Web/Web.csproj'); -await web.withReference(api); +## Configure certificate trust + +Aspire configures each Java resource to trust the development certificate, so HTTPS calls to other local resources succeed without turning off certificate validation. It creates a PKCS12 trust store that contains the development certificate together with the system's root certificates, and points the JVM at it by adding `-Djavax.net.ssl.trustStore` and `-Djavax.net.ssl.trustStoreType=PKCS12` to `JAVA_TOOL_OPTIONS`. + +A trust store that's passed to the JVM replaces the JVM's default trusted certificates rather than adding to them, so the default [trust scope](/app-host/certificate-configuration/#certificate-trust-scopes) for Java resources is [System](/app-host/certificate-configuration/#system-mode). If you change a Java resource's scope to Append with , Aspire can't add the certificate without replacing the defaults, so it leaves the JVM's trust store unchanged and logs a message. + + + For more information, see [Certificate trust configuration](/app-host/certificate-configuration/#certificate-trust-configuration). + + +## Debug Java apps + +To debug Java apps, use the standard debugging flow of the [Aspire VS Code extension](/get-started/aspire-vscode-extension/) with the [Language Support for Java](https://marketplace.visualstudio.com/items?itemName=redhat.java) and [Debugger for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-debug) extensions installed. Debugging is enabled for every app that you add with `AddJavaApp`, `AddSpringBootApp`, or `AddQuarkusApp`. + +When the debugger launches a Java app, VS Code starts the app's JVM directly instead of going through the wrapper, because goals such as `spring-boot:run` and `bootRun` start a second JVM that a debugger attached to the wrapper would never see. Aspire drops the launch goal or task and its arguments for that launch, and passes the app's environment variables — including `JAVA_TOOL_OPTIONS` — and the arguments that you add with `WithArgs` to the debugger. Settings that you pass only through goal or task arguments, such as `-Dspring-boot.run.profiles=dev`, don't apply to a debug launch, so use an environment variable, such as `SPRING_PROFILES_ACTIVE`, for settings that should apply to both. + +The debugger needs the app's main class. Aspire reads it from the JAR that the project's most recent build left in its output directory: the `Start-Class` that the Spring Boot plugins record, or a plain JAR's `Main-Class`. If there's no such JAR, the Java debugger looks for the main class in the project, and asks you to choose one when it finds more than one. To choose the main class in the AppHost instead, call , which affects only launches from the IDE: + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const catalog = await builder.addSpringBootApp('catalog', '../catalog'); +await catalog.withMainClass('com.example.catalog.CatalogApplication'); await builder.build().run(); ``` + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddSpringBootApp("catalog", "../catalog") + .WithMainClass("com.example.catalog.CatalogApplication"); + +builder.Build().Run(); +``` + + + + +For a prebuilt JAR, the debugger puts the JAR on the classpath and launches the `Main-Class` from its manifest, as `java -jar` does, unless you choose another class with `WithMainClass`. A Quarkus app is launched from the fast JAR that its build packages (`quarkus-app/quarkus-run.jar` in the build output), so when VS Code launches a Quarkus app, Aspire runs the app's build first. Breakpoints in your source still bind. + +## Publish Java apps + +When you run `aspire publish` or `aspire deploy`, Aspire packages each Java app as a container image. If the app directory contains a `Dockerfile`, Aspire uses it as-is. Otherwise, Aspire generates a multi-stage Dockerfile that: + +- Builds the project inside the image with the project's wrapper and the arguments from `WithMavenBuild` or `WithGradleBuild`. Without either method, it packages the app with `-B -ntp -DskipTests package` for Maven or `-x test build` for Gradle. Dependency caches are kept in a BuildKit cache mount rather than in an image layer. +- Detects the Java version that the project targets from `pom.xml` or the Gradle build file, defaulting to Java 21, and runs the app on the matching `eclipse-temurin` JRE image. The build stage uses the matching JDK image, unless the Maven or Gradle version that the wrapper pins can't run on that JDK, in which case it uses the nearest JDK version that the tool supports. +- Runs the app as an unprivileged user (`USER 999:999`) with the JVM as process ID 1, so the JVM receives `SIGTERM` directly and shutdown hooks run. +- Copies a build-produced [OpenTelemetry agent](#attach-the-opentelemetry-java-agent) into the image. + +The app directory is the build context, so everything that the build needs must be inside it, including the wrapper when Aspire generates the Dockerfile. For the same reason, changing a Java app's working directory after you add it isn't supported when Aspire generates the Dockerfile. Java containers that you add with `AddJavaContainer` keep using the image that you supply. + + + For more information about publishing, see [Aspire deployment](/deployment/). + + +### Select the published JAR + +A JAR path doesn't by itself mean that the image copies a prebuilt JAR. When the app directory contains a Maven or Gradle build file — or the app has a build step, Maven goal, or Gradle task — the image builds the project, and Aspire selects the JAR to run from the build output in this order: + +1. The JAR that you name with . +1. For a Quarkus app, the artifact that the build's `quarkus-artifact.properties` file names, together with the dependencies that it needs. All three Quarkus packaging types — `fast-jar`, `legacy-jar`, and `uber-jar` — work without extra configuration. +1. The JAR path that you passed to `AddJavaApp`. +1. The only JAR that the build produced, ignoring `-plain`, `-sources`, and `-javadoc` JARs. + +If the last step finds more than one candidate — such as the `original-*.jar` that a shade plugin leaves beside the shaded JAR — the image build fails and lists the candidates. Name the right one with `WithJarArtifact`. The method affects only publishing, so an app can run one JAR locally and publish another. + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const catalog = await builder.addSpringBootApp('catalog', '../catalog'); +await catalog.withJarArtifact('target/catalog-0.0.1-SNAPSHOT-exec.jar'); + +await builder.build().run(); +``` + ```csharp title="AppHost.cs" var builder = DistributedApplication.CreateBuilder(args); -var api = builder.AddJavaApp("java-api", "../java-api") - .WithMavenGoal("spring-boot:run") - .WithHttpEndpoint(targetPort: 8080, env: "SERVER_PORT") - .WithEnvironment("SPRING_PROFILES_ACTIVE", "development") - .WithHttpHealthCheck("/actuator/health"); +builder.AddSpringBootApp("catalog", "../catalog") + .WithJarArtifact("target/catalog-0.0.1-SNAPSHOT-exec.jar"); + +builder.Build().Run(); +``` + + + + +Only an app directory without a build file, for an app without a build step, Maven goal, or Gradle task, publishes by copying the prebuilt JAR into the image. Build output directories usually aren't in source control, so building inside the image avoids publishing a stale local JAR, and still works in a clean clone or on a CI agent. + +### Customize the base images + +The generated Dockerfile builds on `docker.io/library/eclipse-temurin:-jdk` and runs on `docker.io/library/eclipse-temurin:-jre`. The build stage needs only a JDK, because the project's wrapper downloads the Maven or Gradle version that it pins. To use other images, such as mirrors in your own registry, set both in a single call to : + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const catalog = await builder.addSpringBootApp('catalog', '../catalog'); +await catalog.withDockerfileBaseImage({ + buildImage: 'myregistry.azurecr.io/eclipse-temurin:21-jdk', + runtimeImage: 'myregistry.azurecr.io/eclipse-temurin:21-jre', +}); + +await builder.build().run(); +``` + + + -builder.AddProject("web") - .WithReference(api); +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddSpringBootApp("catalog", "../catalog") + .WithDockerfileBaseImage( + buildImage: "myregistry.azurecr.io/eclipse-temurin:21-jdk", + runtimeImage: "myregistry.azurecr.io/eclipse-temurin:21-jre"); builder.Build().Run(); ``` @@ -375,51 +712,91 @@ builder.Build().Run(); -The Java application is responsible for reading `SERVER_PORT` and listening on the endpoint target port. The example requires Spring Boot Actuator to expose `/actuator/health`. `WithHttpHealthCheck` / `withHttpHealthCheck` makes Aspire poll that path; it doesn't add a health endpoint to your app. +### Include files from other resources + +The generated Dockerfile can copy files that another resource produces, such as a frontend's build output, into the Java app's image. Call on the Java app with the source resource and a destination path. A relative destination path is resolved against the image's `/app` working directory, so the following example places the `frontend` app's build output in `/app/static`: + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const catalog = await builder.addSpringBootApp('catalog', '../catalog'); +await catalog.withExternalHttpEndpoints(); + +const frontend = await builder.addViteApp('frontend', '../frontend'); +await frontend.withReference(catalog); +await frontend.waitFor(catalog); + +await catalog.publishWithContainerFiles(frontend, './static'); + +await builder.build().run(); +``` + + + -## Debug Java resources +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); -Install the VS Code [Language Support for Java](https://marketplace.visualstudio.com/items?itemName=redhat.java) and [Debugger for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-debug) extensions. Then use the normal [Aspire debugging workflow](/get-started/aspire-vscode-extension/). +var catalog = builder.AddSpringBootApp("catalog", "../catalog") + .WithExternalHttpEndpoints(); -Aspire launches the application JVM directly for debugging rather than attaching to the wrapper's JVM. Use `WithMainClass` / `withMainClass` to select the entry class when needed. For a prebuilt JAR, Aspire uses the archive's manifest `Main-Class`. +var frontend = builder.AddViteApp("frontend", "../frontend") + .WithReference(catalog) + .WaitFor(catalog); -## Publishing behavior +catalog.PublishWithContainerFiles(frontend, "./static"); -`aspire publish` and `aspire deploy` package executable Java resources as containers. Aspire uses an existing `Dockerfile` in the app directory as-is; otherwise, it generates a multi-stage Dockerfile. The generated image runs as an unprivileged user, with the JVM as PID 1. +builder.Build().Run(); +``` -- Maven and Gradle projects build inside the image using the checked-in wrapper and configured build arguments. -- A supplied JAR path selects the build artifact when the directory also contains a build file. Only a directory without a build file or configured build tool copies the prebuilt JAR directly. -- If the build produces multiple candidate JARs, select the intended one with `WithJarArtifact` / `withJarArtifact`. -- The Java release is detected from Maven or Gradle, defaulting to 21. Generated stages use Eclipse Temurin JDK and JRE images; override them with `WithDockerfileBaseImage` / `withDockerfileBaseImage`. -- All publish inputs must stay inside the app directory. Changing the working directory after adding the resource doesn't change its build context and is rejected when publishing a generated Dockerfile. + + -`AddJavaContainer` resources continue to use the image you supplied. +Aspire builds the source resource before it builds the Java app's image. Copying the files doesn't make the app serve them, so configure the app to serve static files from that directory. Aspire adds the files only to the Dockerfile that it generates, so an authored `Dockerfile` in the app directory doesn't receive them. - For deployment guidance, see the [Aspire deployment overview](/deployment/). + For more information, see [Inject files at publish time](/app-host/container-files/#inject-files-at-publish-time) and [Deploy JavaScript apps](/deployment/javascript-apps/). +## Connection properties + +For the environment variables that a Java app receives — and how other apps discover a Java app's endpoints — see [Connect to Java apps](/integrations/frameworks/java/java-connect/). + +## Hosting integration health checks + +The Java hosting integration doesn't add health checks to Java resources. When your app exposes a health endpoint, such as `/actuator/health` with Spring Boot Actuator or `/q/health` with SmallRye Health, add a check with `WithHttpHealthCheck`. + -## Migrate from Community Toolkit +## Migrate from the Community Toolkit -For Aspire 13.6 or later, remove `CommunityToolkit.Aspire.Hosting.Java` from your AppHost's package references or `aspire.config.json` packages, and add `Aspire.Hosting.Java`. Don't keep both packages: they expose overlapping hosting extensions. +For Aspire 13.6 and later, `Aspire.Hosting.Java` replaces [📦 CommunityToolkit.Aspire.Hosting.Java](https://www.nuget.org/packages/CommunityToolkit.Aspire.Hosting.Java). To migrate an AppHost: -| Toolkit usage | First-party replacement | -| ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | -| `AddJavaContainerApp` / `addJavaContainerApp` | `AddJavaContainer` / `addJavaContainer`; TypeScript supplies the tag in `{ tag: '...' }`. | -| C# named argument `workingDirectory:` | `appDirectory:` on `AddJavaApp`. | -| `JavaAppExecutableResource` and `JavaAppContainerResource` | `JavaAppResource` and `JavaContainerResource`. | -| `Resource.JarPath` or TypeScript `jarPath()` / `setJarPath(...)` | Supply the launch JAR when adding the resource. Use `WithJarArtifact` / `withJarArtifact` for the publish artifact. | -| `AddSpringApp` or overloads accepting `JavaAppExecutableResourceOptions` / `JavaAppContainerResourceOptions` | Use `AddSpringBootApp`, `AddJavaApp`, or `AddJavaContainer`, then configure them with builder methods. | -| `WithMavenBuild(MavenOptions)` | Pass build arguments directly, including the packaging goal. | -| `WithOtelAgent()` used only to configure export | Omit the call: Java resources already configure export. The new parameterless overload locates and attaches an agent. | +1. Remove `CommunityToolkit.Aspire.Hosting.Java` from the AppHost's package references, `#:package` directives, or `aspire.config.json`. Don't keep both packages, because they define overlapping APIs. +1. Add `Aspire.Hosting.Java`, as described in [Installation](#installation). +1. Update your AppHost code with the replacements in the following table. The table uses C# names. TypeScript AppHosts use the same names in camel case, except that the JAR overload of `AddJavaApp` is `addJavaAppWithJar`, and the parameterless `WithOtelAgent` is `withOtelAgentDefaultPath`. +1. Review how your Java apps are published. The first-party integration builds a container image for each app that you add with `AddJavaApp`, `AddSpringBootApp`, or `AddQuarkusApp`, from the app's own `Dockerfile` or from one that Aspire generates. For more information, see [Publish Java apps](#publish-java-apps). -Review publishing assumptions as part of the migration: the first-party integration generates container builds rather than leaving Java packaging entirely to your deployment process. The older Toolkit package's compatibility overloads aren't carried into the first-party package. +| Community Toolkit API | First-party replacement | +| --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `AddJavaContainerApp` | `AddJavaContainer`. In TypeScript, pass the image tag as `{ tag: '...' }`. | +| `AddSpringApp`, and overloads that take `JavaAppExecutableResourceOptions` or `JavaAppContainerResourceOptions` | `AddSpringBootApp`, `AddJavaApp`, or `AddJavaContainer`, configured with builder methods. | +| The `workingDirectory` parameter of `AddJavaApp` | The `appDirectory` parameter. | +| `JavaAppExecutableResource` and `JavaAppContainerResource` | `JavaAppResource` and `JavaContainerResource`. | +| The `JarPath` resource property | Pass the JAR path when you add the app, and use `WithJarArtifact` to choose the published JAR. | +| `WithMavenBuild(MavenOptions)` | `WithMavenBuild` with the build arguments, including the packaging goal. For a custom `Command`, also call `WithWrapperPath`. | +| `WithOtelAgent()`, called only to configure telemetry export | Remove the call, because every Java resource is configured for export. The parameterless method now attaches the agent that the build produces. | ## See also -- [Java documentation](https://dev.java/learn/) -- [OpenTelemetry Java instrumentation](https://opentelemetry.io/docs/zero-code/java/agent/) -- [📦 Aspire.Hosting.Java](https://www.nuget.org/packages/Aspire.Hosting.Java) - [Get started with the Java integration](/integrations/frameworks/java/java-get-started/) +- [Connect to Java apps](/integrations/frameworks/java/java-connect/) +- [📦 Aspire.Hosting.Java](https://www.nuget.org/packages/Aspire.Hosting.Java) +- [OpenTelemetry Java agent](https://opentelemetry.io/docs/zero-code/java/agent/) +- [Maven Wrapper](https://maven.apache.org/tools/wrapper/) +- [Gradle Wrapper](https://docs.gradle.org/current/userguide/gradle_wrapper.html) diff --git a/src/frontend/src/content/docs/integrations/frameworks/rust/rust-connect.mdx b/src/frontend/src/content/docs/integrations/frameworks/rust/rust-connect.mdx new file mode 100644 index 000000000..4f87e4e5f --- /dev/null +++ b/src/frontend/src/content/docs/integrations/frameworks/rust/rust-connect.mdx @@ -0,0 +1,386 @@ +--- +title: Connect to Rust apps +seoTitle: 'Connect to Rust apps in Aspire: URLs and configuration' +description: Learn how other apps call your Rust apps through Aspire, and how Rust apps read the URLs and connection details that Aspire injects and send telemetry to the dashboard. +next: false +--- + +import { Image } from 'astro:assets'; +import { Tabs, TabItem } from '@astrojs/starlight/components'; +import ApiReference from '@components/ApiReference.astro'; +import rustIcon from '@assets/icons/rust-icon.png'; + +Rust logo + +This article describes how the Rust apps in your Aspire solution connect to other resources, in both directions. When another resource references a Rust app, Aspire injects the Rust app's URL into that resource, so apps written in C#, Go, Python, TypeScript, or Rust can call it without hard-coded addresses. When a Rust app references other resources — such as a database, a cache, or another service — Aspire injects their URLs and connection details into the app's environment, where your code reads them with `std::env::var`. It also describes how Rust apps send telemetry to the Aspire dashboard and trust the development certificate. + +For the AppHost APIs that add and configure Rust apps, see [Set up Rust apps in the AppHost](/integrations/frameworks/rust/rust-host/). If you're new to the Rust integration, start with [Get started with the Rust integration](/integrations/frameworks/rust/rust-get-started/). + +## Connect from your AppHost + +The examples in this article use the following AppHost. The `orders` Rust app references a PostgreSQL database and the `catalog` Rust app, and a Node.js `storefront` app references `orders`: + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const postgres = await builder.addPostgres('postgres'); +const ordersDb = await postgres.addDatabase('ordersdb'); + +const catalog = await builder.addRustApp('catalog', '../catalog'); +await catalog.withHttpEndpoint({ env: 'PORT' }); +await catalog.withHttpHealthCheck({ path: '/health' }); + +const orders = await builder.addRustApp('orders', '../orders'); +await orders.withHttpEndpoint({ env: 'PORT' }); +await orders.withHttpHealthCheck({ path: '/health' }); +await orders.withReference(ordersDb); +await orders.withReference(catalog); +await orders.waitFor(ordersDb); +await orders.waitFor(catalog); + +const storefront = await builder.addNodeApp('storefront', '../storefront', 'server.js'); +await storefront.withReference(orders); +await storefront.waitFor(orders); + +await builder.build().run(); +``` + + + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +var postgres = builder.AddPostgres("postgres"); +var ordersDb = postgres.AddDatabase("ordersdb"); + +var catalog = builder.AddRustApp("catalog", "../catalog") + .WithHttpEndpoint(env: "PORT") + .WithHttpHealthCheck("/health"); + +var orders = builder.AddRustApp("orders", "../orders") + .WithHttpEndpoint(env: "PORT") + .WithHttpHealthCheck("/health") + .WithReference(ordersDb) + .WithReference(catalog) + .WaitFor(ordersDb) + .WaitFor(catalog); + +builder.AddNodeApp("storefront", "../storefront", "server.js") + .WithReference(orders) + .WaitFor(orders); + +builder.Build().Run(); +``` + + + + + injects the referenced resource's connection information into the referencing resource, and delays the referencing resource until the referenced one is running — or healthy, when it has a health check. A Rust app can be on either side of a reference, and the environment variables are the same whatever language the other app uses. + + doesn't add an endpoint, so each Rust app in the preceding AppHost gets one from , which passes the app its port in the `PORT` environment variable. Each app also gets a health check from . Without it, `orders` would start as soon as Cargo starts building `catalog`, rather than when `catalog` is ready for requests. + +## Connection properties + +Aspire passes connection information to apps as environment variables. A Rust app is on both sides of that exchange: it exposes endpoints that other apps call, and it receives variables of its own. + +### Variables for apps that reference a Rust app + +When a resource references a Rust app, Aspire injects the URL of each of the Rust app's endpoints. In the preceding AppHost, `storefront` receives the following variables for the `orders` app: + +| Environment variable | Description | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ORDERS_HTTP` | The URL of the `orders` app's `http` endpoint, named `{RESOURCE}_{ENDPOINT}`. Read this variable from apps that don't use .NET service discovery. | +| `services__orders__http__0` | The same URL, in the format that [.NET service discovery](/fundamentals/service-discovery/) reads. | + +`WithHttpEndpoint` names its endpoint `http` unless you pass a name. When you add an endpoint with a different name to a Rust app, the variables use that name instead — for example, an endpoint named `admin` produces `ORDERS_ADMIN`. For the naming rules, see [Endpoint URLs](/fundamentals/environment-variables/#endpoint-urls) and [Service discovery variables](/fundamentals/environment-variables/#service-discovery-variables). + +### Variables that a Rust app receives + +A Rust app receives the variables of every resource that it references. In the preceding AppHost, `orders` receives `CATALOG_HTTP` and `services__catalog__http__0` for the `catalog` app, and the connection properties of the `ordersdb` database, which include: + +| Environment variable | Description | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ORDERSDB_URI` | The connection URI, in the format `postgresql://{Username}:{Password}@{Host}:{Port}/{DatabaseName}`. Rust database crates such as `sqlx` accept it as-is. | +| `ORDERSDB_USERNAME` | The user name for authentication. | +| `ORDERSDB_PASSWORD` | The password for authentication. | +| `ORDERSDB_HOST` | The host name of the PostgreSQL server. | +| `ORDERSDB_PORT` | The port of the PostgreSQL server. | +| `ORDERSDB_DATABASENAME` | The name of the database. | + +Each resource type documents its own connection properties, such as those in [Connect to PostgreSQL](/integrations/databases/postgres/postgres-connect/#connection-properties). For the naming rules, see [Resource properties](/fundamentals/environment-variables/#resource-properties). + +Aspire also sets the following variables on the Rust app itself: + +| Environment variable | Set on | Description | +| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PORT`, or the variable that you name with the `env` parameter of `WithHttpEndpoint` | Rust apps with an endpoint that sets `env` | The port that the app must listen on. | +| `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_PROTOCOL`, `OTEL_SERVICE_NAME`, and the other `OTEL_*` variables | All Rust resources | Where and how to export telemetry. For details, see [OpenTelemetry environment variables](/fundamentals/telemetry/#opentelemetry-environment-variables). | +| `SSL_CERT_DIR`, and `SSL_CERT_FILE` with the System trust scope | Rust apps, when you run the AppHost | The locations of the certificates that the app trusts, including the development certificate. For details, see [Trust the development certificate](#trust-the-development-certificate). | + +## Call a Rust app from other apps + +Apps call a Rust app at the URL that Aspire injects. Each example calls the `orders` app from the `storefront` app in the preceding AppHost. Only the language differs. + + + + +In a project that uses the Aspire service defaults, .NET service discovery resolves the `orders` name from the `services__orders__http__0` variable: + +```csharp title="Program.cs" +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.Services.AddHttpClient("orders", client => +{ + client.BaseAddress = new Uri("https+http://orders"); +}); + +var app = builder.Build(); + +app.MapGet("/orders", async (IHttpClientFactory factory) => +{ + var client = factory.CreateClient("orders"); + + return await client.GetStringAsync("/orders"); +}); + +app.Run(); +``` + +The `https+http` scheme prefers an HTTPS endpoint and falls back to HTTP. Without service discovery, read the URL from the `ORDERS_HTTP` configuration value instead. + + + + +```go title="main.go" +package main + +import ( + "fmt" + "io" + "net/http" + "os" +) + +func main() { + ordersURL := os.Getenv("ORDERS_HTTP") + + response, err := http.Get(ordersURL + "/orders") + if err != nil { + panic(err) + } + defer response.Body.Close() + + body, err := io.ReadAll(response.Body) + if err != nil { + panic(err) + } + + fmt.Println(string(body)) +} +``` + + + + +```python title="app.py" +import os +from urllib.request import urlopen + +orders_url = os.environ["ORDERS_HTTP"] + +with urlopen(f"{orders_url}/orders") as response: + print(response.read().decode()) +``` + + + + +```typescript title="server.ts" +const ordersUrl = process.env.ORDERS_HTTP; + +const response = await fetch(`${ordersUrl}/orders`); + +console.log(await response.text()); +``` + + + + +## Read Aspire configuration in your Rust app + +There's no Aspire client crate for Rust. A Rust app reads the variables that Aspire injects with [`std::env::var`](https://doc.rust-lang.org/std/env/fn.var.html), and passes them to the crates that it already uses. The following example configures the `orders` app from the preceding AppHost to listen on its port, call the `catalog` app, and use the `ordersdb` database. It uses [axum](https://docs.rs/axum) for the web server, [reqwest](https://docs.rs/reqwest) for the HTTP client, and [sqlx](https://docs.rs/sqlx) for the database. Add them to the app: + +```bash title="Terminal" +cargo add axum reqwest +cargo add tokio --features macros,rt-multi-thread +cargo add sqlx --features postgres,runtime-tokio +``` + +Then read the variables when the app starts: + +```rust title="src/main.rs" +use std::{env, net::SocketAddr}; + +use axum::{extract::State, http::StatusCode, routing::get, Router}; +use sqlx::PgPool; + +#[derive(Clone)] +struct AppState { + catalog_url: String, + http: reqwest::Client, + db: PgPool, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let catalog_url = + env::var("CATALOG_HTTP").unwrap_or_else(|_| "http://localhost:8080".to_string()); + let database_url = env::var("ORDERSDB_URI")?; + + let state = AppState { + catalog_url, + http: reqwest::Client::new(), + db: PgPool::connect(&database_url).await?, + }; + + let app = Router::new() + .route("/health", get(|| async { "healthy" })) + .route("/orders", get(order_count)) + .route("/products", get(products)) + .with_state(state); + + let port = env::var("PORT") + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(8081); + + let listener = tokio::net::TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], port))).await?; + axum::serve(listener, app).await?; + + Ok(()) +} + +async fn order_count(State(state): State) -> Result { + let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM orders") + .fetch_one(&state.db) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(format!("{count} orders")) +} + +async fn products(State(state): State) -> Result { + let response = state + .http + .get(format!("{}/products", state.catalog_url)) + .send() + .await + .map_err(|_| StatusCode::BAD_GATEWAY)?; + + response.text().await.map_err(|_| StatusCode::BAD_GATEWAY) +} +``` + +The defaults for `CATALOG_HTTP` and `PORT` keep the app runnable outside Aspire, alongside a `catalog` app that listens on port 8080. Reading `ORDERSDB_URI` with the `?` operator makes the app exit with an error when the variable isn't set. + +## Send telemetry to the dashboard + +Every Rust resource receives the standard OpenTelemetry environment variables, but a Rust app doesn't export telemetry by itself, and Rust has no automatic instrumentation agent. To send telemetry to the dashboard, configure the [OpenTelemetry SDK for Rust](https://opentelemetry.io/docs/languages/rust/) with the OTLP exporter from the [opentelemetry-otlp](https://docs.rs/opentelemetry-otlp) crate. The exporter reads the dashboard's endpoint and the headers that authenticate with it from the `OTEL_EXPORTER_OTLP_ENDPOINT` and `OTEL_EXPORTER_OTLP_HEADERS` variables, and the SDK reads the app's name from `OTEL_SERVICE_NAME`. Configure the exporter as follows: + +- **Use the gRPC transport.** Aspire points `OTEL_EXPORTER_OTLP_ENDPOINT` at the dashboard's gRPC endpoint whenever the dashboard has one, and sets `OTEL_EXPORTER_OTLP_PROTOCOL` to `grpc`. Select the gRPC transport explicitly with `with_tonic()`, because only the gRPC exporter builder accepts the TLS configuration that the next item describes. If the dashboard has only an OTLP/HTTP endpoint, Aspire sets `OTEL_EXPORTER_OTLP_PROTOCOL` to `http/protobuf` instead. In that case, build the exporter with `with_http()`, and configure certificate trust on the HTTP client that it uses. For the dashboard's endpoint settings, see [Dashboard configuration](/app-host/configuration/#dashboard). +- **Trust the native certificate roots.** When you run the AppHost, the dashboard's endpoint typically uses HTTPS with the development certificate. Configure the exporter's TLS with `ClientTlsConfig::new().with_native_roots()`, which loads certificates from the `SSL_CERT_DIR` directory that Aspire sets. +- **Select one TLS provider for both crates.** Enable the same rustls crypto provider — `tls-aws-lc` or `tls-ring` — on `opentelemetry-otlp` and on `tonic`. Cargo features are additive, so selecting different providers compiles both of them, rather than replacing one with the other. + +The following dependencies export traces over gRPC with AWS-LC as the TLS provider, as the [Aspire Rust playground](https://github.com/microsoft/aspire/tree/main/playground/rust) does. Building AWS-LC requires native build tools, such as a C compiler. To use ring instead, replace `tls-aws-lc` with `tls-ring` for both crates. Keep the `tonic` version in step with the one that `opentelemetry-otlp` uses, because the exporter's `with_tls_config` method takes tonic's `ClientTlsConfig` type. + +```toml title="Cargo.toml" +[dependencies] +opentelemetry = "0.33" +opentelemetry_sdk = { version = "0.33", features = ["rt-tokio"] } +opentelemetry-otlp = { version = "0.33", features = ["grpc-tonic", "tls-roots", "tls-aws-lc"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +tonic = { version = "0.14", default-features = false, features = ["transport", "tls-native-roots", "tls-aws-lc"] } +``` + +Create the exporter and a tracer provider when the app starts: + +```rust title="src/telemetry.rs" +use opentelemetry::global; +use opentelemetry_otlp::{SpanExporter, WithTonicConfig}; +use opentelemetry_sdk::trace::SdkTracerProvider; +use tonic::transport::ClientTlsConfig; + +pub fn init_tracing() -> Result> { + let exporter = SpanExporter::builder() + .with_tonic() + .with_tls_config(ClientTlsConfig::new().with_native_roots()) + .build()?; + + let provider = SdkTracerProvider::builder() + .with_batch_exporter(exporter) + .build(); + global::set_tracer_provider(provider.clone()); + + Ok(provider) +} +``` + +Call it from inside the Tokio runtime, and shut down the provider before the app exits, so that the exporter sends the spans that it has buffered: + +```rust title="src/main.rs" +mod telemetry; + +use opentelemetry::{global, trace::Tracer}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let tracer_provider = telemetry::init_tracing()?; + + let result = run().await; + + if let Err(error) = tracer_provider.shutdown() { + eprintln!("failed to shut down the tracer provider: {error}"); + } + + result +} + +async fn run() -> Result<(), Box> { + global::tracer("orders").in_span("load-orders", |_cx| { + // Work that the span measures. + }); + + Ok(()) +} +``` + +The dashboard shows the spans under the app's resource name, which Aspire passes to the SDK in `OTEL_SERVICE_NAME`. To export metrics and logs as well, and to connect the spans and events of the `tracing` crate to OpenTelemetry, see the `telemetry.rs` file of the [Aspire Rust playground](https://github.com/microsoft/aspire/tree/main/playground/rust). An app that doesn't configure the SDK doesn't send telemetry to the dashboard, but its console output still appears in the dashboard's console logs. + +## Trust the development certificate + +When you run the AppHost, Aspire sets `SSL_CERT_DIR` on each Rust app to a directory that contains the development certificate. OpenSSL, and clients that load their trusted certificates with the `rustls-native-certs` crate — such as tonic with `with_native_roots()` — read that variable, so they trust HTTPS endpoints that use the development certificate without any code changes. On Windows and macOS, those clients then typically trust only the development certificate, rather than public certificate authorities, unless you set the app's trust scope to System. Clients that use the operating system's certificate store, and clients that use the built-in certificates of the `webpki-roots` crate, behave differently. For details, see [Configure certificate trust](/integrations/frameworks/rust/rust-host/#configure-certificate-trust). + +## See also + +- [Set up Rust apps in the AppHost](/integrations/frameworks/rust/rust-host/) +- [Get started with the Rust integration](/integrations/frameworks/rust/rust-get-started/) +- [Environment variables in Aspire](/fundamentals/environment-variables/) +- [Service discovery](/fundamentals/service-discovery/) +- [OpenTelemetry Rust](https://opentelemetry.io/docs/languages/rust/) +- [Aspire Rust playground](https://github.com/microsoft/aspire/tree/main/playground/rust) diff --git a/src/frontend/src/content/docs/integrations/frameworks/rust/rust-get-started.mdx b/src/frontend/src/content/docs/integrations/frameworks/rust/rust-get-started.mdx index 0b1ffcb23..255eef1ba 100644 --- a/src/frontend/src/content/docs/integrations/frameworks/rust/rust-get-started.mdx +++ b/src/frontend/src/content/docs/integrations/frameworks/rust/rust-get-started.mdx @@ -1,16 +1,13 @@ --- title: Get started with the Rust integration +seoTitle: 'Get started with Rust in Aspire: run Cargo apps' +description: Run Rust apps from your Aspire AppHost with Cargo, service discovery, OpenTelemetry, trusted development certificates, debugging, and generated Dockerfiles. category: quickstart -description: Run Rust applications with Aspire's first-party Cargo integration, configure endpoints, debug in Visual Studio Code, and publish container images. +prev: false --- -import { - LinkButton, - Steps, - TabItem, - Tabs, -} from '@astrojs/starlight/components'; import { Image } from 'astro:assets'; +import { LinkButton, Steps } from '@astrojs/starlight/components'; import rustIcon from '@assets/icons/rust-icon.png'; -The first-party `Aspire.Hosting.Rust` integration runs Cargo applications alongside the other resources in your Aspire AppHost. Rust resources support endpoints, service discovery, health checks, environment configuration, OpenTelemetry export, debugging, and generated Dockerfiles for publishing. +[Rust](https://www.rust-lang.org/) is a systems programming language that guarantees memory safety without a garbage collector, which makes it a popular choice for fast, reliable web services, workers, and command-line tools. Rust projects are built and run with [Cargo](https://doc.rust-lang.org/cargo/), Rust's build tool and package manager. The Aspire Rust hosting integration lets you model Cargo apps as first-class resources in your AppHost and connect them to the rest of your app, regardless of the language each part is written in. + +:::note[New in Aspire 13.6] +[📦 Aspire.Hosting.Rust](https://www.nuget.org/packages/Aspire.Hosting.Rust) is the official Rust hosting integration. It replaces [📦 CommunityToolkit.Aspire.Hosting.Rust](https://www.nuget.org/packages/CommunityToolkit.Aspire.Hosting.Rust) for Cargo apps in Aspire 13.6 and later. If you use the Community Toolkit package — including its Bacon support, which remains in the Toolkit — see [Migrate from the Community Toolkit](/integrations/frameworks/rust/rust-host/#migrate-from-the-community-toolkit). +::: + +## Why use Rust with Aspire + +Adding Rust apps through Aspire — rather than starting each `cargo run` by hand and wiring up ports, URLs, and credentials yourself — gives you: + +- **Cargo-native configuration.** Aspire runs each app with `cargo run` in its own directory, so Cargo finds the app's manifest, workspace, and lockfile as it does in a terminal. Select features, binaries, examples, workspace members, profiles, and target triples with AppHost methods, and pass your program's own arguments separately. +- **Service discovery and connection information.** Reference a Rust app from another resource — or reference databases, caches, and other services from a Rust app — and Aspire injects URLs and connection details as environment variables that your app reads with `std::env::var`. +- **Telemetry in the dashboard.** Every Rust resource receives the standard OpenTelemetry exporter settings, so an app that exports telemetry with the OpenTelemetry SDK for Rust shows its traces, metrics, and logs in the dashboard. Cargo's build output and your app's console output appear in the dashboard's console logs. +- **Trusted development certificates.** Aspire points OpenSSL and the `rustls-native-certs` crate at a directory that contains the development certificate, so HTTPS calls between local resources work without turning off certificate validation. +- **Debugging.** Debug Rust apps in VS Code with the standard Aspire debugging flow. Aspire builds each app with the same Cargo options that `cargo run` uses, and launches the binary under a native debugger. +- **Publishing without a hand-written Dockerfile.** When you publish, Aspire builds each Rust app into a container image with the `Dockerfile` in the app directory or, when there isn't one, with a multi-stage Dockerfile that it generates. By default, the generated Dockerfile makes a release build — locked to the dependency versions in `Cargo.lock`, when the app has that file — and runs the app as an unprivileged user. + +## Prerequisites + +- The Rust toolchain, with Cargo 1.71 or later on your `PATH`, for each Rust app that the AppHost runs locally. Install it with [rustup](https://www.rust-lang.org/tools/install). When Aspire generates the Dockerfile to publish an app, it builds the app inside the container image, but it still runs `cargo metadata` on your machine to find the app's binary. +- To debug in VS Code, the [C/C++](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) extension on Windows, or the [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) extension on Linux and macOS. +- The [Aspire CLI](/get-started/install-cli/), along with the other [Aspire prerequisites](/get-started/prerequisites/). + +:::note +A C# or TypeScript AppHost can host Rust apps without turning on any experimental features. Writing the AppHost itself in Rust is a separate, experimental scenario that you turn on with the `features.experimentalPolyglot:rust` [configuration setting](/reference/cli/configuration/). +::: ## How the pieces fit together -The integration is installed in the AppHost. The AppHost starts Cargo in the Rust application's working directory, applies standard Aspire resource configuration, and exposes the resulting resource in the dashboard. +The Rust integration is a **hosting integration**. You install it in your AppHost and add a resource for each Rust app. When a Rust app starts, Aspire runs it with Cargo, supplies its configuration as environment variables, and shows its logs, status, and telemetry in the dashboard. ```mermaid architecture-beta @@ -45,77 +67,41 @@ architecture-beta toolchain:R --> L:app ``` -## Prerequisites +There's no Aspire client crate for Rust. Your app reads the environment variables that Aspire injects with `std::env::var`, or with the configuration crate of your choice, and exports telemetry with the OpenTelemetry SDK for Rust. -- Install Rust with [rustup](https://www.rust-lang.org/tools/install) and make Cargo 1.71 or later available on your `PATH`. -- Create an [Aspire AppHost](/get-started/app-host/) in C# or TypeScript. +Getting there is a two-step process: model your Rust apps in the AppHost, then connect them to — and from — the other resources in your app. -1. ### Install the hosting package - - Add the first-party hosting package from the AppHost directory: - - ```bash title="Add Rust hosting" - aspire add Aspire.Hosting.Rust - ``` - -2. ### Add a Rust app +1. ### Model Rust apps in your AppHost - Register the directory that contains your Rust project, then configure an endpoint for the port that the app reads from `PORT`. + Add the Rust hosting integration to your AppHost, then declare a resource for each Rust app. The [Set up Rust apps in the AppHost](/integrations/frameworks/rust/rust-host/) article walks through every capability — endpoints and health checks, Cargo features, binaries, workspace members, profiles, and target triples, program arguments, certificate trust, debugging, and publishing — with side-by-side TypeScript and C# examples. - - + + Set up Rust apps in the AppHost + - ```typescript title="apphost.mts" twoslash - import { createBuilder } from './.aspire/modules/aspire.mjs'; +2. ### Connect to and from your Rust apps - const builder = await createBuilder(); + When you reference a Rust app from another resource, Aspire injects the app's URLs into that resource. When you reference other resources from a Rust app, Aspire injects their connection information into the app's environment. See [Connect to Rust apps](/integrations/frameworks/rust/rust-connect/) for the environment variable reference, examples in C#, Go, Python, and TypeScript, and how Rust apps read Aspire configuration and send telemetry to the dashboard. - const api = await builder.addRustApp('rust-api', '../rust-api'); - await api.withHttpEndpoint({ port: 8080, env: 'PORT' }); - await api.withExternalHttpEndpoints(); - - await builder.build().run(); - ``` - - - - - - ```csharp title="AppHost.cs" - var builder = DistributedApplication.CreateBuilder(args); - - builder.AddRustApp("rust-api", "../rust-api") - .WithHttpEndpoint(port: 8080, env: "PORT") - .WithExternalHttpEndpoints(); - - builder.Build().Run(); - ``` - - - - -3. ### Configure the app resource - - Configure Cargo options separately from application arguments, add health checks, and learn about debugging and publishing in the [Rust AppHost reference](/integrations/frameworks/rust/rust-host/). - - - Configure Rust apps in the AppHost - + + Connect to Rust apps + ## See also -If you use the Community Toolkit package, follow [Migrate from the Community Toolkit](/integrations/frameworks/rust/rust-host/#migrate-from-the-community-toolkit). Bacon support remains Toolkit-only. - -- [Rust documentation](https://www.rust-lang.org/learn) -- [Cargo documentation](https://doc.rust-lang.org/cargo/) -- [Rust AppHost reference](/integrations/frameworks/rust/rust-host/) -- [Aspire Community Toolkit](https://github.com/CommunityToolkit/Aspire) +- [Learn Rust](https://www.rust-lang.org/learn) +- [The Cargo Book](https://doc.rust-lang.org/cargo/) +- [OpenTelemetry Rust](https://opentelemetry.io/docs/languages/rust/) +- [Aspire integrations overview](/integrations/overview/) diff --git a/src/frontend/src/content/docs/integrations/frameworks/rust/rust-host.mdx b/src/frontend/src/content/docs/integrations/frameworks/rust/rust-host.mdx index 04cda1ce4..6adf98b84 100644 --- a/src/frontend/src/content/docs/integrations/frameworks/rust/rust-host.mdx +++ b/src/frontend/src/content/docs/integrations/frameworks/rust/rust-host.mdx @@ -1,10 +1,13 @@ --- -title: Configure Rust apps in the AppHost -description: Configure first-party Rust hosting with Cargo options, debugging, generated Dockerfiles, and migration from the Community Toolkit integration. +title: Set up Rust apps in the AppHost +seoTitle: 'Set up Rust apps in the Aspire AppHost: hosting integration' +description: Learn how to use the Aspire Rust hosting integration to run Cargo apps in your AppHost, configure Cargo options, debug Rust apps, and publish them as container images. --- -import { TabItem, Tabs } from '@astrojs/starlight/components'; import { Image } from 'astro:assets'; +import { Tabs, TabItem } from '@astrojs/starlight/components'; +import ApiReference from '@components/ApiReference.astro'; +import LearnMore from '@components/LearnMore.astro'; import rustIcon from '@assets/icons/rust-icon.png'; -This reference describes the first-party `Aspire.Hosting.Rust` integration. If you are new to it, begin with [Get started with the Rust integration](/integrations/frameworks/rust/rust-get-started/). Existing Toolkit applications should review [migration guidance](#migrate-from-the-community-toolkit). +This article is the reference for the Aspire Rust hosting integration. It enumerates the AppHost APIs — with examples for both `apphost.mts` and `AppHost.cs` — that you use to model Rust apps in your [AppHost](/get-started/app-host/): Cargo packages that Aspire runs with `cargo run`, the Cargo options that select features, binaries, examples, workspace members, profiles, and target triples, and the arguments that Aspire passes to your program. It also describes how Aspire configures certificate trust for Rust apps, debugs them, and builds them into container images when you publish. + +If you're new to the Rust integration, start with [Get started with the Rust integration](/integrations/frameworks/rust/rust-get-started/). For how other resources call your Rust apps — and how your Rust apps read the configuration that Aspire injects and send telemetry to the dashboard — see [Connect to Rust apps](/integrations/frameworks/rust/rust-connect/). If you're moving from the Community Toolkit package, see [Migrate from the Community Toolkit](#migrate-from-the-community-toolkit). :::note[Prerequisites] -Install [Rust and Cargo](https://www.rust-lang.org/tools/install), with Cargo 1.71 or later on `PATH`. +Each Rust app that the AppHost runs on your machine needs the Rust toolchain, with Cargo 1.71 or later on your `PATH`. Install it with [rustup](https://www.rust-lang.org/tools/install). If Aspire can't find `cargo`, it logs a warning that links to the installation instructions. To debug Rust apps in VS Code, install the [C/C++](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) extension on Windows, or the [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) extension on Linux and macOS. ::: -## Install the package +## Installation + +To start modeling Rust apps in your AppHost, install the [📦 Aspire.Hosting.Rust](https://www.nuget.org/packages/Aspire.Hosting.Rust) NuGet package: + + + + +```bash title="Terminal" +aspire add rust +``` + + + Learn more about [`aspire add`](/reference/cli/commands/aspire-add/) in the command reference. + + +This updates your `aspire.config.json` with the Rust hosting integration package: + +```json title="aspire.config.json" ins={3} +{ + "packages": { + "Aspire.Hosting.Rust": "%ASPIRE_VERSION_PREVIEW%" + } +} +``` + + + ```bash title="Terminal" -aspire add Aspire.Hosting.Rust +aspire add rust +``` + + + Learn more about [`aspire add`](/reference/cli/commands/aspire-add/) in the command reference. + + +Or, choose a manual installation approach: + +```csharp title="AppHost.cs" +#:package Aspire.Hosting.Rust@%ASPIRE_VERSION_PREVIEW% +``` + +```xml title="AppHost.csproj" + ``` -This adds [📦 Aspire.Hosting.Rust](https://www.nuget.org/packages/Aspire.Hosting.Rust) to the AppHost and regenerates the SDK for a TypeScript AppHost. + + -## Add a Cargo app +## Add a Rust app -`AddRustApp` / `addRustApp` starts `cargo run` in the supplied working directory. The path is resolved relative to the AppHost directory, so it normally contains the Rust project's `Cargo.toml`. +Use to add a Rust app. The app directory is resolved relative to the AppHost directory. It's Cargo's working directory and, when you publish, the build context of the app's container image. Cargo finds the app's `Cargo.toml` by searching from that directory, as it does when you run `cargo` in a terminal. @@ -43,19 +89,30 @@ import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); -const api = await builder.addRustApp('rust-api', '../rust-api'); +const api = await builder.addRustApp('api', '../api'); +await api.withHttpEndpoint({ env: 'PORT' }); +await api.withHttpHealthCheck({ path: '/health' }); + +const frontend = await builder.addNodeApp('frontend', '../frontend', 'server.js'); +await frontend.withReference(api); +await frontend.waitFor(api); await builder.build().run(); ``` - ```csharp title="AppHost.cs" var builder = DistributedApplication.CreateBuilder(args); -var api = builder.AddRustApp("rust-api", "../rust-api"); +var api = builder.AddRustApp("api", "../api") + .WithHttpEndpoint(env: "PORT") + .WithHttpHealthCheck("/health"); + +builder.AddNodeApp("frontend", "../frontend", "server.js") + .WithReference(api) + .WaitFor(api); builder.Build().Run(); ``` @@ -63,9 +120,41 @@ builder.Build().Run(); -## Configure Cargo and application arguments +In the preceding example, adds an HTTP endpoint to the `api` app and passes its port to the app in the `PORT` environment variable, and makes Aspire poll the app's `/health` endpoint. The `frontend` app uses to receive the `api` app's URL, and to start only after `api` is healthy. + +When a Rust app starts, Aspire: + +- Runs `cargo run` in the app directory, with the Cargo options and program arguments that you configure. Cargo builds the app before it runs it, and the build output appears in the app's console logs in the dashboard. +- Keeps Cargo's defaults for a local run: a `dev` profile build, and a `Cargo.lock` file that Cargo can update. Publishing uses different defaults, as described in [Publish Rust apps](#publish-rust-apps). +- Sets the standard OpenTelemetry environment variables, which point at the dashboard. The app sends telemetry only when it uses the OpenTelemetry SDK for Rust. For details, see [Send telemetry to the dashboard](/integrations/frameworks/rust/rust-connect/#send-telemetry-to-the-dashboard). +- Configures the app to trust the development certificate, as described in [Configure certificate trust](#configure-certificate-trust). + +`AddRustApp` doesn't add an endpoint, because a Rust program doesn't read its port from the environment on its own. Add an endpoint with `WithHttpEndpoint`, name the environment variable that carries the port with its `env` parameter, and make the app listen on that port. Listen on all interfaces rather than on `127.0.0.1`, so the app is also reachable when it runs in a container after you publish. For example, with [axum](https://docs.rs/axum): + +```rust title="src/main.rs" +use std::net::SocketAddr; + +use axum::{routing::get, Router}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let port = std::env::var("PORT") + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(8080); -Keep Cargo build options separate from arguments for your Rust program. Use `WithCargoArgs` / `withCargoArgs` for Cargo and `WithArgs` / `withArgs` for the application; Aspire inserts the separator. Use typed target-selection methods so debugging and publishing can identify the produced binary. + let app = Router::new().route("/health", get(|| async { "healthy" })); + + let listener = tokio::net::TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], port))).await?; + axum::serve(listener, app).await?; + + Ok(()) +} +``` + +## Configure Cargo and program arguments + +Cargo separates its own options from the arguments of the program that it runs with `--`, so Aspire configures the two kinds of arguments separately. Use the `WithCargo*` methods for Cargo, and for your program. Aspire adds the `--` separator. @@ -75,27 +164,24 @@ import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); -const api = await builder.addRustApp('rust-api', '../rust-api'); -await api.withCargoBinTarget('worker'); -await api.withCargoFeatures(['tls']); +const api = await builder.addRustApp('api', '../api'); +await api.withCargoFeatures(['postgres', 'metrics']); await api.withCargoArgs(['--no-default-features']); -await api.withArgs(['--environment', 'development']); +await api.withArgs(['--log-format', 'json']); await builder.build().run(); ``` - ```csharp title="AppHost.cs" var builder = DistributedApplication.CreateBuilder(args); -var api = builder.AddRustApp("rust-api", "../rust-api") - .WithCargoBinTarget("worker") - .WithCargoFeatures("tls") +builder.AddRustApp("api", "../api") + .WithCargoFeatures("postgres", "metrics") .WithCargoArgs("--no-default-features") - .WithArgs("--environment", "development"); + .WithArgs("--log-format", "json"); builder.Build().Run(); ``` @@ -103,34 +189,38 @@ builder.Build().Run(); -| Cargo option | Use it to | -| ------------ | --------- | -| `WithCargoPackage` / `withCargoPackage` | Select a workspace member | -| `WithCargoBinTarget` / `withCargoBinTarget` | Select a binary target | -| `WithCargoExample` / `withCargoExample` | Select an example instead of a binary | -| `WithCargoManifestPath` / `withCargoManifestPath` | Select a manifest relative to the app directory for publishing | -| `WithCargoFeatures` / `withCargoFeatures` | Accumulate feature names | -| `WithCargoReleaseBuild` / `withCargoReleaseBuild` | Use the release profile, enabled by default for publishing | -| `WithCargoLocked` / `withCargoLocked` | Require the lockfile to stay unchanged, enabled by default for publishing when a lockfile exists | -| `WithCargoProfile` / `withCargoProfile` | Choose a named profile instead of the release toggle | -| `WithCargoTarget` / `withCargoTarget` | Select a target triple | +When you run the AppHost, the preceding example starts the app with the following command: -## Migrate from the Community Toolkit +```bash title="Command" +cargo run --features postgres,metrics --no-default-features -- --log-format json +``` -For Cargo applications, replace `CommunityToolkit.Aspire.Hosting.Rust` with `Aspire.Hosting.Rust`. Don't install both packages expecting their `AddRustApp` extension methods to be interchangeable. +Each of the following methods adds a Cargo option. Aspire places the options in the order of the table, before the arguments that you add with ', 'string[]']} />: -- Replace the Toolkit's `workingDirectory` named argument with `appDirectory` in C#. -- Move Cargo options from the Toolkit's `args` array to the first-party Cargo methods above. Move program arguments after the old `--` separator to `WithArgs` / `withArgs`. -- Review publishing: the first-party integration can generate a Dockerfile, but an existing Dockerfile in the app directory takes precedence. -- Check custom target triples, base images, manifests, and workspace paths before publishing. +| Method | Cargo option | Description | +| ---------------------------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| | `--features` | Enables features. Repeated calls add features in call order. | +| | `--bin` | Selects one of the package's binary targets. | +| | `--example` | Runs an example instead of a binary. | +| | `--package` | Selects a workspace member. | +| | `--manifest-path` | Builds from a `Cargo.toml` file other than the one that Cargo finds from the app directory, such as a workspace member's manifest. To publish, the path must be relative to the app directory and inside it. | +| | `--target` | Builds for a target triple, such as `x86_64-unknown-linux-musl`. | +| | `--locked` | Fails the build rather than updating `Cargo.lock`. Pass `false` to turn off the publishing default. | +| | `--profile` | Builds with a named profile. It takes precedence over `WithCargoReleaseBuild`, because Cargo rejects `--profile` together with `--release`. | +| | `--release` | Builds with the `release` profile. Pass `false` to turn off the publishing default. | -### Toolkit-only Bacon support +Aspire passes the values of `WithCargoArgs` to Cargo without interpreting them. Debugging and publishing work out which binary Cargo produces from the methods in the preceding table alone, so select the package, binary, example, manifest, profile, and target triple with those methods rather than with `WithCargoArgs`. In C#, [another overload of `WithCargoArgs`](/reference/api/csharp/aspire.hosting.rust/rusthostingextensions/methods/#withcargoargs-iresourcebuilder-t-action-rustcargoargscallbackcontext) takes a callback that computes Cargo arguments when the app starts. -`AddBaconApp` / `addBaconApp` remains a **Community Toolkit-only** API. Keep `CommunityToolkit.Aspire.Hosting.Rust` and install [Bacon](https://dystroy.org/bacon/) for that workflow; the first-party package doesn't provide a drop-in replacement. The Toolkit runs `bacon run` by default, supports an argument array for other commands, and requires an authored Dockerfile for publishing. Those are Toolkit behaviors, not first-party Cargo publishing guarantees. +### Select the binary to run -## Configure endpoints, environment, and health checks +A Cargo package can contain several binaries and examples, and a Cargo workspace can contain several packages. Aspire reads the layout with `cargo metadata` to find the binary that the app runs, which the debugger launches and the published image copies. It selects: -Rust app resources support standard executable-resource configuration. Use `WithHttpEndpoint` / `withHttpEndpoint` to allocate a port and put it in an environment variable that the application reads. Use `WithHttpHealthCheck` / `withHttpHealthCheck` when the application exposes an HTTP health endpoint. `WithExternalHttpEndpoints` / `withExternalHttpEndpoints` makes an HTTP endpoint externally accessible. +1. The example that you select with `WithCargoExample`. +1. The binary that you select with `WithCargoBinTarget`. +1. The package's `default-run` binary, when its `Cargo.toml` file sets one. +1. The package's only binary. + +When a package has more than one binary and no `default-run` setting, select one with `WithCargoBinTarget`. When the app directory is a workspace whose default members include more than one package with a binary, select the package with `WithCargoPackage`. Library-only members don't count, so an app crate beside library crates needs no extra configuration. @@ -140,27 +230,94 @@ import { createBuilder } from './.aspire/modules/aspire.mjs'; const builder = await createBuilder(); -const api = await builder.addRustApp('rust-api', '../rust-api'); -await api.withEnvironment('RUST_LOG', 'info'); -await api.withHttpEndpoint({ port: 8080, env: 'PORT' }); -await api.withExternalHttpEndpoints(); -await api.withHttpHealthCheck({ path: '/health' }); +const worker = await builder.addRustApp('worker', '../services'); +await worker.withCargoPackage('jobs'); +await worker.withCargoBinTarget('worker'); await builder.build().run(); ``` + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddRustApp("worker", "../services") + .WithCargoPackage("jobs") + .WithCargoBinTarget("worker"); + +builder.Build().Run(); +``` + + + + +## Set environment variables + +Use to set environment variables on a Rust app, such as `RUST_LOG`, which the `env_logger` crate and the `EnvFilter` of the `tracing-subscriber` crate read, and `RUST_BACKTRACE`: + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const api = await builder.addRustApp('api', '../api'); +await api.withEnvironment('RUST_LOG', 'info'); +await api.withEnvironment('RUST_BACKTRACE', '1'); + +await builder.build().run(); +``` + + ```csharp title="AppHost.cs" var builder = DistributedApplication.CreateBuilder(args); -var api = builder.AddRustApp("rust-api", "../rust-api") +builder.AddRustApp("api", "../api") .WithEnvironment("RUST_LOG", "info") - .WithHttpEndpoint(port: 8080, env: "PORT") - .WithExternalHttpEndpoints() - .WithHttpHealthCheck("/health"); + .WithEnvironment("RUST_BACKTRACE", "1"); + +builder.Build().Run(); +``` + + + + +Cargo runs in the same environment, so build settings such as `RUSTFLAGS` and `CARGO_*` variables that you set this way apply to the build as well as to the program. When VS Code debugs the app, Aspire builds it with the same environment. + +## Configure certificate trust + +Aspire configures Rust apps to trust the development certificate with the default [Append](/app-host/certificate-configuration/#append-mode) trust scope. It sets the `SSL_CERT_DIR` environment variable to a directory that contains the development certificate. OpenSSL — which the `native-tls` crate uses on Linux — reads that variable, and so does the `rustls-native-certs` crate, which clients such as tonic with its `tls-native-roots` feature use to load their trusted certificates. When `SSL_CERT_DIR` or `SSL_CERT_FILE` is set, `rustls-native-certs` loads certificates only from those locations, instead of from the operating system's certificate store. + +On Linux, Aspire also adds the system's certificate directories — or the directories in the AppHost's own `SSL_CERT_DIR` — to the variable, so these clients trust both the development certificate and public certificate authorities. On Windows and macOS, unless the AppHost's environment sets `SSL_CERT_DIR`, the variable contains only Aspire's certificate directory, so clients that use `rustls-native-certs` trust the development certificate but reject certificates from public certificate authorities. If a Rust app also calls public HTTPS endpoints, set its [trust scope](/app-host/certificate-configuration/#certificate-trust-scopes) to [System](/app-host/certificate-configuration/#system-mode) with . Aspire then also sets `SSL_CERT_FILE` to a bundle that contains the system's root certificates together with the development certificate: + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder, CertificateTrustScope } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const api = await builder.addRustApp('api', '../api'); +await api.withCertificateTrustScope(CertificateTrustScope.System); + +await builder.build().run(); +``` + + + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddRustApp("api", "../api") + .WithCertificateTrustScope(CertificateTrustScope.System); builder.Build().Run(); ``` @@ -168,19 +325,32 @@ builder.Build().Run(); -The integration configures the Rust app with the OpenTelemetry Protocol exporter. Add OpenTelemetry instrumentation to the Rust application to emit telemetry to Aspire. You can also use standard resource references to model dependencies and provide their configuration to the Rust process. +Clients that use the operating system's certificate store instead, such as `native-tls` on Windows and macOS, ignore these variables and trust the development certificate when it's [trusted on your machine](/app-host/certificate-configuration/#trusting-the-development-certificate). Clients that use the built-in certificates of the `webpki-roots` crate never trust the development certificate. + + + For more information, see [Certificate trust configuration](/app-host/certificate-configuration/#certificate-trust-configuration). + + +## Debug Rust apps -## Debug Rust resources +To debug Rust apps, use the standard debugging flow of the [Aspire VS Code extension](/get-started/aspire-vscode-extension/). Debugging is enabled for every app that you add with `AddRustApp`. -Debugging is enabled automatically by `AddRustApp` / `addRustApp`. Use Aspire's normal **Start Debugging** flow in Visual Studio Code. Install [C/C++](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) on Windows or [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) on Linux and macOS. +When the debugger launches a Rust app, VS Code first builds it with `cargo build`, using the Cargo options that `cargo run` would use and the app's environment variables. It then launches the binary that the build produces under a native debugger, with the arguments that you add with `WithArgs` and the app's environment variables. Aspire finds the binary with `cargo metadata`, as described in [Select the binary to run](#select-the-binary-to-run), so the debugged binary is the same one that `cargo run` would run and that a published image contains. -Rust resources hosted by a C# or TypeScript AppHost are distinct from experimental Rust-language `apphost.rs` AppHosts. You don't need to enable an experimental AppHost-language flag to host a Rust resource. +The debugger depends on your platform: -## Debug Cargo apps on macOS with VS Code +| Platform | Debugger | VS Code extension | +| --------------- | --------------------------------------- | ----------------------------------------------------------------------------------- | +| Windows | The Visual Studio debugger (`cppvsdbg`) | [C/C++](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) | +| Linux and macOS | LLDB | [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) | -When you attach [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) in VS Code to a Rust process started by Aspire, macOS developer mode must permit debugger attachment. This is a macOS developer tools access policy, not the Developer Mode setting for iOS devices. +On Windows, if the C/C++ extension isn't installed but CodeLLDB is, VS Code uses CodeLLDB. Apps that target a Windows GNU toolchain, such as `x86_64-pc-windows-gnu`, produce debug information that the Visual Studio debugger can't read, so they need CodeLLDB. -Check the macOS developer mode status: +When you build for a target triple, select it with `WithCargoTarget`, or with a `CARGO_BUILD_TARGET` environment variable that you set on the app, so that Aspire finds the binary under the target's directory. A `[build] target` setting in a `.cargo/config.toml` file isn't taken into account. + +### Allow debugging on macOS + +On macOS, CodeLLDB can debug a Rust process only when the macOS developer tools security policy permits it. This policy is separate from the Developer Mode setting for iOS devices. To check the policy, run: ```bash title="Terminal" /usr/sbin/DevToolsSecurity -status @@ -192,22 +362,203 @@ If the command reports `Developer mode is currently disabled`, enable it: sudo /usr/sbin/DevToolsSecurity -enable ``` -macOS prompts for administrator authentication. You must authorize the change yourself. Run `/usr/sbin/DevToolsSecurity -status` again to confirm that developer mode is enabled. - -Start your AppHost with `aspire run`, then attach CodeLLDB to the Rust process in VS Code. Enabling macOS developer mode addresses this specific attach prerequisite; it doesn't resolve every possible debugger attachment failure. +macOS prompts you to authenticate as an administrator. Run `/usr/sbin/DevToolsSecurity -status` again to confirm that developer mode is enabled. Enabling developer mode addresses this prerequisite only; it doesn't resolve every debugger failure. ## Publish Rust apps -`aspire publish` and `aspire deploy` use the app directory as the build context. If it contains a `Dockerfile`, Aspire uses it as authored. Otherwise, Aspire generates a multi-stage Dockerfile that compiles the crate and runs it as a non-root `app` user. A `rust-toolchain.toml` pin is installed by rustup in the build image. +When you run `aspire publish` or `aspire deploy`, Aspire packages each Rust app as a container image. If the app directory contains a `Dockerfile`, Aspire uses it as-is. Otherwise, Aspire generates a multi-stage Dockerfile that: -Everything needed by the build must be inside that context. For a crate with workspace inheritance or sibling path dependencies, use the workspace root as `appDirectory` and select the member with `WithCargoPackage` / `withCargoPackage`. +- Builds the app inside the image with `cargo build` and the app's Cargo options, so the app is never compiled on your machine. Aspire still runs `cargo metadata` on your machine to find the app's binary, so publishing needs Cargo too. If the app pins a toolchain with a `rust-toolchain.toml` file, rustup installs it in the build stage. BuildKit cache mounts keep Cargo's registry and the build's target directory between builds. +- Adds `--release`, unless you pass `false` to `WithCargoReleaseBuild` or select a profile with `WithCargoProfile`. It also adds `--locked` when the app directory contains a `Cargo.lock` file, unless you pass `false` to `WithCargoLocked`, so the image builds only the dependency versions that you committed. +- Runs the binary on an Alpine image as an unprivileged `app` user, with the binary at `/app/` as the container's entry point. +- Excludes `target` directories, `.git`, `.env` files, and other files that the build doesn't need from the build context, unless the app directory has its own `.dockerignore` file. + +The app directory is the build context, so everything that the build needs must be inside it, including a manifest that you select with `WithCargoManifestPath`. A Cargo `--config` argument that appears to contain credentials, such as a registry token, makes publishing fail, because a generated Dockerfile would store it in the image. To build with private registry credentials, author a `Dockerfile` that passes them with a BuildKit secret mount. + + + For more information about publishing, see [Aspire deployment](/deployment/). + + +### Publish a workspace member + +A crate that inherits settings from its workspace, such as `version.workspace = true`, or that depends on sibling crates by path, needs the whole workspace in the build context. Use the workspace root as the app directory, and select the crate with `WithCargoPackage`: + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const api = await builder.addRustApp('api', '../services'); +await api.withCargoPackage('api'); +await api.withHttpEndpoint({ env: 'PORT' }); + +await builder.build().run(); +``` + + + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +builder.AddRustApp("api", "../services") + .WithCargoPackage("api") + .WithHttpEndpoint(env: "PORT"); + +builder.Build().Run(); +``` + + + + +### Customize the base images + +The generated Dockerfile builds on `docker.io/library/rust:1.97-alpine3.24` and runs on `docker.io/library/alpine:3.24`. Both images use musl. To use other images, such as mirrors in your own registry or images for another target, set both in a single call to , because each call replaces the images that earlier calls set: + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const api = await builder.addRustApp('api', '../api'); +await api.withDockerfileBaseImage({ + buildImage: 'myregistry.azurecr.io/rust-armv7-build:1.97', + runtimeImage: 'myregistry.azurecr.io/armv7-runtime:latest', +}); + +if (await builder.executionContext.isPublishMode()) { + await api.withCargoTarget('armv7-unknown-linux-gnueabihf'); +} + +await builder.build().run(); +``` + + + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +var api = builder.AddRustApp("api", "../api") + .WithDockerfileBaseImage( + buildImage: "myregistry.azurecr.io/rust-armv7-build:1.97", + runtimeImage: "myregistry.azurecr.io/armv7-runtime:latest"); + +if (builder.ExecutionContext.IsPublishMode) +{ + api.WithCargoTarget("armv7-unknown-linux-gnueabihf"); +} + +builder.Build().Run(); +``` + + + + +`WithDockerfileBaseImage` affects only the published image, but `WithCargoTarget` also applies when you run the AppHost. Cargo then builds the app for the selected target on your machine and runs the result, which works only when your machine can build for that target and run its binaries. For that reason, the preceding example selects the target only in publish mode. + +When you select a target triple with `WithCargoTarget`, the generated Dockerfile installs the target's standard library with `rustup target add`, and builds the image for the matching platform: `linux/amd64` for `x86_64` targets, `linux/arm64` for `aarch64` targets, `linux/386` for 32-bit x86 targets, and `linux/arm` for 32-bit ARM targets. The default images support only `x86_64` and `aarch64` musl targets: + +- A 32-bit musl target, such as `armv7-unknown-linux-musleabihf`, needs a custom build image, but can keep the default runtime image. +- A target for another ABI, such as a `gnu` target, needs both a custom build image and a custom runtime image. +- A non-Linux target, or a Linux target for another architecture, needs an authored `Dockerfile`. + +Aspire doesn't install a linker or other cross-compilation tools, so a custom build image must already support the target. Keep the target, the build image, and the runtime image compatible with each other. + +### Include files from other resources + +The generated Dockerfile can copy files that another resource produces, such as a frontend's build output, into the Rust app's image. Call on the Rust app with the source resource and a destination path. A relative destination path is resolved against the image's `/app` working directory, so the following example places the `frontend` app's build output in `/app/static`: + + + + +```typescript title="apphost.mts" twoslash +import { createBuilder } from './.aspire/modules/aspire.mjs'; + +const builder = await createBuilder(); + +const api = await builder.addRustApp('api', '../api'); +await api.withHttpEndpoint({ env: 'PORT' }); +await api.withExternalHttpEndpoints(); + +const frontend = await builder.addViteApp('frontend', '../frontend'); +await frontend.withReference(api); +await frontend.waitFor(api); + +await api.publishWithContainerFiles(frontend, './static'); + +await builder.build().run(); +``` + + + + +```csharp title="AppHost.cs" +var builder = DistributedApplication.CreateBuilder(args); + +var api = builder.AddRustApp("api", "../api") + .WithHttpEndpoint(env: "PORT") + .WithExternalHttpEndpoints(); + +var frontend = builder.AddViteApp("frontend", "../frontend") + .WithReference(api) + .WaitFor(api); + +api.PublishWithContainerFiles(frontend, "./static"); + +builder.Build().Run(); +``` + + + + +Aspire builds the source resource before it builds the Rust app's image. Copying the files doesn't make the app serve them, so serve them from the app — for example, with the `ServeDir` service of the `tower-http` crate, pointed at the `static` directory. Aspire adds the files only to the Dockerfile that it generates, so an authored `Dockerfile` in the app directory doesn't receive them. + + + For more information, see [Inject files at publish time](/app-host/container-files/#inject-files-at-publish-time) and [Deploy JavaScript apps](/deployment/javascript-apps/). + + +## Connection properties + +For the environment variables that a Rust app receives — and how other apps discover a Rust app's endpoints — see [Connect to Rust apps](/integrations/frameworks/rust/rust-connect/). + +## Hosting integration health checks + +The Rust hosting integration doesn't add health checks to Rust resources. When your app exposes a health endpoint, add a check with `WithHttpHealthCheck`, as shown in [Add a Rust app](#add-a-rust-app). Without a health check, Aspire considers a Rust app ready as soon as its `cargo run` process starts — before Cargo finishes building the app and the app starts listening — so resources that wait for it can start too early. + + + +## Migrate from the Community Toolkit + +For Aspire 13.6 and later, `Aspire.Hosting.Rust` replaces [📦 CommunityToolkit.Aspire.Hosting.Rust](https://www.nuget.org/packages/CommunityToolkit.Aspire.Hosting.Rust) for Cargo apps. To migrate an AppHost: + +1. Remove `CommunityToolkit.Aspire.Hosting.Rust` from the AppHost's package references, `#:package` directives, or `aspire.config.json`, unless you use Bacon, as described in [Toolkit-only Bacon support](#toolkit-only-bacon-support). +1. Add `Aspire.Hosting.Rust`, as described in [Installation](#installation). +1. Update your AppHost code with the replacements in the following table. The table uses C# names; TypeScript AppHosts use the same names in camel case. +1. Review how your Rust apps are published. The Community Toolkit integration requires a `Dockerfile` in the app directory. The first-party integration uses that `Dockerfile` when it exists, and generates one otherwise. + +| Community Toolkit API | First-party replacement | +| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| The `workingDirectory` parameter of `AddRustApp` | The `appDirectory` parameter. | +| Cargo options in the `args` parameter of `AddRustApp` | `WithCargoFeatures`, `WithCargoBinTarget`, `WithCargoReleaseBuild`, and the other `WithCargo*` methods, or `WithCargoArgs` for options that don't have a method. | +| Program arguments after `--` in the `args` parameter | `WithArgs`, without the `--` separator, which Aspire adds. | +| `RustAppExecutableResource` | `RustAppResource`. | +| `AddBaconApp` | No first-party replacement. See [Toolkit-only Bacon support](#toolkit-only-bacon-support). | + +### Toolkit-only Bacon support -The default images use musl. A target triple doesn't install a linker or other cross-compilation tooling. When you need custom images, set both `buildImage` and `runtimeImage` in **one** `WithDockerfileBaseImage` / `withDockerfileBaseImage` call; subsequent calls replace the previous image configuration. Keep the target ABI, build image, and runtime image compatible. Non-Linux targets or architectures without a supported container-platform mapping require an authored Dockerfile. +`AddBaconApp` runs an app with [Bacon](https://dystroy.org/bacon/), a background code checker for Rust that rebuilds and reruns the app when its source changes. It remains a Community Toolkit API, and the first-party integration has no replacement for it. To keep using it, keep `CommunityToolkit.Aspire.Hosting.Rust` alongside `Aspire.Hosting.Rust`, and install Bacon. In that AppHost, add Cargo apps with the first-party form of `AddRustApp` — a name and an `appDirectory`, with Cargo options set through the `WithCargo*` methods — rather than with the Toolkit's `workingDirectory` and `args` parameters. Bacon apps keep the Toolkit's behavior, including its requirement for an authored `Dockerfile` when you publish. ## See also -- [Rust documentation](https://www.rust-lang.org/learn) -- [Cargo documentation](https://doc.rust-lang.org/cargo/) -- [Bacon documentation](https://dystroy.org/bacon/) - [Get started with the Rust integration](/integrations/frameworks/rust/rust-get-started/) -- [Aspire Community Toolkit](https://github.com/CommunityToolkit/Aspire) +- [Connect to Rust apps](/integrations/frameworks/rust/rust-connect/) +- [📦 Aspire.Hosting.Rust](https://www.nuget.org/packages/Aspire.Hosting.Rust) +- [The Cargo Book](https://doc.rust-lang.org/cargo/) +- [Aspire Rust playground](https://github.com/microsoft/aspire/tree/main/playground/rust) diff --git a/src/frontend/src/content/docs/ja/get-started/app-host.mdx b/src/frontend/src/content/docs/ja/get-started/app-host.mdx index 8c13c9b84..9c7cc0833 100644 --- a/src/frontend/src/content/docs/ja/get-started/app-host.mdx +++ b/src/frontend/src/content/docs/ja/get-started/app-host.mdx @@ -136,7 +136,7 @@ architecture-beta このアーキテクチャは、**Java API**(Spring Boot を使用)が **PostgreSQL データベース**に接続し、**React フロントエンド** がその API を利用する構成を示しています。Java API は Spring Boot と Spring Data JPA を使用し、JDBC または Spring Data を用いて PostgreSQL に接続します。React フロントエンドは Vite で構築され、HTTP 経由で API と通信します。 @@ -180,7 +180,7 @@ Java との統合は、NuGet パッケージの [CommunityToolkit.Aspire.Hosting -

      Spring Boot アプリケーションには AddSpringApp() を使います。

      +

      Spring Boot アプリケーションには AddSpringBootApp() を使います。

      @@ -354,7 +354,7 @@ AppHost は分散アプリケーションの設計図であり、残りの処理 - +

      仕組み:

        @@ -456,12 +456,11 @@ AppHost は分散アプリケーションの設計図であり、残りの処理 - +

        行っていること:

          -
        • AddSpringApp("api", "../api", "otel.jar") は、Spring Boot アプリを api という名前のサービスとして登録します。
        • -
        • WithHttpEndpoint(port: 8080) は、Spring Boot アプリをポート 8080 で公開します。
        • +
        • AddSpringBootApp("api", "../api") は、Spring Boot アプリを api という名前のサービスとして登録し、Aspire が割り当てたポートを SERVER_PORT 環境変数でアプリに渡す HTTP エンドポイントを宣言します。
        • WithReference(postgres) は、接続情報を API の構成に注入します。
        • WaitFor(postgres) は、API の起動を PostgreSQL が正常になるまで待機させます。
        @@ -552,7 +551,7 @@ AppHost は分散アプリケーションの設計図であり、残りの処理 - +

        要点:

          From 9ddabae439f9eda061e850e9f9147ebe1b5ecf53 Mon Sep 17 00:00:00 2001 From: David Pine Date: Tue, 29 Sep 2026 10:02:10 -0500 Subject: [PATCH 2/2] Use "AppHost" in the EF Core architecture diagram Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../integrations/efcore/ef-core-aspire-architecture-light.svg | 2 +- .../efcore/ef-core-aspire-architecture.excalidraw | 4 ++-- .../integrations/efcore/ef-core-aspire-architecture.svg | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/assets/integrations/efcore/ef-core-aspire-architecture-light.svg b/src/frontend/src/assets/integrations/efcore/ef-core-aspire-architecture-light.svg index fd381f434..29dd29d3b 100644 --- a/src/frontend/src/assets/integrations/efcore/ef-core-aspire-architecture-light.svg +++ b/src/frontend/src/assets/integrations/efcore/ef-core-aspire-architecture-light.svg @@ -1,4 +1,4 @@ App HostMicroservice ProjectsModel:Entity classesDbContext classDI ContainerDbContext instanceDatabase containerPassreferenceCreateQueries \ No newline at end of file + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,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); }AppHostMicroservice ProjectsModel:Entity classesDbContext classDI ContainerDbContext instanceDatabase containerPassreferenceCreateQueries \ No newline at end of file diff --git a/src/frontend/src/assets/integrations/efcore/ef-core-aspire-architecture.excalidraw b/src/frontend/src/assets/integrations/efcore/ef-core-aspire-architecture.excalidraw index 140aa7de1..5885cf682 100644 --- a/src/frontend/src/assets/integrations/efcore/ef-core-aspire-architecture.excalidraw +++ b/src/frontend/src/assets/integrations/efcore/ef-core-aspire-architecture.excalidraw @@ -163,13 +163,13 @@ "updated": 1742813104835, "link": null, "locked": false, - "text": "App Host", + "text": "AppHost", "fontSize": 20, "fontFamily": 6, "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "App Host", + "originalText": "AppHost", "autoResize": true, "lineHeight": 1.35 }, diff --git a/src/frontend/src/assets/integrations/efcore/ef-core-aspire-architecture.svg b/src/frontend/src/assets/integrations/efcore/ef-core-aspire-architecture.svg index 7ef4f7369..fbea97ab1 100644 --- a/src/frontend/src/assets/integrations/efcore/ef-core-aspire-architecture.svg +++ b/src/frontend/src/assets/integrations/efcore/ef-core-aspire-architecture.svg @@ -1,4 +1,4 @@ App HostMicroservice ProjectsModel:Entity classesDbContext classDI ContainerDbContext instanceDatabase containerPassreferenceCreateQueries \ No newline at end of file + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,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); }AppHostMicroservice ProjectsModel:Entity classesDbContext classDI ContainerDbContext instanceDatabase containerPassreferenceCreateQueries \ No newline at end of file