[EN] Deine erste CI/CD-Pipeline mit GitLab CI und Docker: Ein Schritt-für-Schritt-Guide

Julian | Jul 31, 2025 min read

Hello everyone,

As developers, we know that writing code is one thing; building, testing and rolling it out reliably is quite another. Doing this manually is tedious and error-prone. This is where Continuous Integration (CI) and Continuous Deployment (CD) come into play. They automate exactly these steps and ensure that your code ends up in production faster, more securely and more consistently.

In this article, we’ll dive into practice and build your first CI/CD pipeline with GitLab CI and Docker. Don’t worry, it’s easier than it sounds! At the end of this guide, you will have a working pipeline that builds your code, tests it, and creates a Docker image.

Why GitLab CI and Docker?

  • GitLab CI: Is directly integrated into GitLab, which means short paths and a seamless developer experience. No external tooling, no additional accounts.
  • Docker: Allows us to package our application and its dependencies in an isolated, portable environment. “It works on my machine” is now a thing of the past!

Let’s say we have a simple Spring Boot application in Java that we want to build and package as a Docker image.

Step 1: Prepare the project – your Dockerfile

First we need a Dockerfile in our project directory. This file describes how our Docker image should be built.

src/main/docker/Dockerfile (or simply in the root directory of your project)

# Offizielles OpenJDK Image als Basis
FROM openjdk:17-jdk-slim

# Metadaten für das Image
LABEL authors="Julian Paul"
LABEL description="Spring Boot Anwendung fuer CI/CD Demo"

# Arbeitsverzeichnis im Container
WORKDIR /app

# Die kompilierte Spring Boot JAR-Datei in den Container kopieren
# Annahme: Deine Build-Pipeline erstellt eine JAR unter 'target/your-app-name.jar'
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar

# Port, den die Anwendung im Container exponiert
EXPOSE 8080

# Befehl zum Starten der Anwendung, wenn der Container läuft
ENTRYPOINT ["java","-jar","/app/app.jar"]

Short explanation of the Dockerfile:

  • FROM openjdk:17-jdk-slim: We start with a slim OpenJDK 17 image.
  • WORKDIR /app: Sets /app as our working directory in the container.
  • ARG JAR_FILE and COPY: Here the .jar file generated after compilation is copied into the image. The ARG command is useful when the exact file name of the JAR varies (e.g. through versions).
  • EXPOSE 8080: Informs Docker that the container will listen on port 8080.
  • ENTRYPOINT: Defines the command that will be executed when the container is started.

Step 2: Define your GitLab CI pipeline – The .gitlab-ci.yml

The heart of our pipeline is the .gitlab-ci.yml file. This file must be in the root directory of your GitLab project. GitLab reads this file with every push and executes the jobs defined in it.

.gitlab-in.yml

# Definiert das Docker-Image, das fuer alle Jobs standardmaessig verwendet wird
# Hier ein Maven-Image, da wir eine Java/Spring Boot Anwendung bauen
# Hier koennt ihr natuerlich jedes andere Maven Image nutzen
image: maven:3.9.11-amazoncorretto-24-al2023

# Definiert verschiedene Stufen (Stages) in unserer Pipeline
# Jobs in spaeteren Stages werden erst ausgefuehrt, wenn alle Jobs der vorherigen Stage erfolgreich waren
stages:
  - build
  - test
  - package
  - deploy # Diese Stage wird spaeter fuer das Deployment genutzt

