Skip to content

Commit d5695c1

Browse files
dkurepapremunlbussell
authored
Add support for extra Docker build options (#2064)
Ports #2063 to my fork since I don't have access to Premeks --------- Co-authored-by: Premek Vysoky <premek.vysoky@microsoft.com> Co-authored-by: Logan Bussell <loganbussell@microsoft.com>
1 parent 5b05e5b commit d5695c1

8 files changed

Lines changed: 116 additions & 8 deletions

File tree

eng/docker-tools/CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,27 @@ All breaking changes and new features in `eng/docker-tools` will be documented i
44

55
---
66

7+
## 2026-04-02: Extra Docker build options can be passed through ImageBuilder
8+
9+
- Pull request: [#2063](https://github.com/dotnet/docker-tools/pull/2063)
10+
11+
ImageBuilder's `build` command now accepts repeated `--build-option` arguments and forwards them directly to
12+
`docker build`. This allows repos to pass options such as `--ulimit nofile=65536:65536` or `--network host`
13+
through `imageBuilderBuildArgs`, in addition to standard Dockerfile `--build-arg` values.
14+
15+
**How to use:**
16+
17+
```yaml
18+
customBuildInitSteps:
19+
- powershell: |
20+
$args = '--build-option "--ulimit nofile=65536:65536"'
21+
echo "##vso[task.setvariable variable=imageBuilderBuildArgs]$args"
22+
```
23+
24+
Repeat `--build-option` for multiple Docker arguments, and quote values that contain spaces.
25+
26+
---
27+
728
## 2026-03-25: Manifest list creation moved to Post_Build
829

930
- Issue: [#2002](https://github.com/dotnet/docker-tools/issues/2002)

eng/docker-tools/DEV-GUIDE.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ To force a rebuild regardless of cache state, set the `noCache` parameter to `tr
370370

371371
### Pattern: Adding Build Arguments
372372

373-
Pass additional arguments to Docker builds via ImageBuilder:
373+
Pass Dockerfile `ARG` values via ImageBuilder:
374374

375375
```yaml
376376
customBuildInitSteps:
@@ -379,6 +379,15 @@ customBuildInitSteps:
379379
echo "##vso[task.setvariable variable=imageBuilderBuildArgs]$args"
380380
```
381381

382+
To pass raw options directly to `docker build`, use `--build-option`. Quote values that contain spaces:
383+
384+
```yaml
385+
customBuildInitSteps:
386+
- powershell: |
387+
$args = '--build-option "--ulimit nofile=65536:65536"'
388+
echo "##vso[task.setvariable variable=imageBuilderBuildArgs]$args"
389+
```
390+
382391
### Pattern: Re-running Stages with `stages` and `sourceBuildPipelineRunId`
383392

384393
A powerful pattern is combining the `stages` variable with the `sourceBuildPipelineRunId` pipeline parameter to run specific stages using artifacts from a previous build. This is useful for:

src/ImageBuilder.Tests/BuildCommandTests.cs

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,7 @@ public async Task BuildCommand_Publish()
527527
TagInfo.GetFullyQualifiedName(repoName, sharedTag)
528528
},
529529
It.IsAny<IDictionary<string, string>>(),
530+
It.IsAny<IEnumerable<string>>(),
530531
It.IsAny<bool>(),
531532
It.IsAny<bool>()));
532533

@@ -694,6 +695,61 @@ public async Task BuildCommand_BuildArgs()
694695
It.IsAny<List<string>>(),
695696
It.Is<Dictionary<string, string>>(
696697
args => args.Count == 3 && args["arg1"] == "val1" && args["arg2"] == "val2b" && args["arg3"] == "val3"),
698+
It.IsAny<IEnumerable<string>>(),
699+
It.IsAny<bool>(),
700+
It.IsAny<bool>()));
701+
dockerServiceMock.Verify(
702+
o => o.GetImageSize(It.IsAny<string>(), false));
703+
}
704+
705+
/// <summary>
706+
/// Verifies that Docker build options can be passed through to the Docker build command.
707+
/// </summary>
708+
[Fact]
709+
public async Task BuildCommand_DockerBuildOptions()
710+
{
711+
const string repoName = "runtime";
712+
const string tag = "tag";
713+
const string baseImageRepo = "baserepo";
714+
string baseImageTag = $"{baseImageRepo}:basetag";
715+
716+
using TempFolderContext tempFolderContext = TestHelper.UseTempFolder();
717+
Mock<IDockerService> dockerServiceMock = CreateDockerServiceMock();
718+
719+
BuildCommand command = CreateBuildCommand(
720+
dockerService: dockerServiceMock.Object,
721+
copyImageService: Mock.Of<ICopyImageService>(),
722+
manifestServiceFactory: CreateManifestServiceFactoryMock().Object,
723+
imageCacheService: new ImageCacheService(Mock.Of<ILogger<ImageCacheService>>(), Mock.Of<IGitService>()));
724+
command.Options.Manifest = Path.Combine(tempFolderContext.Path, "manifest.json");
725+
command.Options.DockerBuildOptions = ["--ulimit nofile=65536:65536", "--network host"];
726+
727+
Platform platform = CreatePlatform(
728+
DockerfileHelper.CreateDockerfile("1.0/runtime/os", tempFolderContext, baseImageTag),
729+
new string[] { tag });
730+
731+
Manifest manifest = CreateManifest(
732+
CreateRepo(repoName,
733+
CreateImage(
734+
new Platform[]
735+
{
736+
platform
737+
}))
738+
);
739+
740+
File.WriteAllText(Path.Combine(tempFolderContext.Path, command.Options.Manifest), JsonConvert.SerializeObject(manifest));
741+
742+
command.LoadManifest();
743+
await command.ExecuteAsync();
744+
745+
dockerServiceMock.Verify(
746+
o => o.BuildImage(
747+
It.IsAny<string>(),
748+
It.IsAny<string>(),
749+
It.IsAny<string>(),
750+
It.IsAny<List<string>>(),
751+
It.IsAny<IDictionary<string, string>>(),
752+
It.Is<IEnumerable<string>>(args => args.SequenceEqual(command.Options.DockerBuildOptions)),
697753
It.IsAny<bool>(),
698754
It.IsAny<bool>()));
699755
dockerServiceMock.Verify(
@@ -757,6 +813,7 @@ public async Task BuildCommand_NoBaseImage_Build()
757813
TagInfo.GetFullyQualifiedName(repoName, sharedTag)
758814
},
759815
It.IsAny<IDictionary<string, string>>(),
816+
It.IsAny<IEnumerable<string>>(),
760817
It.IsAny<bool>(),
761818
It.IsAny<bool>()));
762819
dockerServiceMock.Verify(
@@ -949,7 +1006,7 @@ public async Task BuildCommand_NoBaseImage_Cached()
9491006
o.BuildImage(
9501007
PathHelper.NormalizePath(Path.Combine(tempFolderContext.Path, runtimeDepsLinuxDockerfileRelativePath)),
9511008
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<string>>(), It.IsAny<IDictionary<string, string>>(),
952-
It.IsAny<bool>(), It.IsAny<bool>()),
1009+
It.IsAny<IEnumerable<string>>(), It.IsAny<bool>(), It.IsAny<bool>()),
9531010
Times.Never);
9541011
dockerServiceMock.Verify(
9551012
o => o.GetImageSize(It.IsAny<string>(), false),
@@ -1679,7 +1736,7 @@ public async Task BuildCommand_Caching_SharedDockerfile_MissingSourceImageInfoEn
16791736
dockerServiceMock.Verify(o =>
16801737
o.BuildImage(
16811738
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<string>>(),
1682-
It.IsAny<IDictionary<string, string>>(), It.IsAny<bool>(), It.IsAny<bool>()),
1739+
It.IsAny<IDictionary<string, string>>(), It.IsAny<IEnumerable<string>>(), It.IsAny<bool>(), It.IsAny<bool>()),
16831740
Times.Never);
16841741

16851742
dockerServiceMock.VerifyNoOtherCalls();
@@ -1987,6 +2044,7 @@ public async Task BuildCommand_Caching_SharedDockerfile_MissingSourceImageInfoEn
19872044
It.IsAny<string>(),
19882045
It.IsAny<IEnumerable<string>>(),
19892046
It.IsAny<IDictionary<string, string>>(),
2047+
It.IsAny<IEnumerable<string>>(),
19902048
It.IsAny<bool>(),
19912049
It.IsAny<bool>()));
19922050
dockerServiceMock.Verify(o => o.GetImageSize($"{runtimeDeps3Repo}:{tag}", false));
@@ -2193,6 +2251,7 @@ public async Task BuildCommand_Caching_SharedDockerfile_NoExistingImageInfoEntri
21932251
It.IsAny<string>(),
21942252
new string[] { expectedTag },
21952253
It.IsAny<IDictionary<string, string>>(),
2254+
It.IsAny<IEnumerable<string>>(),
21962255
It.IsAny<bool>(),
21972256
It.IsAny<bool>()),
21982257
Times.Once);
@@ -2436,6 +2495,7 @@ public async Task BuildCommand_SharedDockerfile()
24362495
It.IsAny<string>(),
24372496
new string[] { expectedTag },
24382497
It.IsAny<IDictionary<string, string>>(),
2498+
It.IsAny<IEnumerable<string>>(),
24392499
It.IsAny<bool>(),
24402500
It.IsAny<bool>()),
24412501
Times.Once);
@@ -2653,7 +2713,7 @@ public async Task BuildCommand_Caching_TagUpdate()
26532713
o.BuildImage(
26542714
PathHelper.NormalizePath(Path.Combine(tempFolderContext.Path, runtimeDepsLinuxDockerfileRelativePath)),
26552715
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<string>>(), It.IsAny<IDictionary<string, string>>(),
2656-
It.IsAny<bool>(), It.IsAny<bool>()),
2716+
It.IsAny<IEnumerable<string>>(), It.IsAny<bool>(), It.IsAny<bool>()),
26572717
Times.Never);
26582718
dockerServiceMock.Verify(
26592719
o => o.GetImageSize(It.IsAny<string>(), false),
@@ -2912,7 +2972,7 @@ public async Task BuildCommand_Caching_SharedDockerfile_TagUpdate()
29122972
dockerServiceMock.Verify(o =>
29132973
o.BuildImage(
29142974
It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<IEnumerable<string>>(),
2915-
It.IsAny<IDictionary<string, string>>(), It.IsAny<bool>(), It.IsAny<bool>()),
2975+
It.IsAny<IDictionary<string, string>>(), It.IsAny<IEnumerable<string>>(), It.IsAny<bool>(), It.IsAny<bool>()),
29162976
Times.Never);
29172977
dockerServiceMock.Verify(o => o.GetCreatedDate(It.IsAny<string>(), false));
29182978

@@ -3272,6 +3332,7 @@ public async Task BuildCommand_MirroredImages(bool hasCachedImage, string srcBas
32723332
It.IsAny<string>(),
32733333
It.IsAny<IEnumerable<string>>(),
32743334
It.IsAny<IDictionary<string, string>>(),
3335+
It.IsAny<IEnumerable<string>>(),
32753336
It.IsAny<bool>(),
32763337
It.IsAny<bool>()));
32773338

@@ -3417,6 +3478,7 @@ public async Task BuildCommand_MirroredImages_External(string baseImageRegistry,
34173478
It.IsAny<string>(),
34183479
It.IsAny<IEnumerable<string>>(),
34193480
It.IsAny<IDictionary<string, string>>(),
3481+
It.IsAny<IEnumerable<string>>(),
34203482
It.IsAny<bool>(),
34213483
It.IsAny<bool>()));
34223484
dockerServiceMock.Verify(o => o.GetImageSize($"{RegistryOverride}/{SamplesRepo}:{Tag}", false));
@@ -3558,6 +3620,7 @@ public async Task BuildCommand_MirroredImages_BaseImageTagOverride()
35583620
It.IsAny<string>(),
35593621
It.IsAny<IEnumerable<string>>(),
35603622
It.IsAny<IDictionary<string, string>>(),
3623+
It.IsAny<IEnumerable<string>>(),
35613624
It.IsAny<bool>(),
35623625
It.IsAny<bool>()));
35633626
dockerServiceMock.Verify(
@@ -3621,6 +3684,7 @@ private static Mock<IDockerService> CreateDockerServiceMock(string buildOutput =
36213684
It.IsAny<string>(),
36223685
It.IsAny<IEnumerable<string>>(),
36233686
It.IsAny<IDictionary<string, string>>(),
3687+
It.IsAny<IEnumerable<string>>(),
36243688
It.IsAny<bool>(),
36253689
It.IsAny<bool>()))
36263690
.Returns(buildOutput ?? string.Empty);

src/ImageBuilder/Commands/BuildCommand.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@ private void BuildImage(PlatformInfo platform, IEnumerable<string> allTags)
493493
platform.PlatformLabel,
494494
allTags,
495495
GetBuildArgs(platform),
496+
GetDockerBuildOptions(),
496497
Options.IsRetryEnabled,
497498
Options.IsDryRun);
498499

@@ -554,6 +555,9 @@ private void BuildImage(PlatformInfo platform, IEnumerable<string> allTags)
554555
return buildArgs;
555556
}
556557

558+
private IEnumerable<string> GetDockerBuildOptions() =>
559+
Options.DockerBuildOptions.Where(option => !string.IsNullOrWhiteSpace(option));
560+
557561
private async Task OnCacheHitAsync(RepoInfo repo, IEnumerable<TagInfo> allTags, bool pullImage, string sourceDigest)
558562
{
559563
_logger.LogInformation(string.Empty);

src/ImageBuilder/Commands/BuildOptions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class BuildOptions : ManifestOptions, IFilterableOptions
2626
public bool NoCache { get; set; }
2727
public string? SourceRepoPrefix { get; set; }
2828
public IDictionary<string, string> BuildArgs { get; set; } = new Dictionary<string, string>();
29+
public string[] DockerBuildOptions { get; set; } = [];
2930
public bool SkipPlatformCheck { get; set; }
3031
public string? OutputVariableName { get; set; }
3132
public bool Internal { get; set; }
@@ -67,6 +68,8 @@ public override IEnumerable<Option> GetCliOptions() =>
6768
"Prefix to add to the external base image names when pulling them"),
6869
CreateDictionaryOption("build-arg", nameof(BuildOptions.BuildArgs),
6970
"Build argument to pass to the Dockerfiles (<name>=<value>)"),
71+
CreateMultiOption<string>("build-option", nameof(BuildOptions.DockerBuildOptions),
72+
"Additional argument to pass directly to docker build. Repeat for multiple arguments and quote values containing spaces."),
7073
CreateOption<bool>("skip-platform-check", nameof(BuildOptions.SkipPlatformCheck),
7174
"Skips validation that ensures the Dockerfile's base image's platform matches the manifest configuration"),
7275
CreateOption<string>("digests-out-var", nameof(BuildOptions.OutputVariableName),

src/ImageBuilder/DockerService.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public void CreateManifestList(string manifestListTag, IEnumerable<string> image
3434
string platform,
3535
IEnumerable<string> tags,
3636
IDictionary<string, string?> buildArgs,
37+
IEnumerable<string> dockerBuildOptions,
3738
bool isRetryEnabled,
3839
bool isDryRun)
3940
{
@@ -43,7 +44,12 @@ public void CreateManifestList(string manifestListTag, IEnumerable<string> image
4344
.Select(buildArg => $" --build-arg {buildArg.Key}={buildArg.Value}");
4445
string buildArgsString = string.Join(string.Empty, buildArgList);
4546

46-
string dockerArgs = $"build --platform {platform} {tagArgs} -f {dockerfilePath}{buildArgsString} {buildContextPath}";
47+
IEnumerable<string> dockerBuildOptionList = dockerBuildOptions
48+
.Where(option => !string.IsNullOrWhiteSpace(option))
49+
.Select(option => $" {option}");
50+
string dockerBuildOptionsString = string.Join(string.Empty, dockerBuildOptionList);
51+
52+
string dockerArgs = $"build --platform {platform} {tagArgs} -f {dockerfilePath}{buildArgsString}{dockerBuildOptionsString} {buildContextPath}";
4753

4854
if (isRetryEnabled)
4955
{

src/ImageBuilder/DockerServiceCache.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ public DockerServiceCache(IDockerService inner)
3232

3333
public string? BuildImage(
3434
string dockerfilePath, string buildContextPath, string platform, IEnumerable<string> tags,
35-
IDictionary<string, string?> buildArgs, bool isRetryEnabled, bool isDryRun) =>
36-
_inner.BuildImage(dockerfilePath, buildContextPath, platform, tags, buildArgs, isRetryEnabled, isDryRun);
35+
IDictionary<string, string?> buildArgs, IEnumerable<string> dockerBuildOptions, bool isRetryEnabled, bool isDryRun) =>
36+
_inner.BuildImage(dockerfilePath, buildContextPath, platform, tags, buildArgs, dockerBuildOptions, isRetryEnabled, isDryRun);
3737

3838
public (Architecture Arch, string? Variant) GetImageArch(string image, bool isDryRun) =>
3939
_architectureCache.GetOrAdd(image, _ =>_inner.GetImageArch(image, isDryRun));

src/ImageBuilder/IDockerService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public interface IDockerService
2929
string platform,
3030
IEnumerable<string> tags,
3131
IDictionary<string, string?> buildArgs,
32+
IEnumerable<string> dockerBuildOptions,
3233
bool isRetryEnabled,
3334
bool isDryRun);
3435

0 commit comments

Comments
 (0)