Skip to content

Support pulling private images via Docker credentials#18

Open
franzejr wants to merge 5 commits intobasecamp:mainfrom
franzejr:add-private-registry-support
Open

Support pulling private images via Docker credentials#18
franzejr wants to merge 5 commits intobasecamp:mainfrom
franzejr:add-private-registry-support

Conversation

@franzejr
Copy link

@franzejr franzejr commented Mar 19, 2026

Deploying apps from private registries (e.g. ghcr.io, GCR, ECR) would fail because image pulls always used anonymous access. I was trying to use it, even on DHH presentation he had an issue with this. This PR adds credential resolution from the local Docker config, so Once can pull private images the same way docker pull does — without any extra configuration from the user, just using the normal login in the machine.

Test Plan

Automated

  • Run go test ./internal/docker/... — all registry auth tests pass

Manual (private image)

  • Tag and push a test image to a private registry you have credentials for
    (e.g. a private ghcr.io package)
  • Ensure docker pull <image> works on the machine (confirms credentials
    are in ~/.docker/config.json)
  • Run once deploy with that image — confirm it pulls successfully without
    any auth errors

Fallback behaviour

  • Deploy a public image — confirm it still works (falls back to anonymous
    access as before)
  • Remove or corrupt ~/.docker/config.json — confirm Once falls back
    gracefully to anonymous access rather than erroring out

Image pulls always used anonymous access, causing failures for private
registries. Users had to work around this manually.

Add registry_auth.go which reads ~/.docker/config.json and resolves
credentials via credential helpers, credential stores, or inline base64
auth entries. Pass the resolved token to ImagePull via RegistryAuth.
Falls back to anonymous access on any error or missing credentials.
Copilot AI review requested due to automatic review settings March 19, 2026 10:32
@franzejr
Copy link
Author

@kevinmcconnell I tested this locally, and it worked really well. Would like to hear your thoughts.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Docker registry credential resolution so image pulls can authenticate against private registries using the local Docker config (similar to docker pull), and wires that auth into the Docker ImagePull call.

Changes:

  • Add registry auth resolution from ~/.docker/config.json, including credHelpers, credsStore, and inline auths.
  • Pass resolved auth token into image.PullOptions.RegistryAuth during image pulls.
  • Add unit tests covering helper/store/inline paths and error handling.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
internal/docker/registry_auth.go Implements docker config parsing + credential helper execution + token encoding.
internal/docker/registry_auth_test.go Adds unit tests for registry host parsing and all credential resolution paths.
internal/docker/application.go Uses resolved RegistryAuth when pulling images.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +43 to +51
home, err := os.UserHomeDir()
if err != nil {
return ""
}

cfg, err := loadDockerConfig(filepath.Join(home, ".docker", "config.json"))
if err != nil {
return ""
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in: 47f4d08

Comment on lines +61 to +63
if entry, ok := cfg.Auths[host]; ok && entry.Auth != "" {
return authFromInlineEntry(entry.Auth)
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This proposed solution is a bit over-engineering. I simplified the idea and addressed it here: ddf9466

Comment on lines +90 to +93
func authFromCredHelper(helper, serverURL string) string {
cmd := exec.Command("docker-credential-"+helper, "get")
cmd.Stdin = strings.NewReader(serverURL)
out, err := cmd.Output()
Copy link
Author

@franzejr franzejr Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Over-engineering approach. I addressed the concern in a simpler way: 32cdf8e

Comment on lines +29 to +32
type encodedAuthConfig struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
}
assert.Equal(t, "inline-user", ac.Username)
assert.Equal(t, "inline-pass", ac.Password)
})

Comment on lines +201 to +202
dir := t.TempDir()
t.Setenv("HOME", dir)
Credential resolution always read ~/.docker/config.json, ignoring the
DOCKER_CONFIG environment variable. Users with a non-default Docker config
directory would have Once fall back to anonymous pulls even though
docker pull worked correctly for them.

Extract dockerConfigPath() which checks DOCKER_CONFIG first and falls
back to ~/.docker/config.json, matching Docker's own resolution behaviour.

