diff --git a/docs/app-host/executable-resources.md b/docs/app-host/executable-resources.md new file mode 100644 index 0000000000..cdc194eaa7 --- /dev/null +++ b/docs/app-host/executable-resources.md @@ -0,0 +1,263 @@ +--- +title: Host external executables in .NET Aspire +description: Learn how to use ExecutableResource and AddExecutable to host external executable applications in your .NET Aspire app host. +ms.date: 08/04/2025 +--- + +# Host external executables in .NET Aspire + +In .NET Aspire, you can host external executable applications alongside your .NET projects using the method. This capability is useful when you need to integrate non-.NET applications or tools into your distributed application, such as Node.js applications, Python scripts, or specialized CLI tools. + +## When to use executable resources + +Use executable resources when you need to: + +- Host non-.NET applications that don't have containerized equivalents. +- Integrate command-line tools or utilities into your application. +- Run external processes that other resources depend on. +- Develop with tools that provide local development servers. + +Common examples include: + +- **Frontend development servers**: Tools like [Vercel CLI](https://vercel.com/docs/cli), Vite, or webpack dev server. +- **Language-specific applications**: Node.js apps, Python scripts, or Go applications. +- **Database tools**: Migration utilities or database seeders. +- **Build tools**: Asset processors or code generators. + +## Basic usage + +The method requires a resource name, the executable path, and optionally command-line arguments and a working directory: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +// Basic executable without arguments +var nodeApp = builder.AddExecutable("frontend", "node", ".", "server.js"); + +// Executable with command-line arguments +var pythonApp = builder.AddExecutable("api", "python", ".", "-m", "uvicorn") + .WithArgs("main:app", "--reload", "--host", "0.0.0.0", "--port", "8000"); + +builder.Build().Run(); +``` + +This code demonstrates setting up a basic executable resource. The first example runs a Node.js server script, while the second starts a Python application using Uvicorn with specific configuration options. + +## Configure command-line arguments + +You can provide command-line arguments in several ways: + +### Arguments in the AddExecutable call + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +// Arguments provided directly in AddExecutable +var app = builder.AddExecutable("vercel-dev", "vercel", ".", "dev") + .WithArgs("--listen", "3000"); +``` + +### Arguments using WithArgs + +Use the method to add arguments after creating the resource: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var app = builder.AddExecutable("webpack-dev", "npm", ".", "run") + .WithArgs("dev", "--", "--port", "3000"); +``` + +### Dynamic arguments with WithEnvironment + +For arguments that depend on other resources, use environment variables: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var database = builder.AddPostgres("postgres").AddDatabase("db"); + +var migrator = builder.AddExecutable("migrator", "dotnet", ".", "run") + .WithReference(database); +``` + +## Work with resource dependencies + +Executable resources can reference other resources and access their connection information: + +### Basic resource references + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var redis = builder.AddRedis("cache"); +var postgres = builder.AddPostgres("postgres").AddDatabase("appdb"); + +var app = builder.AddExecutable("worker", "python", ".", "worker.py") + .WithReference(redis) // Provides ConnectionStrings__cache + .WithReference(postgres); // Provides ConnectionStrings__appdb +``` + +### Access specific endpoint information + +For more control over how connection information is passed to your executable: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var redis = builder.AddRedis("cache"); + +var app = builder.AddExecutable("app", "node", ".", "app.js") + .WithReference(redis) + .WithEnvironment(context => + { + // Provide individual connection details + context.EnvironmentVariables["REDIS_HOST"] = redis.Resource.PrimaryEndpoint.Property(EndpointProperty.Host); + context.EnvironmentVariables["REDIS_PORT"] = redis.Resource.PrimaryEndpoint.Property(EndpointProperty.Port); + context.EnvironmentVariables["REDIS_URL"] = $"redis://{redis.Resource.PrimaryEndpoint}"; + }); +``` + +## Practical example: Vercel CLI + +Here's a complete example using the [Vercel CLI](https://vercel.com/docs/cli) to host a frontend application with a backend API: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +// Backend API +var api = builder.AddProject("api") + .WithExternalHttpEndpoints(); + +// Frontend with Vercel CLI +var frontend = builder.AddExecutable("vercel-dev", "vercel", ".", "dev") + .WithArgs("--listen", "3000") + .WithEnvironment("API_URL", api.GetEndpoint("http")) + .WithHttpEndpoint(port: 3000, name: "http"); + +builder.Build().Run(); +``` + +## Configure endpoints + +Executable resources can expose HTTP endpoints that other resources can reference: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var frontend = builder.AddExecutable("vite-dev", "npm", ".", "run") + .WithArgs("dev", "--", "--port", "5173", "--host", "0.0.0.0") + .WithHttpEndpoint(port: 5173, name: "http"); + +// Another service can reference the frontend +var e2eTests = builder.AddExecutable("playwright", "npx", ".", "playwright", "test") + .WithEnvironment("BASE_URL", frontend.GetEndpoint("http")); +``` + +## Environment configuration + +Configure environment variables for your executable: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var app = builder.AddExecutable("api", "uvicorn", ".", "main:app") + .WithArgs("--reload", "--host", "0.0.0.0") + .WithEnvironment("DEBUG", "true") + .WithEnvironment("LOG_LEVEL", "info") + .WithEnvironment(context => + { + // Dynamic environment variables + context.EnvironmentVariables["START_TIME"] = DateTimeOffset.UtcNow.ToString(); + }); +``` + +## Publishing with PublishAsDockerfile + +For production deployment, executable resources need to be containerized. Use the method to specify how the executable should be packaged: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var app = builder.AddExecutable("frontend", "npm", ".", "start") + .WithArgs("--port", "3000") + .PublishAsDockerfile(); +``` + +When you call `PublishAsDockerfile()`, .NET Aspire generates a Dockerfile during the publish process. You can customize this by providing your own Dockerfile: + +### Custom Dockerfile for publishing + +Create a `Dockerfile` in your executable's working directory: + +```dockerfile +FROM node:22-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production +COPY . . +EXPOSE 3000 +CMD ["npm", "start"] +``` + +Then reference it in your app host: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var app = builder.AddExecutable("frontend", "npm", ".", "start") + .PublishAsDockerfile([new DockerfileBuildArg("NODE_ENV", "production")]); +``` + +## Advanced scenarios + +### Multiple executable arguments + +For complex command lines, you can chain multiple `WithArgs` calls: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var app = builder.AddExecutable("complex-tool", "myapp", ".") + .WithArgs("--config", "config.json") + .WithArgs("--verbose") + .WithArgs("--output", "/tmp/results") + .WithArgs("--workers", "4"); +``` + +### Conditional execution + +Use explicit start control for executables that should only run under certain conditions: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var database = builder.AddPostgres("postgres").AddDatabase("appdb"); + +var migrator = builder.AddExecutable("migrator", "dotnet", ".", "ef") + .WithArgs("database", "update") + .WithReference(database) + .WithExplicitStart(); // Only run when manually started + +var api = builder.AddProject("api") + .WithReference(database) + .WaitFor(migrator); // API waits for migrator to complete +``` + +## Best practices + +When working with executable resources: + +1. **Use explicit paths**: For better reliability, use full paths to executables when possible. +2. **Handle dependencies**: Use `WithReference` to establish proper dependency relationships. +3. **Configure explicit start**: Use `WithExplicitStart()` for executables that shouldn't start automatically. +4. **Prepare for deployment**: Always use `PublishAsDockerfile()` for production scenarios. +5. **Environment isolation**: Use environment variables rather than command-line arguments for sensitive configuration. +6. **Resource naming**: Use descriptive names that clearly identify the executable's purpose. + +## See also + +- [App host overview](../fundamentals/app-host-overview.md) +- [Add Dockerfiles to the app model](withdockerfile.md) +- [Node.js apps in .NET Aspire](../get-started/build-aspire-apps-with-nodejs.md) +- [Python apps in .NET Aspire](../get-started/build-aspire-apps-with-python.md) diff --git a/docs/toc.yml b/docs/toc.yml index 1f5d459171..30ee5695b8 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -71,6 +71,9 @@ items: - name: Custom resource URLs displayName: withurl,withurlforendpoint,withurls href: fundamentals/custom-resource-urls.md + - name: Host external executables + href: app-host/executable-resources.md + displayName: executable,addexecutable,external apps,cli tools - name: Add Dockerfiles to the app model href: app-host/withdockerfile.md displayName: dockerfile,docker