diff --git a/config/custom-environment-variables.yml b/config/custom-environment-variables.yml
index ec769d2eff73615d331974638fc117d4b6ec351d..56919b467f1a48eebd55374ccf60fb618a90cd8b 100644
--- a/config/custom-environment-variables.yml
+++ b/config/custom-environment-variables.yml
@@ -79,6 +79,8 @@ private:
   bitbucket_server_password: 'BITBUCKET_SERVER_PASS'
   curseforge_api_key: 'CURSEFORGE_API_KEY'
   discord_bot_token: 'DISCORD_BOT_TOKEN'
+  dockerhub_username: 'DOCKERHUB_USER'
+  dockerhub_pat: 'DOCKERHUB_PAT'
   drone_token: 'DRONE_TOKEN'
   gh_client_id: 'GH_CLIENT_ID'
   gh_client_secret: 'GH_CLIENT_SECRET'
diff --git a/core/base-service/auth-helper.js b/core/base-service/auth-helper.js
index 069291b512cbcbb7c30dfcd60b8e93ae08a8d507..4fadbc99bcd58354be7b434b8f57212c6be46bca 100644
--- a/core/base-service/auth-helper.js
+++ b/core/base-service/auth-helper.js
@@ -1,5 +1,13 @@
 import { URL } from 'url'
