Docker to GitHub Registry

Build Docker images and push to GitHub Container Registry (ghcr.io).

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:

MethodDescription
GITHUB_TOKENEnvironment variable (preferred for CI/CD)
gh auth loginUses 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.

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

OptionDescription
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