# === BUILD STAGE ===
build-job:
  stage: build
  script:
    - echo "Starte den Build-Prozess..."
    - mvn clean package -DskipTests
  artifacts: # Artefakte sind Dateien, die von diesem Job generiert und fuer spaetere Jobs benoetigt werden
    paths:
      - target/*.jar # Wir speichern unsere kompilierte JAR-Datei
    expire_in: 1 week # Wie lange die Artefakte gespeichert bleiben sollen
  only:
    - main # Dieser Job wird nur auf Aenderungen im 'main'-Branch ausgefuehrt

# === TEST STAGE ===
test-job:
  stage: test
  script:
    - echo "Starte die Tests..."
    - mvn test
  only:
    - main

# === PACKAGE STAGE (Docker Image Bau) ===
package-job:
  stage: package
  image: docker:latest # Fuer diesen Job benoetigen wir das Docker-Image
  services: # Ermoeglicht die Nutzung eines Docker-Daemons im Job (Docker-in-Docker)
    - docker:dind
  script:
    - echo "Baue das Docker-Image..."
    # Login in die GitLab Container Registry
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    # Build des Docker-Images. $CI_REGISTRY_IMAGE ist eine vordefinierte GitLab Variable
    # $CI_COMMIT_SHORT_SHA ist der kurze Hash des aktuellen Commits - gut fuer eindeutige Tags
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
    # Push des Docker-Images in die GitLab Container Registry
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
    # Zusaetzlich einen 'latest'-Tag pushen, wenn es der main-Branch ist
    - if [ "$CI_COMMIT_BRANCH" == "main" ]; then
        docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA $CI_REGISTRY_IMAGE:latest;
        docker push $CI_REGISTRY_IMAGE:latest;
      fi
  dependencies: # Dieser Job ist abhaengig vom 'build-job', um die JAR-Datei zu bekommen
    - build-job
  only:
    - main

# === DEPLOYMENT STAGE (Optional, Beispiel fuer spaeter) ===
# Hier wuerde dein Deployment Code stehen, z.B. auf Kubernetes, einer VM etc.
# deploy-job:
#   stage: deploy
#   script:
#     - echo "Bereitstellung des Docker-Images..."
#     # Beispiel: kubectl apply -f kubernetes/deployment.yaml
#   environment:
#     name: production
#   only:
#     - main
#   when: manual # Manuelles Deployment zur Sicherheit

Short explanation of .gitlab-ci.yml:

  • image: Defines the base Docker image in which your jobs run. For our Java build, a maven image is perfect.
  • stages: Here we define the stages of our pipeline: build (compile), test (run tests), package (build Docker image), deploy (make available). Jobs in a stage only start when all previous stages have been successful.
  • build-job:
  • stage: build: Assigns the job to the build stage.
  • script: The commands that will be executed. mvn clean package -DskipTests compiles our project and packages it into a JAR file, skipping tests (they run in the next job).
  • artifacts: The generated .jar file is saved as an artifact so that it can be used in subsequent jobs (e.g. package-job).
  • only: - main: This job will only run when changes are pushed to the main branch.
  • test-job:
  • stage: test: Assigns the job to the test phase.
  • script: Runs the unit tests with mvn test.
  • package-job:
  • stage: package: Assigns the job to the packaging stage.
  • image: docker:latest and services: - docker:dind: Very important! In order to be able to execute Docker commands within a GitLab CI job (e.g. docker build, docker push), we need the docker image as a base and the docker:dind (Docker-in-Docker) service.
  • script: Logs in to the GitLab Container Registry (the variables $CI_REGISTRY_USER, $CI_REGISTRY_PASSWORD, $CI_REGISTRY are predefined by GitLab and contain the credentials and URL of your registry). The Docker image is then built and pushed with a unique tag ($CI_COMMIT_SHORT_SHA) and an optional latest tag.
  • dependencies: - build-job: Ensures that the artifacts (our JAR file) from the build-job are available for this job.

Step 3: GitLab project setup

  1. Push repository: Create a new project in GitLab (or use an existing one) and push your code including Dockerfile and .gitlab-ci.yml to the main branch.
  2. Check Runners: Make sure shared runners (GitLab’s default runners) are available for your project or set up your own runners.
  3. Watch pipeline: Go to CI/CD -> Pipelines in your GitLab project. You should see your pipeline start and progress through each job.

Conclusion

Congratulations! You have just successfully implemented your first CI/CD pipeline with GitLab CI and Docker. Your code will now be automatically built, tested and deployed as a Docker image in the GitLab registry. This is the foundation for automated and efficient software development.

From here you can expand the pipeline: add static code analysis, implement additional test levels (integration tests, end-to-end tests) and of course the actual deployment to your target environment (Kubernetes, cloud VMs, etc.).

CI/CD is not a luxury, but a must for modern development teams. It saves time, reduces errors, and allows you to focus on what really matters: writing great code.

Do you have any questions or your own tips for CI/CD pipelines? Share them in the comments!

Example code

You can find the code for this post here: GitLab Repository