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
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
Inspect the contents:
ls -la
find . -maxdepth 2 -type f | sort
A typical Python project for this workflow might contain:
app.py
requirements.txt
tests/test_app.py
Before touching CI/CD, understand how the project works.
Check the main entry file:
sed -n '1,200p' app.py
Check dependencies:
cat requirements.txt
Check tests:
sed -n '1,200p' tests/test_app.py
3. Open the project in VS Code
From the project root:
code .
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
If the project uses Flask or a simple Python entry point, test it with:
python3 app.py
If it starts successfully, open another terminal and verify the endpoint:
curl http://localhost:5000
If the project uses another port, adapt accordingly.
Stop the process with:
Ctrl + C
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
If pytest is missing:
pip install pytest
pytest
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
Set your identity if needed:
git config user.name "Your Name"
git config user.email "your.email@example.com"
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
You can verify:
git branch
If you prefer to start work on develop:
git checkout develop
7. Create the remote project in GitLab
Open GitLab in the browser at:
http://gitlab.localdomain
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
Check:
git remote -v
Push the initial code.
If the remote repository is empty:
git add .
git commit -m "Initial project import"
git push -u origin main
Then push the other branches:
git checkout develop
git push -u origin develop
git checkout test
git push -u origin test
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/
Then commit it:
git add .gitignore
git commit -m "Add gitignore"
git push
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"]
If the entry point is not app.py, replace it with the correct file.
Build the image locally:
docker build -t python-app:latest .
Run it:
docker run -d --name python-app-dev -p 5000:5000 python-app:latest
Verify:
docker ps
curl http://localhost:5000
Stop and remove it:
docker stop python-app-dev
docker rm python-app-dev
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"
Then test it:
docker compose up -d --build
docker ps
curl http://localhost:5000
docker compose down
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
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_URL→http://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
This pipeline does four things:
- runs the tests
- builds the Docker image
- analyzes code quality with SonarQube
- 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
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
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_testspasses -
build_imagepasses -
sonarqube_checkruns -
deploy_devstarts the new container
16. Verify the deployment manually
After the deploy job completes, check the container from the terminal:
docker ps
Then validate the app:
curl http://localhost:5000
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
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
docker build fails
Check:
- the main Python file name
requirements.txt- the
CMDin the Dockerfile
Deployment succeeds but app does not respond
Inspect:
docker ps -a
docker logs python-app-dev
SonarQube fails
Check:
SONAR_HOST_URLSONAR_TOKEN- that the token exists in GitLab variables
- that
sonar-project.propertiesmatches 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
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
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)