{"id":317,"date":"2026-03-13T11:50:35","date_gmt":"2026-03-13T11:50:35","guid":{"rendered":"https:\/\/blog.ngocha.biz\/?p=317"},"modified":"2026-03-13T11:50:35","modified_gmt":"2026-03-13T11:50:35","slug":"docker-image-build-promotion-piepeline","status":"publish","type":"post","link":"https:\/\/blog.ngocha.biz\/?p=317","title":{"rendered":"Docker Image Build &#038; Promotion Piepeline with GitHub Actions"},"content":{"rendered":"<p>In real <a href=\"https:\/\/devopscube.com\/devops-projects\/\" rel=\"noreferrer\">DevOps projects<\/a>, you do not just build an image and push it to a registry. The image needs to pass through several quality and security gates first.<\/p>\n<p>In this hands-on guide, I will walk you through a <strong>complete, production-grade Docker image build and promotion pipeline<\/strong> using GitHub Actions just like how it is done in real enterprise environments.<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\">If you want first look at the entire CI\/CD design for docker build, please refer the <a href=\"https:\/\/devopscube.com\/docker-image-build-and-promotion-pipeline\/\" rel=\"noreferrer\">Production Guide for Docker Image Build and Promotion Pipeline<\/a> where we explained the key production considerations and workflows.<\/div>\n<\/div>\n<p>Here is what is covered in this blog.<\/p>\n<ul>\n<li>Automating a Java application build and Dockerization with GitHub Actions<\/li>\n<li>Managing container registry credentials securely with GitHub Secrets<\/li>\n<li>Promoting images through dev, stage and prod registries<\/li>\n<li>Signing the final container image using Cosign<\/li>\n<li>Build caching, image tagging strategy, registry architecture, and more..<\/li>\n<\/ul>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\">We have used a Java application as an example. This workflow applies to any application. Just the application build steps would change. The rest of the production process remains the same.<\/div>\n<\/div>\n<h2 id=\"setup-prerequisites\">Setup Prerequisites<\/h2>\n<p>The following are the prerequisites for this guide. <\/p>\n<ul>\n<li>GitHub Repository<\/li>\n<li>Three Container Repositories ( Dev, Stage and Prod) &#8211; <a href=\"https:\/\/devopscube.com\/docker-tutorial-getting-started-with-docker-swarm\/\" rel=\"noreferrer\">Docker<\/a> Hub is used in this guide<\/li>\n<li>Basic knowledge of how GitHub Actions works<\/li>\n<li>Cosign <a href=\"https:\/\/docs.sigstore.dev\/cosign\/system_config\/installation\/?ref=devopscube.com\" rel=\"noreferrer\">installed<\/a> in your local system to create keys.<\/li>\n<\/ul>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\">This workflow does not depend on any cloud platform. Anyone with access to GitHub and Docker Hub can follow the entire setup.<\/div>\n<\/div>\n<h2 id=\"image-promotion-workflow\">Image Promotion Workflow<\/h2>\n<p>Given below is complete workflow of the image promotion workflow we are going to do.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-promotion6-1.png\" class=\"kg-image\" alt=\"Docker Image build and Promotion Workflow\" loading=\"lazy\" width=\"2000\" height=\"917\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-promotion6-1.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-promotion6-1.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2026\/03\/image-promotion6-1.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w2400\/2026\/03\/image-promotion6-1.png 2400w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>We won&#8217;t be deploying in this guide, just to show what happens in each environment, deployment is also shown in the image.<\/p>\n<p>In our project, we used three image repositories dev, stage, and prod. But the number of repositories may vary based on the projectm, there will be more repositories for each environment.<\/p>\n<p>And if you have multiple micro services, you have to create registries for each service for each environment.<\/p>\n<p>An example structure is given below.<\/p>\n<pre><code class=\"language-bash\">registry.company.com\/\n\u251c\u2500\u2500 dev\/\n\u251c\u2500\u2500 stage\/\n\u251c\u2500\u2500 release\/\n\u2514\u2500\u2500 prod\/<\/code><\/pre>\n<h2 id=\"project-directory-structure\">Project Directory Structure<\/h2>\n<p>I have pushed all the necessary files for this guide to our&nbsp;<a href=\"https:\/\/github.com\/techiescamp\/docker-image-pipeline.git?ref=devopscube.com\" rel=\"noreferrer\">GitHub repository<\/a>, including a simple Spring Boot Java application code.<\/p>\n<p>Fork and clone the repository to your system.<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\u26a0\ufe0f<\/div>\n<div class=\"kg-callout-text\">While cloning to local, use your forked repository URL in the below clone command.<\/div>\n<\/div>\n<p>Use the following command to clone it to your system.<\/p>\n<pre><code class=\"language-bash\">git clone https:\/\/github.com\/techiescamp\/docker-image-pipeline.git<\/code><\/pre>\n<p>Then move into the <code>docker-image-pipeline<\/code> folder.<\/p>\n<pre><code class=\"language-bash\">cd docker-image-pipeline<\/code><\/pre>\n<p>You will see the following directory structure.<\/p>\n<pre><code class=\"language-bash\">.\n\u251c\u2500\u2500 .github\n\u2502   \u2514\u2500\u2500 workflows\n\u2502        \u251c\u2500\u2500 pr-checks.yaml\n\u2502        \u251c\u2500\u2500 image-build-pipeline.yaml\n\u2502        \u251c\u2500\u2500 promote-to-stage.yaml\n\u2502        \u2514\u2500\u2500 promote-to-prod.yaml\n\u251c\u2500\u2500 java-app\n\u2502    \u2514\u2500\u2500 &lt;pom and other application files&gt;\n\u251c\u2500\u2500 Dockerfile\n\u251c\u2500\u2500 README.md\n\u2514\u2500\u2500 tests\n    \u2514\u2500\u2500 container-structure-test.yaml\n<\/code><\/pre>\n<p>You can see four workflow files inside the directory <code>.github\/workflows<\/code> and also, you will see 4 branches:<\/p>\n<ul>\n<li>Main<\/li>\n<li>Release-&lt;commit-id&gt;<\/li>\n<li>Develop<\/li>\n<li>Feature (cr-161)<\/li>\n<\/ul>\n<p>During the hands-on section, we will start from the <code>Feature<\/code> branch.<\/p>\n<p>In the next sections, we will look into the Dockerfile.<\/p>\n<h2 id=\"dockerfile\">Dockerfile<\/h2>\n<p>Below is the Dockerfile, which we will be using to build and Dockerize the Java application.<\/p>\n<pre><code class=\"language-dockerfile\">FROM maven:3.9-eclipse-temurin-21 AS builder\n\nWORKDIR \/build\n\nCOPY java-app\/pom.xml .\nRUN mvn -B -e dependency:go-offline\n\nCOPY java-app\/src .\/src\nRUN mvn -B clean package -DskipTests\n\nFROM gcr.io\/distroless\/java21-debian12\n\nWORKDIR \/app\n\nCOPY --from=builder \/build\/target\/*.jar app.jar\n\nEXPOSE 8080\n\nCMD [\"app.jar\"]<\/code><\/pre>\n<p>This is a multi-stage Dockerfile, which builds the application in the first stage and keeps only the required files to run the application in the final image.<\/p>\n<p>The Dockerfile uses the <code>maven:3.9-eclipse-temurin-21<\/code> image as the base image for the build stage, which contains <a href=\"https:\/\/devopscube.com\/install-maven-guide\/\" rel=\"noreferrer\">Java<\/a> and <a href=\"https:\/\/devopscube.com\/build-java-application-using-maven\/\" rel=\"noreferrer\">Maven<\/a> to build the application.<\/p>\n<p>First, it sets a working directory <code>\/build<\/code>, and then it copies the source code and uses Maven to build the application Jar file.<\/p>\n<p>And in the second and final stage, we use the <code>gcr.io\/distroless\/java21-debian12<\/code> image as the base image, and it will be the base image of the final Docker image.<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\">A distroless image helps you to create a minimalistic Docker image and removes the shell and unwanted packages, which is best for production.<\/p>\n<p>But be cautious, if you are using a distroless image, you cannot enter the container for debugging.<\/p><\/div>\n<\/div>\n<p>Then it copies the Jar file from the previous build location to the working directory <code>\/app<\/code> and exposes the port <code>8080<\/code>.<\/p>\n<p>This distroless image already has an entrypoint that runs&nbsp;<code>java -jar<\/code>, so we just need to give the file name&nbsp;<code>app.jar<\/code>&nbsp;in CMD.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-promotion7.png\" class=\"kg-image\" alt=\"multi stage docker image build\" loading=\"lazy\" width=\"1777\" height=\"2949\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-promotion7.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-promotion7.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2026\/03\/image-promotion7.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-promotion7.png 1777w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\">We used a simple Dockerfile for this example, but in the actual environment, the Dockerfile will be built with all the&nbsp;<a href=\"https:\/\/devopscube.com\/build-docker-image\" rel=\"noreferrer\">best practices<\/a>&nbsp;to make the container secure.<\/div>\n<\/div>\n<p>In the next section, we will see about creating GitHub secrets with DockerHub credentials and Cosign keys.<\/p>\n<h2 id=\"create-github-secrets\">Create GitHub Secrets<\/h2>\n<p>Before triggering the workflow, you need to save two things as GitHub Secrets:<\/p>\n<ul>\n<li>DockerHub credentials<\/li>\n<li>Cosign private key and password<\/li>\n<\/ul>\n<p>To create GitHub Secrets, go to&nbsp;<code>Settings -&gt; Secrets and Variables -&gt; Actions<\/code>&nbsp;and click the&nbsp;<code>New repository secret<\/code>&nbsp;as shown below.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/09\/image-137.png\" class=\"kg-image\" alt=\"creating github secrets\" loading=\"lazy\" width=\"711\" height=\"447\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/09\/image-137.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/09\/image-137.png 711w\"><\/figure>\n<h3 id=\"save-docker-credentials\">Save Docker Credentials<\/h3>\n<p>Create two secrets to save Docker credentials, which will be used in the image build workflow.<\/p>\n<p>Create secrets as <code>DOCKERHUB_USERNAME<\/code> for username and <code>DOCKERHUB_TOKEN<\/code> for password.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/01\/image-85.png\" class=\"kg-image\" alt=\"creating github secrets\" loading=\"lazy\" width=\"597\" height=\"309\"><\/figure>\n<p>This will be used in the workflows to sign into the registry.<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\u26a0\ufe0f<\/div>\n<div class=\"kg-callout-text\">In an enterprise setting, static secrets (usernames\/passwords) are a major security risk. Enterprises prefer solutions like&nbsp;<a href=\"https:\/\/devopscube.com\/github-actions-oidc-aws\/\" rel=\"noreferrer\"><b><strong style=\"white-space: pre-wrap;\">OpenID Connect (OIDC)<\/strong><\/b><\/a> for authentication&nbsp;to eliminate long-lived secrets.<\/div>\n<\/div>\n<h3 id=\"save-cosign-private-key-and-password\">Save Cosign Private Key and Password<\/h3>\n<p>First, create a Cosign Private key from your local, use the following command.<\/p>\n<pre><code>cosign generate-key-pair\n<\/code><\/pre>\n<p>It will ask you to give a password for the private key, which is required to use the private key during the signing process.<\/p>\n<p>Once you specify the password, it creates two key files&nbsp;<code>cosign.key<\/code>&nbsp;and&nbsp;<code>cosign.pub<\/code>.<\/p>\n<p>Copy the content of <code>cosign.key<\/code> and save it as <code>COSIGN_PRIVATE_KEY<\/code> and save the password as <code>COSIGN_PASSWORD<\/code>.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/11\/image-122.png\" class=\"kg-image\" alt=\"created cosign secrets in github secrets\" loading=\"lazy\" width=\"422\" height=\"304\"><\/figure>\n<p>Now that the credentials, key, and password are saved, let&#8217;s trigger the workflows in the next section.<\/p>\n<h2 id=\"how-to-build-and-promote-a-docker-image-using-github-actions\">How to build and promote a Docker Image using GitHub Actions?<\/h2>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\">In the workflows, we are using GitHub Actions hosted runner ubuntu-latest to run the jobs, but for the actual production setup, you have to set up a <a href=\"https:\/\/devopscube.com\/github-actions-runner-aws-eks\/\" rel=\"noreferrer\">self-hosted runner<\/a> for the jobs. <\/div>\n<\/div>\n<p>We will be using four types of workflow files for this build and promotion workflow, the files are:<\/p>\n<ul>\n<li>pr-checks.yaml<\/li>\n<li>image-build-pipeline.yaml<\/li>\n<li>promote-to-stage.yaml<\/li>\n<li>promote-to-prod.yaml<\/li>\n<\/ul>\n<p>Let&#8217;s start triggering the workflows one by one.<\/p>\n<h3 id=\"trigger-pr-workflow\">Trigger PR Workflow<\/h3>\n<p>The PR workflow is <code>pr-checks.yaml<\/code> gets triggered automatically when you raise a PR to the Develop branch.<\/p>\n<blockquote><p>To trigger the workflow, modify the container repository URL with your repository URL in the <code>image-build-pipeline.yaml<\/code> and <code>promote-to-prod.yaml<\/code> workflow files in the Feature(cr-161) branch and raise a PR to the Develop branch.<\/p><\/blockquote>\n<p>Since my workflow file has the correct URL, I will just update the <code>README<\/code> file and raise the PR.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/11\/image-107.png\" class=\"kg-image\" alt=\"create pull reqyest to develop branch\" loading=\"lazy\" width=\"925\" height=\"583\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/11\/image-107.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/11\/image-107.png 925w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Once the PR is raised to Develop, go to Actions, and you can see a new job is triggered.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/11\/image-108.png\" class=\"kg-image\" alt=\"pr workflow gets triggered\" loading=\"lazy\" width=\"740\" height=\"344\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/11\/image-108.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/11\/image-108.png 740w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>If the job is successful, you will see the status of the job as shown below.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/11\/image-109.png\" class=\"kg-image\" alt=\"pr build completed successfully\" loading=\"lazy\" width=\"990\" height=\"523\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2025\/11\/image-109.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/11\/image-109.png 990w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>If you click the job, you can see the steps run in the job and its details.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-39.png\" class=\"kg-image\" alt=\"step runned in pr build\" loading=\"lazy\" width=\"1502\" height=\"1432\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-39.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-39.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-39.png 1502w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Once the PR workflow run has been completed, you can get the SBOM report from the workflow link.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/01\/image-66.png\" class=\"kg-image\" alt=\"Downloading the SBOM report\" loading=\"lazy\" width=\"777\" height=\"361\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/01\/image-66.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/01\/image-66.png 777w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Click the link to download the SBOM report.<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\">In this workflow, the SBOM report is uploaded to the GitHub Actions artifact. But in an actual production setup, the SBOM report will be saved in artifact storage like <a href=\"https:\/\/devopscube.com\/setup-argocd-image-updater\/\" rel=\"noreferrer\">AWS ECR<\/a>, JFrog, Harbor, etc.<\/div>\n<\/div>\n<p>Let&#8217;s move on to the image-building and pushing stage.<\/p>\n<h3 id=\"trigger-image-build-workflow\">Trigger Image Build Workflow<\/h3>\n<p>The next stage is to trigger the image-building workflow <code>image-build-pipeline.yaml<\/code>.<\/p>\n<p>This will be triggered automatically, when the PR is merged to the Develop branch.<\/p>\n<p>First, go to the Pull Request menu,<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/11\/image-111.png\" class=\"kg-image\" alt=\"Pull request in develop branch\" loading=\"lazy\" width=\"588\" height=\"397\"><\/figure>\n<p>And, merge the PR.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2025\/11\/image-112.png\" class=\"kg-image\" alt=\"Merge pull request\" loading=\"lazy\" width=\"564\" height=\"264\"><\/figure>\n<p>Refresh the page, and you will see a new job is triggered.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-38.png\" class=\"kg-image\" alt=\"build triggered from merging to develop branch\" loading=\"lazy\" width=\"1764\" height=\"892\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-38.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-38.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2026\/03\/image-38.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-38.png 1764w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Select the job, and you will see the steps run in this job.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-41.png\" class=\"kg-image\" alt=\"steps in image build and push workflow\" loading=\"lazy\" width=\"2000\" height=\"1153\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-41.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-41.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2026\/03\/image-41.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-41.png 2140w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<blockquote><p>Make sure to save the SHA ID of the image, it is required to trigger the next workflow.<\/p><\/blockquote>\n<p>Now, check your dev container registry, you will see two image tags as shown below.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-40.png\" class=\"kg-image\" alt=\"dev image registry\" loading=\"lazy\" width=\"1496\" height=\"1020\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-40.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-40.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-40.png 1496w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Now, the image has been built and pushed to the stage repository, let&#8217;s move on to the image promotion stage to stage.<\/p>\n<h3 id=\"trigger-image-promotion-workflow-to-stage\">Trigger Image Promotion Workflow to Stage<\/h3>\n<p>The image promotion workflow <code>promote-to-stage.yaml<\/code> will not be triggered automatically,first, we need to create a release branch with the commit ID for the previous build.<\/p>\n<p>For example, my previous commit ID is <code>0165857<\/code>, the my release branch name will be <code>release\/v1.0.0-sha-0165857<\/code>.<\/p>\n<p>The image promotion workflow <code>promote-to-stage.yaml<\/code> will not be triggered automatically, we need to push the changes to the main branch and trigger it manually.<\/p>\n<p>Create a new branch from the develop branch, give the name, and press create.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-44.png\" class=\"kg-image\" alt=\"create a release brach from develop\" loading=\"lazy\" width=\"1202\" height=\"642\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-44.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-44.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-44.png 1202w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Then, go to the Actions menu, select the promotion to stage workflow file, and click the Run workflow toggle key.<\/p>\n<p>It will ask for the image&#8217;s SHA ID, which you want to promote. Once you enter the ID, click the Run workflow button to start the job.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-45.png\" class=\"kg-image\" alt=\"\" loading=\"lazy\" width=\"1928\" height=\"1268\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-45.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-45.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2026\/03\/image-45.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-45.png 1928w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>This will trigger the promotion workflow for stage.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-46.png\" class=\"kg-image\" alt=\"promotion workflow for stage.\" loading=\"lazy\" width=\"1498\" height=\"948\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-46.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-46.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-46.png 1498w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Below are the steps runned by the stage promotion workflow.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-47.png\" class=\"kg-image\" alt=\"\" loading=\"lazy\" width=\"2000\" height=\"1417\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-47.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-47.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2026\/03\/image-47.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-47.png 2018w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Copy the SHA ID, it is required for the promotion to prod.<\/p>\n<p>Now, check your stage container registry, you will see two image tags as shown below.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-48.png\" class=\"kg-image\" alt=\"stagging image registry\" loading=\"lazy\" width=\"1420\" height=\"1026\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-48.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-48.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-48.png 1420w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Now, the image has been promoted to the stage repository, let&#8217;s move on to the image promotion stage to prod<\/p>\n<h3 id=\"trigger-image-promotion-workflow-to-prod\">Trigger Image Promotion Workflow to Prod<\/h3>\n<p>Same like the previous step, the image promotion to prod workflow <code>promote-to-prod.yaml<\/code> will not be triggered automatically, we need to push the changes to the main branch and trigger it manually.<\/p>\n<p>During the trigger, it will ask for the Docker image&#8217;s SHA ID, which we got from the previous job.<\/p>\n<p>First, push the code changes to the main branch from the release branch.<\/p>\n<p>Then, go to the Actions menu, select the promotion workflow file, and click the Run workflow toggle key, to select the branch and enter the SHA ID. <\/p>\n<p>It will ask for the image&#8217;s SHA ID, which you want to promote. Once you enter the ID, click the Run workflow button to start the job.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-49.png\" class=\"kg-image\" alt=\"trigger prod promotion pipeline manually\" loading=\"lazy\" width=\"1010\" height=\"629\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-49.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-49.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-49.png 1010w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Refresh the page and you can see a new job is triggered.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-52.png\" class=\"kg-image\" alt=\"prod promotion pipeline triggered\" loading=\"lazy\" width=\"1468\" height=\"896\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-52.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-52.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-52.png 1468w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Select the job, and you can see the steps run in it.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-53.png\" class=\"kg-image\" alt=\"promotion pipeline steps\" loading=\"lazy\" width=\"1364\" height=\"1414\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-53.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-53.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-53.png 1364w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Now, go to your prod repository, and you can see two images that are the same as in the staging repo, as shown below.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-54.png\" class=\"kg-image\" alt=\"prod image registry\" loading=\"lazy\" width=\"1324\" height=\"1000\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-54.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-54.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-54.png 1324w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>Now, you can verify if the images are signed using the public Cosign key in our local environment, use the following command.<\/p>\n<pre><code>cosign verify --key cosign.pub &lt;registry-name&gt;\/&lt;image-name&gt;:&lt;tag&gt;\n<\/code><\/pre>\n<p>This will verify if the image has not changed after signing.<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\">The image is continuously scanned for vulnerability even after it&#8217;s pushed to the production registry, it is scanned periodically using cron jobs for any new vulnerability.<\/p>\n<p>And, on the deployment side, especially in <a href=\"https:\/\/devopscube.com\/kubernetes-tutorials-beginners\/\" rel=\"noreferrer\">Kubernetes<\/a>, you can use tools like Kyverno to set policies to set certain standards, for example, block images without a valid signed key.<\/div>\n<\/div>\n<h2 id=\"build-caching\">Build Caching<\/h2>\n<p>Caching is the process of reusing stored files between builds to save build time.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-promotion3.png\" class=\"kg-image\" alt=\"github actions build caching\" loading=\"lazy\" width=\"1939\" height=\"2803\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-promotion3.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-promotion3.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2026\/03\/image-promotion3.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-promotion3.png 1939w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>In the above workflow files, we have configured caching for the image build step as shown below.<\/p>\n<pre><code class=\"language-yaml\">cache-from: type=gha,scope=java\ncache-to: type=gha,scope=java,mode=max<\/code><\/pre>\n<p>In our example, we are saving the cache in GitHub Actions itself using the <code>type=gha<\/code> argument.<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\">For production environemnt, configure cloud sorages like <a href=\"https:\/\/devopscube.com\/terraform-state-locking-with-s3\/\" rel=\"noreferrer\">AWS S3<\/a> for storing the caches of the build.<\/div>\n<\/div>\n<p>We use the cache saved during PR check-in in the image building workflow to save the build time.<\/p>\n<div class=\"kg-card kg-callout-card kg-callout-card-blue\">\n<div class=\"kg-callout-emoji\">\ud83d\udca1<\/div>\n<div class=\"kg-callout-text\"><b><strong style=\"white-space: pre-wrap;\">Note:<\/strong><\/b> For small applications like what we used, caching may not much differance. But in real world projects with large depecnies, caching helps you to reduce the time by 50-70%.<\/div>\n<\/div>\n<h2 id=\"observability-layer\">Observability Layer<\/h2>\n<p>Setting up observability to check the health of the CI\/CD pipeline is very important.<\/p>\n<p>Without proper visibility, you may not know whether there are resource constraints in the runner, builds are queuing up, which build is wasting time, or taking more time, etc.<\/p>\n<p>Below are the key things to track:<\/p>\n<ul>\n<li>Build queue depth and wait times &#8211; To identify if you need more runners<\/li>\n<li>Success\/failure rates per service &#8211; To identify builds with issues<\/li>\n<li>Average build duration trends &#8211; To find the time taken for builds<\/li>\n<li>Cache hit ratios &#8211; To check if caching is working<\/li>\n<\/ul>\n<p>Still, we are using GitHub Actions, and we will get basic information about the runs in the Actions tab itself.<\/p>\n<p>For further in-depth information, configure <a href=\"https:\/\/devopscube.com\/setup-prometheus-monitoring-on-kubernetes\/\" rel=\"noreferrer\">Prometheus<\/a> and <a href=\"https:\/\/devopscube.com\/grafana-loki-architecture\/\" rel=\"noreferrer\">Grafana<\/a> for monitoring the workflow and exporting metrics to them.<\/p>\n<h2 id=\"reusable-workflows\">Reusable Workflows<\/h2>\n<p>In this example, we have kept every workflow file inside the same repository, it is fine for small projects.<\/p>\n<p>But think, if you have more than 100+, what will you do?<\/p>\n<p>Maintaining more workflow files that have the same steps is a waste of time and difficult to manage.<\/p>\n<p>That is where reusable workflows help you.<\/p>\n<figure class=\"kg-card kg-image-card\"><img decoding=\"async\" src=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/2026\/03\/image-promotion8.png\" class=\"kg-image\" alt=\"Reusable Workflows\" loading=\"lazy\" width=\"2000\" height=\"1145\" srcset=\"https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w600\/2026\/03\/image-promotion8.png 600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1000\/2026\/03\/image-promotion8.png 1000w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w1600\/2026\/03\/image-promotion8.png 1600w, https:\/\/storage.ghost.io\/c\/5f\/2f\/5f2f4d20-2abf-4534-8d40-7aa233aedd43\/content\/images\/size\/w2400\/2026\/03\/image-promotion8.png 2400w\" sizes=\"auto, (min-width: 720px) 720px\"><\/figure>\n<p>A reusable workflow is like a template for jobs and steps that can be reused across multiple repositories and projects.<\/p>\n<p>Instead of defining the same steps separately in each repository, we can create centralized, reusable workflows in a repository and use them from there.<\/p>\n<p>An example of how reusable workflows are called is given below.<\/p>\n<pre><code class=\"language-yaml\">jobs:\n  pr-checks:\n    uses: your-org\/org-workflows-repo\/.github\/workflows\/reusable-pr-checks.yml@main\n    with:\n      config-path: .github\/reusable-pr-checks.yaml\n    secrets: inherit<\/code><\/pre>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>In this guide, you have learned about the complete workflow of how a Java application is built and Dockerized using GitHub Actions with most of the image security best practices.<\/p>\n<p>Not just Java applications, all language applications are built using the same format, just the application build and Dockerfile will differ based on the application language.<\/p>\n<p>Also, we have gone through how an image is built and promoted from a dev to the prod image registry.<\/p>\n<p>Not only that, but we have also signed the image at the end to give proof that the image has not changed after the promotion.<\/p>\n<p>If you have any doubts about this guide, drop a comment.<\/p>\n<p>We will help you to clear it!<\/p>\n<hr>\n<p><strong>Ngu\u1ed3n:<\/strong> <a href=\"https:\/\/devopscube.com\/docker-image-build-promotion-piepeline\/\" target=\"_blank\" rel=\"noopener noreferrer\">Docker Image Build &amp; Promotion Piepeline with GitHub Actions \u2014 DevOpsCube<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Source: https:\/\/devopscube.com\/docker-image-build-promotion-piepeline\/<\/p>\n","protected":false},"author":1,"featured_media":318,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-317","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops"],"_links":{"self":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/317","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=317"}],"version-history":[{"count":0,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/posts\/317\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=\/wp\/v2\/media\/318"}],"wp:attachment":[{"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=317"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=317"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.ngocha.biz\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=317"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}