DEV Community

lajibolala
lajibolala

Posted on

From a Fresh VM to a Working CI/CD Pipeline

When working in a training lab or a self-hosted environment, you do not always start with a polished developer workstation. Sometimes you get a plain VM, a zipped project, a local GitLab instance, a local SonarQube server, and a deadline.

This guide walks through the full process of going from a fresh Debian-based VM to a working CI/CD pipeline for a Python project using:

  • VS Code
  • Git
  • GitLab
  • SonarQube
  • Docker
  • optionally Docker Compose

The environment used here is a local VM with GitLab available at http://gitlab.localdomain, SonarQube at http://localhost:9000, and a GitLab Runner already configured as a shell runner.


1. Boot the VM and verify the platform

After starting the VM, do not rush immediately into GitLab. In this environment, GitLab may need around two minutes to finish booting. The VM documentation also indicates that Docker, Python 3, VS Code, GitLab, and SonarQube are already installed.

Open a terminal and run:

uname -a
python3 --version
git --version
docker --version
Enter fullscreen mode Exit fullscreen mode

Then check that the local services are reachable in the browser:

  • GitLab: http://gitlab.localdomain
  • SonarQube: http://localhost:9000

2. Unzip and inspect the project

Assume you received a zipped Python project.

Create a working directory, unzip the project, and move into it:

mkdir -p ~/work
cd ~/work
unzip project.zip -d project
cd project
Enter fullscreen mode Exit fullscreen mode

Inspect the contents:

ls -la
find . -maxdepth 2 -type f | sort
Enter fullscreen mode Exit fullscreen mode

A typical Python project for this workflow might contain:

app.py
requirements.txt
tests/test_app.py
Enter fullscreen mode Exit fullscreen mode

Before touching CI/CD, understand how the project works.

Check the main entry file:

sed -n '1,200p' app.py
Enter fullscreen mode Exit fullscreen mode

Check dependencies:

cat requirements.txt
Enter fullscreen mode Exit fullscreen mode

Check tests:

sed -n '1,200p' tests/test_app.py
Enter fullscreen mode Exit fullscreen mode

3. Open the project in VS Code

From the project root:

code .
Enter fullscreen mode Exit fullscreen mode

At this point, review:

  • the Python entry point
  • the listening port
  • environment variable usage
  • how tests are structured

Do not write pipeline files yet if you still do not know how the app starts.


4. Run the application locally first

A CI/CD pipeline is easier to build when the project already works locally.

Install dependencies:

pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

If the project uses Flask or a simple Python entry point, test it with:

python3 app.py
Enter fullscreen mode Exit fullscreen mode

If it starts successfully, open another terminal and verify the endpoint:

curl http://localhost:5000
Enter fullscreen mode Exit fullscreen mode

If the project uses another port, adapt accordingly.

Stop the process with:

Ctrl + C
Enter fullscreen mode Exit fullscreen mode

5. Run the tests before writing CI

This is a very important habit: do not let GitLab be the first place where your tests run.

Execute:

pytest
Enter fullscreen mode Exit fullscreen mode

If pytest is missing:

pip install pytest
pytest
Enter fullscreen mode Exit fullscreen mode

If tests fail here, fix or understand the failure before moving to Docker or GitLab.


6. Initialize Git and create a branch strategy

If the unzipped project is not yet a Git repository:

git init
git branch -M main
Enter fullscreen mode Exit fullscreen mode

Set your identity if needed:

git config user.name "Your Name"
git config user.email "your.email@example.com"
Enter fullscreen mode Exit fullscreen mode

A simple branching model for this setup could be:

  • main → stable version
  • develop → integration branch
  • test → optional branch for validation or pipeline experiments

Create them:

git checkout -b develop
git checkout -b test
git checkout main
Enter fullscreen mode Exit fullscreen mode

You can verify:

git branch
Enter fullscreen mode Exit fullscreen mode

If you prefer to start work on develop:

git checkout develop
Enter fullscreen mode Exit fullscreen mode

7. Create the remote project in GitLab

Open GitLab in the browser at:

http://gitlab.localdomain
Enter fullscreen mode Exit fullscreen mode

Create a new blank project from the GitLab UI.

Then add the remote in your local repository. It will look something like this:

git remote add origin http://gitlab.localdomain/username/project-name.git
Enter fullscreen mode Exit fullscreen mode

