DEV Community

Cover image for Building, Securing, and Deploying a Go App with GitLab CI/CD EP 4.1: Build and Push Docker Image to GAR
Booranasak Kanthong
Booranasak Kanthong

Posted on

Building, Securing, and Deploying a Go App with GitLab CI/CD EP 4.1: Build and Push Docker Image to GAR

Introduction

Welcome back to our CI/CD journey! Last time, you learned how to scan your Go code for bugs and vulnerabilities with SonarQube. In this episode, we’ll take your project to the next level by packaging your app as a Docker image and pushing it to Google Artifact Registry (GAR).

By the end, you’ll know how to:

  • Build a Docker image of your Go app in your GitLab pipeline
  • Authenticate your pipeline with Google Cloud
  • Push your image to GAR for safe storage and sharing

1. What is a Docker Image and Why Use a Registry?

A Docker image is like a recipe that describes how to run your app.
It bundles your code, dependencies, and configuration so your app runs the same everywhere.

But once you build an image, you need a place to store it. That’s where a container registry like Google Artifact Registry (GAR) comes in!
A registry lets you store, share, and deploy images—just like how GitHub lets you store and share code.


Docker Image to Registry Flow

[GitLab Pipeline Triggered]
           |
           v
[Docker Build Job: Build App Image]
           |
           v
[Save Docker Image as .tar File]
           |
           v
[Push Job: Authenticate with Google Cloud]
           |
           v
[Load Docker Image from .tar File]
           |
           v
[Push Docker Image to Google Artifact Registry]
           |
           v
[Image Available in Google Artifact Registry]
Enter fullscreen mode Exit fullscreen mode

2. Setting Up Google Artifact Registry (GAR)

To push images to GAR, you need:

  • A Google Cloud account
  • A Docker repository set up in Artifact Registry

A. Enable Artifact Registry

1.) Go to the Google Cloud Console.
2.) Make sure your project is selected.
3.) Enable the Artifact Registry API (search for it in “APIs & Services”).

B. Create a Docker Repository

1.) In the Cloud Console sidebar, find Artifact Registry.

You can search from Search Bar

You can search from Search Bar

2.) Click Repositories → CREATE REPOSITORY.

3.) Choose type: Docker.
4.) Pick a region (e.g., asia-southeast1).
5.) Give it a name, like booranasak-artifact-registry-docker.

C. Note Your Repository URL

It will look something like:


asia-southeast1-docker.pkg.dev/YOUR_PROJECT/YOUR_REPO/YOUR_IMAGE:TAG
Enter fullscreen mode Exit fullscreen mode

3. Building Your Docker Image in GitLab CI/CD

You can automate Docker image builds using GitLab pipelines with Docker-in-Docker.

Here’s the job from your .gitlab-ci.yml:

variables:
  IMAGE_NAME_GOOGLE: "booranasak-bank-api-golang"
  IMAGE_VERSION_TAG: "1.0.0"
  IMAGE_TAG_GOOGLE: "asia-southeast1-docker.pkg.dev/escian/booranasak-artifact-registry-docker/$IMAGE_NAME_GOOGLE:$IMAGE_VERSION_TAG"  
  IMAGE_TAR: todo-api.tar

build_image:
  stage: build
  image: docker:latest
  script:
    - docker build -t $IMAGE_TAG_GOOGLE .
    - docker save $IMAGE_TAG_GOOGLE -o $IMAGE_TAR
  artifacts:
    name: "docker-image-tar"
    paths:
      - $IMAGE_TAR
    expire_in: 1 hour
Enter fullscreen mode Exit fullscreen mode

What’s happening here?

  • docker build: Builds your image and tags it for GAR
  • docker save: Packages the image as a .tar file so you can use it in the next stage

4. Authenticating GitLab CI/CD with Google Cloud

To push images to GAR, your pipeline needs permission.
You do this with a Google Service Account and a GitLab CI/CD variable.

A. Create a Service Account and Key

1.) In Google Cloud, go to IAM & Admin → Service Accounts.
2.) Click Create Service Account.
3.) Give it Artifact Registry permissions (roles/artifactregistry.writer).
4.) Create and download a JSON key.

B. Add Service Account Key to GitLab

1.) In GitLab, go to your project → Settings → CI/CD → Variables.
2.) Add a variable named GCP_SA_KEY.
3.) Paste your JSON key as the value. (In my case, I convert it to base64 and save it. when I need to use it, I decode it from base64.)


5. Pushing the Image to GAR

Here’s the next job in your .gitlab-ci.yml:

push_image_to_registry:
  stage: push
  image: google/cloud-sdk:latest
  dependencies:
    - build_image
  before_script:
    - apt-get update && apt-get install -y docker.io
    - echo "$GCP_SA_KEY" | base64 -d > $CI_PROJECT_DIR/gcp-key.json
    - gcloud auth activate-service-account --key-file=$CI_PROJECT_DIR/gcp-key.json
    - gcloud config set project YOUR_PROJECT_ID
    - gcloud auth configure-docker asia-southeast1-docker.pkg.dev --quiet
  script:
    - docker load -i $IMAGE_TAR
    - docker push $IMAGE_TAG_GOOGLE
Enter fullscreen mode Exit fullscreen mode

What’s happening here?

  • Installs Docker CLI (needed for pushing)
  • Sets up Google authentication using your service account key
  • Authenticates Docker to GAR
  • Loads the built image from the previous stage
  • Pushes the image to your GAR repository

(Insert screenshot: GitLab pipeline with push_image_to_registry job)


6. Merge scripts together

It will look like this

image: docker:latest

services:
  - docker:dind

