diff --git a/README.md b/README.md
index 78e22c7695568c52e43d317ff4522069b1e2d8e4..8b8e58ae8b96a91377bbc110b8f9003089495bfd 100644
--- a/README.md
+++ b/README.md
@@ -1,74 +1,100 @@
 # GCP Cloud Run manager terraform module
 
-This module manages a Cloud Run-hosted container. It takes care of making
-sure the container is connected to a Cloud SQL instance and sets
-environment variables on the application.
+This module manages a Cloud Run-hosted container. It takes care of making sure the container is
+connected to a Cloud SQL instance and sets environment variables on the application.
 
-Specify the project to deploy into on the command line. So, for
-example, to deploy to the project ``my-project``:
+Specify the project to deploy into on the command line. So, for example, to deploy to the project
+`my-project`:
 
 ```console
 $ terraform init
 $ terraform apply -var project=my-project
 ```
 
-In this example, terraform is configured to use default application credentials.
-For Google APIs and these credentials should correspond to a user with owner or
-editor access to the target project. You can use the ``gcloud`` command line
-tool to set your personal credentials as application default credentials. See
-the ``gcloud auth application-default`` command output for more information.
+In this example, terraform is configured to use default application credentials. For Google APIs and
+these credentials should correspond to a user with owner or editor access to the target project. You
+can use the `gcloud` command line tool to set your personal credentials as application default
+credentials. See the `gcloud auth application-default` command output for more information.
 
 ## Versioning
 
-The `master` branch contains the tip of development and corresponds to the `v4`
-branch. The `v1`, `v2` and `v3` branches will maintain source compatibility with
-the initial release.
+The `master` branch contains the tip of development and corresponds to the `v8` branch. The `v1`,
+`v2`, `v3` etc. branches will maintain source compatibility with the initial release.
 
 ## Ingress style
-There are two supported ingress styles depending on `var.ingress_style` variable.
-
-`var.ingress_style` can be: 
 
