container-publish — Claude Code skill: containers without a Dockerfile
Builds OCI images straight from dotnet publish /t:PublishContainer, no Dockerfile involved. Useful when a .NET 10 app has to ship as a container and land in a registry.
npx skills add codewithmukesh/dotnet-claude-kit --skill container-publish -a claude-codeWhat container-publish does
The skill covers container publishing through the .NET 10 SDK: the PublishContainer MSBuild target builds an image from your project, and every setting lives in the .csproj as an MSBuild property — ContainerRepository, ContainerFamily, ContainerPort, environment variables, labels. Nothing separate to keep in sync with the code.
Base image variants are laid out: noble-chiseled with no shell or package manager, noble-chiseled-extra with ICU for localization, alpine with a shell. With Native AOT the SDK picks chiseled-aot on its own. Multi-arch builds go through ContainerRuntimeIdentifiers, and there are sections on multiple tags, saving to a tarball on a machine with no Docker, and a GitHub Actions pipeline using docker/login-action.
One limit matters: if the app needs OS packages, SDK publishing is out — there is no RUN equivalent, so you build a custom base image with a Dockerfile first and point ContainerBaseImage at it. Pushing to a registry also requires authentication; skip docker login and the publish fails with CONTAINER1013. On Windows, pass --os linux --arch x64 or you may end up with a Windows container.
What it helps with
- build a .NET service image with no Dockerfile
- shrink a production image with chiseled base variants
- produce a multi-arch image in one publish
- save an image as a tarball for CI scanning
- push an image to ghcr.io or Azure Container Registry
Example prompts for the agent
- Build a container from my ASP.NET Core project without a Dockerfile and push it to ghcr.io tagged 1.0.0
- Switch the project to noble-chiseled and add a multi-arch build for linux-x64 and linux-arm64
- Set up a GitHub Actions pipeline that publishes the image to ghcr.io tagged with the commit SHA
How to install container-publish
1Skills CLI (Vercel)
npx skills add codewithmukesh/dotnet-claude-kit --skill container-publish -a claude-code2Manually: put the files in place
~/.claude/skills/container-publish/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/container-publish/
1Skills CLI (Vercel)
npx skills add codewithmukesh/dotnet-claude-kit --skill container-publish -a codex2Manually: put the files in place
.agents/skills/container-publish/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add codewithmukesh/dotnet-claude-kit --skill container-publish -a cursor2Manually: put the files in place
.cursor/skills/container-publish/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install codewithmukesh/dotnet-claude-kit container-publish2Manually: put the files in place
.github/skills/container-publish/SKILL.md1Skills CLI (Vercel)
npx skills add codewithmukesh/dotnet-claude-kit --skill container-publish -a gemini-cli2Platform CLI
gemini skills install https://github.com/codewithmukesh/dotnet-claude-kit --path skills/container-publishManually: put the files in place
.devin/skills/container-publish/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add codewithmukesh/dotnet-claude-kit --skill container-publish -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about container-publish
How do I install container-publish in Claude?
Run npx skills add codewithmukesh/dotnet-claude-kit --skill container-publish -a claude-code. The skill lands in ~/.claude/skills/container-publish/SKILL.md.
What does container-publish do?
It documents containerization through dotnet publish /t:PublishContainer: MSBuild properties, chiseled images, multi-arch builds and registry publishing. No Dockerfile is written.
When should I skip SDK publishing?
When the app needs OS packages — there is nowhere to run something like apt-get. Build a custom base image with a Dockerfile and reference it via ContainerBaseImage.
Source text
SKILL.md · MIT
---
name: container-publish
description: >
Dockerfile-less containerization using the .NET 10 SDK container publishing
feature. Covers MSBuild properties, chiseled images, multi-arch builds, and
registry publishing — all without writing a Dockerfile.
Load this skill when the user wants to containerize without a Dockerfile, or
mentions "dotnet publish container", "PublishContainer", "ContainerRepository",
"ContainerFamily", "chiseled", "distroless", "container publish", "SDK
container", "no Dockerfile", or "containerize without Docker".
---
# Container Publishing (No Dockerfile)
## Core Principles
1. **No Dockerfile needed** — The .NET 10 SDK builds OCI-compliant container images directly from `dotnet publish /t:PublishContainer`. No Dockerfile to write or maintain.
2. **Chiseled images for production** — Use `noble-chiseled` base images: no shell, no package manager, 7 Linux components vs 100+. Smallest attack surface.
3. **Non-root by default** — .NET 10 container images run as the `app` user automatically. Never override to root in production.
4. **Configuration in the .csproj** — All container settings are MSBuild properties, versioned with your project. No separate files to drift.
## Patterns
### Minimal Container Publish
No project file changes needed. Just publish:
```bash
dotnet publish /t:PublishContainer --os linux --arch x64
```
This creates a container image in your local Docker daemon using the default `aspnet:10.0` base image.
### Production-Ready .csproj Configuration
```xml
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ContainerRepository>mycompany/myapp-api</ContainerRepository>
<ContainerFamily>noble-chiseled</ContainerFamily>
</PropertyGroup>
<ItemGroup>
<ContainerPort Include="8080" Type="tcp" />
<ContainerEnvironmentVariable Include="ASPNETCORE_HTTP_PORTS" Value="8080" />
<ContainerEnvironmentVariable Include="DOTNET_EnableDiagnostics" Value="0" />
<ContainerLabel Include="org.opencontainers.image.vendor" Value="MyCompany" />
</ItemGroup>
</Project>
```
### Publishing to a Registry
Authenticate with `docker login` first, then specify the registry:
```bash
# GitHub Container Registry
docker login ghcr.io
dotnet publish /t:PublishContainer --os linux --arch x64 \
-p ContainerRegistry=ghcr.io \
-p ContainerImageTag=1.0.0
# Azure Container Registry
az acr login --name myregistry
dotnet publish /t:PublishContainer --os linux --arch x64 \
-p ContainerRegistry=myregistry.azurecr.io
# Docker Hub (requires username prefix in repository)
dotnet publish /t:PublishContainer --os linux --arch x64 \
-p ContainerRegistry=docker.io \
-p ContainerRepository=myuser/myapp
```
### Multi-Architecture Images
Build images for multiple platforms with a single publish:
```xml
<PropertyGroup>
<RuntimeIdentifiers>linux-x64;linux-arm64</RuntimeIdentifiers>
<ContainerRuntimeIdentifiers>linux-x64;linux-arm64</ContainerRuntimeIdentifiers>
</PropertyGroup>
```
```bash
dotnet publish /t:PublishContainer
```
This produces an OCI Image Index — registries serve the correct architecture automatically.
### Multiple Tags
```bash
# Bash — note the quoting for semicolons
dotnet publish /t:PublishContainer --os linux --arch x64 \
-p ContainerImageTags='"1.0.0;latest"'
```
Or in the project file:
```xml
<ContainerImageTags>1.0.0;latest</ContainerImageTags>
```
### Save as Tarball (No Docker Required)
No container runtime needed on the build machine. Useful for CI scanning:
```bash
dotnet publish /t:PublishContainer --os linux --arch x64 \
-p ContainerArchiveOutputPath=./images/myapp.tar.gz
# Scan with Trivy before pushing
trivy image --input ./images/myapp.tar.gz
```
### Chiseled Image Variants
| ContainerFamily | Use Case | Shell | Size |
|----------------|----------|-------|------|
| *(default)* | General purpose (Debian) | Yes | ~220 MB |
| `noble-chiseled` | Production (no shell) | No | ~110 MB |
| `noble-chiseled-extra` | Production with localization (ICU) | No | ~120 MB |
| `alpine` | Small size, has shell | Yes | ~112 MB |
```xml
<!-- Standard chiseled (InvariantGlobalization=true) -->
<ContainerFamily>noble-chiseled</ContainerFamily>
<!-- Chiseled with ICU for localization -->
<ContainerFamily>noble-chiseled-extra</ContainerFamily>
```
For Native AOT, the SDK auto-selects `chiseled-aot`:
```xml
<PublishAot>true</PublishAot>
<!-- SDK picks runtime-deps:10.0-noble-chiseled-aot automatically -->
```
### CI/CD with GitHub Actions
```yaml
jobs:
publish:
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- uses: actions/checkout@v5
- uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- run: |
dotnet publish src/MyApp.Api/MyApp.Api.csproj \
/t:PublishContainer --os linux --arch x64 \
-p ContainerRegistry=ghcr.io \
-p ContainerRepository=${{ github.repository_owner }}/myapp \
-p ContainerImageTag=${{ github.sha }}
```
## Anti-patterns
### Don't Use the Deprecated Property Names
```xml
<!-- BAD — ContainerImageName is deprecated -->
<ContainerImageName>myapp</ContainerImageName>
<!-- GOOD — use ContainerRepository -->
<ContainerRepository>myapp</ContainerRepository>
```
### Don't Use PublishProfile=DefaultContainer
```bash
# BAD — old approach, inconsistent across project types
dotnet publish -p:PublishProfile=DefaultContainer
# GOOD — use the MSBuild target directly
dotnet publish /t:PublishContainer
```
### Don't Forget to Target Linux
```bash
# BAD on Windows — may produce a Windows container
dotnet publish /t:PublishContainer
# GOOD — explicitly target Linux
dotnet publish /t:PublishContainer --os linux --arch x64
```
### Don't Skip Authentication Before Push
```bash
# BAD — fails with CONTAINER1013 error
dotnet publish /t:PublishContainer -p ContainerRegistry=ghcr.io
# GOOD — authenticate first
docker login ghcr.io
dotnet publish /t:PublishContainer -p ContainerRegistry=ghcr.io
```
### Don't Use SDK Publishing When You Need OS Packages
```xml
<!-- BAD — SDK container publish cannot run apt-get or install native packages -->
<!-- There is no RUN equivalent -->
<!-- GOOD — create a custom base image with a Dockerfile first, then reference it -->
<ContainerBaseImage>myregistry/custom-base:1.0</ContainerBaseImage>
```
## Decision Guide
| Scenario | Recommendation |
|----------|---------------|
| Standard ASP.NET Core API | SDK container publishing with `noble-chiseled` |
| Worker service / console app | SDK container publishing (native .NET 10 support) |
| Needs native OS packages | Dockerfile (or custom base image + SDK publishing) |
| Azure Functions | Dockerfile (not supported by SDK publishing) |
| CI without Docker daemon | Tarball output with `ContainerArchiveOutputPath` |
| Multi-arch deployment (x64 + arm64) | `ContainerRuntimeIdentifiers` property |
| Production image size | `noble-chiseled` (~110 MB) or Native AOT (~10 MB) |
| Local development | `dotnet publish /t:PublishContainer --os linux --arch x64` |
| Registry push | `ContainerRegistry` + `docker login` |
© codewithmukesh · MIT · codewithmukesh/dotnet-claude-kit