Skip to content

Commit eaf4efb

Browse files
committed
Limit registry connections to prevent 503 errors during push
Add HTTP client configuration with connection limits to prevent registry overload when pushing images. This addresses 503 errors that occur when too many parallel requests are made to registries. Changes: - Add MaxConnsPerHost (default: 5) and MaxIdleConns (default: 50) options - Add RequestTimeout configuration for large uploads - Apply limits to both HTTPS and HTTP fallback scenarios - Maintain backward compatibility with existing configurations Signed-off-by: Adam Rozman <[email protected]>
1 parent 4675207 commit eaf4efb

File tree

4 files changed

+371
-19
lines changed

4 files changed

+371
-19
lines changed

cmd/nerdctl/image/image_push.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,17 @@ func PushCommand() *cobra.Command {
6969

7070
cmd.Flags().Bool(allowNonDistFlag, false, "Allow pushing images with non-distributable blobs")
7171

72+
// #region connection limit flags
73+
cmd.Flags().Int("max-conns-per-host", 5, "Maximum number of connections per registry host")
74+
cmd.Flags().Int("max-idle-conns", 50, "Maximum number of idle connections")
75+
cmd.Flags().Int("request-timeout", 300, "Request timeout in seconds")
76+
// #endregion
77+
78+
// #region retry flags
79+
cmd.Flags().Int("max-retries", 3, "Maximum number of retry attempts for 503 errors")
80+
cmd.Flags().Int("retry-initial-delay", 1000, "Initial delay before first retry in milliseconds")
81+
// #endregion
82+
7283
return cmd
7384
}
7485

@@ -113,6 +124,26 @@ func pushOptions(cmd *cobra.Command) (types.ImagePushOptions, error) {
113124
if err != nil {
114125
return types.ImagePushOptions{}, err
115126
}
127+
maxConnsPerHost, err := cmd.Flags().GetInt("max-conns-per-host")
128+
if err != nil {
129+
return types.ImagePushOptions{}, err
130+
}
131+
maxIdleConns, err := cmd.Flags().GetInt("max-idle-conns")
132+
if err != nil {
133+
return types.ImagePushOptions{}, err
134+
}
135+
requestTimeout, err := cmd.Flags().GetInt("request-timeout")
136+
if err != nil {
137+
return types.ImagePushOptions{}, err
138+
}
139+
maxRetries, err := cmd.Flags().GetInt("max-retries")
140+
if err != nil {
141+
return types.ImagePushOptions{}, err
142+
}
143+
retryInitialDelay, err := cmd.Flags().GetInt("retry-initial-delay")
144+
if err != nil {
145+
return types.ImagePushOptions{}, err
146+
}
116147
return types.ImagePushOptions{
117148
GOptions: globalOptions,
118149
SignOptions: signOptions,
@@ -124,6 +155,11 @@ func pushOptions(cmd *cobra.Command) (types.ImagePushOptions, error) {
124155
IpfsAddress: ipfsAddress,
125156
Quiet: quiet,
126157
AllowNondistributableArtifacts: allowNonDist,
158+
MaxConnsPerHost: maxConnsPerHost,
159+
MaxIdleConns: maxIdleConns,
160+
RequestTimeout: requestTimeout,
161+
MaxRetries: maxRetries,
162+
RetryInitialDelay: retryInitialDelay,
127163
Stdout: cmd.OutOrStdout(),
128164
}, nil
129165
}

pkg/api/types/image_types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,16 @@ type ImagePushOptions struct {
200200
Quiet bool
201201
// AllowNondistributableArtifacts allow pushing non-distributable artifacts
202202
AllowNondistributableArtifacts bool
203+
// MaxConnsPerHost maximum number of connections per registry host (default: 5)
204+
MaxConnsPerHost int
205+
// MaxIdleConns maximum number of idle connections (default: 50)
206+
MaxIdleConns int
207+
// RequestTimeout timeout for registry requests in seconds (default: 300)
208+
RequestTimeout int
209+
// MaxRetries maximum number of retry attempts for 503 errors (default: 3)
210+
MaxRetries int
211+
// RetryInitialDelay initial delay before first retry in milliseconds (default: 1000)
212+
RetryInitialDelay int
203213
}
204214

205215
// RemoteSnapshotterFlags are used for pulling with remote snapshotters

pkg/cmd/image/push.go

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"net/http"
2525
"os"
2626
"path/filepath"
27+
"time"
2728

2829
"github.com/opencontainers/go-digest"
2930
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
@@ -34,7 +35,6 @@ import (
3435
"github.com/containerd/containerd/v2/core/images/converter"
3536
"github.com/containerd/containerd/v2/core/remotes"
3637
"github.com/containerd/containerd/v2/core/remotes/docker"
37-
dockerconfig "github.com/containerd/containerd/v2/core/remotes/docker/config"
3838
"github.com/containerd/containerd/v2/pkg/reference"
3939
"github.com/containerd/log"
4040
"github.com/containerd/stargz-snapshotter/estargz"
@@ -165,17 +165,29 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, options
165165
}
166166
dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(options.GOptions.HostsDir))
167167