-- `domain-mapping` (default): passing DNS domains as `var.dns_names` or `var.dns_name`,
-which takes precedence over `var.dns_names`, will create domain mappings to the Cloud
-Run service. Before setting this, you *must* have verified ownership of the provided
-domains with Google. [Instructions on how to do
-this](https://guidebook.devops.uis.cam.ac.uk/en/latest/notes/google-domain-verification/)
-can be found in the DevOps division guidebook.
+There are two supported ingress styles depending on `var.ingress_style` variable.
 
-- `load-balancer`: a load balancer will be configured instead of a domain mapping. The
-DNS domains in `var.dns_names` or `var.dns_name`, which takes precedence over `var.dns_names`,
-will get Google-managed or custom TLS certificates depending on `var.use_ssl_certificates`
-and `var.ssl_certificates`. An IPv6 address can also be allocated to the load balancer if
-`var.enable_ipv6` is `true`.
+`var.ingress_style` can be:
+
+- `domain-mapping` (default): passing DNS domains as `var.dns_names` or `var.dns_name`, which takes
+  precedence over `var.dns_names`, will create domain mappings to the Cloud Run service. Before
+  setting this, you _must_ have verified ownership of the provided domains with Google. [Instructions
+  on how to do
+  this](https://guidebook.devops.uis.cam.ac.uk/en/latest/notes/google-domain-verification/) can be
+  found in the DevOps division guidebook.
+
+- `load-balancer`: a load balancer will be configured instead of a domain mapping. The DNS domains
+  in `var.dns_names` or `var.dns_name`, which takes precedence over `var.dns_names`, will get
+  Google-managed or custom TLS certificates depending on `var.use_ssl_certificates` and
+  `var.ssl_certificates`. An IPv6 address can also be allocated to the load balancer if
+  `var.enable_ipv6` is `true`.
+
+## Pre-deploy Cloud Run job
+
+The `v8` release introduced an `enable_pre_deploy_job` variable. When set to `true` a [Cloud
+Run job](https://cloud.google.com/run/docs/create-jobs) is created to execute a configurable command
+_before_ the main Cloud Run service is deployed. The initial use case for this is to run database
+migrations, however in the future we're sure there'll be more.
+
+The pre-deploy job uses the image specified in `var.pre_deploy_job_image_name` if set, otherwise it
+falls back to the same `var.image_name` that the main service uses. The command and arguments that
+the job executes are configurable via the `pre_deploy_job_command` and `pre_deploy_job_args`
+variables.
+
+A `null_resource` is also configured to execute the pre-deploy job whenever it detects that the
+value of `var.image_name` has changed (or at every `apply` if `var.force_pre_deploy_job` is set to
+`true`). This uses the `gcloud run jobs execute` command and is run in the context of the
+`terraform-deploy` service account via an access token. Using `null_resource` is never ideal.
+However, in this situation it provides a very handy way to trigger this simple job so it has been
+accepted.
+
+To ensure that the pre-deploy job always runs _before_ a new revision of the Cloud Run webapp
+service is deployed, the resources in question are explicitly configured with `depends_on`
+relationships, as follows.
+
+1. The `google_cloud_run_v2_job.pre_deploy` Cloud Run job has no `depends_on` relationships defined
+   and is therefore deployed first.
+2. The `null_resource.pre_deploy_job_trigger` resource depends on
+   `google_cloud_run_v2_job.pre_deploy` and therefore won't be deployed until the Cloud Run job is
+   deployed successfully.
+3. Finally, the `google_cloud_run_service.webapp` Cloud Run service depends on
+   `null_resource.pre_deploy_job_trigger`, meaning it is only deployed once the
+   `null_resource.pre_deploy_job_trigger` has executed successfully.
 
 ## Monitoring and Alerting
 
-If the variable [alerting_email_address](variables.tf) is set, the module adds
-basic uptime *alerting* via email for failing http polling.
+If the variable [alerting_email_address](variables.tf) is set, the module adds basic uptime
+_alerting_ via email for failing http polling.
 
-If the variable [disable_monitoring](variables.tf) is true, the module will
-disable *monitoring*. This is different from disabling alerting; if no
-alerting email addresses are provided, the uptime checks will still be
-configured, there just won't be any alerts sent if they fail. Disabling
-monitoring will also disable alerting as without any monitoring there is nothing
-to alert(!)
+If the variable [disable_monitoring](variables.tf) is true, the module will disable _monitoring_.
+This is different from disabling alerting; if no alerting email addresses are provided, the uptime
+checks will still be configured, there just won't be any alerts sent if they fail. Disabling
+monitoring will also disable alerting as without any monitoring there is nothing to alert(!)
 
 See [variables.tf](variables.tf) for how to configure alerting and monitoring.
 
-Note that the project containing resources to be monitored must be in a
-Stackdriver monitoring workspace and this must be configured manually. At the
-time of writing there is no terraform support for this. This module will error
-when applying if this is not so.
+Note that the project containing resources to be monitored must be in a Stackdriver monitoring
+workspace and this must be configured manually. At the time of writing there is no terraform support
+for this. This module will error when applying if this is not so.
 
-Stackdriver distinguishes between workspaces and projects within those
-workspaces. Each workspace must have a host project and that project *must* be
-the default project of the `google.stackdriver` provider used by this module.
-The `google.stackdriver` must be configured with credentials allowing monitoring
-resources to be created in the *host* project.
+Stackdriver distinguishes between workspaces and projects within those workspaces. Each workspace
+must have a host project and that project _must_ be the default project of the `google.stackdriver`
+provider used by this module. The `google.stackdriver` must be configured with credentials allowing
+monitoring resources to be created in the _host_ project.
 
-If the workspace host project differs from the project which contains the
-resources to be monitored, you can use a provider alias:
+If the workspace host project differs from the project which contains the resources to be monitored,
+you can use a provider alias:
 
 ```tf
 provider "google" {
@@ -96,12 +122,11 @@ module "cloud_run_service" {
 ### Monitoring instances which require service account authentication
 
 If `allow_unauthenticated_invocations` is not true, a Cloud Function will be created which
-authenticates via a service account, allowing the StackDriver monitoring to call the Cloud
-Function, with the Cloud Function authentication and proxying the request to the Cloud Run
-instance.
+authenticates via a service account, allowing the StackDriver monitoring to call the Cloud Function,
+with the Cloud Function authentication and proxying the request to the Cloud Run instance.
 
-Because this requires a Cloud Function to be created, the `cloudfunctions.googleapis.com`
-service should be enabled on the project that houses the Cloud Run instance.
+Because this requires a Cloud Function to be created, the `cloudfunctions.googleapis.com` service
+should be enabled on the project that houses the Cloud Run instance.
 
 ## Static Egress IP
 
@@ -112,26 +137,25 @@ from the cloud run through a static ip.
 **Important!**
 
 The static ip is configured with `prevent_destroy = true`, meaning that it cannot be destroyed
-without removing it from terraform state using `terraform state rm` and then manually destroying
-the resource within the GCP console. This is to prevent accidental destruction of an IP which
-is likely to be whitelisted within firewall configuration that lives outside of our deployments.
+without removing it from terraform state using `terraform state rm` and then manually destroying the
+resource within the GCP console. This is to prevent accidental destruction of an IP which is likely
+to be whitelisted within firewall configuration that lives outside of our deployments.
 
 ## Secrets as Volumes and Env Vars
 
-Secret Manager secrets can be as environment variables or volume mounts (files) in the 
-running container.
+Secret Manager secrets can be as environment variables or volume mounts (files) in the running
+container.
 
 The service account that Cloud Run runs as needs access to the secrets for this feature to work.
 Thus, this module gives `secretAccessor` role to that service account for the secrets passed on
 `secrets_volume` and `secrets_envars`.
 
-Any number of items in the list is supported and not defining these variables
-when calling this module is acceptable. The path of the mounted file will be
-based on `path/name`.
+Any number of items in the list is supported and not defining these variables when calling this
+module is acceptable. The path of the mounted file will be based on `path/name`.
 
-For the example configuration below the files will be `/secrets-1/foobarfile1`
-and `/secrets-2/foobarfile2`. A common `path` for multiple secrets is not supported, they must
-be unique.
+For the example configuration below the files will be `/secrets-1/foobarfile1` and
+`/secrets-2/foobarfile2`. A common `path` for multiple secrets is not supported, they must be
+unique.
 
 > Note: `name` should only have alphanumeric characters, hyphens and underscores.
 
diff --git a/locals.tf b/locals.tf
index a98d3da264404e2feed05b1f1315f2061580cdb9..bdaba8fad803a6adcc279bd377b7580c3519a381 100644
--- a/locals.tf
+++ b/locals.tf
@@ -34,6 +34,8 @@ locals {
     ]
   ))
 
+  pre_deploy_job_image_name = var.pre_deploy_job_image_name == null ? var.image_name : var.pre_deploy_job_image_name
+
   # Certain ingress styles imply that we disallow external access to the base Cloud Run service.
   webapp_allowed_ingress = lookup({
     load-balancer = "internal-and-cloud-load-balancing"
diff --git a/main.tf b/main.tf
index d68624d4d10a5e45f5c9904628cbb424d80999d5..a81814536465ccea1983430d62a4451d33254e9e 100644
--- a/main.tf
+++ b/main.tf
@@ -1,5 +1,8 @@
 # main.tf configures top-level resources
 
+# This data source pulls the configuration for the default google provider.
+data "google_client_config" "current" {}
+
 # A service account which the webapp runs in the context of.
 resource "google_service_account" "webapp" {
   project      = var.project
@@ -199,7 +202,9 @@ resource "google_cloud_run_service" "webapp" {
 
   depends_on = [
     google_secret_manager_secret_iam_member.secrets_access,
+    null_resource.pre_deploy_job_trigger
   ]
+
   # Google Beta provider is required for mounting secrets AToW
   provider = google-beta
 }
@@ -283,3 +288,119 @@ data "google_cloud_run_service" "webapp" {
   name     = var.name
   location = var.cloud_run_region
 }
+
+# Configure a Cloud Run Job which will be executed before the deployment of the google_cloud_run_service.webapp
+# resource. This is primarily useful to run database migrations, however other use cases may exist.
+resource "google_cloud_run_v2_job" "pre_deploy" {
+  count = var.enable_pre_deploy_job ? 1 : 0
+
+  name     = "${var.name}-pre-deploy"
+  location = var.cloud_run_region
+  project  = var.project
+
+  template {
+    template {
+      service_account = google_service_account.webapp.email
+
+      dynamic "volumes" {
+        for_each = var.sql_instance_connection_name != "" ? [1] : []
+
+        content {
+          name = "cloudsql"
+          cloud_sql_instance {
+            instances = [var.sql_instance_connection_name]
+          }
+        }
+      }
+      dynamic "volumes" {
+        for_each = var.secrets_volume
+
+        content {
+          name = volumes.value["name"]
+          secret {
+            secret = volumes.value["id"]
+            items {
+              version = coalesce(volumes.value["version"], "latest")
+              path    = volumes.value["name"]
+              mode    = 0
+            }
+          }
+        }
+      }
+
+      containers {
+        image   = local.pre_deploy_job_image_name
+        command = var.pre_deploy_job_command
+        args    = var.pre_deploy_job_args
+
+        dynamic "env" {
+          for_each = var.environment_variables
+
+          content {
+            name  = env.key
+            value = env.value
+          }
+        }
+        dynamic "env" {
+          for_each = var.secrets_envars
+
+          content {
+            name = env.value["name"]
+            value_source {
+              secret_key_ref {
+                secret  = env.value["id"]
+                version = coalesce(env.value["version"], "latest")
+              }
+            }
+          }
+        }
+
+        dynamic "volume_mounts" {
+          for_each = var.sql_instance_connection_name != "" ? [1] : []
+
+          content {
+            name       = "cloudsql"
+            mount_path = "/cloudsql"
+          }
+        }
+        dynamic "volume_mounts" {
+          for_each = var.secrets_volume
+
+          content {
+            name       = volume_mounts.value["name"]
+            mount_path = volume_mounts.value["path"]
+          }
+        }
+      }
+    }
+  }
+}
+
+# Trigger the pre-deploy job using the gcloud CLI whenever the var.image_name value changes.
+resource "null_resource" "pre_deploy_job_trigger" {
+  count = var.enable_pre_deploy_job && var.trigger_pre_deploy_job ? 1 : 0
+
+  triggers = merge({
+    image_name = var.image_name
+    }, var.force_pre_deploy_job ? {
+    timestamp = timestamp()
+  } : {})
+
+  provisioner "local-exec" {
+    command = <<EOI
+gcloud --project ${var.project} run jobs execute \
+  --region ${var.cloud_run_region} --wait ${google_cloud_run_v2_job.pre_deploy[0].name}
+EOI
+
+    environment = {
+      # This environment variable tells gcloud CLI to authenticate using an access token. We're using the access token
+      # configured in the default google provider via the google_client_config data source.
+      CLOUDSDK_AUTH_ACCESS_TOKEN = data.google_client_config.current.access_token
+    }
+  }
+
+  depends_on = [
+    google_cloud_run_v2_job.pre_deploy
+  ]
+}
+
diff --git a/variables.tf b/variables.tf
index e4cc43ce66353c0e4c9d5d3b1dd7315f3eb1a74b..eb2bc0441b7c11b1801a3340e787c5064698454c 100644
--- a/variables.tf
+++ b/variables.tf
@@ -19,6 +19,57 @@ variable "sql_instance_connection_name" {
   default     = ""
 }
 
+variable "enable_pre_deploy_job" {
+  description = <<EOI
+Configure a Cloud Run Job to be executed *before* the main Cloud Run service is deployed. This is useful for running
+database migrations among other things.
+EOI
+  type        = bool
+  default     = false
+}
+
+variable "trigger_pre_deploy_job" {
+  description = <<EOI
+When true, the pre-deploy Cloud Run job is executed via a null_resource-triggered gcloud command whenever Terraform
+detects that var.image_name has changed.
+EOI
+  type        = bool
+  default     = true
+}
+
+variable "force_pre_deploy_job" {
+  description = <<EOI
+When true, and only when used in addition to var.trigger_pre_deploy_job, the pre-deploy Cloud Run job is executed at
+every terraform apply, regardless of the status of var.image_name. This is sometimes useful for development
+environments where the "latest" tag is deployed, as without this the pre-deploy command would never run. For staging
+and production environments this should never be required as the var.image_name should change with each
+release/deployment of an application.
+EOI
+  type        = bool
+  default     = false
+}
+
+variable "pre_deploy_job_image_name" {
+  description = <<EOI
+Specify the URL of a container image to use for the pre-deploy Cloud Run job. By default the var.image_name URL is used
+(see locals.tf).
+EOI
+  type        = string
+  default     = null
+}
+
+variable "pre_deploy_job_command" {
+  description = "The command to run in the pre-deploy Cloud Run job."
+  type        = list(string)
+  default     = null
+}
+
+variable "pre_deploy_job_args" {
+  description = "Arguments supplied to the command in the pre-deploy Cloud Run job."
+  type        = list(string)
+  default     = null
+}
+
 variable "grant_sql_client_role_to_webapp_sa" {
   description = <<EOI
     When set to true the 'roles/cloudsql.client' role will be granted to the