Building Docker Images for Go Projects with Gitlab Ecosystem

Introduction⌗
There are many ways to build Go projects. How can you find one that suits your team?
Today I’ll share some experiences I’ve discovered through exploration. By leveraging Gitlab’s powerful ecosystem, we can make Go projects more efficient from development to deployment!
Environment⌗
For this experiment, I ran the following services locally using Docker:
- Traefik: For service auto-discovery and reverse proxy
- Gitlab: For testing CI/CD workflows (with Container Registry enabled)
- Gitlab Runner: For executing CI/CD tasks
For Traefik and Gitlab deployment, you can refer to my previous two articles with detailed deployment solutions!
Domains⌗
- gitlab.test
- registry.test
Since this is an internal test environment, all service HTTPS certificates are self-signed using mkcert. This created a significant challenge for the upcoming infrastructure setup, but through troubleshooting, I gained a deeper understanding of the underlying principles!
Registering a Runner⌗
In the latest version of Gitlab, you can access the Runner management page at https://gitlab.test/admin/runners
:
- Once there, click the
New instance runner
button to create one - Fill in Tags and description information, then click
Create runner
- Save the Gitlab-Runner registration Token
Next, write the Gitlab Runner configuration file and docker-compose.yaml
file:
tree
.
├── README.md
├── config
│ ├── certs
│ │ └── ca.crt # CA certificate for self-signed certificates
│ └── config.toml # Gitlab Runner configuration file
└── docker-compose.yml
3 directories, 4 files
config/config.toml
file:
user = "gitlab-runner"
log_level = "error"
log_format = "text"
concurrent = 5
check_interval = 10
shutdown_timeout = 0
[session_server]
session_timeout = 1800
[[runners]]
id = 1
url = "https://gitlab.test"
name = "docker-runner"
token = "glrt-fgsyprdA3oxDJzxQJxbB"
executor = "docker"
cache_dir = "/cache"
builds_dir = "/builds"
token_obtained_at = 2023-11-22T06:12:41Z
token_expires_at = 0001-01-01T00:00:00Z
[runners.cache]
MaxUploadedArchiveSize = 0
[runners.docker]
image = "docker:latest"
volumes = ["/cache", "/builds", "/var/run/docker.sock:/var/run/docker.sock:ro"]
shm_size = 0
privileged = true
tls_verify = false
extra_hosts = ["gitlab.test:10.8.10.254", "registry.test:10.8.10.254"]
network_mtu = 0
pull_policy = ["if-not-present"]
network_mode = "traefik"
tls_cert_path = "/certs"
disable_cache = false
oom_kill_disable = false
allowed_services = ["docker:*"]
services_privileged = true
allowed_pull_policies = ["if-not-present"]
disable_entrypoint_overwrite = false
- On lines 13 and 15, add the URL and Token obtained earlier
- On line 25, add
/var/run/docker.sock:/var/run/docker.sock:ro
tovolumes
- On line 29, set custom TLD resolution, where
10.8.10.254
is the IP I manually set for Traefik in the traefik network
docker-compose.yaml
file:
services:
runner:
image: gitlab/gitlab-runner:latest
restart: always
hostname: runner
container_name: runner
extra_hosts:
- gitlab.test:10.8.10.254
volumes:
- ./config:/etc/gitlab-runner
- runner-cache:/cache
- runner-builds:/builds
- /var/run/docker.sock:/var/run/docker.sock
environment:
TZ: PRC
CA_CERTIFICATES_PATH: /etc/gitlab-runner/certs/ca.crt
CI_SERVER_TLS_CA_FILE: /etc/gitlab-runner/certs/ca.crt
volumes:
runner-cache:
name: runner-cache
runner-builds:
name: runner-builds
networks:
traefik:
external: true
- Lines 7-8 set HOST resolution for Gitlab Runner
- Lines 16-17 set trusted certificates for Gitlab Runner when requesting https://gitlab.test domain!
docker compose up -d
[+] Building 0.0s (0/0) docker:orbstack
[+] Running 1/1
✔ Container runner Started
After successful startup, go to the Gitlab Admin Runner page to check if the Runner is working properly:
Project Adaptation⌗
To simulate a real development environment, I created two project groups and two projects:
The main configuration is in the services/sendbox
project, with the following directory structure:
tree -a -I '.git|.idea'
.
├── .gitignore
├── .gitlab-ci.yml
├── Dockerfile
├── LICENSE
├── README.md
├── cmd
│ ├── root.go
│ └── serve.go
├── go.mod
├── go.sum
├── go.work
├── go.work.sum
└── main.go
2 directories, 12 files
I used Go Workspace locally to import local packages:
cat go.work
go 1.21.4
use .
replace gitlab.test/modules/mail => ../modules/mail
Dockerfile⌗
FROM golang:latest AS compiler-source-code
ARG VERSION=latest
ARG CI_JOB_USER=gitlab-ci-token
ARG GITLAB_HOST=gitlab.test
ARG CI_JOB_TOKEN
ARG CA_CERTIFICATE
WORKDIR /go/src/sendbox
RUN echo "machine gitlab.test login $CI_JOB_USER password $CI_JOB_TOKEN" >> ~/.netrc && \
echo $CA_CERTIFICATE >> /usr/local/share/ca-certificates/ca.crt && \
update-ca-certificates
ADD . /go/src/sendbox
RUN go env -w GOPRIVATE=gitlab.test && go mod tidy && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w -X gitlab.test/services/sendbox/cmd.version=$VERSION -X gitlab.test/services/sendbox/cmd.commit=$(git rev-parse HEAD)" -o sendbox main.go
FROM ubuntu:latest AS build-docker-image
COPY --from=compiler-source-code /go/src/sendbox/sendbox /usr/local/bin/sendbox
ENTRYPOINT ["/usr/local/bin/sendbox"]
- Line 10: Write authentication information to
~/.netrc
- Line 11: Write the self-signed CA certificate into the system (can be deleted if not using self-signed certificates or HTTPS)
- Line 12: Update the system’s trusted CA certificates (can be deleted if not using self-signed certificates or HTTPS)
- Line 14: Use
-ldflags
to write version information into project variables, which may be used later for log analysis and version stability analysis
During Docker image building, you can add settings to pull different private packages based on the environment. For example, in a test environment, you can execute
go get gitlab.test/modules/mail@develop
before building to pull the latest commit from the develop branch. This avoids issues in the test environment after a private package has been tagged, which would otherwise require multiple releases of the private package!
CI/CD Configuration⌗
Note that the latest version of Gitlab defaults to using the .yml
suffix, so using .yaml
will cause CI/CD to fail to retrieve it. If you must use the latter, you can set it in Gitlab:
You can modify the default CI configuration filename under
Default CI/CD configuration file
in the image above!
The .gitlab-ci.yml
configuration is as follows:
default:
image: docker:latest
before_script:
- export # You can delete this if you don't need to print all environment variables
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
variables:
GITLAB_IP: 10.0.6.3
CI_JOB_USER: gitlab-ci-token
GITLAB_HOST: gitlab.test
CA_CERTIFICATE: $CA_CERTIFICATE
CONTAINER_VER_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
CONTAINER_LATEST_TAG: $CI_REGISTRY_IMAGE:latest
stages:
- build
- release
build:
tags:
- docker
stage: build
script:
- docker build --add-host gitlab.test:"$GITLAB_IP" --pull --tag "$CONTAINER_VER_TAG" --build-arg VERSION="$CI_COMMIT_REF_SLUG" --build-arg CI_JOB_USER="$CI_JOB_USER" --build-arg GITLAB_HOST="$GITLAB_HOST" --build-arg CI_JOB_TOKEN="$CI_JOB_TOKEN" --build-arg CA_CERTIFICATE="$CA_CERTIFICATE" .
- docker push $CONTAINER_VER_TAG
release:
only:
refs:
- tags
tags:
- docker
stage: release
script:
- docker pull $CONTAINER_VER_TAG
- docker tag $CONTAINER_VER_TAG $CONTAINER_LATEST_TAG
- docker push $CONTAINER_LATEST_TAG
Gitlab CI preset environment variables:
- CI_REGISTRY: Container Registry HOST, which corresponds to
registry.test
here - CI_REGISTRY_USER: gitlab-ci-token
- CI_REGISTRY_PASSWORD: Same value as
CI_JOB_TOKEN
. If you need to operate on images from other projects, refer to thePermission Issues
section below - CI_REGISTRY_IMAGE: Automatically generated based on the project’s namespace, e.g., registry.test/services/sendbox in my case
- CI_COMMIT_REF_NAME: The name of the commit. If it’s a branch, it’s the branch name; if it’s a tag, it’s the corresponding version, e.g., master, develop, or v1.0.0
Using CI_COMMIT_REF_NAME directly for Image Tag might cause issues. For example, if the branch is feature/auth, the generated Image Tag would be
registry.test/services/sendbox:feature/auth
, which clearly doesn’t conform to Tag naming conventions. Instead, you can use another environment variableCI_COMMIT_REF_SLUG
, which replaces non-alphanumeric characters with slugs, e.g., feature/auth becomes feature-auth!
For more preset environment variables, refer to the Gitlab official documentation GitLab CI/CD variables.
- Line 4: Used to print all preset Gitlab CI environment variables. If you don’t want to read the documentation, you can execute
export
. Sensitive information will be marked as[MASKED]
- Line 26:
--add-host gitlab.test:"$GITLAB_IP"
is not needed if gitlab is not running locally. In my case, I used dnsmasq to resolve the *.test TLD to 127.0.0.1, which causes buildx to request 127.0.0.1:443 when accessing gitlab.test! - Lines 36-37: Tag the latest version as latest
Permission Issues⌗
If you use a Personal Token for authentication in CI/CD, the owner of that Personal Token needs access permissions to the private package repository.
I’m using CI_JOB_TOKEN
here, which is valid during the Pipeline lifecycle. After Gitlab 15.9
, for security reasons, cross-project access is not allowed. This means that when using CI_JOB_TOKEN
in the services/sendbox
project’s CI/CD to pull the modules/mail
private package, authentication will fail, with an error as shown on line 233 in the image below:
When this problem occurs, you can either disable the Limit Access to the project
feature or add the project you want to access to the Allow CI job tokens from the following projects to access this project
list below:
After this, you’ll be able to successfully retrieve the private package code on the next execution!
Summary⌗
At this point, the project’s build and distribution issues have been well resolved. This combination of Overwrite DNS + Self-signed certificates can really drive you crazy!
However, after going through the most difficult process, I gained a better understanding of how services in the Gitlab ecosystem interact with each other, which might be a blessing in disguise.
I’ll share more about CD best practices in the future, so stay tuned…
I hope this is helpful, Happy hacking…