-import { InvalidParameter } from './errors.js'
+import dayjs from 'dayjs'
+import Joi from 'joi'
+import checkErrorResponse from './check-error-response.js'
+import { InvalidParameter, InvalidResponse } from './errors.js'
+import { fetch } from './got.js'
+import { parseJson } from './json.js'
+import validate from './validate.js'
+
+let jwtCache = Object.create(null)
 
 class AuthHelper {
   constructor(
@@ -87,7 +95,7 @@ class AuthHelper {
     }
   }
 
-  shouldAuthenticateRequest({ url, options = {} }) {
+  isAllowedOrigin(url) {
     let parsed
     try {
       parsed = new URL(url)
@@ -97,7 +105,11 @@ class AuthHelper {
 
     const { protocol, host } = parsed
     const origin = `${protocol}//${host}`
-    const originViolation = !this._authorizedOrigins.includes(origin)
+    return this._authorizedOrigins.includes(origin)
+  }
+
+  shouldAuthenticateRequest({ url, options = {} }) {
+    const originViolation = !this.isAllowedOrigin(url)
 
     const strictSslCheckViolation =
       this._requireStrictSslToAuthenticate &&
@@ -218,6 +230,103 @@ class AuthHelper {
       }),
     )
   }
+
+  static _getJwtExpiry(token, max = dayjs().add(1, 'hours').unix()) {
+    // get the expiry timestamp for this JWT (capped at a max length)
+    const parts = token.split('.')
+
+    if (parts.length < 2) {
+      throw new InvalidResponse({
+        prettyMessage: 'invalid response data from auth endpoint',
+      })
+    }
+
+    const json = validate(
+      {
+        ErrorClass: InvalidResponse,
+        prettyErrorMessage: 'invalid response data from auth endpoint',
+      },
+      parseJson(Buffer.from(parts[1], 'base64').toString()),
+      Joi.object({ exp: Joi.number().required() }).required(),
+    )
+
+    return Math.min(json.exp, max)
+  }
+
+  static _isJwtValid(expiry) {
+    // we consider the token valid if the expiry
+    // datetime is later than (now + 1 minute)
+    return dayjs.unix(expiry).isAfter(dayjs().add(1, 'minutes'))
+  }
+
+  async _getJwt(loginEndpoint) {
+    const { _user: username, _pass: password } = this
+
+    // attempt to get JWT from cache
+    if (
+      jwtCache?.[loginEndpoint]?.[username]?.token &&
+      jwtCache?.[loginEndpoint]?.[username]?.expiry &&
+      this.constructor._isJwtValid(jwtCache[loginEndpoint][username].expiry)
+    ) {
+      // cache hit
+      return jwtCache[loginEndpoint][username].token
+    }
+
+    // cache miss - request a new JWT
+    const originViolation = !this.isAllowedOrigin(loginEndpoint)
+    if (originViolation) {
+      throw new InvalidParameter({
+        prettyMessage: 'requested origin not authorized',
+      })
+    }
+
+    const { buffer } = await checkErrorResponse({})(
+      await fetch(loginEndpoint, {
+        method: 'POST',
+        form: { username, password },
+      }),
+    )
+
+    const json = validate(
+      {
+        ErrorClass: InvalidResponse,
+        prettyErrorMessage: 'invalid response data from auth endpoint',
+      },
+      parseJson(buffer),
+      Joi.object({ token: Joi.string().required() }).required(),
+    )
+
+    const token = json.token
+    const expiry = this.constructor._getJwtExpiry(token)
+
+    // store in the cache
+    if (!(loginEndpoint in jwtCache)) {
+      jwtCache[loginEndpoint] = {}
+    }
+    jwtCache[loginEndpoint][username] = { token, expiry }
+
+    return token
+  }
+
+  async _getJwtAuthHeader(loginEndpoint) {
+    if (!this.isConfigured) {
+      return undefined
+    }
+
+    const token = await this._getJwt(loginEndpoint)
+    return { Authorization: `Bearer ${token}` }
+  }
+
+  async withJwtAuth(requestParams, loginEndpoint) {
+    const authHeader = await this._getJwtAuthHeader(loginEndpoint)
+    return this._withAnyAuth(requestParams, requestParams =>
+      this.constructor._mergeHeaders(requestParams, authHeader),
+    )
+  }
+}
+
+function clearJwtCache() {
+  jwtCache = Object.create(null)
 }
 
-export { AuthHelper }
+export { AuthHelper, clearJwtCache }
diff --git a/core/base-service/auth-helper.spec.js b/core/base-service/auth-helper.spec.js
index 4ea06374ec8c91ceb25b803df53974f0f4a5e7f7..beb4142eb2e88419727fbefba75804f42e86050d 100644
--- a/core/base-service/auth-helper.spec.js
+++ b/core/base-service/auth-helper.spec.js
@@ -1,7 +1,32 @@
+import dayjs from 'dayjs'
+import nock from 'nock'
 import { expect } from 'chai'
 import { test, given, forCases } from 'sazerac'
-import { AuthHelper } from './auth-helper.js'
-import { InvalidParameter } from './errors.js'
+import { AuthHelper, clearJwtCache } from './auth-helper.js'
+import { InvalidParameter, InvalidResponse } from './errors.js'
+
+function base64UrlEncode(input) {
+  const base64 = btoa(JSON.stringify(input))
+  return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
+}
+
+function getMockJwt(extras) {
+  // this function returns a mock JWT that contains enough
+  // for a unit test but ignores important aspects e.g: signing
+
+  const header = {
+    alg: 'HS256',
+    typ: 'JWT',
+  }
+  const payload = {
+    iat: Math.floor(Date.now() / 1000),
+    ...extras,
+  }
+
+  const encodedHeader = base64UrlEncode(header)
+  const encodedPayload = base64UrlEncode(payload)
+  return `${encodedHeader}.${encodedPayload}`
+}
 
 describe('AuthHelper', function () {
   describe('constructor checks', function () {
@@ -381,4 +406,153 @@ describe('AuthHelper', function () {
       ).to.throw(InvalidParameter)
     })
   })
+
+  context('JTW Auth', function () {
+    describe('_isJwtValid', function () {
+      test(AuthHelper._isJwtValid, () => {
+        given(dayjs().add(1, 'month').unix()).expect(true)
+        given(dayjs().add(2, 'minutes').unix()).expect(true)
+        given(dayjs().add(30, 'seconds').unix()).expect(false)
+        given(dayjs().unix()).expect(false)
+        given(dayjs().subtract(1, 'seconds').unix()).expect(false)
+      })
+    })
+
+    describe('_getJwtExpiry', function () {
+      it('extracts expiry from valid JWT', function () {
+        const nowPlus30Mins = dayjs().add(30, 'minutes').unix()
+        expect(
+          AuthHelper._getJwtExpiry(getMockJwt({ exp: nowPlus30Mins })),
+        ).to.equal(nowPlus30Mins)
+      })
+
+      it('caps expiry at max', function () {
+        const nowPlus1Hour = dayjs().add(1, 'hours').unix()
+        const nowPlus2Hours = dayjs().add(2, 'hours').unix()
+        expect(
+          AuthHelper._getJwtExpiry(getMockJwt({ exp: nowPlus2Hours })),
+        ).to.equal(nowPlus1Hour)
+      })
+
+      it('throws if JWT does not contain exp', function () {
+        expect(() => {
+          AuthHelper._getJwtExpiry(getMockJwt({}))
+        }).to.throw(InvalidResponse)
+      })
+
+      it('throws if JWT is invalid', function () {
+        expect(() => {
+          AuthHelper._getJwtExpiry('abc')
+        }).to.throw(InvalidResponse)
+      })
+    })
+
+    describe('withJwtAuth', function () {
+      const authHelper = new AuthHelper(
+        {
+          userKey: 'jwt_user',
+          passKey: 'jwt_pass',
+          authorizedOrigins: ['https://example.com'],
+          isRequired: false,
+        },
+        { private: { jwt_user: 'fred', jwt_pass: 'abc123' } },
+      )
+
+      beforeEach(function () {
+        clearJwtCache()
+      })
+
+      it('should use cached response if valid', async function () {
+        // the expiry is far enough in the future that the token
+        // will still be valid on the second hit
+        const mockToken = getMockJwt({ exp: dayjs().add(1, 'hours').unix() })
+
+        // .times(1) ensures if we try to make a second call to this endpoint,
+        // we will throw `Nock: No match for request`
+        nock('https://example.com')
+          .post('/login')
+          .times(1)
+          .reply(200, { token: mockToken })
+        const params1 = await authHelper.withJwtAuth(
+          { url: 'https://example.com/some-endpoint' },
+          'https://example.com/login',
+        )
+        expect(nock.isDone()).to.equal(true)
+        expect(params1).to.deep.equal({
+          options: {
+            headers: {
+              Authorization: `Bearer ${mockToken}`,
+            },
+          },
+          url: 'https://example.com/some-endpoint',
+        })
+
+        // second time round, we'll get the same response again
+        // but this time served from cache
+        const params2 = await authHelper.withJwtAuth(
+          { url: 'https://example.com/some-endpoint' },
+          'https://example.com/login',
+        )
+        expect(params2).to.deep.equal({
+          options: {
+            headers: {
+              Authorization: `Bearer ${mockToken}`,
+            },
+          },
+          url: 'https://example.com/some-endpoint',
+        })
+
+        nock.cleanAll()
+      })
+
+      it('should not use cached response if expired', async function () {
+        // this time we define a token expiry is close enough
+        // that the token will not be valid on the second call
+        const mockToken1 = getMockJwt({
+          exp: dayjs().add(20, 'seconds').unix(),
+        })
+        nock('https://example.com')
+          .post('/login')
+          .times(1)
+          .reply(200, { token: mockToken1 })
+        const params1 = await authHelper.withJwtAuth(
+          { url: 'https://example.com/some-endpoint' },
+          'https://example.com/login',
+        )
+        expect(nock.isDone()).to.equal(true)
+        expect(params1).to.deep.equal({
+          options: {
+            headers: {
+              Authorization: `Bearer ${mockToken1}`,
+            },
+          },
+          url: 'https://example.com/some-endpoint',
+        })
+
+        // second time round we make another network request
+        const mockToken2 = getMockJwt({
+          exp: dayjs().add(20, 'seconds').unix(),
+        })
+        nock('https://example.com')
+          .post('/login')
+          .times(1)
+          .reply(200, { token: mockToken2 })
+        const params2 = await authHelper.withJwtAuth(
+          { url: 'https://example.com/some-endpoint' },
+          'https://example.com/login',
+        )
+        expect(nock.isDone()).to.equal(true)
+        expect(params2).to.deep.equal({
+          options: {
+            headers: {
+              Authorization: `Bearer ${mockToken2}`,
+            },
+          },
+          url: 'https://example.com/some-endpoint',
+        })
+
+        nock.cleanAll()
+      })
+    })
+  })
 })
