diff --git a/lib/modules/datasource/docker/artifactory.ts b/lib/modules/datasource/docker/artifactory.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e5c09df01cdecfafa981411ce1b185072fae9c7e
--- /dev/null
+++ b/lib/modules/datasource/docker/artifactory.ts
@@ -0,0 +1,10 @@
+import is from '@sindresorhus/is';
+import type { HttpResponse } from '../../../util/http/types';
+
+const JFROG_ARTIFACTORY_RES_HEADER = 'x-jfrog-version';
+
+export function isArtifactoryServer<T = unknown>(
+  res: HttpResponse<T> | undefined
+): boolean {
+  return is.string(res?.headers[JFROG_ARTIFACTORY_RES_HEADER]);
+}
diff --git a/lib/modules/datasource/docker/common.spec.ts b/lib/modules/datasource/docker/common.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c851267d10fd4ec610b7e66db87dfb15823e7017
--- /dev/null
+++ b/lib/modules/datasource/docker/common.spec.ts
@@ -0,0 +1,189 @@
+import * as httpMock from '../../../../test/http-mock';
+import { mocked } from '../../../../test/util';
+import { PAGE_NOT_FOUND_ERROR } from '../../../constants/error-messages';
+import * as _hostRules from '../../../util/host-rules';
+import { Http } from '../../../util/http';
+import {
+  dockerDatasourceId,
+  getAuthHeaders,
+  getRegistryRepository,
+} from './common';
+
+const hostRules = mocked(_hostRules);
+
+const http = new Http(dockerDatasourceId);
+
+jest.mock('../../../util/host-rules');
+
+describe('modules/datasource/docker/common', () => {
+  beforeEach(() => {
+    hostRules.find.mockReturnValue({
+      username: 'some-username',
+      password: 'some-password',
+    });
+    hostRules.hosts.mockReturnValue([]);
+  });
+
+  describe('getRegistryRepository', () => {
+    it('handles local registries', () => {
+      const res = getRegistryRepository(
+        'registry:5000/org/package',
+        'https://index.docker.io'
+      );
+      expect(res).toStrictEqual({
+        dockerRepository: 'org/package',
+        registryHost: 'https://registry:5000',
+      });
+    });
+
+    it('supports registryUrls', () => {
+      const res = getRegistryRepository(
+        'my.local.registry/prefix/image',
+        'https://my.local.registry/prefix'
+      );
+      expect(res).toStrictEqual({
+        dockerRepository: 'prefix/image',
+        registryHost: 'https://my.local.registry',
+      });
+    });
+
+    it('supports http registryUrls', () => {
+      const res = getRegistryRepository(
+        'my.local.registry/prefix/image',
+        'http://my.local.registry/prefix'
+      );
+      expect(res).toStrictEqual({
+        dockerRepository: 'prefix/image',
+        registryHost: 'http://my.local.registry',
+      });
+    });
+
+    it('supports schemeless registryUrls', () => {
+      const res = getRegistryRepository(
+        'my.local.registry/prefix/image',
+        'my.local.registry/prefix'
+      );
+      expect(res).toStrictEqual({
+        dockerRepository: 'prefix/image',
+        registryHost: 'https://my.local.registry',
+      });
+    });
+  });
+
+  describe('getAuthHeaders', () => {
+    it('throw page not found exception', async () => {
+      httpMock
+        .scope('https://my.local.registry')
+        .get('/v2/repo/tags/list?n=1000')
+        .reply(404, {});
+
+      await expect(
+        getAuthHeaders(
+          http,
+          'https://my.local.registry',
+          'repo',
+          'https://my.local.registry/v2/repo/tags/list?n=1000'
+        )
+      ).rejects.toThrow(PAGE_NOT_FOUND_ERROR);
+    });
+
+    it('returns "authType token" if both provided', async () => {
+      httpMock
+        .scope('https://my.local.registry')
+        .get('/v2/', undefined, { badheaders: ['authorization'] })
+        .reply(401, '', { 'www-authenticate': 'Authenticate you must' });
+      hostRules.hosts.mockReturnValue([]);
+      hostRules.find.mockReturnValue({
+        authType: 'some-authType',
+        token: 'some-token',
+      });
+
+      const headers = await getAuthHeaders(
+        http,
+        'https://my.local.registry',
+        'https://my.local.registry/prefix'
+      );
+
+      // do not inline, otherwise we get false positive from codeql
+      expect(headers).toMatchInlineSnapshot(`
+        {
+          "authorization": "some-authType some-token",
+        }
+      `);
+    });
+
+    it('returns "Bearer token" if only token provided', async () => {
+      httpMock
+        .scope('https://my.local.registry')
+        .get('/v2/', undefined, { badheaders: ['authorization'] })
+        .reply(401, '', { 'www-authenticate': 'Authenticate you must' });
+      hostRules.hosts.mockReturnValue([]);
+      hostRules.find.mockReturnValue({
+        token: 'some-token',
+      });
+
+      const headers = await getAuthHeaders(
+        http,
+        'https://my.local.registry',
+        'https://my.local.registry/prefix'
+      );
+
+      // do not inline, otherwise we get false positive from codeql
+      expect(headers).toMatchInlineSnapshot(`
+        {
+          "authorization": "Bearer some-token",
+        }
+      `);
+    });
+
+    it('fails', async () => {
+      httpMock
+        .scope('https://my.local.registry')
+        .get('/v2/', undefined, { badheaders: ['authorization'] })
+        .reply(401, '', { 'www-authenticate': 'Authenticate you must' });
+      hostRules.hosts.mockReturnValue([]);
+      httpMock.clear(false);
+
+      httpMock
+        .scope('https://my.local.registry')
+        .get('/v2/', undefined, { badheaders: ['authorization'] })
+        .reply(401, '', {});
+
+      const headers = await getAuthHeaders(
+        http,
+        'https://my.local.registry',
+        'https://my.local.registry/prefix'
+      );
+
+      expect(headers).toBeNull();
+    });
+
+    it('use resources URL and resolve scope in www-authenticate header', async () => {
+      httpMock
+        .scope('https://my.local.registry')
+        .get('/v2/my/node/resource')
+        .reply(401, '', {
+          'www-authenticate':
+            'Bearer realm="https://my.local.registry/oauth2/token",service="my.local.registry",scope="repository:my/node:whatever"',
+        })
+        .get(
+          '/oauth2/token?service=my.local.registry&scope=repository:my/node:whatever'
+        )
+        .reply(200, { token: 'some-token' });
+
+      const headers = await getAuthHeaders(
+        http,
+        'https://my.local.registry',
+        'my/node/prefix',
+        'https://my.local.registry/v2/my/node/resource'
+      );
+
+      // do not inline, otherwise we get false positive from codeql
+      expect(headers).toMatchInlineSnapshot(`
+        {
+          "authorization": "Bearer some-token",
+        }
+      `);
+    });
+  });
+});
diff --git a/lib/modules/datasource/docker/common.ts b/lib/modules/datasource/docker/common.ts
index 467755d9779f6b74775586a3b0b7292fd2d6c1a1..6f6b6cdb8fcdb1fbf7ee55c16c1339a9c1c478d1 100644
--- a/lib/modules/datasource/docker/common.ts
+++ b/lib/modules/datasource/docker/common.ts
@@ -1,5 +1,32 @@
 import is from '@sindresorhus/is';
-import type { HttpResponse } from '../../../util/http/types';
+import { parse } from 'auth-header';
+import hasha from 'hasha';
+import {
+  HOST_DISABLED,
+  PAGE_NOT_FOUND_ERROR,
+} from '../../../constants/error-messages';
+import { logger } from '../../../logger';
+import type { HostRule } from '../../../types';
+import { ExternalHostError } from '../../../types/errors/external-host-error';
+import * as hostRules from '../../../util/host-rules';
+import type { Http } from '../../../util/http';
+import type {
+  HttpOptions,
+  HttpResponse,
+  OutgoingHttpHeaders,
+} from '../../../util/http/types';
+import { regEx } from '../../../util/regex';
+import { addSecretForSanitizing } from '../../../util/sanitize';
+import {
+  ensureTrailingSlash,
+  parseUrl,
+  trimTrailingSlash,
+} from '../../../util/url';
+import { api as dockerVersioning } from '../../versioning/docker';
+import { ecrRegex, getECRAuthToken } from './ecr';
+import type { RegistryRepository } from './types';
+
+export const dockerDatasourceId = 'docker' as const;
 
 export const sourceLabels: string[] = [
   'org.opencontainers.image.source',
@@ -8,10 +35,261 @@ export const sourceLabels: string[] = [
 
 export const gitRefLabel = 'org.opencontainers.image.revision';
 
-const JFROG_ARTIFACTORY_RES_HEADER = 'x-jfrog-version';
+export const DOCKER_HUB = 'https://index.docker.io';
+
+export function isDockerHost(host: string): boolean {
+  const regex = regEx(/(?:^|\.)docker\.io$/);
+  return regex.test(host);
+}
+
+export async function getAuthHeaders(
+  http: Http,
+  registryHost: string,
+  dockerRepository: string,
+  apiCheckUrl = `${registryHost}/v2/`
+): Promise<OutgoingHttpHeaders | null> {
+  try {
+    const options = {
+      throwHttpErrors: false,
+      noAuth: true,
+    };
+    const apiCheckResponse = apiCheckUrl.endsWith('/v2/')
+      ? await http.get(apiCheckUrl, options)
+      : // use json request, as this will be cached for tags, so it returns json
+        // TODO: add cache test
+        await http.getJson(apiCheckUrl, options);
+
+    if (apiCheckResponse.statusCode === 200) {
+      logger.debug(`No registry auth required for ${apiCheckUrl}`);
+      return {};
+    }
+    if (apiCheckResponse.statusCode === 404) {
+      logger.debug(`Page Not Found ${apiCheckUrl}`);
+      // throw error up to be caught and potentially retried with library/ prefix
+      throw new Error(PAGE_NOT_FOUND_ERROR);
+    }
+    if (
+      apiCheckResponse.statusCode !== 401 ||
+      !is.nonEmptyString(apiCheckResponse.headers['www-authenticate'])
+    ) {
+      logger.warn(
+        { apiCheckUrl, res: apiCheckResponse },
+        'Invalid registry response'
+      );
+      return null;
+    }
+
+    const authenticateHeader = parse(
+      apiCheckResponse.headers['www-authenticate']
+    );
+
+    const opts: HostRule & HttpOptions = hostRules.find({
+      hostType: dockerDatasourceId,
+      url: apiCheckUrl,
+    });
+    if (ecrRegex.test(registryHost)) {
+      logger.trace(
+        { registryHost, dockerRepository },
+        `Using ecr auth for Docker registry`
+      );
+      const [, region] = ecrRegex.exec(registryHost) ?? [];
+      const auth = await getECRAuthToken(region, opts);
+      if (auth) {
+        opts.headers = { authorization: `Basic ${auth}` };
+      }
+    } else if (opts.username && opts.password) {
+      logger.trace(
+        { registryHost, dockerRepository },
+        `Using basic auth for Docker registry`
+      );
+      const auth = Buffer.from(`${opts.username}:${opts.password}`).toString(
+        'base64'
+      );
+      opts.headers = { authorization: `Basic ${auth}` };
+    } else if (opts.token) {
+      const authType = opts.authType ?? 'Bearer';
+      logger.trace(
+        { registryHost, dockerRepository },
+        `Using ${authType} token for Docker registry`
+      );
+      opts.headers = { authorization: `${authType} ${opts.token}` };
+    }
+    delete opts.username;
+    delete opts.password;
+    delete opts.token;
+
+    // If realm isn't an url, we should directly use auth header
+    // Can happen when we get a Basic auth or some other auth type
+    // * WWW-Authenticate: Basic realm="Artifactory Realm"
+    // * Www-Authenticate: Basic realm="https://123456789.dkr.ecr.eu-central-1.amazonaws.com/",service="ecr.amazonaws.com"
+    // * www-authenticate: Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull"
+    // * www-authenticate: Bearer realm="https://auth.docker.io/token",service="registry.docker.io"
+    if (
+      authenticateHeader.scheme.toUpperCase() !== 'BEARER' ||
+      !is.string(authenticateHeader.params.realm) ||
+      parseUrl(authenticateHeader.params.realm) === null
+    ) {
+      logger.trace(
+        { registryHost, dockerRepository, authenticateHeader },
+        `Invalid realm, testing direct auth`
+      );
+      return opts.headers ?? null;
+    }
+
+    let scope = `repository:${dockerRepository}:pull`;
+    // repo isn't known to server yet, so causing wrong scope `repository:user/image:pull`
+    if (
+      is.string(authenticateHeader.params.scope) &&
+      !apiCheckUrl.endsWith('/v2/')
+    ) {
+      scope = authenticateHeader.params.scope;
+    }
+
+    let service = authenticateHeader.params.service;
+    if (is.string(service)) {
+      service = `service=${service}&`;
+    } else {
+      service = ``;
+    }
+
+    const authUrl = `${authenticateHeader.params.realm}?${service}scope=${scope}`;
+    logger.trace(
+      { registryHost, dockerRepository, authUrl },
+      `Obtaining docker registry token`
+    );
+    opts.noAuth = true;
+    const authResponse = (
+      await http.getJson<{ token?: string; access_token?: string }>(
+        authUrl,
+        opts
+      )
+    ).body;
+
+    const token = authResponse.token ?? authResponse.access_token;
+    // istanbul ignore if
+    if (!token) {
+      logger.warn('Failed to obtain docker registry token');
+      return null;
+    }
+    // sanitize token
+    addSecretForSanitizing(token);
+    return {
+      authorization: `Bearer ${token}`,
+    };
+  } catch (err) /* istanbul ignore next */ {
+    if (err.host === 'quay.io') {
+      // TODO: debug why quay throws errors (#9604)
+      return null;
+    }
+    if (err.statusCode === 401) {
+      logger.debug(
+        { registryHost, dockerRepository },
+        'Unauthorized docker lookup'
+      );
+      logger.debug({ err });
+      return null;
+    }
+    if (err.statusCode === 403) {
+      logger.debug(
+        { registryHost, dockerRepository },
+        'Not allowed to access docker registry'
+      );
+      logger.debug({ err });
+      return null;
+    }
+    if (err.name === 'RequestError' && isDockerHost(registryHost)) {
+      throw new ExternalHostError(err);
+    }
+    if (err.statusCode === 429 && isDockerHost(registryHost)) {
+      throw new ExternalHostError(err);
+    }
+    if (err.statusCode >= 500 && err.statusCode < 600) {
+      throw new ExternalHostError(err);
+    }
+    if (err.message === PAGE_NOT_FOUND_ERROR) {
+      throw err;
+    }
+    if (err.message === HOST_DISABLED) {
+      logger.trace({ registryHost, dockerRepository, err }, 'Host disabled');
+      return null;
+    }
+    logger.warn(
+      { registryHost, dockerRepository, err },
+      'Error obtaining docker token'
+    );
+    return null;
+  }
+}
+
+export function getRegistryRepository(
+  packageName: string,
+  registryUrl: string
+): RegistryRepository {
+  if (registryUrl !== DOCKER_HUB) {
+    const registryEndingWithSlash = ensureTrailingSlash(
+      registryUrl.replace(regEx(/^https?:\/\//), '')
+    );
+    if (packageName.startsWith(registryEndingWithSlash)) {
+      let registryHost = trimTrailingSlash(registryUrl);
+      if (!regEx(/^https?:\/\//).test(registryHost)) {
+        registryHost = `https://${registryHost}`;
+      }
+      let dockerRepository = packageName.replace(registryEndingWithSlash, '');
+      const fullUrl = `${registryHost}/${dockerRepository}`;
+      const { origin, pathname } = parseUrl(fullUrl)!;
+      registryHost = origin;
+      dockerRepository = pathname.substring(1);
+      return {
+        registryHost,
+        dockerRepository,
+      };
+    }
+  }
+  let registryHost: string | undefined;
+  const split = packageName.split('/');
+  if (split.length > 1 && (split[0].includes('.') || split[0].includes(':'))) {
+    [registryHost] = split;
+    split.shift();
+  }
+  let dockerRepository = split.join('/');
+  if (!registryHost) {
+    registryHost = registryUrl.replace(
+      'https://docker.io',
+      'https://index.docker.io'
+    );
+  }
+  if (registryHost === 'docker.io') {
+    registryHost = 'index.docker.io';
+  }
+  if (!regEx(/^https?:\/\//).exec(registryHost)) {
+    registryHost = `https://${registryHost}`;
+  }
+  const opts = hostRules.find({
+    hostType: dockerDatasourceId,
+    url: registryHost,
+  });
+  if (opts?.insecureRegistry) {
+    registryHost = registryHost.replace('https', 'http');
+  }
+  if (registryHost.endsWith('.docker.io') && !dockerRepository.includes('/')) {
+    dockerRepository = 'library/' + dockerRepository;
+  }
+  return {
+    registryHost,
+    dockerRepository,
+  };
+}
+
+export function extractDigestFromResponseBody(
+  manifestResponse: HttpResponse
+): string {
+  return 'sha256:' + hasha(manifestResponse.body, { algorithm: 'sha256' });
+}
+
+export function findLatestStable(tags: string[]): string | null {
+  const versions = tags
+    .filter((v) => dockerVersioning.isValid(v) && dockerVersioning.isStable(v))
+    .sort((a, b) => dockerVersioning.sortVersions(a, b));
 
-export function isArtifactoryServer<T = unknown>(
-  res: HttpResponse<T> | undefined
-): boolean {
-  return is.string(res?.headers[JFROG_ARTIFACTORY_RES_HEADER]);
+  return versions.pop() ?? tags.slice(-1).pop() ?? null;
 }
diff --git a/lib/modules/datasource/docker/ecr.ts b/lib/modules/datasource/docker/ecr.ts
new file mode 100644
index 0000000000000000000000000000000000000000..08e244f7da809fc65b2df69230e99022f080dd78
--- /dev/null
+++ b/lib/modules/datasource/docker/ecr.ts
@@ -0,0 +1,54 @@
+import { ECR, ECRClientConfig } from '@aws-sdk/client-ecr';
+import { logger } from '../../../logger';
+import type { HostRule } from '../../../types';
+import type { HttpError } from '../../../util/http';
+import type { HttpResponse } from '../../../util/http/types';
+import { regEx } from '../../../util/regex';
+import { addSecretForSanitizing } from '../../../util/sanitize';
+
+export const ecrRegex = regEx(/\d+\.dkr\.ecr\.([-a-z0-9]+)\.amazonaws\.com/);
+export const ecrPublicRegex = regEx(/public\.ecr\.aws/);
+
+export async function getECRAuthToken(
+  region: string,
+  opts: HostRule
+): Promise<string | null> {
+  const config: ECRClientConfig = { region };
+  if (opts.username && opts.password) {
+    config.credentials = {
+      accessKeyId: opts.username,
+      secretAccessKey: opts.password,
+      ...(opts.token && { sessionToken: opts.token }),
+    };
+  }
+
+  const ecr = new ECR(config);
+  try {
+    const data = await ecr.getAuthorizationToken({});
+    const authorizationToken = data?.authorizationData?.[0]?.authorizationToken;
+    if (authorizationToken) {
+      // sanitize token
+      addSecretForSanitizing(authorizationToken);
+      return authorizationToken;
+    }
+    logger.warn(
+      'Could not extract authorizationToken from ECR getAuthorizationToken response'
+    );
+  } catch (err) {
+    logger.trace({ err }, 'err');
+    logger.debug('ECR getAuthorizationToken error');
+  }
+  return null;
+}
+
+export function isECRMaxResultsError(err: HttpError): boolean {
+  const resp = err.response as HttpResponse<any> | undefined;
+  return !!(
+    resp?.statusCode === 405 &&
+    resp.headers?.['docker-distribution-api-version'] &&
+    // https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_DescribeRepositories.html#ECR-DescribeRepositories-request-maxResults
+    resp.body?.['errors']?.[0]?.message?.includes(
+      'Member must have value less than or equal to 1000'
+    )
+  );
+}
diff --git a/lib/modules/datasource/docker/index.spec.ts b/lib/modules/datasource/docker/index.spec.ts
index fe3979b58e57cced4f6b14cb6afd49a2dd7db8cf..6d1ffb0558b793d2912e03f37d7ec06c6dea0274 100644
--- a/lib/modules/datasource/docker/index.spec.ts
+++ b/lib/modules/datasource/docker/index.spec.ts
@@ -8,18 +8,12 @@ import { getDigest, getPkgReleases } from '..';
 import { range } from '../../../../lib/util/range';
 import * as httpMock from '../../../../test/http-mock';
 import { logger, mocked } from '../../../../test/util';
-import {
-  EXTERNAL_HOST_ERROR,
-  PAGE_NOT_FOUND_ERROR,
-} from '../../../constants/error-messages';
+import { EXTERNAL_HOST_ERROR } from '../../../constants/error-messages';
 import * as _hostRules from '../../../util/host-rules';
-import { Http } from '../../../util/http';
-import { DockerDatasource, getAuthHeaders, getRegistryRepository } from '.';
+import { DockerDatasource } from '.';
 
 const hostRules = mocked(_hostRules);
 
-const http = new Http(DockerDatasource.id);
-
 jest.mock('../../../util/host-rules');
 
 const ecrMock = mockClient(ECRClient);
@@ -48,169 +42,6 @@ describe('modules/datasource/docker/index', () => {
     hostRules.hosts.mockReturnValue([]);
   });
 
-  describe('getRegistryRepository', () => {
-    it('handles local registries', () => {
-      const res = getRegistryRepository(
-        'registry:5000/org/package',
-        'https://index.docker.io'
-      );
-      expect(res).toStrictEqual({
-        dockerRepository: 'org/package',
-        registryHost: 'https://registry:5000',
-      });
-    });
-
-    it('supports registryUrls', () => {
-      const res = getRegistryRepository(
-        'my.local.registry/prefix/image',
-        'https://my.local.registry/prefix'
-      );
-      expect(res).toStrictEqual({
-        dockerRepository: 'prefix/image',
-        registryHost: 'https://my.local.registry',
-      });
-    });
-
-    it('supports http registryUrls', () => {
-      const res = getRegistryRepository(
-        'my.local.registry/prefix/image',
-        'http://my.local.registry/prefix'
-      );
-      expect(res).toStrictEqual({
-        dockerRepository: 'prefix/image',
-        registryHost: 'http://my.local.registry',
-      });
-    });
-
-    it('supports schemeless registryUrls', () => {
-      const res = getRegistryRepository(
-        'my.local.registry/prefix/image',
-        'my.local.registry/prefix'
-      );
-      expect(res).toStrictEqual({
-        dockerRepository: 'prefix/image',
-        registryHost: 'https://my.local.registry',
-      });
-    });
-  });
-
-  describe('getAuthHeaders', () => {
-    it('throw page not found exception', async () => {
-      httpMock
-        .scope('https://my.local.registry')
-        .get('/v2/repo/tags/list?n=1000')
-        .reply(404, {});
-
-      await expect(
-        getAuthHeaders(
-          http,
-          'https://my.local.registry',
-          'repo',
-          'https://my.local.registry/v2/repo/tags/list?n=1000'
-        )
-      ).rejects.toThrow(PAGE_NOT_FOUND_ERROR);
-    });
-
-    it('returns "authType token" if both provided', async () => {
-      httpMock
-        .scope('https://my.local.registry')
-        .get('/v2/', undefined, { badheaders: ['authorization'] })
-        .reply(401, '', { 'www-authenticate': 'Authenticate you must' });
-      hostRules.hosts.mockReturnValue([]);
-      hostRules.find.mockReturnValue({
-        authType: 'some-authType',
-        token: 'some-token',
-      });
-
-      const headers = await getAuthHeaders(
-        http,
-        'https://my.local.registry',
-        'https://my.local.registry/prefix'
-      );
-
-      // do not inline, otherwise we get false positive from codeql
-      expect(headers).toMatchInlineSnapshot(`
-        {
-          "authorization": "some-authType some-token",
-        }
-      `);
-    });
-
-    it('returns "Bearer token" if only token provided', async () => {
-      httpMock
-        .scope('https://my.local.registry')
-        .get('/v2/', undefined, { badheaders: ['authorization'] })
-        .reply(401, '', { 'www-authenticate': 'Authenticate you must' });
-      hostRules.hosts.mockReturnValue([]);
-      hostRules.find.mockReturnValue({
-        token: 'some-token',
-      });
-
-      const headers = await getAuthHeaders(
-        http,
-        'https://my.local.registry',
-        'https://my.local.registry/prefix'
-      );
-
-      // do not inline, otherwise we get false positive from codeql
-      expect(headers).toMatchInlineSnapshot(`
-        {
-          "authorization": "Bearer some-token",
-        }
-      `);
-    });
-
-    it('fails', async () => {
-      httpMock
-        .scope('https://my.local.registry')
-        .get('/v2/', undefined, { badheaders: ['authorization'] })
-        .reply(401, '', { 'www-authenticate': 'Authenticate you must' });
-      hostRules.hosts.mockReturnValue([]);
-      httpMock.clear(false);
-
-      httpMock
-        .scope('https://my.local.registry')
-        .get('/v2/', undefined, { badheaders: ['authorization'] })
-        .reply(401, '', {});
-
-      const headers = await getAuthHeaders(
-        http,
-        'https://my.local.registry',
-        'https://my.local.registry/prefix'
-      );
-
-      expect(headers).toBeNull();
-    });
-
-    it('use resources URL and resolve scope in www-authenticate header', async () => {
-      httpMock
-        .scope('https://my.local.registry')
-        .get('/v2/my/node/resource')
-        .reply(401, '', {
-          'www-authenticate':
-            'Bearer realm="https://my.local.registry/oauth2/token",service="my.local.registry",scope="repository:my/node:whatever"',
-        })
-        .get(
-          '/oauth2/token?service=my.local.registry&scope=repository:my/node:whatever'
-        )
-        .reply(200, { token: 'some-token' });
-
-      const headers = await getAuthHeaders(
-        http,
-        'https://my.local.registry',
-        'my/node/prefix',
-        'https://my.local.registry/v2/my/node/resource'
-      );
-
-      // do not inline, otherwise we get false positive from codeql
-      expect(headers).toMatchInlineSnapshot(`
-        {
-          "authorization": "Bearer some-token",
-        }
-      `);
-    });
-  });
-
   describe('getDigest', () => {
     it('returns null if no token', async () => {
       httpMock
diff --git a/lib/modules/datasource/docker/index.ts b/lib/modules/datasource/docker/index.ts
index 88ab4354ffcea126e7d422400ce47d335ba37f9c..ca5f5d728aa9dd723f101b4b06a70dee003defd8 100644
--- a/lib/modules/datasource/docker/index.ts
+++ b/lib/modules/datasource/docker/index.ts
@@ -1,353 +1,42 @@
-import { ECR } from '@aws-sdk/client-ecr';
-import type { ECRClientConfig } from '@aws-sdk/client-ecr';
 import is from '@sindresorhus/is';
-import { parse } from 'auth-header';
-import hasha from 'hasha';
-import {
-  HOST_DISABLED,
-  PAGE_NOT_FOUND_ERROR,
-} from '../../../constants/error-messages';
+import { PAGE_NOT_FOUND_ERROR } from '../../../constants/error-messages';
 import { logger } from '../../../logger';
-import type { HostRule } from '../../../types';
 import { ExternalHostError } from '../../../types/errors/external-host-error';
 import { cache } from '../../../util/cache/package/decorator';
-import * as hostRules from '../../../util/host-rules';
-import { Http, HttpError } from '../../../util/http';
-import type {
-  HttpOptions,
-  HttpResponse,
-  OutgoingHttpHeaders,
-} from '../../../util/http/types';
+import { HttpError } from '../../../util/http';
+import type { HttpResponse } from '../../../util/http/types';
 import { hasKey } from '../../../util/object';
 import { regEx } from '../../../util/regex';
-import { addSecretForSanitizing } from '../../../util/sanitize';
 import { isDockerDigest } from '../../../util/string';
 import {
   ensurePathPrefix,
-  ensureTrailingSlash,
   joinUrlParts,
   parseLinkHeader,
-  parseUrl,
-  trimTrailingSlash,
 } from '../../../util/url';
-import {
-  api as dockerVersioning,
-  id as dockerVersioningId,
-} from '../../versioning/docker';
+import { id as dockerVersioningId } from '../../versioning/docker';
 import { Datasource } from '../datasource';
 import type { DigestConfig, GetReleasesConfig, ReleaseResult } from '../types';
-import { gitRefLabel, isArtifactoryServer, sourceLabels } from './common';
+import { isArtifactoryServer } from './artifactory';
+import {
+  DOCKER_HUB,
+  dockerDatasourceId,
+  extractDigestFromResponseBody,
+  findLatestStable,
+  getAuthHeaders,
+  getRegistryRepository,
+  gitRefLabel,
+  isDockerHost,
+  sourceLabels,
+} from './common';
+import { ecrPublicRegex, ecrRegex, isECRMaxResultsError } from './ecr';
 import type {
   Image,
   ImageConfig,
   ImageList,
   OciImage,
   OciImageList,
-  RegistryRepository,
 } from './types';
 
-export const DOCKER_HUB = 'https://index.docker.io';
-
-export const ecrRegex = regEx(/\d+\.dkr\.ecr\.([-a-z0-9]+)\.amazonaws\.com/);
-export const ecrPublicRegex = regEx(/public\.ecr\.aws/);
-
-function isDockerHost(host: string): boolean {
-  const regex = regEx(/(?:^|\.)docker\.io$/);
-  return regex.test(host);
-}
-
-export async function getAuthHeaders(
-  http: Http,
-  registryHost: string,
-  dockerRepository: string,
-  apiCheckUrl = `${registryHost}/v2/`
-): Promise<OutgoingHttpHeaders | null> {
-  try {
-    const options = {
-      throwHttpErrors: false,
-      noAuth: true,
-    };
-    const apiCheckResponse = apiCheckUrl.endsWith('/v2/')
-      ? await http.get(apiCheckUrl, options)
-      : // use json request, as this will be cached for tags, so it returns json
-        // TODO: add cache test
-        await http.getJson(apiCheckUrl, options);
-
-    if (apiCheckResponse.statusCode === 200) {
-      logger.debug(`No registry auth required for ${apiCheckUrl}`);
-      return {};
-    }
-    if (apiCheckResponse.statusCode === 404) {
-      logger.debug(`Page Not Found ${apiCheckUrl}`);
-      // throw error up to be caught and potentially retried with library/ prefix
-      throw new Error(PAGE_NOT_FOUND_ERROR);
-    }
-    if (
-      apiCheckResponse.statusCode !== 401 ||
-      !is.nonEmptyString(apiCheckResponse.headers['www-authenticate'])
-    ) {
-      logger.warn(
-        { apiCheckUrl, res: apiCheckResponse },
-        'Invalid registry response'
-      );
-      return null;
-    }
-
-    const authenticateHeader = parse(
-      apiCheckResponse.headers['www-authenticate']
-    );
-
-    const opts: HostRule & HttpOptions = hostRules.find({
-      hostType: DockerDatasource.id,
-      url: apiCheckUrl,
-    });
-    if (ecrRegex.test(registryHost)) {
-      logger.trace(
-        { registryHost, dockerRepository },
-        `Using ecr auth for Docker registry`
-      );
-      const [, region] = ecrRegex.exec(registryHost) ?? [];
-      const auth = await getECRAuthToken(region, opts);
-      if (auth) {
-        opts.headers = { authorization: `Basic ${auth}` };
-      }
-    } else if (opts.username && opts.password) {
-      logger.trace(
-        { registryHost, dockerRepository },
-        `Using basic auth for Docker registry`
-      );
-      const auth = Buffer.from(`${opts.username}:${opts.password}`).toString(
-        'base64'
-      );
-      opts.headers = { authorization: `Basic ${auth}` };
-    } else if (opts.token) {
-      const authType = opts.authType ?? 'Bearer';
-      logger.trace(
-        { registryHost, dockerRepository },
-        `Using ${authType} token for Docker registry`
-      );
-      opts.headers = { authorization: `${authType} ${opts.token}` };
-    }
-    delete opts.username;
-    delete opts.password;
-    delete opts.token;
-
-    // If realm isn't an url, we should directly use auth header
-    // Can happen when we get a Basic auth or some other auth type
-    // * WWW-Authenticate: Basic realm="Artifactory Realm"
-    // * Www-Authenticate: Basic realm="https://123456789.dkr.ecr.eu-central-1.amazonaws.com/",service="ecr.amazonaws.com"
-    // * www-authenticate: Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull"
-    // * www-authenticate: Bearer realm="https://auth.docker.io/token",service="registry.docker.io"
-    if (
-      authenticateHeader.scheme.toUpperCase() !== 'BEARER' ||
-      !is.string(authenticateHeader.params.realm) ||
-      parseUrl(authenticateHeader.params.realm) === null
-    ) {
-      logger.trace(
-        { registryHost, dockerRepository, authenticateHeader },
-        `Invalid realm, testing direct auth`
-      );
-      return opts.headers ?? null;
-    }
-
-    let scope = `repository:${dockerRepository}:pull`;
-    // repo isn't known to server yet, so causing wrong scope `repository:user/image:pull`
-    if (
-      is.string(authenticateHeader.params.scope) &&
-      !apiCheckUrl.endsWith('/v2/')
-    ) {
-      scope = authenticateHeader.params.scope;
-    }
-
-    let service = authenticateHeader.params.service;
-    if (is.string(service)) {
-      service = `service=${service}&`;
-    } else {
-      service = ``;
-    }
-
-    const authUrl = `${authenticateHeader.params.realm}?${service}scope=${scope}`;
-    logger.trace(
-      { registryHost, dockerRepository, authUrl },
-      `Obtaining docker registry token`
-    );
-    opts.noAuth = true;
-    const authResponse = (
-      await http.getJson<{ token?: string; access_token?: string }>(
-        authUrl,
-        opts
-      )
-    ).body;
-
-    const token = authResponse.token ?? authResponse.access_token;
-    // istanbul ignore if
-    if (!token) {
-      logger.warn('Failed to obtain docker registry token');
-      return null;
-    }
-    // sanitize token
-    addSecretForSanitizing(token);
-    return {
-      authorization: `Bearer ${token}`,
-    };
-  } catch (err) /* istanbul ignore next */ {
-    if (err.host === 'quay.io') {
-      // TODO: debug why quay throws errors (#9604)
-      return null;
-    }
-    if (err.statusCode === 401) {
-      logger.debug(
-        { registryHost, dockerRepository },
-        'Unauthorized docker lookup'
-      );
-      logger.debug({ err });
-      return null;
-    }
-    if (err.statusCode === 403) {
-      logger.debug(
-        { registryHost, dockerRepository },
-        'Not allowed to access docker registry'
-      );
-      logger.debug({ err });
-      return null;
-    }
-    if (err.name === 'RequestError' && isDockerHost(registryHost)) {
-      throw new ExternalHostError(err);
-    }
-    if (err.statusCode === 429 && isDockerHost(registryHost)) {
-      throw new ExternalHostError(err);
-    }
-    if (err.statusCode >= 500 && err.statusCode < 600) {
-      throw new ExternalHostError(err);
-    }
-    if (err.message === PAGE_NOT_FOUND_ERROR) {
-      throw err;
-    }
-    if (err.message === HOST_DISABLED) {
-      logger.trace({ registryHost, dockerRepository, err }, 'Host disabled');
-      return null;
-    }
-    logger.warn(
-      { registryHost, dockerRepository, err },
-      'Error obtaining docker token'
-    );
-    return null;
-  }
-}
-
-async function getECRAuthToken(
-  region: string,
-  opts: HostRule
-): Promise<string | null> {
-  const config: ECRClientConfig = { region };
-  if (opts.username && opts.password) {
-    config.credentials = {
-      accessKeyId: opts.username,
-      secretAccessKey: opts.password,
-      ...(opts.token && { sessionToken: opts.token }),
-    };
-  }
-
-  const ecr = new ECR(config);
-  try {
-    const data = await ecr.getAuthorizationToken({});
-    const authorizationToken = data?.authorizationData?.[0]?.authorizationToken;
-    if (authorizationToken) {
-      // sanitize token
-      addSecretForSanitizing(authorizationToken);
-      return authorizationToken;
-    }
-    logger.warn(
-      'Could not extract authorizationToken from ECR getAuthorizationToken response'
-    );
-  } catch (err) {
-    logger.trace({ err }, 'err');
-    logger.debug('ECR getAuthorizationToken error');
-  }
-  return null;
-}
-
-export function getRegistryRepository(
-  packageName: string,
-  registryUrl: string
-): RegistryRepository {
-  if (registryUrl !== DOCKER_HUB) {
-    const registryEndingWithSlash = ensureTrailingSlash(
-      registryUrl.replace(regEx(/^https?:\/\//), '')
-    );
-    if (packageName.startsWith(registryEndingWithSlash)) {
-      let registryHost = trimTrailingSlash(registryUrl);
-      if (!regEx(/^https?:\/\//).test(registryHost)) {
-        registryHost = `https://${registryHost}`;
-      }
-      let dockerRepository = packageName.replace(registryEndingWithSlash, '');
-      const fullUrl = `${registryHost}/${dockerRepository}`;
-      const { origin, pathname } = parseUrl(fullUrl)!;
-      registryHost = origin;
-      dockerRepository = pathname.substring(1);
-      return {
-        registryHost,
-        dockerRepository,
-      };
-    }
-  }
-  let registryHost: string | undefined;
-  const split = packageName.split('/');
-  if (split.length > 1 && (split[0].includes('.') || split[0].includes(':'))) {
-    [registryHost] = split;
-    split.shift();
-  }
-  let dockerRepository = split.join('/');
-  if (!registryHost) {
-    registryHost = registryUrl.replace(
-      'https://docker.io',
-      'https://index.docker.io'
-    );
-  }
-  if (registryHost === 'docker.io') {
-    registryHost = 'index.docker.io';
-  }
-  if (!regEx(/^https?:\/\//).exec(registryHost)) {
-    registryHost = `https://${registryHost}`;
-  }
-  const opts = hostRules.find({
-    hostType: DockerDatasource.id,
-    url: registryHost,
-  });
-  if (opts?.insecureRegistry) {
-    registryHost = registryHost.replace('https', 'http');
-  }
-  if (registryHost.endsWith('.docker.io') && !dockerRepository.includes('/')) {
-    dockerRepository = 'library/' + dockerRepository;
-  }
-  return {
-    registryHost,
-    dockerRepository,
-  };
-}
-
-function digestFromManifestStr(str: hasha.HashaInput): string {
-  return 'sha256:' + hasha(str, { algorithm: 'sha256' });
-}
-
-export function extractDigestFromResponseBody(
-  manifestResponse: HttpResponse
-): string {
-  return digestFromManifestStr(manifestResponse.body);
-}
-
-export function isECRMaxResultsError(err: HttpError): boolean {
-  const resp = err.response as HttpResponse<any> | undefined;
-  return !!(
-    resp?.statusCode === 405 &&
-    resp.headers?.['docker-distribution-api-version'] &&
-    // https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_DescribeRepositories.html#ECR-DescribeRepositories-request-maxResults
-    resp.body?.['errors']?.[0]?.message?.includes(
-      'Member must have value less than or equal to 1000'
-    )
-  );
-}
-
 const defaultConfig = {
   commitMessageTopic: '{{{depName}}} Docker tag',
   commitMessageExtra:
@@ -372,16 +61,8 @@ const defaultConfig = {
   },
 };
 
-function findLatestStable(tags: string[]): string | null {
-  const versions = tags
-    .filter((v) => dockerVersioning.isValid(v) && dockerVersioning.isStable(v))
-    .sort((a, b) => dockerVersioning.sortVersions(a, b));
-
-  return versions.pop() ?? tags.slice(-1).pop() ?? null;
-}
-
 export class DockerDatasource extends Datasource {
-  static readonly id = 'docker';
+  static readonly id = dockerDatasourceId;
 
   override readonly defaultVersioning = dockerVersioningId;