Check:

git remote -v
Enter fullscreen mode Exit fullscreen mode

Push the initial code.

If the remote repository is empty:

git add .
git commit -m "Initial project import"
git push -u origin main
Enter fullscreen mode Exit fullscreen mode

Then push the other branches:

git checkout develop
git push -u origin develop

git checkout test
git push -u origin test
Enter fullscreen mode Exit fullscreen mode

In this VM setup, the documentation notes that no SSH key is configured, so pushes are done with the GitLab username/password over HTTP.


8. Add a .gitignore

Before going further, clean the repository.

Create .gitignore:

__pycache__/
*.pyc
.pytest_cache/
.venv/
venv/
.env
.sonar/
Enter fullscreen mode Exit fullscreen mode

Then commit it:

git add .gitignore
git commit -m "Add gitignore"
git push
Enter fullscreen mode Exit fullscreen mode

9. Create the Dockerfile

Now containerize the application.

Dockerfile

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "app.py"]
Enter fullscreen mode Exit fullscreen mode

If the entry point is not app.py, replace it with the correct file.

Build the image locally:

docker build -t python-app:latest .
Enter fullscreen mode Exit fullscreen mode

Run it:

docker run -d --name python-app-dev -p 5000:5000 python-app:latest
Enter fullscreen mode Exit fullscreen mode

Verify:

docker ps
curl http://localhost:5000
Enter fullscreen mode Exit fullscreen mode

Stop and remove it:

docker stop python-app-dev
docker rm python-app-dev
Enter fullscreen mode Exit fullscreen mode

10. Add Docker Compose only if it helps

For a single service, docker run is often enough. In the training VM example, deployment is shown with docker run, and the result is verified with docker ps and a browser call to localhost.

Still, if you want a cleaner local deployment workflow, create docker-compose.yml.

docker-compose.yml

version: "3.8"

services:
  app:
    build: .
    container_name: python-app-dev
    ports:
      - "5000:5000"
Enter fullscreen mode Exit fullscreen mode

Then test it:

docker compose up -d --build
docker ps
curl http://localhost:5000
docker compose down
Enter fullscreen mode Exit fullscreen mode

For a mono-service app, Compose is optional. I would use it only if the workflow explicitly asks for it or if I want a cleaner deployment command.


11. Configure SonarQube

Create the file sonar-project.properties:

sonar.projectKey=python-app
sonar.projectName=python-app
sonar.sources=.
sonar.tests=tests
sonar.sourceEncoding=UTF-8
sonar.qualitygate.wait=true
Enter fullscreen mode Exit fullscreen mode

At this point, you also need to prepare the GitLab CI/CD variables for SonarQube.

Open GitLab:

  • Project
  • Settings
  • CI/CD
  • Variables

Common variables to define:

  • SONAR_HOST_URLhttp://localhost:9000
  • SONAR_TOKEN → generated from SonarQube

To generate a token in SonarQube:

  • log in to SonarQube at http://localhost:9000
  • go to your account security settings
  • create a token
  • copy it once
  • store it in GitLab CI/CD variables as SONAR_TOKEN

The VM documentation confirms that SonarQube is local and reachable at http://localhost:9000.


12. Write the GitLab CI pipeline

Create .gitlab-ci.yml.

Option A — simple and practical pipeline

stages:
  - test
  - build
  - sonarqube
  - deploy

variables:
  IMAGE_NAME: python-app
  CONTAINER_NAME: python-app-dev

run_tests:
  stage: test
  script:
    - pip install -r requirements.txt
    - pytest

build_image:
  stage: build
  script:
    - docker build --network=host -t $IMAGE_NAME:latest .

sonarqube_check:
  stage: sonarqube
  image:
    name: sonarsource/sonar-scanner-cli:latest
    entrypoint: [""]
  variables:
    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
    GIT_DEPTH: "0"
  cache:
    key: "${CI_JOB_NAME}"
    paths:
      - .sonar/cache
  script:
    - sonar-scanner -Dsonar.host.url=$SONAR_HOST_URL -Dsonar.token=$SONAR_TOKEN
  allow_failure: true
  only:
    - main
    - develop

deploy_dev:
  stage: deploy
  script:
    - docker stop $CONTAINER_NAME || true
    - docker rm $CONTAINER_NAME || true
    - docker run -d --network host --name $CONTAINER_NAME $IMAGE_NAME:latest
  needs: ["run_tests", "build_image"]
  only:
    - develop