This does not support the legacy ~/.dockercfg format. Supporting it
would require additional fallback logic but covers a very small number of
users on modern Docker installations.
Direct lookup of cfg.Auths[host] missed entries where docker login
stored the key as a full URL (e.g. https://index.docker.io/v1/ instead
of docker.io). Affected users would silently fall back to anonymous
pulls despite having valid credentials configured.

Add authEntryFor which tries an exact match first, then falls back
to parsing URL-style keys with url.Parse and comparing the extracted
host via canonicalHost, which maps the known Docker Hub aliases to
docker.io.

This does not handle every possible key format Docker has used
historically (e.g. registry-1.docker.io without a scheme). Adding more
aliases to canonicalHost is straightforward if other cases emerge.
Copilot AI review requested due to automatic review settings March 19, 2026 10:46
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds support for pulling images from private registries by resolving credentials from the local Docker config (similar to docker pull) and passing the resulting auth token to Docker’s ImagePull API.

Changes:

  • Implement Docker registry auth resolution via ~/.docker/config.json / $DOCKER_CONFIG with support for credHelpers, credsStore, and inline auths.
  • Use the resolved registry auth when pulling application images.
  • Add unit tests covering host parsing, docker config parsing, inline auth decoding, and credential helper execution paths.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
internal/docker/registry_auth.go Adds credential discovery and encoding logic for Docker registry pulls.
internal/docker/registry_auth_test.go Adds unit tests for config parsing and auth resolution behavior.
internal/docker/application.go Passes resolved RegistryAuth into ImagePull for app deployments.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +66 to +70
func dockerConfigPath() string {
if dir := os.Getenv("DOCKER_CONFIG"); dir != "" {
return filepath.Join(dir, "config.json")
}
home, err := os.UserHomeDir()
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in: 9d3b3b4

Comment on lines +44 to +47
cfg, err := loadDockerConfig(dockerConfigPath())
if err != nil {
return ""
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in: 9d3b3b4

Credential helpers were called with the bare host (e.g. docker.io),
but Docker Hub stores credentials under https://index.docker.io/v1/ —
the URL docker login uses. Helpers looking up docker.io would find
nothing and fall back to anonymous pulls.

Add credHelperServerURL which maps docker.io to the canonical Docker
Hub URL and returns the bare host unchanged for all other registries.

Other registries could also have legacy URL-keyed entries in their
helpers. Those are rare in practice; adding more mappings to
credHelperServerURL is straightforward if cases emerge.
Two behaviours introduced in this branch had no end-to-end test
coverage: reading config from a DOCKER_CONFIG-specified directory, and
falling back to anonymous access when config.json exists but contains
invalid JSON.

Add subtests in TestRegistryAuthFor for both cases. The DOCKER_CONFIG
test also revealed that config.json must be written directly into the
directory DOCKER_CONFIG points to, not in a .docker/ subdirectory —
which is how Docker itself resolves the path.
Copilot AI review requested due to automatic review settings March 19, 2026 11:04
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds Docker registry authentication resolution (from local Docker config/credential helpers) so once deploy can pull private images the same way docker pull does, falling back to anonymous pulls when credentials aren’t available.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Changes:

  • Introduce registry auth resolution via ~/.docker/config.json / DOCKER_CONFIG, supporting credHelpers, credsStore, and inline auths.
  • Add a comprehensive unit test suite covering host resolution, config loading, helper execution, and fallback behavior.
  • Wire registry auth into the image pull path so deployments can authenticate to private registries.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
internal/docker/registry_auth.go Implements Docker config parsing and credential helper/inline auth resolution for registry pulls.
internal/docker/registry_auth_test.go Adds tests for registry host parsing, config lookup, helper execution, and graceful fallbacks.
internal/docker/application.go Passes resolved RegistryAuth into ImagePull to enable authenticated pulls.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +132 to +136
func authFromCredHelper(helper, serverURL string) string {
cmd := exec.Command("docker-credential-"+helper, "get")
cmd.Stdin = strings.NewReader(serverURL)
out, err := cmd.Output()
if err != nil {
Comment on lines +21 to +23
type dockerAuthEntry struct {
Auth string `json:"auth"`
}
Comment on lines +57 to +59
if entry, ok := authEntryFor(cfg.Auths, host); ok && entry.Auth != "" {
return authFromInlineEntry(entry.Auth)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants