diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index 7658749dac56e866781b0a780152d42cd1bc7895..7f8ad491624a8550920717a50511daad2aa2d699 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -3636,6 +3636,43 @@ Example:
 }
 ```
 
+## versionCompatibility
+
+This option is used for advanced use cases where the version string embeds more data than just the version.
+It's typically used with docker and tags datasources.
+
+Here are two examples:
+
+- The image tag `ghcr.io/umami-software/umami:postgresql-v1.37.0` embeds text like `postgresql-` as a prefix to the actual version to differentiate different DB types.
+- Docker image tags like `node:18.10.0-alpine` embed the base image as a suffix to the version.
+
+Here is an example of solving these types of cases:
+
+```json
+{
+  "packageRules": [
+    {
+      "matchDatasources": ["docker"],
+      "matchPackageNames": ["ghcr.io/umami-software/umami"],
+      "versionCompatibility": "^(?<compatibility>.*)-(?<version>.*)$",
+      "versioning": "semver"
+    },
+    {
+      "matchDatasources": ["docker"],
+      "matchPackageNames": ["node"],
+      "versionCompatibility": "^(?<version>.*)(?<compatibility>-.*)?$",
+      "versioning": "node"
+    }
+  ]
+}
+```
+
+This feature is most useful when the `currentValue` is a version and not a range/constraint.
+
+This feature _can_ be used in combination with `extractVersion` although that's likely only a rare edge case.
+When combined, `extractVersion` is applied to datasource results first, and then `versionCompatibility`.
+`extractVersion` should be used when the raw version string returned by the `datasource` contains extra details (such as a `v` prefix) when compared to the value/version used within the repository.
+
 ## versioning
 
 Usually, each language or package manager has a specific type of "versioning":
diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts
index 4a232fef74075f690f1e77c4d64adfbbd5127f72..f540e3fb88c7d545732366eca9236d3164522ab4 100644
--- a/lib/config/options/index.ts
+++ b/lib/config/options/index.ts
@@ -990,6 +990,15 @@ const options: RenovateOptions[] = [
     cli: false,
     env: false,
   },
+  {
+    name: 'versionCompatibility',
+    description:
+      'A regex (`re2`) with named capture groups to show how version and compatibility are split from a raw version string.',
+    type: 'string',
+    format: 'regex',
+    cli: false,
+    env: false,
+  },
   {
     name: 'versioning',
     description: 'Versioning to use for filtering and comparisons.',
diff --git a/lib/modules/datasource/common.spec.ts b/lib/modules/datasource/common.spec.ts
index 7a57cd455a613d365b67a2c33da2b822d2dcc030..fcf249fa14f0a4e8353da4245438960bfb1fb499 100644
--- a/lib/modules/datasource/common.spec.ts
+++ b/lib/modules/datasource/common.spec.ts
@@ -3,6 +3,7 @@ import { defaultVersioning } from '../versioning';
 import {
   applyConstraintsFiltering,
   applyExtractVersion,
+  applyVersionCompatibility,
   filterValidVersions,
   getDatasourceFor,
   getDefaultVersioning,
@@ -226,4 +227,41 @@ describe('modules/datasource/common', () => {
       });
     });
   });
+
+  describe('applyVersionCompatibility', () => {
+    let input: ReleaseResult;
+
+    beforeEach(() => {
+      input = {
+        releases: [
+          { version: '1.0.0' },
+          { version: '2.0.0' },
+          { version: '2.0.0-alpine' },
+        ],
+      };
+    });
+
+    it('returns immediately if no versionCompatibility', () => {
+      const result = applyVersionCompatibility(input, undefined, undefined);
+      expect(result).toBe(input);
+    });
+
+    it('filters out non-matching', () => {
+      const versionCompatibility = '^(?<version>[^-]+)$';
+      expect(
+        applyVersionCompatibility(input, versionCompatibility, undefined)
+      ).toMatchObject({
+        releases: [{ version: '1.0.0' }, { version: '2.0.0' }],
+      });
+    });
+
+    it('filters out incompatible', () => {
+      const versionCompatibility = '^(?<version>[^-]+)(?<compatibility>.*)?$';
+      expect(
+        applyVersionCompatibility(input, versionCompatibility, '-alpine')
+      ).toMatchObject({
+        releases: [{ version: '2.0.0' }],
+      });
+    });
+  });
 });
diff --git a/lib/modules/datasource/common.ts b/lib/modules/datasource/common.ts
index c839724b7f8d5b704b65def8eba4743b6f615e05..335effc116945e45feadf7e32b127920b0c6908d 100644
--- a/lib/modules/datasource/common.ts
+++ b/lib/modules/datasource/common.ts
@@ -53,6 +53,31 @@ export function isGetPkgReleasesConfig(
   );
 }
 
+export function applyVersionCompatibility(
+  releaseResult: ReleaseResult,
+  versionCompatibility: string | undefined,
+  currentCompatibility: string | undefined
+): ReleaseResult {
+  if (!versionCompatibility) {
+    return releaseResult;
+  }
+
+  const versionCompatibilityRegEx = regEx(versionCompatibility);
+  releaseResult.releases = filterMap(releaseResult.releases, (release) => {
+    const regexResult = versionCompatibilityRegEx.exec(release.version);
+    if (!regexResult?.groups?.version) {
+      return null;
+    }
+    if (regexResult?.groups?.compatibility !== currentCompatibility) {
+      return null;
+    }
+    release.version = regexResult.groups.version;
+    return release;
+  });
+
+  return releaseResult;
+}
+
 export function applyExtractVersion(
   releaseResult: ReleaseResult,
   extractVersion: string | undefined
diff --git a/lib/modules/datasource/index.ts b/lib/modules/datasource/index.ts
index cd6b3b155f50f562db7a4af47a194712af946240..29fc0a39a06d92d316b263ec500d30ecfb0e3aa9 100644
--- a/lib/modules/datasource/index.ts
+++ b/lib/modules/datasource/index.ts
@@ -13,6 +13,7 @@ import datasources from './api';
 import {
   applyConstraintsFiltering,
   applyExtractVersion,
+  applyVersionCompatibility,
   filterValidVersions,
   getDatasourceFor,
   sortAndRemoveDuplicates,
@@ -363,6 +364,11 @@ export function applyDatasourceFilters(
 ): ReleaseResult {
   let res = releaseResult;
   res = applyExtractVersion(res, config.extractVersion);
+  res = applyVersionCompatibility(
+    res,
+    config.versionCompatibility,
+    config.currentCompatibility
+  );
   res = filterValidVersions(res, config);
   res = sortAndRemoveDuplicates(res, config);
   res = applyConstraintsFiltering(res, config);
diff --git a/lib/modules/datasource/types.ts b/lib/modules/datasource/types.ts
index 4d47e3fbc46b866fddd0c2ada5ecf2a29e9d6e36..7cd06d0697b06b741a26331455539d3baf8d16ab 100644
--- a/lib/modules/datasource/types.ts
+++ b/lib/modules/datasource/types.ts
@@ -39,6 +39,8 @@ export interface GetPkgReleasesConfig {
   packageName: string;
   versioning?: string;
   extractVersion?: string;
+  versionCompatibility?: string;
+  currentCompatibility?: string;
   constraints?: Record<string, string>;
   replacementName?: string;
   replacementVersion?: string;
diff --git a/lib/workers/repository/process/lookup/index.spec.ts b/lib/workers/repository/process/lookup/index.spec.ts
index 3affbb7634c1d66342016103ee45093c94c8ccac..3b0ea3fa6b33c49ad4f9d3c40bc989fbf8ca32c3 100644
--- a/lib/workers/repository/process/lookup/index.spec.ts
+++ b/lib/workers/repository/process/lookup/index.spec.ts
@@ -1744,6 +1744,44 @@ describe('workers/repository/process/lookup/index', () => {
       });
     });
 
+    it('applies versionCompatibility for 18.10.0', async () => {
+      config.currentValue = '18.10.0-alpine';
+      config.packageName = 'node';
+      config.versioning = nodeVersioningId;
+      config.versionCompatibility = '^(?<version>[^-]+)(?<compatibility>-.*)?$';
+      config.datasource = DockerDatasource.id;
+      getDockerReleases.mockResolvedValueOnce({
+        releases: [
+          { version: '18.18.0' },
+          { version: '18.19.0-alpine' },
+          { version: '18.20.0' },
+        ],
+      });
+      const res = await lookup.lookupUpdates(config);
+      expect(res).toMatchObject({
+        updates: [{ newValue: '18.19.0-alpine', updateType: 'minor' }],
+      });
+    });
+
+    it('handles versionCompatibility mismatch', async () => {
+      config.currentValue = '18.10.0-alpine';
+      config.packageName = 'node';
+      config.versioning = nodeVersioningId;
+      config.versionCompatibility = '^(?<version>[^-]+)-slim$';
+      config.datasource = DockerDatasource.id;
+      getDockerReleases.mockResolvedValueOnce({
+        releases: [
+          { version: '18.18.0' },
+          { version: '18.19.0-alpine' },
+          { version: '18.20.0' },
+        ],
+      });
+      const res = await lookup.lookupUpdates(config);
+      expect(res).toMatchObject({
+        updates: [],
+      });
+    });
+
     it('handles digest pin for up to date version', async () => {
       config.currentValue = '8.1.0';
       config.packageName = 'node';
diff --git a/lib/workers/repository/process/lookup/index.ts b/lib/workers/repository/process/lookup/index.ts
index d24af3939af89867bd43738a7f33217801528f96..f8f9b4426bdc676c6b94cff268d5709b6355612a 100644
--- a/lib/workers/repository/process/lookup/index.ts
+++ b/lib/workers/repository/process/lookup/index.ts
@@ -70,14 +70,43 @@ export async function lookupUpdates(
       res.skipReason = 'invalid-config';
       return res;
     }
-    const isValid =
-      is.string(config.currentValue) && versioning.isValid(config.currentValue);
+    let compareValue = config.currentValue;
+    if (
+      is.string(config.currentValue) &&
+      is.string(config.versionCompatibility)
+    ) {
+      const versionCompatbilityRegEx = regEx(config.versionCompatibility);
+      const regexMatch = versionCompatbilityRegEx.exec(config.currentValue);
+      if (regexMatch?.groups) {
+        logger.debug(
+          {
+            versionCompatibility: config.versionCompatibility,
+            currentValue: config.currentValue,
+            packageName: config.packageName,
+            groups: regexMatch.groups,
+          },
+          'version compatibility regex match'
+        );
+        config.currentCompatibility = regexMatch.groups.compatibility;
+        compareValue = regexMatch.groups.version;
+      } else {
+        logger.debug(
+          {
+            versionCompatibility: config.versionCompatibility,
+            currentValue: config.currentValue,
+            packageName: config.packageName,
+          },
+          'version compatibility regex mismatch'
+        );
+      }
+    }
+    const isValid = is.string(compareValue) && versioning.isValid(compareValue);
 
     if (unconstrainedValue || isValid) {
       if (
         !config.updatePinnedDependencies &&
         // TODO #22198
-        versioning.isSingleVersion(config.currentValue!)
+        versioning.isSingleVersion(compareValue!)
       ) {
         res.skipReason = 'is-pinned';
         return res;
@@ -163,16 +192,15 @@ export async function lookupUpdates(
         allVersions = allVersions.filter(
           (v) =>
             v.version === taggedVersion ||
-            (v.version === config.currentValue &&
-              versioning.isGreaterThan(taggedVersion, config.currentValue))
+            (v.version === compareValue &&
+              versioning.isGreaterThan(taggedVersion, compareValue))
         );
       }
       // Check that existing constraint can be satisfied
       const allSatisfyingVersions = allVersions.filter(
         (v) =>
           // TODO #22198
-          unconstrainedValue ||
-          versioning.matches(v.version, config.currentValue!)
+          unconstrainedValue || versioning.matches(v.version, compareValue!)
       );
       if (!allSatisfyingVersions.length) {
         logger.debug(
@@ -187,7 +215,7 @@ export async function lookupUpdates(
           res.warnings.push({
             topic: config.packageName,
             // TODO: types (#22198)
-            message: `Can't find version matching ${config.currentValue!} for ${
+            message: `Can't find version matching ${compareValue!} for ${
               config.datasource
             } package ${config.packageName}`,
           });
