Container files
Aspire provides APIs to inject files and directories into containers, enabling you to configure containerized resources with custom configuration files, scripts, certificates, and other assets. There are two complementary APIs:
WithContainerFiles: Injects files into containers at development time when they start duringaspire run.PublishWithContainerFiles: Copies files from one resource's container into another resource's container as build artifacts at publish time duringaspire publish.
Inject files at development time
The WithContainerFiles extension method creates or updates files and directories inside a container at a specified destination path. It supports three approaches depending on your needs: inline entries for declarative file definitions, a source path for copying from the host file system, and an async callback for dynamic file generation.
Caution
WithContainerFiles is primarily intended for development-time configuration and is not supported at publish time. To inject files into containers during publish, use PublishWithContainerFiles instead.
Inline entries
Use the inline entries overload to declaratively define files and directories using ContainerFileSystemItem objects. This is useful when file contents are known at build time or can be expressed as string literals.
C#
var builder = DistributedApplication.CreateBuilder(args);
builder.AddContainer("myapp", "myapp:latest")
.WithContainerFiles("/app/config", [
new ContainerFile
{
Name = "appsettings.json",
Contents = """
{
"Logging": {
"LogLevel": {
"Default": "Information"
}
}
}
"""
},
new ContainerDirectory
{
Name = "scripts",
Entries = [
new ContainerFile
{
Name = "init.sh",
Contents = "#!/bin/bash\necho 'Initializing...'",
Mode = UnixFileMode.UserRead
| UnixFileMode.UserWrite
| UnixFileMode.UserExecute
}
]
}
]);
builder.Build().Run();
TypeScript
Note
The withContainerFiles API is not yet available in the TypeScript AppHost SDK.
In the preceding example:
- A JSON configuration file is created at
/app/config/appsettings.jsonwith the specified contents. - A nested
scriptsdirectory is created at/app/config/scripts/containing an executable shell script.
Source path
Use the source path overload to copy files from a directory on the host machine into the container. This is useful when you have existing configuration files or assets on disk.
C#
var builder = DistributedApplication.CreateBuilder(args);
builder.AddContainer("myapp", "myapp:latest")
.WithContainerFiles("/app/config", "./config-files");
builder.Build().Run();
TypeScript
Note
The withContainerFiles API is not yet available in the TypeScript AppHost SDK.
Unless the source path is a rooted (absolute) path, it's interpreted as relative to the AppHost project directory. All files in the source directory are copied to the destination path in the container at startup.
Async callback
Use the callback overload to generate files dynamically when the container starts. The callback receives a ContainerFileSystemCallbackContext that provides access to the IServiceProvider and the resource's IResource model, enabling you to resolve services or inspect the app model.
C#
var builder = DistributedApplication.CreateBuilder(args);
builder.AddContainer("worker", "worker:latest")
.WithContainerFiles("/app/config",
async (context, cancellationToken) =>
{
var config = new
{
MachineName = Environment.MachineName,
Timestamp = DateTime.UtcNow
};
return
[
new ContainerFile
{
Name = "runtime-config.json",
Contents = JsonSerializer.Serialize(config)
}
];
});
builder.Build().Run();
TypeScript
Note
The withContainerFiles API is not yet available in the TypeScript AppHost SDK.
The callback is invoked each time the container starts, so the generated files always reflect the current state.
File types
The WithContainerFiles API uses a type hierarchy rooted at the abstract ContainerFileSystemItem class. Each type represents a different kind of file system entry.
ContainerFile
ContainerFile represents a standard file. Set either Contents (a string) or SourcePath (an absolute path on the host) to provide the file data — the two are mutually exclusive.
// File with inline contents
var configYaml = new ContainerFile
{
Name = "config.yaml",
Contents = "key: value"
};
// File sourced from the host file system
var dataCsv = new ContainerFile
{
Name = "data.csv",
SourcePath = "/path/to/data.csv"
};
Set ContinueOnError to true to allow the container to start even if creating this particular file fails:
var optionalJson = new ContainerFile
{
Name = "optional-config.json",
Contents = "{}",
ContinueOnError = true
};
ContainerDirectory
ContainerDirectory represents a directory that can contain nested ContainerFileSystemItem entries, allowing you to build arbitrary directory trees.
var certsDir = new ContainerDirectory
{
Name = "certs",
Entries = [
new ContainerFile
{
Name = "ca.pem",
SourcePath = "/path/to/ca.pem"
},
new ContainerDirectory
{
Name = "private",
Entries = [
new ContainerFile
{
Name = "server.key",
SourcePath = "/path/to/server.key",
Mode = UnixFileMode.UserRead
}
]
}
]
};
You can also populate a ContainerDirectory from files on disk using the static GetFileSystemItemsFromPath method:
var assetsDir = new ContainerDirectory
{
Name = "assets",
Entries = ContainerDirectory.GetFileSystemItemsFromPath(
"/path/to/assets",
searchOptions: SearchOption.AllDirectories)
};
ContainerOpenSSLCertificateFile
ContainerOpenSSLCertificateFile represents a PEM-encoded public certificate. In addition to placing the certificate file in the container, Aspire automatically creates an OpenSSL-compatible symlink ([subject hash].[n]) in the same directory — equivalent to running openssl rehash. This enables containers that use OpenSSL for certificate validation to discover the certificate automatically.
C#
builder.AddContainer("myapp", "myapp:latest")
.WithContainerFiles("/certs", [
new ContainerOpenSSLCertificateFile
{
Name = "ca-cert.pem",
Contents = pemCertificateString
}
]);
TypeScript
Note
The withContainerFiles API is not yet available in the TypeScript AppHost SDK.
File permissions and ownership
All WithContainerFiles overloads accept optional parameters to control file ownership and permissions.
Owner and group
The defaultOwner and defaultGroup parameters set the default UID and GID applied to all created files and directories. Both default to 0 (root) when not specified. You can override ownership on individual items using the Owner and Group properties on any ContainerFileSystemItem.
C#
builder.AddContainer("myapp", "myapp:latest")
.WithContainerFiles("/app/data",
[
new ContainerFile
{
Name = "shared.txt",
Contents = "shared data"
},
new ContainerFile
{
Name = "user-only.txt",
Contents = "private data",
Owner = 1000,
Group = 1000
}
],
defaultOwner: 33, // www-data
defaultGroup: 33);
TypeScript
Note
The withContainerFiles API is not yet available in the TypeScript AppHost SDK.
In this example, shared.txt inherits the default owner/group of 33, while user-only.txt overrides with UID/GID 1000.
Umask
The umask parameter controls default permissions by subtracting (masking) permission bits from the base defaults. Without an explicit Mode set on an item:
- Directories start with
0777(read/write/execute for all) and have the umask subtracted - Files start with
0666(read/write for all) and have the umask subtracted
The default umask is 0022, which results in:
- Directories:
0755(owner: rwx, group: r-x, others: r-x) - Files:
0644(owner: rw-, group: r--, others: r--)
You can set Mode directly on individual items to override the umask-based default:
C#
builder.AddContainer("myapp", "myapp:latest")
.WithContainerFiles("/app/scripts",
[
new ContainerFile
{
Name = "run.sh",
Contents = "#!/bin/bash\necho 'Running'",
Mode = UnixFileMode.UserRead
| UnixFileMode.UserWrite
| UnixFileMode.UserExecute
}
],
umask: UnixFileMode.OtherRead
| UnixFileMode.OtherWrite
| UnixFileMode.OtherExecute);
TypeScript
Note
The withContainerFiles API is not yet available in the TypeScript AppHost SDK.
Persistent containers
For containers with ContainerLifetime.Persistent, changing the contents of container file entries causes the container to be recreated. Ensure any data written through WithContainerFiles is idempotent for a given app model configuration to avoid unintended container restarts.
Inject files at publish time
The PublishWithContainerFiles method copies files from one resource's container into another resource's container during aspire publish. This is the preferred approach for injecting files into containers at publish time.
A key use case is embedding single-page application (SPA) or static JavaScript frontends into a reverse proxy or web server container for production deployment. During development, frontend apps like Vite or React typically run as standalone dev servers. In production, however, the compiled static assets are often served by a backend API or a dedicated web server like Nginx. PublishWithContainerFiles bridges this gap by copying the built frontend output into the serving container as part of the publish process — no manual file copying or multi-stage Dockerfile required.
Embed a frontend in a backend
C#
var builder = DistributedApplication.CreateBuilder(args);
var frontend = builder.AddViteApp("frontend", "../frontend");
var api = builder.AddProject<Projects.Api>("api")
.PublishWithContainerFiles(frontend, "./wwwroot");
builder.Build().Run();
TypeScript
const builder = await createBuilder();
const frontend = await builder.addViteApp("frontend", "../frontend");
const api = await builder.addProject("api", "./Api/Api.csproj", "https")
.publishWithContainerFiles(frontend, "./wwwroot");
await builder.build().run();
In this example:
- The
frontendresource builds inside its container, producing compiled JavaScript, CSS, and HTML. - During publish, Aspire copies those files from the frontend container into the
apicontainer at./wwwroot. - The resulting
apicontainer includes both the API code and the frontend static assets, ready to serve the full application.
Serve a frontend from YARP
You can also embed frontend assets into a dedicated reverse proxy container:
C#
var builder = DistributedApplication.CreateBuilder(args);
var frontend = builder.AddViteApp("frontend", "../frontend");
var nginx = builder.AddYarp("gateway")
.PublishWithStaticFiles(frontend);
builder.Build().Run();
TypeScript
const builder = await createBuilder();
const frontend = await builder.addViteApp("frontend", "../frontend");
const nginx = await builder.addYarp("gateway")
.publishWithStaticFiles(frontend);
await builder.build().run();
This produces a self-contained Nginx container that serves the frontend application, with no external volume mounts or runtime file copying needed.
PublishWithContainerFiles only applies in publish mode — it has no effect during aspire run. The destination resource must implement IContainerFilesDestinationResource (such as ProjectResource), and the source resource must implement IResourceWithContainerFiles.
Customize the source path
By default, the source resource exports files from its container based on its configured output paths. Use WithContainerFilesSource to specify which path inside the source container to copy from:
C#
var frontend = builder.AddViteApp("frontend", "../frontend")
.WithContainerFilesSource("/app/dist");
var api = builder.AddProject<Projects.Api>("api")
.PublishWithContainerFiles(frontend, "./wwwroot");
TypeScript
Note
The withContainerFiles API is not yet available in the TypeScript AppHost SDK.
Use ClearContainerFilesSources to remove any previously configured source paths before adding new ones.