168-
ho, err := dockerconfigresolver.NewHostOptions(ctx, refDomain, dOpts...)
169-
if err != nil {
170-
return err
168+
// Configure connection limits to prevent registry overload (503 errors)
169+
if options.MaxConnsPerHost > 0 {
170+
dOpts = append(dOpts, dockerconfigresolver.WithMaxConnsPerHost(options.MaxConnsPerHost))
171171
}
172-
173-
resolverOpts := docker.ResolverOptions{
174-
Tracker: pushTracker,
175-
Hosts: dockerconfig.ConfigureHosts(ctx, *ho),
172+
if options.MaxIdleConns > 0 {
173+
dOpts = append(dOpts, dockerconfigresolver.WithMaxIdleConns(options.MaxIdleConns))
174+
}
175+
if options.RequestTimeout > 0 {
176+
dOpts = append(dOpts, dockerconfigresolver.WithRequestTimeout(time.Duration(options.RequestTimeout)*time.Second))
176177
}
178+
if options.MaxRetries > 0 {
179+
dOpts = append(dOpts, dockerconfigresolver.WithMaxRetries(options.MaxRetries))
180+
}
181+
if options.RetryInitialDelay > 0 {
182+
dOpts = append(dOpts, dockerconfigresolver.WithRetryInitialDelay(time.Duration(options.RetryInitialDelay)*time.Millisecond))
183+
}
184+
// Use the local push tracker for this operation
185+
dOpts = append(dOpts, dockerconfigresolver.WithTracker(pushTracker))
177186

178-
resolver := docker.NewResolver(resolverOpts)
187+
resolver, err := dockerconfigresolver.New(ctx, refDomain, dOpts...)
188+
if err != nil {
189+
return err
190+
}
179191
if err = pushFunc(resolver); err != nil {
180192
// In some circumstance (e.g. people just use 80 port to support pure http), the error will contain message like "dial tcp <port>: connection refused"
181193
if !errors.Is(err, http.ErrSchemeMismatch) && !errutil.IsErrConnectionRefused(err) {
@@ -184,6 +196,22 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, options
184196
if options.GOptions.InsecureRegistry {
185197
log.G(ctx).WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", refDomain)
186198
dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true))
199+
// Apply same connection limits for HTTP fallback
200+
if options.MaxConnsPerHost > 0 {
201+
dOpts = append(dOpts, dockerconfigresolver.WithMaxConnsPerHost(options.MaxConnsPerHost))
202+
}
203+
if options.MaxIdleConns > 0 {
204+
dOpts = append(dOpts, dockerconfigresolver.WithMaxIdleConns(options.MaxIdleConns))
205+
}
206+
if options.RequestTimeout > 0 {
207+
dOpts = append(dOpts, dockerconfigresolver.WithRequestTimeout(time.Duration(options.RequestTimeout)*time.Second))
208+
}
209+
if options.MaxRetries > 0 {
210+
dOpts = append(dOpts, dockerconfigresolver.WithMaxRetries(options.MaxRetries))
211+
}
212+
if options.RetryInitialDelay > 0 {
213+
dOpts = append(dOpts, dockerconfigresolver.WithRetryInitialDelay(time.Duration(options.RetryInitialDelay)*time.Millisecond))
214+
}
187215
resolver, err = dockerconfigresolver.New(ctx, refDomain, dOpts...)
188216
if err != nil {
189217
return err

0 commit comments

Comments
 (0)