@@ -215,7 +243,7 @@ export async function lookupUpdates(
       // TODO #22198
       currentVersion ??=
         getCurrentVersion(
-          config.currentValue!,
+          compareValue!,
           config.lockedVersion!,
           versioning,
           rangeStrategy!,
@@ -223,7 +251,7 @@ export async function lookupUpdates(
           nonDeprecatedVersions
         ) ??
         getCurrentVersion(
-          config.currentValue!,
+          compareValue!,
           config.lockedVersion!,
           versioning,
           rangeStrategy!,
@@ -236,17 +264,17 @@ export async function lookupUpdates(
       }
       res.currentVersion = currentVersion!;
       if (
-        config.currentValue &&
+        compareValue &&
         currentVersion &&
         rangeStrategy === 'pin' &&
-        !versioning.isSingleVersion(config.currentValue)
+        !versioning.isSingleVersion(compareValue)
       ) {
         res.updates.push({
           updateType: 'pin',
           isPin: true,
           // TODO: newValue can be null! (#22198)
           newValue: versioning.getNewValue({
-            currentValue: config.currentValue,
+            currentValue: compareValue,
             rangeStrategy,
             currentVersion,
             newVersion: currentVersion,
@@ -277,8 +305,7 @@ export async function lookupUpdates(
       ).filter(
         (v) =>
           // Leave only compatible versions
-          unconstrainedValue ||
-          versioning.isCompatible(v.version, config.currentValue)
+          unconstrainedValue || versioning.isCompatible(v.version, compareValue)
       );
       if (config.isVulnerabilityAlert && !config.osvVulnerabilityAlerts) {
         filteredReleases = filteredReleases.slice(0, 1);
@@ -335,7 +362,7 @@ export async function lookupUpdates(
         if (pendingReleases!.length) {
           update.pendingVersions = pendingReleases!.map((r) => r.version);
         }
-        if (!update.newValue || update.newValue === config.currentValue) {
+        if (!update.newValue || update.newValue === compareValue) {
           if (!config.lockedVersion) {
             continue;
           }
@@ -360,9 +387,9 @@ export async function lookupUpdates(
 
         res.updates.push(update);
       }
-    } else if (config.currentValue) {
+    } else if (compareValue) {
       logger.debug(
-        `Dependency ${config.packageName} has unsupported/unversioned value ${config.currentValue} (versioning=${config.versioning})`
+        `Dependency ${config.packageName} has unsupported/unversioned value ${compareValue} (versioning=${config.versioning})`
       );
 
       if (!config.pinDigests && !config.currentDigest) {
@@ -382,11 +409,8 @@ export async function lookupUpdates(
     if (config.lockedVersion) {
       res.currentVersion = config.lockedVersion;
       res.fixedVersion = config.lockedVersion;
-    } else if (
-      config.currentValue &&
-      versioning.isSingleVersion(config.currentValue)
-    ) {
-      res.fixedVersion = config.currentValue.replace(regEx(/^=+/), '');
+    } else if (compareValue && versioning.isSingleVersion(compareValue)) {
+      res.fixedVersion = compareValue.replace(regEx(/^=+/), '');
     }
     // Add digests if necessary
     if (supportsDigests(config.datasource)) {
@@ -423,6 +447,23 @@ export async function lookupUpdates(
         config.registryUrls = [res.registryUrl];
       }
 
+      // massage versionCompatibility
+      if (
+        is.string(config.currentValue) &&
+        is.string(compareValue) &&
+        is.string(config.versionCompatibility)
+      ) {
+        for (const update of res.updates) {
+          logger.debug({ update });
+          if (is.string(config.currentValue)) {
+            update.newValue = config.currentValue.replace(
+              compareValue,
+              update.newValue
+            );
+          }
+        }
+      }
+
       // update digest for all
       for (const update of res.updates) {
         if (config.pinDigests === true || config.currentDigest) {