Enter fullscreen mode Exit fullscreen mode

This pipeline does four things:

  1. runs the tests
  2. builds the Docker image
  3. analyzes code quality with SonarQube
  4. deploys the container

The VM example shown in the documentation follows that same overall chain: build, test, sonarqube, deploy.


13. Alternative: deploy with Docker Compose

If you prefer Compose and the environment supports it, replace the deploy job with:

deploy_dev:
  stage: deploy
  script:
    - docker compose down || true
    - docker compose up -d --build
  needs: ["run_tests", "build_image"]
  only:
    - develop
Enter fullscreen mode Exit fullscreen mode

Again, for a single Python service, this is optional.


14. Commit and push the pipeline files

Add the new files:

git add Dockerfile .gitlab-ci.yml sonar-project.properties docker-compose.yml .gitignore
git commit -m "Add Docker, SonarQube, and GitLab CI pipeline"
git push
Enter fullscreen mode Exit fullscreen mode

If you did not create docker-compose.yml, just omit it from the command.


15. Watch the pipeline in GitLab

Open GitLab:

  • go to your project
  • open Build > Pipelines
  • open the latest pipeline
  • inspect each job log

This matches the usage shown in the VM documentation, where the sample project is pushed to GitLab and the pipeline is visible from the GitLab UI.

What you should validate:

  • run_tests passes
  • build_image passes
  • sonarqube_check runs
  • deploy_dev starts the new container

16. Verify the deployment manually

After the deploy job completes, check the container from the terminal:

docker ps
Enter fullscreen mode Exit fullscreen mode

Then validate the app:

curl http://localhost:5000
Enter fullscreen mode Exit fullscreen mode

Or open it in the browser.

The VM walkthrough uses the same idea: after deployment, verify the result through the browser and with docker ps.

If the container exits unexpectedly:

docker ps -a
docker logs python-app-dev
Enter fullscreen mode Exit fullscreen mode

17. Typical troubleshooting

GitLab is not opening

Wait a bit longer after VM startup. In this environment, GitLab may take around two minutes to become available.

Push is rejected

Check the remote URL and make sure you are using the correct username/password flow, since the VM is configured without SSH keys by default.

pytest fails

Install dependencies again and rerun locally:

pip install -r requirements.txt
pytest
Enter fullscreen mode Exit fullscreen mode

docker build fails

Check:

  • the main Python file name
  • requirements.txt
  • the CMD in the Dockerfile

Deployment succeeds but app does not respond

Inspect:

docker ps -a
docker logs python-app-dev
Enter fullscreen mode Exit fullscreen mode

SonarQube fails

Check:

  • SONAR_HOST_URL
  • SONAR_TOKEN
  • that the token exists in GitLab variables
  • that sonar-project.properties matches your project structure

18. Suggested branch flow

A lightweight and practical flow is:

  • main → production-ready or validated branch
  • develop → day-to-day integration
  • test → optional validation branch

Example workflow:

git checkout develop
# work
git add .
git commit -m "Implement pipeline"
git push

git checkout test
git merge develop
git push

git checkout main
git merge test
git push
Enter fullscreen mode Exit fullscreen mode

You can simplify this if the exercise or project does not require that many branches.


19. Final checklist

Before calling the setup done, verify all of this:

git remote -v
git branch
pytest
docker build -t python-app:latest .
docker run -d --name python-app-dev -p 5000:5000 python-app:latest
docker ps
curl http://localhost:5000
docker stop python-app-dev
docker rm python-app-dev
Enter fullscreen mode Exit fullscreen mode

And from the web UI:

  • GitLab project exists
  • branches are pushed
  • CI/CD variables are configured
  • pipeline runs
  • SonarQube analysis appears

20. Closing thoughts

The hardest part of setting up CI/CD in a lab VM is usually not writing the YAML file. It is understanding the full chain:

  • unzip and inspect the project
  • make it run locally
  • version it correctly
  • connect it to GitLab
  • configure quality analysis
  • containerize it
  • automate it
  • verify it

Once you understand that sequence, the rest becomes much more predictable.

CI/CD is not just about tools. It is about turning manual, fragile steps into a repeatable delivery process.

Top comments (0)