Support pulling private images via Docker credentials#18
Support pulling private images via Docker credentials#18franzejr wants to merge 5 commits intobasecamp:mainfrom
Conversation
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.
|
@kevinmcconnell I tested this locally, and it worked really well. Would like to hear your thoughts. |
There was a problem hiding this comment.
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, includingcredHelpers,credsStore, and inlineauths. - Pass resolved auth token into
image.PullOptions.RegistryAuthduring 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.
internal/docker/registry_auth.go
Outdated
| home, err := os.UserHomeDir() | ||
| if err != nil { | ||
| return "" | ||
| } | ||
|
|
||
| cfg, err := loadDockerConfig(filepath.Join(home, ".docker", "config.json")) | ||
| if err != nil { | ||
| return "" | ||
| } |
internal/docker/registry_auth.go
Outdated
| if entry, ok := cfg.Auths[host]; ok && entry.Auth != "" { | ||
| return authFromInlineEntry(entry.Auth) | ||
| } |
There was a problem hiding this comment.
This proposed solution is a bit over-engineering. I simplified the idea and addressed it here: ddf9466
| func authFromCredHelper(helper, serverURL string) string { | ||
| cmd := exec.Command("docker-credential-"+helper, "get") | ||
| cmd.Stdin = strings.NewReader(serverURL) | ||
| out, err := cmd.Output() |
There was a problem hiding this comment.
Over-engineering approach. I addressed the concern in a simpler way: 32cdf8e
| 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) | ||
| }) | ||
|
|
| 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.
There was a problem hiding this comment.
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_CONFIGwith support forcredHelpers,credsStore, and inlineauths. - 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.
| func dockerConfigPath() string { | ||
| if dir := os.Getenv("DOCKER_CONFIG"); dir != "" { | ||
| return filepath.Join(dir, "config.json") | ||
| } | ||
| home, err := os.UserHomeDir() |
| cfg, err := loadDockerConfig(dockerConfigPath()) | ||
| if err != nil { | ||
| return "" | ||
| } |
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.
There was a problem hiding this comment.
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, supportingcredHelpers,credsStore, and inlineauths. - 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.
| 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 { |
| type dockerAuthEntry struct { | ||
| Auth string `json:"auth"` | ||
| } |
| if entry, ok := authEntryFor(cfg.Auths, host); ok && entry.Auth != "" { | ||
| return authFromInlineEntry(entry.Auth) | ||
| } |
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 waydocker pulldoes — without any extra configuration from the user, just using the normal login in the machine.Test Plan
Automated
go test ./internal/docker/...— all registry auth tests passManual (private image)
(e.g. a private
ghcr.iopackage)docker pull <image>works on the machine (confirms credentialsare in
~/.docker/config.json)once deploywith that image — confirm it pulls successfully withoutany auth errors
Fallback behaviour
access as before)
~/.docker/config.json— confirm Once falls backgracefully to anonymous access rather than erroring out