variables:
  DOCKER_HOST: tcp://docker:2375/
  DOCKER_TLS_CERTDIR: ""
  GO_VERSION: "1.24.3"
  SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
  IMAGE_NAME_GOOGLE: "booranasak-bank-api-golang"
  IMAGE_VERSION_TAG: "1.0.0"
  IMAGE_TAG_GOOGLE: "asia-southeast1-docker.pkg.dev/escian/booranasak-artifact-registry-docker/$IMAGE_NAME_GOOGLE:$IMAGE_VERSION_TAG"  
  IMAGE_TAR: todo-api.tar
  GIT_DEPTH: "0"

stages:
  - lint
  - test
  - sast
  - build
  - push

.go-job-template: &go-job-template
  image: debian:bullseye
  before_script:
    - apt update && apt install -y curl git tar gzip
    - curl -LO https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz
    - rm -rf /usr/local/go && tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz
    - export PATH="/usr/local/go/bin:$PATH"
    - go version

lint_golint:
  stage: lint
  <<: *go-job-template
  script:
    - export PATH="/usr/local/go/bin:$PATH"
    - go install golang.org/x/lint/golint@latest
    - export PATH="$PATH:$(go env GOPATH)/bin"
    - echo "Linting files:"
    - find . -name '*.go'
    - golint ./... | tee lint-report.txt
    - echo "--- Lint report preview ---"
    - cat lint-report.txt || echo "lint-report.txt is empty"
  allow_failure: true
  artifacts:
    name: "golint-report"
    paths:
      - lint-report.txt
    expire_in: 1 week

unit_test_and_coverage:
  stage: test
  <<: *go-job-template
  script:
    - export PATH="/usr/local/go/bin:$PATH"
    - go mod tidy
    - go test -v -cover ./...
    - go test -v -coverprofile=coverage.out ./...
  artifacts:
    paths:
      - coverage.out
    expire_in: 1 hour

sonarqube-check:
  stage: sast
  image:
    name: sonarsource/sonar-scanner-cli:11
    entrypoint: [""]
  dependencies:
    - unit_test_and_coverage
  script: 
    - sonar-scanner -Dsonar.host.url="${SONAR_HOST_URL}" -Dsonar.go.coverage.reportPaths=coverage.out -Dsonar.exclusions=**/*_test.go
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
    - if: $CI_COMMIT_BRANCH == 'master'
    - if: $CI_COMMIT_BRANCH == 'main'
    - if: $CI_COMMIT_BRANCH == 'develop'

sonarqube-vulnerability-report:
  stage: sast
  image:
    name: sonarsource/sonar-scanner-cli:11
    entrypoint: [""]
  script:
    - 'curl -u "${SONAR_TOKEN}:" "${SONAR_HOST_URL}/api/issues/gitlab_sast_export?projectKey=booranasak-golang-sast&branch=${CI_COMMIT_BRANCH}&pullRequest=${CI_MERGE_REQUEST_IID}" -o gl-sast-sonar-report.json'
  allow_failure: true
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
    - if: $CI_COMMIT_BRANCH == 'master'
    - if: $CI_COMMIT_BRANCH == 'main'
    - if: $CI_COMMIT_BRANCH == 'develop'
  artifacts:
    expire_in: 1 day
    reports:
      sast: gl-sast-sonar-report.json

build_image:
  stage: build
  image: docker:latest
  script:
    - docker build -t $IMAGE_TAG_GOOGLE .
    - docker save $IMAGE_TAG_GOOGLE -o $IMAGE_TAR
  artifacts:
    name: "docker-image-tar"
    paths:
      - $IMAGE_TAR
    expire_in: 1 hour

push_image_to_registry:
  stage: push
  image: google/cloud-sdk:latest
  dependencies:
    - build_image
  before_script:
    - echo "Install Docker"
    - apt-get update && apt-get install -y docker.io
    - echo "Setting up Google Cloud authentication..."
    - echo "$GCP_SA_KEY" | base64 -d > $CI_PROJECT_DIR/gcp-key.json
    - gcloud auth activate-service-account --key-file=$CI_PROJECT_DIR/gcp-key.json
    - gcloud config set project escian
    - gcloud auth configure-docker asia-southeast1-docker.pkg.dev --quiet
  script:
    - docker load -i $IMAGE_TAR
    - docker push $IMAGE_TAG_GOOGLE
Enter fullscreen mode Exit fullscreen mode


7. Checking Your Image in Google Cloud

Once your pipeline finishes:

  • Go to Artifact Registry in Google Cloud Console

You can search from Search Bar

  • Click your Docker repository

  • You should see your image (with its tag) listed!

Test Pulling Your Image (Optional)

On your local machine (with gcloud and Docker installed):

gcloud auth configure-docker asia-southeast1-docker.pkg.dev
docker pull asia-southeast1-docker.pkg.dev/YOUR_PROJECT/YOUR_REPO/YOUR_IMAGE:TAG
Enter fullscreen mode Exit fullscreen mode

8. Troubleshooting Tips

  • Auth errors? Double-check your service account’s permissions and the GCP_SA_KEY variable.
  • Pipeline fails at Docker commands? Make sure Docker-in-Docker (docker:dind) is running and your job uses the correct image.
  • Image not visible in GAR? Double-check your image tags and repository URL.

9. What’s Next?

Awesome work! Your Go app is now packaged in a Docker image and stored in Google Artifact Registry—ready to be deployed anywhere.

But before we go live, let’s scan your image for vulnerabilities.
In the next episode, we’ll add Trivy to your pipeline to automatically check for security issues in your Docker image.


Summary

  • You learned how to build and push Docker images in GitLab CI/CD
  • You configured authentication for secure image uploads to GAR
  • Your app is ready for secure deployment and scanning!

Ready for EP 5.1? Stay tuned — we’re adding Trivy to scan images in Google Artifact Registry (GAR) for vulnerabilities!

Top comments (0)