Overview
GitHub Container Registry (ghcr.io) provides free container hosting for public repositories and affordable storage for private ones. This recipe shows how to build Docker images and push them to ghcr.io using ANDO.
Prerequisites
Authentication
GitHub operations require authentication. ANDO checks these sources in order:
| Method | Description |
|---|---|
GITHUB_TOKEN | Environment variable (preferred for CI/CD) |
gh auth login | Uses token from gh CLI if logged in locally |
For CI/CD, use the built-in GITHUB_TOKEN in GitHub Actions. For local development, run gh auth login once.
Token Permissions
Your token needs the write:packages scope to push images. Create a token at github.com/settings/tokens.
Recommended: Atomic Build and Push
Best practice: Use Docker.Build with WithPush() to build and push in a single atomic operation. This ensures:
- Both version and latest tags point to the same manifest
- No cache staleness issues between build and push
- Multi-platform support works correctly
- Simpler, more reliable builds
var version = "1.0.0";
// Build and push in one atomic operation
Docker.Install();
Docker.Build("Dockerfile", o => o
.WithPlatforms("linux/amd64", "linux/arm64")
.WithTag($"ghcr.io/my-org/myapp:{version}")
.WithTag("ghcr.io/my-org/myapp:latest")
.WithPush());
The image will be available at ghcr.io/my-org/myapp:1.0.0 and ghcr.io/my-org/myapp:latest, with both tags pointing to identical multi-arch manifests.
Why Atomic Build+Push?
Using WithPush() leverages buildx’s --push flag to atomically build and push:
- All tags point to the same manifest (no cache staleness)
- Multi-platform builds work correctly
- Single operation, fewer network round-trips
- Automatic ghcr.io authentication
Full .NET Application Workflow
Build a .NET application, containerize it, and push to ghcr.io:
var version = "1.0.0";
var project = Dotnet.Project("./src/MyApp/MyApp.csproj");
// Build the .NET application
Dotnet.SdkInstall();
Dotnet.Restore(project);
Dotnet.Build(project, o => o.Configuration = Configuration.Release);
Dotnet.Test(Dotnet.Project("./tests/MyApp.Tests/MyApp.Tests.csproj"));
// Publish for Linux container
Dotnet.Publish(project, o => o
.Output(Root / "publish")
.WithConfiguration(Configuration.Release)
.WithRuntime("linux-x64"));
// Build and push multi-arch image atomically
Docker.Install();
Docker.Build("Dockerfile", o => o
.WithPlatforms("linux/amd64", "linux/arm64")
.WithTag($"ghcr.io/my-org/myapp:{version}")
.WithTag("ghcr.io/my-org/myapp:latest")
.WithPush());
Dockerfile Example
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS runtime
WORKDIR /app
COPY publish/ .
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"]
Build Arguments
Inject version numbers or build metadata into the image:
var version = "1.0.0";
var buildNumber = Env("BUILD_NUMBER", required: false) ?? "local";
Docker.Build("Dockerfile", o => o
.WithPlatforms("linux/amd64", "linux/arm64")
.WithTag($"ghcr.io/my-org/myapp:{version}")
.WithBuildArg("VERSION", version)
.WithBuildArg("BUILD_NUMBER", buildNumber)
.WithPush());
Access in Dockerfile:
ARG VERSION
ARG BUILD_NUMBER
ENV APP_VERSION=${VERSION}
ENV APP_BUILD=${BUILD_NUMBER}
Conditional Publishing with Profiles
Separate build and publish steps:
var publish = DefineProfile("publish");
var project = Dotnet.Project("./src/MyApp/MyApp.csproj");
var version = project.Version;
// Always build and test
Dotnet.Build(project);
Dotnet.Test(Dotnet.Project("./tests/MyApp.Tests/MyApp.Tests.csproj"));
// Always publish for container (for verification)
Dotnet.Publish(project, o => o
.Output(Root / "publish")
.WithConfiguration(Configuration.Release));
// Only build and push container with -p publish
if (publish)
{
Docker.Install();
Docker.Build("Dockerfile", o => o
.WithPlatforms("linux/amd64", "linux/arm64")
.WithTag($"ghcr.io/my-org/myapp:{version}")
.WithTag("ghcr.io/my-org/myapp:latest")
.WithPush());
Log.Info($"Pushed ghcr.io/my-org/myapp:{version}");
}
Usage:
# Build and test only
ando
# Build, test, and push to ghcr.io
ando -p publish
GitHub Actions Integration
Complete workflow for building and pushing on tag:
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Install ANDO
run: dotnet tool install -g ando
- name: Build and push container
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ando -p publish
Release with Container and Binaries
Combine container push with GitHub releases:
var release = DefineProfile("release");
var project = Dotnet.Project("./src/MyApp/MyApp.csproj");
var version = project.Version;
// Build and test
Dotnet.Build(project);
Dotnet.Test(Dotnet.Project("./tests/MyApp.Tests/MyApp.Tests.csproj"));
if (release)
{
// Publish for container
Dotnet.Publish(project, o => o
.Output(Root / "publish")
.WithConfiguration(Configuration.Release));
// Build and push container atomically
Docker.Install();
Docker.Build("Dockerfile", o => o
.WithPlatforms("linux/amd64", "linux/arm64")
.WithTag($"ghcr.io/my-org/myapp:{version}")
.WithTag("ghcr.io/my-org/myapp:latest")
.WithBuildArg("VERSION", version)
.WithPush());
// Create GitHub release
Git.Tag($"v{version}", o => o.WithSkipIfExists());
Git.Push();
Git.PushTags();
GitHub.CreateRelease(o => o
.WithTag($"v{version}")
.WithTitle($"v{version}")
.WithNotes($@"
## Container Image
```bash
docker pull ghcr.io/my-org/myapp:{version}
What’s Changed
See commit history for changes. ”) .WithGeneratedNotes()); }
## Single Platform Builds
For simpler use cases without multi-arch requirements:
```csharp
// Single platform build with push
Docker.Build("Dockerfile", o => o
.WithPlatform("linux/amd64")
.WithTag($"ghcr.io/my-org/myapp:{version}")
.WithPush());
Local Development (Build Only)
For local testing, build without pushing:
// Build and load into local Docker (no push)
Docker.Build("Dockerfile", o => o
.WithTag("myapp:dev"));
Clean Builds
Force a fresh Docker build without cache:
Docker.Build("Dockerfile", o => o
.WithPlatforms("linux/amd64", "linux/arm64")
.WithTag($"ghcr.io/my-org/myapp:{version}")
.WithNoCache()
.WithPush());
Use WithNoCache() when:
- Debugging build issues
- Ensuring dependencies are updated
- Building release versions
Options Reference
Docker.Build Options
| Option | Description |
|---|---|
WithTag(string) | Add image tag (can call multiple times). Use full registry path for push (e.g., ghcr.io/org/app:v1) |
WithPlatform(string) | Single target platform (e.g., “linux/amd64”) |
WithPlatforms(params string[]) | Multiple target platforms for multi-arch builds (e.g., “linux/amd64”, “linux/arm64”) |
WithContext(string) | Build context directory |
WithBuildArg(key, value) | Pass build argument |
WithPush() | Push images to registry after building (recommended) |
WithNoCache() | Disable build cache |
See Also
- Docker Provider - Docker operations reference
- GitHub Provider - GitHub operations reference
- GitHub Releases Recipe - Creating GitHub releases