diff --git a/core/server/server.js b/core/server/server.js
index fb3c5ef090d69fabd7bec8e4b83ab1a55e235e9c..d6c6256a1165edb7fd64801d27be4baf807ffdb6 100644
--- a/core/server/server.js
+++ b/core/server/server.js
@@ -165,6 +165,8 @@ const privateConfigSchema = Joi.object({
   azure_devops_token: Joi.string(),
   curseforge_api_key: Joi.string(),
   discord_bot_token: Joi.string(),
+  dockerhub_username: Joi.string(),
+  dockerhub_pat: Joi.string(),
   drone_token: Joi.string(),
   gh_client_id: Joi.string(),
   gh_client_secret: Joi.string(),
diff --git a/doc/server-secrets.md b/doc/server-secrets.md
index 636a7dcb47adee979df3d2b1fab069ff97e5e407..f7406b6d8bb8907bb0c429a4665ef5f1227771bd 100644
--- a/doc/server-secrets.md
+++ b/doc/server-secrets.md
@@ -119,6 +119,18 @@ Using a token for Discord is optional but will allow higher API rates.
 Register an application in the [Discord developer console](https://discord.com/developers).
 To obtain a token, simply create a bot for your application.
 
+### DockerHub
+
+Using authentication for DockerHub is optional but can be used to allow
+higher API rates or access to private repos.
+
+- `DOCKERHUB_USER` (yml: `private.dockerhub_username`)
+- `DOCKERHUB_PAT` (yml: `private.dockerhub_pat`)
+
+`DOCKERHUB_PAT` is a Personal Access Token. Generate a token in your
+[account security settings](https://hub.docker.com/settings/security) with
+"Read-Only" or "Public Repo Read-Only", depending on your needs.
+
 ### Drone
 
 - `DRONE_ORIGINS` (yml: `public.services.drone.authorizedOrigins`)
diff --git a/services/docker/docker-automated.service.js b/services/docker/docker-automated.service.js
index 0560e21c4bd327abfdc6b07b16dbce233c0976cc..bf8c946e371198d7ccd6d3136eb5f7dd335cb81e 100644
--- a/services/docker/docker-automated.service.js
+++ b/services/docker/docker-automated.service.js
@@ -5,6 +5,7 @@ import {
   buildDockerUrl,
   getDockerHubUser,
 } from './docker-helpers.js'
+import { fetch } from './docker-hub-common-fetch.js'
 
 const automatedBuildSchema = Joi.object({
   is_automated: Joi.boolean().required(),
@@ -13,6 +14,17 @@ const automatedBuildSchema = Joi.object({
 export default class DockerAutomatedBuild extends BaseJsonService {
   static category = 'build'
   static route = buildDockerUrl('automated')
+
+  static auth = {
+    userKey: 'dockerhub_username',
+    passKey: 'dockerhub_pat',
+    authorizedOrigins: [
+      'https://hub.docker.com',
+      'https://registry.hub.docker.com',
+    ],
+    isRequired: false,
+  }
+
   static openApi = {
     '/docker/automated/{user}/{repo}': {
       get: {
@@ -44,7 +56,7 @@ export default class DockerAutomatedBuild extends BaseJsonService {
   }
 
   async fetch({ user, repo }) {
-    return this._requestJson({
+    return await fetch(this, {
       schema: automatedBuildSchema,
       url: `https://registry.hub.docker.com/v2/repositories/${getDockerHubUser(
         user,
diff --git a/services/docker/docker-cloud-automated.service.js b/services/docker/docker-cloud-automated.service.js
index f808cea3b686a7da996c841c922b4a6c595daecb..feb392994a7b69f558064a9894889a67a43fdcc0 100644
--- a/services/docker/docker-cloud-automated.service.js
+++ b/services/docker/docker-cloud-automated.service.js
@@ -5,6 +5,14 @@ import { fetchBuild } from './docker-cloud-common-fetch.js'
 export default class DockerCloudAutomatedBuild extends BaseJsonService {
   static category = 'build'
   static route = buildDockerUrl('cloud/automated')
+
+  static auth = {
+    userKey: 'dockerhub_username',
+    passKey: 'dockerhub_pat',
+    authorizedOrigins: ['https://hub.docker.com', 'https://cloud.docker.com'],
+    isRequired: false,
+  }
+
   static examples = [
     {
       title: 'Docker Cloud Automated build',
diff --git a/services/docker/docker-cloud-build.service.js b/services/docker/docker-cloud-build.service.js
index 3435dbf3e6adceb83fe56ddfc1f42269b2cd779b..31713155a81825e9fe4c97f177abcfbb6d16b12b 100644
--- a/services/docker/docker-cloud-build.service.js
+++ b/services/docker/docker-cloud-build.service.js
@@ -5,6 +5,14 @@ import { fetchBuild } from './docker-cloud-common-fetch.js'
 export default class DockerCloudBuild extends BaseJsonService {
   static category = 'build'
   static route = buildDockerUrl('cloud/build')
+
+  static auth = {
+    userKey: 'dockerhub_username',
+    passKey: 'dockerhub_pat',
+    authorizedOrigins: ['https://hub.docker.com', 'https://cloud.docker.com'],
+    isRequired: false,
+  }
+
   static examples = [
     {
       title: 'Docker Cloud Build Status',
diff --git a/services/docker/docker-cloud-common-fetch.js b/services/docker/docker-cloud-common-fetch.js
index 60e12ffb0b38a6daab1fb3116dfcd954193dcccd..e340fbba2eacee1413b6bc2acf0af9535d860f3d 100644
--- a/services/docker/docker-cloud-common-fetch.js
+++ b/services/docker/docker-cloud-common-fetch.js
@@ -12,12 +12,17 @@ const cloudBuildSchema = Joi.object({
 }).required()
 
 async function fetchBuild(serviceInstance, { user, repo }) {
-  return serviceInstance._requestJson({
-    schema: cloudBuildSchema,
-    url: 'https://cloud.docker.com/api/build/v1/source',
-    options: { searchParams: { image: `${user}/${repo}` } },
-    httpErrors: { 404: 'repo not found' },
-  })
+  return serviceInstance._requestJson(
+    await serviceInstance.authHelper.withJwtAuth(
+      {
+        schema: cloudBuildSchema,
+        url: 'https://cloud.docker.com/api/build/v1/source',
+        options: { searchParams: { image: `${user}/${repo}` } },
+        httpErrors: { 404: 'repo not found' },
+      },
+      'https://hub.docker.com/v2/users/login/',
+    ),
+  )
 }
 
 export { fetchBuild }
diff --git a/services/docker/docker-cloud-common-fetch.spec.js b/services/docker/docker-cloud-common-fetch.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..ada82d943f9071010152ea247a6ec977154ebf03
--- /dev/null
+++ b/services/docker/docker-cloud-common-fetch.spec.js
@@ -0,0 +1,23 @@
+import sinon from 'sinon'
+import { expect } from 'chai'
+import { fetchBuild } from './docker-cloud-common-fetch.js'
+
+describe('fetchBuild', function () {
+  it('invokes withJwtAuth', async function () {
+    const serviceInstance = {
+      _requestJson: sinon.stub().resolves('fake-response'),
+      authHelper: {
+        withJwtAuth: sinon.stub(),
+      },
+    }
+
+    const resp = await fetchBuild(serviceInstance, {
+      user: 'user',
+      repo: 'repo',
+    })
+
+    expect(serviceInstance.authHelper.withJwtAuth.calledOnce).to.be.true
+    expect(serviceInstance._requestJson.calledOnce).to.be.true
+    expect(resp).to.equal('fake-response')
+  })
+})
diff --git a/services/docker/docker-hub-common-fetch.js b/services/docker/docker-hub-common-fetch.js
new file mode 100644
index 0000000000000000000000000000000000000000..c5ef9c39e59e7d893d31511c59cfebf54a0ec192
--- /dev/null
+++ b/services/docker/docker-hub-common-fetch.js
@@ -0,0 +1,10 @@
+async function fetch(serviceInstance, params) {
+  return serviceInstance._requestJson(
+    await serviceInstance.authHelper.withJwtAuth(
+      params,
+      'https://hub.docker.com/v2/users/login/',
+    ),
+  )
+}
+
+export { fetch }
diff --git a/services/docker/docker-hub-common-fetch.spec.js b/services/docker/docker-hub-common-fetch.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..0aac7416ca0ec17586a0dd2a077b36993975f0d5
--- /dev/null
+++ b/services/docker/docker-hub-common-fetch.spec.js
@@ -0,0 +1,20 @@
+import sinon from 'sinon'
+import { expect } from 'chai'
+import { fetch } from './docker-hub-common-fetch.js'
+
+describe('fetch', function () {
+  it('invokes withJwtAuth', async function () {
+    const serviceInstance = {
+      _requestJson: sinon.stub().resolves('fake-response'),
+      authHelper: {
+        withJwtAuth: sinon.stub(),
+      },
+    }
+
+    const resp = await fetch(serviceInstance, {})
+
+    expect(serviceInstance.authHelper.withJwtAuth.calledOnce).to.be.true
+    expect(serviceInstance._requestJson.calledOnce).to.be.true
+    expect(resp).to.equal('fake-response')
+  })
+})
diff --git a/services/docker/docker-pulls.service.js b/services/docker/docker-pulls.service.js
index 630e6d452f173914e106bc154fdc925f07c805d3..520bc9e10744bd698682d6bc6d0f1682486de23e 100644
--- a/services/docker/docker-pulls.service.js
+++ b/services/docker/docker-pulls.service.js
@@ -7,6 +7,7 @@ import {
   buildDockerUrl,
   getDockerHubUser,
 } from './docker-helpers.js'
+import { fetch } from './docker-hub-common-fetch.js'
 
 const pullsSchema = Joi.object({
   pull_count: nonNegativeInteger,
@@ -15,6 +16,14 @@ const pullsSchema = Joi.object({
 export default class DockerPulls extends BaseJsonService {
   static category = 'downloads'
   static route = buildDockerUrl('pulls')
+
+  static auth = {
+    userKey: 'dockerhub_username',
+    passKey: 'dockerhub_pat',
+    authorizedOrigins: ['https://hub.docker.com'],
+    isRequired: false,
+  }
+
   static openApi = {
     '/docker/pulls/{user}/{repo}': {
       get: {
@@ -42,7 +51,7 @@ export default class DockerPulls extends BaseJsonService {
   }
 
   async fetch({ user, repo }) {
-    return this._requestJson({
+    return await fetch(this, {
       schema: pullsSchema,
       url: `https://hub.docker.com/v2/repositories/${getDockerHubUser(
         user,
diff --git a/services/docker/docker-size.service.js b/services/docker/docker-size.service.js
index 481483eaebacd6ab0af56d63c1ad0a6965e26e2a..8368a27a4e2119f8df10be16d8794dfc2cb75e8e 100644
--- a/services/docker/docker-size.service.js
+++ b/services/docker/docker-size.service.js
@@ -9,6 +9,7 @@ import {
   getDockerHubUser,
   getMultiPageData,
 } from './docker-helpers.js'
+import { fetch } from './docker-hub-common-fetch.js'
 
 const buildSchema = Joi.object({
   name: Joi.string().required(),
@@ -61,6 +62,17 @@ function getImageSizeForArch(images, arch) {
 export default class DockerSize extends BaseJsonService {
   static category = 'size'
   static route = { ...buildDockerUrl('image-size', true), queryParamSchema }
+
+  static auth = {
+    userKey: 'dockerhub_username',
+    passKey: 'dockerhub_pat',
+    authorizedOrigins: [
+      'https://hub.docker.com',
+      'https://registry.hub.docker.com',
+    ],
+    isRequired: false,
+  }
+
   static examples = [
     {
       title: 'Docker Image Size (latest by date)',
@@ -102,7 +114,7 @@ export default class DockerSize extends BaseJsonService {
 
   async fetch({ user, repo, tag, page }) {
     page = page ? `&page=${page}` : ''
-    return this._requestJson({
+    return await fetch(this, {
       schema: tag ? buildSchema : pagedSchema,
       url: `https://registry.hub.docker.com/v2/repositories/${getDockerHubUser(
         user,
diff --git a/services/docker/docker-stars.service.js b/services/docker/docker-stars.service.js
index 6b696df1ab5b4c8e873e57af5eb7a3dbe5bb6b0a..ed03e4bd9c59dc03fdd25d39e0db2e3a5c9656f6 100644
--- a/services/docker/docker-stars.service.js
+++ b/services/docker/docker-stars.service.js
@@ -7,6 +7,7 @@ import {
   buildDockerUrl,
   getDockerHubUser,
 } from './docker-helpers.js'
+import { fetch } from './docker-hub-common-fetch.js'
 
 const schema = Joi.object({
   star_count: nonNegativeInteger.required(),
@@ -15,6 +16,14 @@ const schema = Joi.object({
 export default class DockerStars extends BaseJsonService {
   static category = 'rating'
   static route = buildDockerUrl('stars')
+
+  static auth = {
+    userKey: 'dockerhub_username',
+    passKey: 'dockerhub_pat',
+    authorizedOrigins: ['https://hub.docker.com'],
+    isRequired: false,
+  }
+
   static openApi = {
     '/docker/stars/{user}/{repo}': {
       get: {
@@ -45,7 +54,7 @@ export default class DockerStars extends BaseJsonService {
   }
 
   async fetch({ user, repo }) {
-    return this._requestJson({
+    return await fetch(this, {
       schema,
       url: `https://hub.docker.com/v2/repositories/${getDockerHubUser(
         user,
diff --git a/services/docker/docker-version.service.js b/services/docker/docker-version.service.js
index f3152432dcfffbfd0edc1d8d9c5f46e5765d112f..f611a15cdc52685057036657c911a81768cbc3f6 100644
--- a/services/docker/docker-version.service.js
+++ b/services/docker/docker-version.service.js
@@ -9,6 +9,7 @@ import {
   getMultiPageData,
   getDigestSemVerMatches,
 } from './docker-helpers.js'
+import { fetch } from './docker-hub-common-fetch.js'
 
 const buildSchema = Joi.object({
   count: nonNegativeInteger.required(),
@@ -33,6 +34,17 @@ const queryParamSchema = Joi.object({
 export default class DockerVersion extends BaseJsonService {
   static category = 'version'
   static route = { ...buildDockerUrl('v', true), queryParamSchema }
+
+  static auth = {
+    userKey: 'dockerhub_username',
+    passKey: 'dockerhub_pat',
+    authorizedOrigins: [
+      'https://hub.docker.com',
+      'https://registry.hub.docker.com',
+    ],
+    isRequired: false,
+  }
+
   static examples = [
     {
       title: 'Docker Image Version (latest by date)',
@@ -64,7 +76,7 @@ export default class DockerVersion extends BaseJsonService {
 
   async fetch({ user, repo, page }) {
     page = page ? `&page=${page}` : ''
-    return this._requestJson({
+    return await fetch(this, {
       schema: buildSchema,
       url: `https://registry.hub.docker.com/v2/repositories/${getDockerHubUser(
         user,
diff --git a/services/docker/docker-version.tester.js b/services/docker/docker-version.tester.js
index c737cb8abe1f65262c8e027a1f68cec8cf82de57..9ded09bf8496061b6adf57ed70f5c4ff0f0f6516 100644
--- a/services/docker/docker-version.tester.js
+++ b/services/docker/docker-version.tester.js
@@ -2,7 +2,7 @@ import { isSemver } from '../test-validators.js'
 import { createServiceTester } from '../tester.js'
 export const t = await createServiceTester()
 
-t.create('docker version (valid, library)').get('/_/alpine.json').expectBadge({
+t.create('docker version (valid, library)').get('/_/redis.json').expectBadge({
   label: 'version',
   message: isSemver,
 })