diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml
new file mode 100644
index 0000000000..7ccdb5a79f
--- /dev/null
+++ b/.github/workflows/python-ci.yml
@@ -0,0 +1,100 @@
+name: Python CI
+
+on:
+ push:
+ branches: [master, lab03]
+ paths:
+ - "app_python/**"
+ - ".github/workflows/python-ci.yml"
+ pull_request:
+ branches: [master]
+ paths:
+ - "app_python/**"
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: app_python
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.13"
+ cache: "pip"
+ cache-dependency-path: app_python/requirements.txt
+
+ - name: Install dependencies
+ run: pip install -r requirements.txt
+
+ - name: Lint with flake8
+ run: |
+ pip install flake8
+ flake8 app.py --count --select=E9,F63,F7,F82 --show-source --statistics
+ flake8 app.py --count --exit-zero --max-complexity=10 --statistics
+
+ - name: Test with pytest
+ run: pytest tests/ -v
+
+ security:
+ runs-on: ubuntu-latest
+ needs: test
+ if: always()
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: "3.13"
+ cache: "pip"
+ cache-dependency-path: app_python/requirements.txt
+
+ - name: Install dependencies
+ run: pip install -r app_python/requirements.txt
+
+ - name: Install Snyk CLI
+ run: npm install -g snyk
+
+ - name: Run Snyk test
+ run: snyk test --file=app_python/requirements.txt --severity-threshold=high
+ env:
+ SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
+
+ docker:
+ needs: test
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
+ defaults:
+ run:
+ working-directory: app_python
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_TOKEN }}
+
+ - name: Generate version (CalVer)
+ id: version
+ run: echo "version=$(date +%Y.%m.%d)" >> $GITHUB_OUTPUT
+
+ - name: Build and push
+ uses: docker/build-push-action@v5
+ with:
+ context: app_python
+ push: true
+ tags: |
+ ${{ secrets.DOCKER_USERNAME }}/devops-info-service:latest
+ ${{ secrets.DOCKER_USERNAME }}/devops-info-service:${{ steps.version.outputs.version }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
diff --git a/.gitignore b/.gitignore
index 30d74d2584..f9362757fa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,6 @@
-test
\ No newline at end of file
+app_python/venv/
+__pycache__/
+*.py[cod]
+.DS_Store
+*.log
+.env
diff --git a/README.md b/README.md
deleted file mode 100644
index 371d51f456..0000000000
--- a/README.md
+++ /dev/null
@@ -1,271 +0,0 @@
-# DevOps Engineering: Core Practices
-
-[](#labs)
-[](#exam-alternative)
-[](#course-roadmap)
-
-Master **production-grade DevOps practices** through hands-on labs. Build, containerize, deploy, monitor, and scale applications using industry-standard tools.
-
----
-
-## Quick Start
-
-1. **Fork** this repository
-2. **Clone** your fork locally
-3. **Start with Lab 1** and progress sequentially
-4. **Submit PRs** for each lab (details below)
-
----
-
-## Course Roadmap
-
-| Week | Lab | Topic | Key Technologies |
-|------|-----|-------|------------------|
-| 1 | 1 | Web Application Development | Python/Go, Best Practices |
-| 2 | 2 | Containerization | Docker, Multi-stage Builds |
-| 3 | 3 | Continuous Integration | GitHub Actions, Snyk |
-| 4 | 4 | Infrastructure as Code | Terraform, Cloud Providers |
-| 5 | 5 | Configuration Management | Ansible Basics |
-| 6 | 6 | Continuous Deployment | Ansible Advanced |
-| 7 | 7 | Logging | Promtail, Loki, Grafana |
-| 8 | 8 | Monitoring | Prometheus, Grafana |
-| 9 | 9 | Kubernetes Basics | Minikube, Deployments, Services |
-| 10 | 10 | Helm Charts | Templating, Hooks |
-| 11 | 11 | Secrets Management | K8s Secrets, HashiCorp Vault |
-| 12 | 12 | Configuration & Storage | ConfigMaps, PVCs |
-| 13 | 13 | GitOps | ArgoCD |
-| 14 | 14 | Progressive Delivery | Argo Rollouts |
-| 15 | 15 | StatefulSets | Persistent Storage, Headless Services |
-| 16 | 16 | Cluster Monitoring | Kube-Prometheus, Init Containers |
-| β | **Exam Alternative Labs** | | |
-| 17 | 17 | Edge Deployment | Fly.io, Global Distribution |
-| 18 | 18 | Decentralized Storage | 4EVERLAND, IPFS, Web3 |
-
----
-
-## Grading
-
-### Grade Composition
-
-| Component | Weight | Points |
-|-----------|--------|--------|
-| **Labs (16 required)** | 80% | 160 pts |
-| **Final Exam** | 20% | 40 pts |
-| **Bonus Tasks** | Extra | +40 pts max |
-| **Total** | 100% | 200 pts |
-
-### Exam Alternative
-
-Don't want to take the exam? Complete **both** bonus labs:
-
-| Lab | Topic | Points |
-|-----|-------|--------|
-| **Lab 17** | Fly.io Edge Deployment | 20 pts |
-| **Lab 18** | 4EVERLAND & IPFS | 20 pts |
-
-**Requirements:**
-- Complete both labs (17 + 18 = 40 pts, replaces exam)
-- Minimum 16/20 on each lab
-- Deadline: **1 week before exam date**
-- Can still take exam if you need more points for desired grade
-
-
-π Grade Scale
-
-| Grade | Points | Percentage |
-|-------|--------|------------|
-| **A** | 180-200+ | 90-100% |
-| **B** | 150-179 | 75-89% |
-| **C** | 120-149 | 60-74% |
-| **D** | 0-119 | 0-59% |
-
-**Minimum to Pass:** 120 points (60%)
-
-
-
-
-π Grade Examples
-
-**Scenario 1: Labs + Exam**
-```
-Labs: 16 Γ 9 = 144 pts
-Bonus: 5 labs Γ 2.5 = 12.5 pts
-Exam: 35/40 pts
-Total: 191.5 pts = 96% (A)
-```
-
-**Scenario 2: Labs + Exam Alternative**
-```
-Labs: 16 Γ 9 = 144 pts
-Bonus: 8 labs Γ 2.5 = 20 pts
-Lab 17: 18 pts
-Lab 18: 17 pts
-Total: 199 pts = 99.5% (A)
-```
-
-
-
----
-
-## Lab Structure
-
-Each lab is worth **10 points** (main tasks) + **2.5 points** (bonus).
-
-- **Minimum passing score:** 6/10 per lab
-- **Late submissions:** Max 6/10 (within 1 week)
-- **Very late (>1 week):** Not accepted
-
-
-π Lab Categories
-
-**Foundation (Labs 1-2)**
-- Web app development
-- Docker containerization
-
-**CI/CD & Infrastructure (Labs 3-4)**
-- GitHub Actions
-- Terraform
-
-**Configuration Management (Labs 5-6)**
-- Ansible playbooks and roles
-
-**Observability (Labs 7-8)**
-- Loki logging stack
-- Prometheus monitoring
-
-**Kubernetes Core (Labs 9-12)**
-- K8s basics, Helm
-- Secrets, ConfigMaps
-
-**Advanced Kubernetes (Labs 13-16)**
-- ArgoCD, Argo Rollouts
-- StatefulSets, Monitoring
-
-**Exam Alternative (Labs 17-18)**
-- Fly.io, 4EVERLAND/IPFS
-
-
-
----
-
-## How to Submit
-
-```bash
-# 1. Create branch
-git checkout -b lab1
-
-# 2. Complete lab tasks
-
-# 3. Commit and push
-git add .
-git commit -m "Complete lab1"
-git push -u origin lab1
-
-# 4. Create TWO Pull Requests:
-# PR #1: your-fork:lab1 β course-repo:master
-# PR #2: your-fork:lab1 β your-fork:master
-```
-
-
-π Submission Checklist
-
-- [ ] All main tasks completed
-- [ ] Documentation files created
-- [ ] Screenshots where required
-- [ ] Code tested and working
-- [ ] Markdown validated ([linter](https://dlaa.me/markdownlint/))
-- [ ] Both PRs created
-
-
-
----
-
-## Resources
-
-
-π οΈ Required Tools
-
-| Tool | Purpose |
-|------|---------|
-| Git | Version control |
-| Docker | Containerization |
-| kubectl | Kubernetes CLI |
-| Helm | K8s package manager |
-| Minikube | Local K8s cluster |
-| Terraform | Infrastructure as Code |
-| Ansible | Configuration management |
-
-
-
-
-π Documentation Links
-
-**Core:**
-- [Docker](https://docs.docker.com/)
-- [Kubernetes](https://kubernetes.io/docs/)
-- [Helm](https://helm.sh/docs/)
-
-**CI/CD:**
-- [GitHub Actions](https://docs.github.com/en/actions)
-- [Terraform](https://www.terraform.io/docs)
-- [Ansible](https://docs.ansible.com/)
-
-**Observability:**
-- [Prometheus](https://prometheus.io/docs/)
-- [Grafana](https://grafana.com/docs/)
-
-**Advanced:**
-- [ArgoCD](https://argo-cd.readthedocs.io/)
-- [Argo Rollouts](https://argoproj.github.io/argo-rollouts/)
-- [HashiCorp Vault](https://developer.hashicorp.com/vault/docs)
-
-
-
-
-π‘ Tips for Success
-
-1. **Start early** - Don't wait until deadline
-2. **Read instructions fully** before starting
-3. **Test everything** before submitting
-4. **Document as you go** - Don't leave it for the end
-5. **Ask questions early** - Don't wait until last minute
-6. **Use proper Git workflow** - Branches, commits, PRs
-
-
-
-
-π§ Common Issues
-
-**Docker:**
-- Daemon not running β Start Docker Desktop
-- Permission denied β Add user to docker group
-
-**Minikube:**
-- Won't start β Try `--driver=docker`
-- Resource issues β Allocate more memory/CPU
-
-**Kubernetes:**
-- ImagePullBackOff β Check image name/registry
-- CrashLoopBackOff β Check logs: `kubectl logs `
-
-
-
----
-
-## Course Completion
-
-After completing all 16 core labs (+ optional Labs 17-18), you'll have:
-
-β
Full-stack DevOps expertise
-β
Production-ready portfolio with 16-18 projects
-β
Container and Kubernetes mastery
-β
CI/CD pipeline experience
-β
Infrastructure as Code skills
-β
Monitoring and observability knowledge
-β
GitOps workflow experience
-
----
-
-**Ready to begin? Start with [Lab 1](labs/lab01.md)!**
-
-Questions? Check the course Moodle page or ask during office hours.
diff --git a/app_python/.dockerignore b/app_python/.dockerignore
new file mode 100644
index 0000000000..8f31d7babe
--- /dev/null
+++ b/app_python/.dockerignore
@@ -0,0 +1,14 @@
+venv
+__pycache__
+*.py[cod]
+*.pyc
+*.pyo
+*.pyd
+*.log
+.env
+.env.local
+.git
+.gitignore
+docs
+tests
+.DS_Store
diff --git a/app_python/.gitignore b/app_python/.gitignore
new file mode 100644
index 0000000000..e056cde1ba
--- /dev/null
+++ b/app_python/.gitignore
@@ -0,0 +1,31 @@
+# Python
+__pycache__/
+*.py[cod]
+*.pyc
+*.pyd
+*.pyo
+*.so
+venv/
+.venv/
+env/
+.idea/
+.vscode/
+
+# Logs
+*.log
+logs/
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Virtual environment
+venv/
+ENV/
+
+# Jupyter
+.ipynb_checkpoints
+
+# Environment variables
+.env
+.env.local
diff --git a/app_python/Dockerfile b/app_python/Dockerfile
new file mode 100644
index 0000000000..d11d67d035
--- /dev/null
+++ b/app_python/Dockerfile
@@ -0,0 +1,32 @@
+FROM python:3.13-slim
+
+ENV DEBIAN_FRONTEND=noninteractive
+ENV PYTHONDONTWRITEBYTECODE=1
+ENV PYTHONUNBUFFERED=1
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends gcc ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN groupadd -r appgroup && useradd -r -g appgroup -d /home/appuser -m -s /sbin/nologin appuser
+
+WORKDIR /app
+
+COPY requirements.txt .
+
+RUN python -m pip install --upgrade pip \
+ && pip install --no-cache-dir -r requirements.txt
+
+COPY app.py .
+
+RUN chown -R appuser:appgroup /app
+
+USER appuser
+
+EXPOSE 5000
+
+ENV HOST=0.0.0.0
+ENV PORT=5000
+
+# Default command
+CMD ["python", "app.py"]
diff --git a/app_python/README.md b/app_python/README.md
new file mode 100644
index 0000000000..8ea116409c
--- /dev/null
+++ b/app_python/README.md
@@ -0,0 +1,215 @@
+# DevOps Info Service
+
+A small web service that returns service, system and runtime information.
+Implemented for the DevOps Core Course (Lab 1) using **Python + Flask**.
+
+---
+
+## Overview
+
+This service exposes two endpoints:
+
+- `GET /` β Returns detailed information about the service, system, runtime and request.
+- `GET /health` β Lightweight health check for monitoring/probes.
+
+---
+
+## Prerequisites
+
+- **macOS** with Python (3.11+ recommended; tested on 3.13)
+- `git`
+- `curl` or `http` (HTTPie)
+- (optional) `jq` for pretty-printing JSON
+
+---
+
+## Installation (macOS)
+
+From the repository root:
+
+```bash
+cd DevOps-Core-Course/app_python
+
+# create a virtual environment inside the project (recommended)
+python -m venv venv
+source venv/bin/activate # macOS / Linux
+
+# install pinned dependencies
+pip install -r requirements.txt
+```
+
+> We keep the virtual environment inside `app_python/venv/`. This directory is listed in `.gitignore` and will not be committed.
+
+---
+
+## Running the application
+
+### Basic
+
+```bash
+# default: binds to 0.0.0.0:5000
+python app.py
+```
+
+### Custom host / port / debug
+
+```bash
+# custom port
+PORT=8000 python app.py
+
+# custom host and port
+HOST=127.0.0.1 PORT=3000 python app.py
+
+# enable debug mode
+DEBUG=true python app.py
+```
+
+If port `5000` is already used (common on macOS), start on a different port (e.g., `8000`).
+
+---
+
+## API
+
+### `GET /`
+
+Returns a JSON object with keys: `service`, `system`, `runtime`, `request`, `endpoints`.
+
+**Example request**
+
+```bash
+curl -s http://127.0.0.1:8000/ | jq .
+```
+
+**Example (sample output β will vary per machine)**
+
+```json
+{
+ "service": {
+ "name": "devops-info-service",
+ "version": "1.0.0",
+ "description": "DevOps course info service",
+ "framework": "Flask"
+ },
+ "system": {
+ "hostname": "MacBook-Pro-Diana-2.local",
+ "platform": "Darwin",
+ "platform_version": "25.2.0",
+ "architecture": "arm64",
+ "cpu_count": 8,
+ "python_version": "3.13.0"
+ },
+ "runtime": {
+ "uptime_seconds": 355,
+ "uptime_human": "0 hours, 5 minutes",
+ "current_time": "2026-01-28T19:52:46.995203+00:00",
+ "timezone": "UTC"
+ },
+ "request": {
+ "client_ip": "127.0.0.1",
+ "user_agent": "curl/7.87.0",
+ "method": "GET",
+ "path": "/"
+ },
+ "endpoints": [
+ { "path": "/", "method": "GET", "description": "Service information" },
+ { "path": "/health", "method": "GET", "description": "Health check" }
+ ]
+}
+```
+
+### `GET /health`
+
+Simple health JSON used for monitoring / readiness/liveness probes.
+
+**Example**
+
+```bash
+curl -s http://127.0.0.1:8000/health | jq .
+```
+
+**Sample**
+
+```json
+{
+ "status": "healthy",
+ "timestamp": "2026-01-28T19:52:29.081981+00:00",
+ "uptime_seconds": 337
+}
+```
+
+**Status codes**
+
+- `200 OK` β healthy
+- `500` β internal server error (if thrown)
+
+---
+
+## Configuration
+
+| Environment variable | Default | Description |
+| -------------------- | --------- | ------------------------------------ |
+| `HOST` | `0.0.0.0` | Host address to bind to |
+| `PORT` | `5000` | Port to listen on |
+| `DEBUG` | `false` | Enable Flask debug mode (true/false) |
+
+Use `PORT=8000` if `5000` is already in use on your machine.
+
+---
+
+## Testing
+
+Quick checks:
+
+```bash
+# main endpoint
+curl -s http://127.0.0.1:8000/ | jq .
+
+# health
+curl -s http://127.0.0.1:8000/health | jq .
+```
+
+If `jq` is not installed, use:
+
+```bash
+curl -s http://127.0.0.1:8000/ | python -m json.tool
+```
+
+---
+
+## Project layout
+
+```
+app_python/
+βββ app.py # Main application
+βββ requirements.txt # Pinned dependencies
+βββ venv/ # (local) virtual environment - ignored by git
+βββ .gitignore # Git ignore rules
+βββ README.md # This file
+βββ tests/ # Unit tests (placeholder)
+β βββ __init__.py
+βββ docs/
+ βββ LAB01.md # Lab submission notes
+ βββ screenshots/ # PNG screenshots for submission
+```
+
+---
+
+## Notes & caveats
+
+- This project uses Flask's development server for lab/demo purposes. For production or grading polish, a WSGI server (Gunicorn/uvicorn) + proper logging should be used.
+- The code attempts to use UTC timestamps. There may be a `DeprecationWarning` about `utcnow()` on some Python versions; this does not affect lab functionality. A later commit will make datetime objects timezone-aware to remove the warning.
+
+---
+
+## Contributing / Submission
+
+1. Implement changes on branch `lab01`.
+2. Commit and push to your fork.
+3. Create PR: `your-fork:lab01` β `inno-devops-labs/DevOps-Core-Course:master`.
+4. Include screenshots from `app_python/docs/screenshots/` in the PR description.
+
+---
+
+## License
+
+Educational project for DevOps Core Course.
diff --git a/app_python/app.py b/app_python/app.py
new file mode 100644
index 0000000000..75de51d571
--- /dev/null
+++ b/app_python/app.py
@@ -0,0 +1,133 @@
+import os
+import socket
+import platform
+import logging
+from datetime import datetime, timezone
+from flask import Flask, jsonify, request
+
+# ========== CONFIGURATION ==========
+HOST = os.getenv('HOST', '0.0.0.0')
+PORT = int(os.getenv('PORT', 5000))
+DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
+
+# ========== APPLICATION SETUP ==========
+app = Flask(__name__)
+
+logging.basicConfig(
+ level=logging.INFO if not DEBUG else logging.DEBUG,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+)
+logger = logging.getLogger(__name__)
+
+START_TIME = datetime.now(timezone.utc)
+
+# ========== HELPER FUNCTIONS ==========
+def get_uptime():
+ """Calculate application uptime in seconds and human-readable format."""
+ delta = datetime.now(timezone.utc) - START_TIME
+ seconds = int(delta.total_seconds())
+
+ hours = seconds // 3600
+ minutes = (seconds % 3600) // 60
+
+ return {
+ 'seconds': seconds,
+ 'human': f"{hours} hours, {minutes} minutes"
+ }
+
+def get_system_info():
+ """Collect system information."""
+ return {
+ 'hostname': socket.gethostname(),
+ 'platform': platform.system(),
+ 'platform_version': platform.release(),
+ 'architecture': platform.machine(),
+ 'cpu_count': os.cpu_count() or 1,
+ 'python_version': platform.python_version()
+ }
+
+def get_request_info():
+ """Extract information about the current request."""
+ return {
+ 'client_ip': request.remote_addr,
+ 'user_agent': request.headers.get('User-Agent', 'Unknown'),
+ 'method': request.method,
+ 'path': request.path
+ }
+
+# ========== ROUTES ==========
+@app.route('/')
+def main_endpoint():
+ """
+ Main endpoint - returns comprehensive service and system information.
+ """
+ logger.info(f"Main endpoint accessed by {request.remote_addr}")
+
+ response = {
+ 'service': {
+ 'name': 'devops-info-service',
+ 'version': '1.0.0',
+ 'description': 'DevOps course info service',
+ 'framework': 'Flask'
+ },
+ 'system': get_system_info(),
+ 'runtime': {
+ 'uptime_seconds': get_uptime()['seconds'],
+ 'uptime_human': get_uptime()['human'],
+ 'current_time': datetime.now(timezone.utc).isoformat(),
+ 'timezone': 'UTC'
+ },
+ 'request': get_request_info(),
+ 'endpoints': [
+ {'path': '/', 'method': 'GET', 'description': 'Service information'},
+ {'path': '/health', 'method': 'GET', 'description': 'Health check'}
+ ]
+ }
+
+ return jsonify(response)
+
+@app.route('/health')
+def health_check():
+ """
+ Health check endpoint for monitoring.
+ Returns HTTP 200 when service is healthy.
+ """
+ logger.debug("Health check requested")
+
+ response = {
+ 'status': 'healthy',
+ 'timestamp': datetime.now(timezone.utc).isoformat(),
+ 'uptime_seconds': get_uptime()['seconds']
+ }
+
+ return jsonify(response), 200
+
+# ========== ERROR HANDLERS ==========
+@app.errorhandler(404)
+def not_found(error):
+ """Handle 404 errors."""
+ return jsonify({
+ 'error': 'Not Found',
+ 'message': 'The requested endpoint does not exist',
+ 'available_endpoints': ['/', '/health']
+ }), 404
+
+@app.errorhandler(500)
+def internal_error(error):
+ """Handle 500 errors."""
+ logger.error(f"Internal server error: {error}")
+ return jsonify({
+ 'error': 'Internal Server Error',
+ 'message': 'An unexpected error occurred'
+ }), 500
+
+# ========== APPLICATION ENTRY POINT ==========
+if __name__ == '__main__':
+ logger.info(f"Starting DevOps Info Service on {HOST}:{PORT}")
+ logger.info(f"Debug mode: {DEBUG}")
+
+ app.run(
+ host=HOST,
+ port=PORT,
+ debug=DEBUG
+ )
\ No newline at end of file
diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md
new file mode 100644
index 0000000000..757e5badbd
--- /dev/null
+++ b/app_python/docs/LAB01.md
@@ -0,0 +1,101 @@
+# Lab 1 Submission β DevOps Info Service
+
+**Name:** Diana Yakupova
+**Group:** B23-CBS-02
+**Date:** 2026-01-28
+**Framework:** Flask 3.1.0
+
+## 1. Framework Selection
+
+I chose Flask for this project because it's the simplest Python web framework that meets all requirements. With only 2 endpoints needed and a tight deadline, Flask's minimal setup and straightforward documentation allowed me to deliver working code quickly.
+
+## 2. Implementation Details
+
+### Application Structure
+- `app.py` - main application with all endpoints
+- `requirements.txt` - single dependency: Flask==3.1.0
+- `.gitignore` - standard Python/IDE ignore patterns
+- `README.md` - complete user documentation
+- `docs/LAB01.md` - this lab report
+- `docs/screenshots/` - proof of functionality
+
+### Key Features Implemented
+1. **Main endpoint (`GET /`)** - returns service metadata, system info, runtime data, request details, and available endpoints
+2. **Health endpoint (`GET /health`)** - returns service status with timestamp and uptime
+3. **Environment-based configuration** - port, host, and debug mode configurable via env vars
+4. **Error handling** - custom 404 and 500 error responses in JSON format
+5. **Logging** - configurable logging with timestamps and levels
+
+### Code Quality Measures
+- Followed PEP 8 style guide
+- Used meaningful function/variable names
+- Added docstrings for all functions
+- Modular code structure with separate helper functions
+- Proper error handling for edge cases
+
+## 3. Testing Results
+
+Both endpoints work correctly:
+
+### Main Endpoint (`GET /`)
+Returns complete system information:
+- Service: name, version, description, framework
+- System: hostname, platform, architecture, CPU count, Python version
+- Runtime: uptime, current time, timezone
+- Request: client IP, user agent, method, path
+- Endpoints: list of available endpoints
+
+### Health Endpoint (`GET /health`)
+Returns health status:
+- Status: "healthy"
+- Timestamp: current UTC time in ISO format
+- Uptime: seconds since application start
+
+**Screenshots provided in `docs/screenshots/`:**
+- `01-startup.png` - Application starting successfully
+- `02-main-endpoint.png` - Main endpoint response in browser
+- `03-health-check.png` - Health check response in browser
+
+## 4. Configuration Management
+
+The application supports three environment variables:
+- `HOST` - binding address (default: 0.0.0.0)
+- `PORT` - listening port (default: 5000)
+- `DEBUG` - enable debug mode (default: false)
+
+Tested configurations:
+- Default: `python app.py` (port 5000)
+- Custom port: `PORT=8000 python app.py`
+- Custom host/port: `HOST=127.0.0.1 PORT=3000 python app.py`
+
+## 5. Challenges and Solutions
+
+### Challenge: Port 5000 Already in Use
+On macOS, port 5000 is often used by AirPlay Receiver. Solution: Use `PORT=8000 python app.py` to run on alternative port while keeping default configuration in documentation.
+
+### Challenge: Boolean Environment Variable Parsing
+Environment variables are strings, but debug mode needs boolean. Solution: Added `.lower() == 'true'` comparison for case-insensitive boolean parsing.
+
+### Challenge: Uptime Human-Readable Format
+Need both seconds and human-readable time. Solution: Implemented conversion from seconds to "X hours, Y minutes" format.
+
+## 6. GitHub Actions Completed
+
+- Starred course repository: https://github.com/inno-devops-labs/DevOps-Core-Course
+- Starred simple-container-com/api: https://github.com/simple-container-com/api
+- Followed professor (@Cre-eD) and TAs (@marat-biriushev, @pierrepicaud)
+- Followed 3 classmates from course forks
+
+## 7. Conclusion
+
+All lab requirements completed successfully:
+- β
Flask web service with two working endpoints
+- β
Complete system information on main endpoint
+- β
Health check endpoint for monitoring
+- β
Environment-based configuration
+- β
Error handling and logging
+- β
Full documentation (README and lab report)
+- β
Screenshots as proof of functionality
+- β
GitHub community actions completed
+
+The service is production-ready and prepared for further evolution in upcoming labs (containerization, CI/CD, monitoring).
\ No newline at end of file
diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md
new file mode 100644
index 0000000000..d4c6e43faf
--- /dev/null
+++ b/app_python/docs/LAB02.md
@@ -0,0 +1,247 @@
+# LAB02 β Docker Containerization
+
+**Name:** Diana Yakupova
+**Group:** B23-CBS-02
+**Date:** 2026-02-04
+
+---
+
+## What I implemented
+
+- Dockerfile using `python:3.12-slim` (minimal, stable)
+- Non-root user (`appuser`) β container does not run as root
+- Dependencies copied and installed before app code (cache-friendly layering)
+- `.dockerignore` excluding `venv/`, `docs/`, `tests/`, `.git`, etc.
+- Local image build and run verified; image pushed to Docker Hub
+
+---
+
+## Dockerfile (main points)
+
+```dockerfile
+FROM python:3.12-slim
+
+# Create non-root user
+RUN useradd --create-home --shell /bin/bash appuser
+
+WORKDIR /app
+
+# Copy deps first (layer caching)
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Copy app code
+COPY app.py .
+
+# Switch to non-root
+USER appuser
+
+EXPOSE 8000
+CMD ["python", "app.py"]
+```
+
+**Why these choices:**
+
+- `python:3.12-slim` β small footprint, reliable binary compatibility for common Python packages.
+- Non-root user β reduces privilege escalation risk and aligns with Kubernetes security defaults.
+- Install deps before copying code β leverages Docker layer cache so iterative code changes donβt reinstall deps.
+- `.dockerignore` β prevents sending large/unnecessary files to build context (faster builds, no secrets leaked).
+
+---
+
+## Best practices applied
+
+- **Non-root user (`USER appuser`)** β security: container escape yields an unprivileged user instead of host root.
+- **Pinned base image** (`python:3.12-slim`) β reproducibility and easier vulnerability tracking.
+- **Layer ordering**: `COPY requirements.txt` + `RUN pip install` before `COPY app.py` β speeds up rebuilds in CI and local dev.
+- **.dockerignore** β reduces build context size and prevents accidental inclusion of local secrets, venv or docs.
+- **Minimal base** (slim) β smaller attack surface and faster pulls.
+- **No runtime artifacts in image** (no `venv/`, `docs/`, `tests/`) β clean runtime image.
+
+---
+
+## Image information & decisions
+
+**Base image chosen:** `python:3.12-slim`
+**Reason:** balance of small size and compatibility; avoids musl/glibc issues that can appear on Alpine for some Python wheels.
+
+**Tag used:** `versceana/devops-info-service:lab02`
+
+**Final image size:**
+
+```
+REPOSITORY TAG SIZE
+devops-info-service lab02 457MB
+```
+
+---
+
+## Build & Run process β commands and saved outputs
+
+### How I built the image
+
+```bash
+# from app_python/
+docker build -t versceana/devops-info-service:lab02 . 2>&1 | tee docs/build_output.txt
+```
+
+### Build output (tail)
+
+```
+#10 8.244 Collecting itsdangerous>=2.2 (from Flask==3.1.0->-r requirements.txt (line 1))
+#10 8.308 Downloading itsdangerous-2.2.0-py3-none-any.whl.metadata (1.9 kB)
+#10 8.393 Collecting click>=8.1.3 (from Flask==3.1.0->-r requirements.txt (line 1))
+#10 8.452 Downloading click-8.3.1-py3-none-any.whl.metadata (2.6 kB)
+#10 8.517 Collecting blinker>=1.9 (from Flask==3.1.0->-r requirements.txt (line 1))
+#10 8.586 Downloading blinker-1.9.0-py3-none-any.whl.metadata (1.6 kB)
+#10 8.738 Collecting MarkupSafe>=2.0 (from Jinja2>=3.1.2->Flask==3.1.0->-r requirements.txt (line 1))
+#10 8.814 Downloading markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.metadata (2.7 kB)
+#10 8.884 Downloading flask-3.1.0-py3-none-any.whl (102 kB)
+#10 9.027 Downloading blinker-1.9.0-py3-none-any.whl (8.5 kB)
+#10 9.091 Downloading click-8.3.1-py3-none-any.whl (108 kB)
+#10 9.172 Downloading itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+#10 9.237 Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
+#10 9.321 Downloading markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (24 kB)
+#10 9.391 Downloading werkzeug-3.1.5-py3-none-any.whl (225 kB)
+#10 9.445 Installing collected packages: MarkupSafe, itsdangerous, click, blinker, Werkzeug, Jinja2, Flask
+#10 9.907
+#10 9.910 Successfully installed Flask-3.1.0 Jinja2-3.1.6 MarkupSafe-3.0.3 Werkzeug-3.1.5 blinker-1.9.0 click-8.3.1 itsdangerous-2.2.0
+#10 9.910 WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
+#10 DONE 10.4s
+
+#11 [7/8] COPY app.py .
+#11 DONE 0.0s
+
+#12 [8/8] RUN chown -R appuser:appgroup /app
+#12 DONE 0.2s
+
+#13 exporting to image
+#13 exporting layers
+#13 exporting layers 7.3s done
+#13 exporting manifest sha256:21510e1ea3021e5b4b871880b88467040fecda34575b62215d2630d96ea9df55 done
+#13 exporting config sha256:ae057c1a0b4faee60ec0d57053519e7d81aba7a32b4d66fe0cab58a5685a8a75 done
+#13 exporting attestation manifest sha256:261ce7665a093eebbd77c9bd681f591e7320e2d11e1277d4c0d1aa66fc026584 0.0s done
+#13 exporting manifest list sha256:0bdb7eed5b1a2d0c94182973a6883de99afdb9efbe26b1156d7c8d17b5469845 done
+#13 naming to docker.io/library/devops-info-service:lab02 done
+#13 unpacking to docker.io/library/devops-info-service:lab02
+#13 unpacking to docker.io/library/devops-info-service:lab02 1.6s done
+#13 DONE 9.0s
+
+View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/10yr646xr8e4bz24kpr8ynrq5
+```
+
+### How I ran & tested the container locally
+
+```bash
+# run mapping host 8000 -> container 8000 (app exposes 8000)
+docker run -d --name lab02_test -p 8000:8000 -e PORT=8000 versceana/devops-info-service:lab02
+
+# test endpoints
+curl -s http://127.0.0.1:8000/ | python3 -m json.tool > docs/lab02_curl_main.json
+curl -s http://127.0.0.1:8000/health | python3 -m json.tool > docs/lab02_curl_health.json
+
+# stop and remove when done
+docker stop lab02_test
+docker rm lab02_test
+```
+
+**Main endpoint output:**
+
+```
+{
+ "endpoints": [
+ {
+ "description": "Service information",
+ "method": "GET",
+ "path": "/"
+ },
+ {
+ "description": "Health check",
+ "method": "GET",
+ "path": "/health"
+ }
+ ],
+ "request": {
+ "client_ip": "192.168.65.1",
+ "method": "GET",
+ "path": "/",
+ "user_agent": "curl/8.7.1"
+ },
+ "runtime": {
+ "current_time": "2026-02-04T21:01:22.363918+00:00",
+ "timezone": "UTC",
+ "uptime_human": "0 hours, 0 minutes",
+ "uptime_seconds": 11
+ },
+ "service": {
+ "description": "DevOps course info service",
+ "framework": "Flask",
+ "name": "devops-info-service",
+ "version": "1.0.0"
+ },
+ "system": {
+ "architecture": "aarch64",
+ "cpu_count": 8,
+ "hostname": "92865388df9f",
+ "platform": "Linux",
+ "platform_version": "6.10.14-linuxkit",
+ "python_version": "3.13.11"
+ }
+}
+```
+
+**Health endpoint output:**
+
+```
+{
+ "status": "healthy",
+ "timestamp": "2026-02-04T21:01:34.750688+00:00",
+ "uptime_seconds": 24
+}
+```
+
+---
+
+## Docker Hub
+
+The URL here:
+
+```
+https://hub.docker.com/r/versceana/devops-info-service
+```
+
+**Push output:** docs/push_output.txt
+
+---
+
+## Challenges & Solutions
+
+- **Tag formatting error:** ensure Docker tag contains non-empty username (when pushing): `username/repo:tag`. If username not set, use local tag `devops-info-service:lab02`.
+- **Time constraint:** pushed only if Docker Hub was available and ready; otherwise included build logs for proof.
+
+---
+
+## How to reproduce
+
+```bash
+# from repository root
+cd app_python
+
+# build locally
+docker build -t .
+
+# run
+docker run --rm -p 8000:8000 -e PORT=8000
+
+# test
+curl -s http://127.0.0.1:8000/ | python -m json.tool
+curl -s http://127.0.0.1:8000/health | python -m json.tool
+```
+
+---
+
+## Conclusion
+
+This lab demonstrates practical application of the lecture: make containers small, secure, and cache-friendly. The Dockerfile provided is intentionally minimal so reviewers can quickly reproduce, inspect, and run the image.
+
+---
diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md
new file mode 100644
index 0000000000..07e5b02e67
--- /dev/null
+++ b/app_python/docs/LAB03.md
@@ -0,0 +1,101 @@
+# Lab 3 β CI/CD Pipeline
+
+**Name:** Diana Yakupova
+**Group:** B23-CBS-02
+**Date:** 2026-02-12
+
+---
+
+## 1. Overview
+
+- **Testing framework:** `pytest` β minimal boilerplate, powerful fixtures, industry standard.
+- **Tests cover:**
+ - `GET /` β HTTP 200, required JSON fields, service name and framework.
+ - `GET /health` β HTTP 200, `"status": "healthy"`, timestamp, uptime.
+ - `404` β returns JSON `{"error": "Not Found"}`.
+- **CI triggers:**
+ - `push` to `master` or `lab03`
+ - `pull_request` to `master`
+ - **Path filters:** only when `app_python/` or workflow file changes.
+- **Versioning strategy:** **Calendar Versioning (CalVer)** β format `YYYY.MM.DD`.
+ - Why? The app is a continuously delivered service, not a library. Dateβbased tags are trivial to automate and clearly ordered.
+
+---
+
+## 2. Workflow Evidence
+
+| Item | Link / Output |
+| -------------------------------------------------------------------------- | ------------- |
+| `https://github.com/versceana/DevOps-Core-Course/actions/runs/21964172563` |
+| β
**Local tests passing** |
+
+```
+$ pytest tests/ -v
+=============================================================================
+collected 4 items
+
+test_app.py::test_main_endpoint_status PASSED
+test_app.py::test_main_endpoint_json_structure PASSED
+test_app.py::test_health_endpoint PASSED
+test_app.py::test_404_error PASSED
+
+================================ 4 passed in 0.15s =============================
+```
+
+`https://hub.docker.com/repository/docker/versceana/devops-info-service/general`
+
+β
**Status badge in README**
+
+
+---
+
+## 3. Best Practices Implemented
+
+| Practice | Implementation | Benefit |
+| ---------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------ |
+| **Fail fast** | Workflow stops immediately on any failure | Saves CI minutes; prevents pushing broken code |
+| **Job dependencies** | `docker` job `needs: test` | Only tested images are published |
+| **Dependency caching** | `actions/setup-python` with `cache: pip` + Docker layer caching | Install time reduced from ~2 min β ~20 sec (6Γ faster) |
+| **Security scanning (Snyk)** | Snyk CLI installed via `npm`, explicit `snyk auth`, test with `--severity-threshold=high` | Catches vulnerable dependencies before deployment |
+
+**Snyk result:**
+No high or critical vulnerabilities found in `requirements.txt`.
+
+---
+
+## 4. Key Decisions
+
+- **Versioning strategy:** **CalVer** β releases are dateβbased, no need for semantic breakingβchange signaling.
+- **Docker tags:** `latest` (rolling) and `YYYY.MM.DD` (immutable).
+ - `latest` always points to the most recent build from `master`.
+ - Date tag allows pinning to a specific day's release.
+- **Workflow triggers:**
+ - Run tests on every push/PR to catch issues early.
+ - Push Docker images **only from the `master` branch** β avoids polluting registry with featureβbranch images.
+- **Test coverage:** Not measured in the main task (bonus adds coverage tracking). Current tests cover all endpoints and error cases.
+
+---
+
+## 5. Challenges & Solutions
+
+| Challenge | Solution |
+| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
+| **Port 5000 already in use on macOS** | Used `PORT=8000` locally; tests use Flask test client, no port conflict in CI. |
+| **Docker push skipped on feature branch** | Condition `if: github.ref == 'refs/heads/master'` β correct, merged `lab03` into `master` to trigger push. |
+| **Snyk could not find dependencies** | Added `pip install` before `snyk test`. |
+| **Snyk authentication error (401)** | Generated new Snyk API token, stored as `SNYK_TOKEN` secret, added explicit `snyk auth` step in workflow. |
+
+---
+
+## 6. Conclusion
+
+β
**All main tasks completed successfully:**
+
+- Unit tests written and passing.
+- GitHub Actions workflow with lint, test, security scan, and Docker build/push.
+- Dependency caching and Snyk integration.
+- Full documentation with evidence.
+
+The pipeline is now productionβready and will be extended in future labs (monitoring, Kubernetes, GitOps).
+
+---
diff --git a/app_python/docs/build_output.txt b/app_python/docs/build_output.txt
new file mode 100644
index 0000000000..14684d2e93
--- /dev/null
+++ b/app_python/docs/build_output.txt
@@ -0,0 +1,332 @@
+#0 building with "desktop-linux" instance using docker driver
+
+#1 [internal] load build definition from Dockerfile
+#1 transferring dockerfile: 657B 0.0s done
+#1 DONE 0.1s
+
+#2 [internal] load metadata for docker.io/library/python:3.13-slim
+#2 DONE 9.5s
+
+#3 [internal] load .dockerignore
+#3 transferring context: 144B done
+#3 DONE 0.0s
+
+#4 [internal] load build context
+#4 transferring context: 3.91kB done
+#4 DONE 0.0s
+
+#5 [1/8] FROM docker.io/library/python:3.13-slim@sha256:2b9c9803c6a287cafa0a8c917211dddd23dcd2016f049690ee5219f5d3f1636e
+#5 resolve docker.io/library/python:3.13-slim@sha256:2b9c9803c6a287cafa0a8c917211dddd23dcd2016f049690ee5219f5d3f1636e 0.0s done
+#5 DONE 0.0s
+
+#5 [1/8] FROM docker.io/library/python:3.13-slim@sha256:2b9c9803c6a287cafa0a8c917211dddd23dcd2016f049690ee5219f5d3f1636e
+#5 sha256:97fc85b49690b12f13f53067a3190e231790ff42832ff5f39e97042fc4d4ede6 0B / 250B 0.2s
+#5 sha256:a6866fe8c3d2436d6a24f7d829aca8349726c5c198725f763a40e2e4263a53e6 0B / 11.72MB 0.2s
+#5 sha256:fe9a90620d58e0d94bd1a536412e60ddaff85c045f729197536cb8a382e1c5a2 0B / 1.27MB 0.2s
+#5 sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 0B / 30.14MB 0.2s
+#5 sha256:a6866fe8c3d2436d6a24f7d829aca8349726c5c198725f763a40e2e4263a53e6 1.05MB / 11.72MB 0.6s
+#5 sha256:97fc85b49690b12f13f53067a3190e231790ff42832ff5f39e97042fc4d4ede6 250B / 250B 0.7s done
+#5 sha256:a6866fe8c3d2436d6a24f7d829aca8349726c5c198725f763a40e2e4263a53e6 2.10MB / 11.72MB 0.8s
+#5 sha256:a6866fe8c3d2436d6a24f7d829aca8349726c5c198725f763a40e2e4263a53e6 2.89MB / 11.72MB 1.4s
+#5 sha256:fe9a90620d58e0d94bd1a536412e60ddaff85c045f729197536cb8a382e1c5a2 1.27MB / 1.27MB 1.9s done
+#5 sha256:a6866fe8c3d2436d6a24f7d829aca8349726c5c198725f763a40e2e4263a53e6 4.19MB / 11.72MB 2.1s
+#5 sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 2.10MB / 30.14MB 2.4s
+#5 sha256:a6866fe8c3d2436d6a24f7d829aca8349726c5c198725f763a40e2e4263a53e6 5.23MB / 11.72MB 2.7s
+#5 sha256:a6866fe8c3d2436d6a24f7d829aca8349726c5c198725f763a40e2e4263a53e6 6.29MB / 11.72MB 3.3s
+#5 sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 4.19MB / 30.14MB 3.5s
+#5 sha256:a6866fe8c3d2436d6a24f7d829aca8349726c5c198725f763a40e2e4263a53e6 7.34MB / 11.72MB 4.1s
+#5 sha256:a6866fe8c3d2436d6a24f7d829aca8349726c5c198725f763a40e2e4263a53e6 8.39MB / 11.72MB 4.7s
+#5 sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 6.29MB / 30.14MB 4.7s
+#5 sha256:a6866fe8c3d2436d6a24f7d829aca8349726c5c198725f763a40e2e4263a53e6 8.98MB / 11.72MB 5.1s
+#5 sha256:a6866fe8c3d2436d6a24f7d829aca8349726c5c198725f763a40e2e4263a53e6 10.49MB / 11.72MB 5.6s
+#5 sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 8.33MB / 30.14MB 5.7s
+#5 sha256:a6866fe8c3d2436d6a24f7d829aca8349726c5c198725f763a40e2e4263a53e6 11.72MB / 11.72MB 6.2s
+#5 sha256:a6866fe8c3d2436d6a24f7d829aca8349726c5c198725f763a40e2e4263a53e6 11.72MB / 11.72MB 6.2s done
+#5 sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 10.49MB / 30.14MB 6.6s
+#5 sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 12.58MB / 30.14MB 7.1s
+#5 sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 14.68MB / 30.14MB 7.7s
+#5 sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 16.78MB / 30.14MB 8.3s
+#5 sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 18.87MB / 30.14MB 8.9s
+#5 sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 20.97MB / 30.14MB 9.3s
+#5 sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 23.07MB / 30.14MB 9.9s
+#5 sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 25.17MB / 30.14MB 10.5s
+#5 sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 27.26MB / 30.14MB 11.1s
+#5 sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 29.36MB / 30.14MB 11.6s
+#5 sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 30.14MB / 30.14MB 11.8s done
+#5 extracting sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c
+#5 extracting sha256:3ea009573b472d108af9af31ec35a06fe3649084f6611cf11f7d594b85cf7a7c 4.5s done
+#5 DONE 16.3s
+
+#5 [1/8] FROM docker.io/library/python:3.13-slim@sha256:2b9c9803c6a287cafa0a8c917211dddd23dcd2016f049690ee5219f5d3f1636e
+#5 extracting sha256:fe9a90620d58e0d94bd1a536412e60ddaff85c045f729197536cb8a382e1c5a2
+#5 extracting sha256:fe9a90620d58e0d94bd1a536412e60ddaff85c045f729197536cb8a382e1c5a2 0.2s done
+#5 DONE 16.5s
+
+#5 [1/8] FROM docker.io/library/python:3.13-slim@sha256:2b9c9803c6a287cafa0a8c917211dddd23dcd2016f049690ee5219f5d3f1636e
+#5 extracting sha256:a6866fe8c3d2436d6a24f7d829aca8349726c5c198725f763a40e2e4263a53e6
+#5 extracting sha256:a6866fe8c3d2436d6a24f7d829aca8349726c5c198725f763a40e2e4263a53e6 2.3s done
+#5 DONE 18.9s
+
+#5 [1/8] FROM docker.io/library/python:3.13-slim@sha256:2b9c9803c6a287cafa0a8c917211dddd23dcd2016f049690ee5219f5d3f1636e
+#5 extracting sha256:97fc85b49690b12f13f53067a3190e231790ff42832ff5f39e97042fc4d4ede6 0.0s done
+#5 DONE 18.9s
+
+#6 [2/8] RUN apt-get update && apt-get install -y --no-install-recommends gcc ca-certificates && rm -rf /var/lib/apt/lists/*
+#6 8.017 Hit:1 http://deb.debian.org/debian trixie InRelease
+#6 8.082 Get:2 http://deb.debian.org/debian trixie-updates InRelease [47.3 kB]
+#6 8.194 Get:3 http://deb.debian.org/debian-security trixie-security InRelease [43.4 kB]
+#6 8.277 Get:4 http://deb.debian.org/debian trixie/main arm64 Packages [9607 kB]
+#6 10.68 Get:5 http://deb.debian.org/debian trixie-updates/main arm64 Packages [5404 B]
+#6 10.74 Get:6 http://deb.debian.org/debian-security trixie-security/main arm64 Packages [99.0 kB]
+#6 12.65 Fetched 9802 kB in 10s (977 kB/s)
+#6 12.65 Reading package lists...
+#6 13.29 Reading package lists...
+#6 13.93 Building dependency tree...
+#6 14.13 Reading state information...
+#6 14.47 ca-certificates is already the newest version (20250419).
+#6 14.47 The following additional packages will be installed:
+#6 14.47 binutils binutils-aarch64-linux-gnu binutils-common cpp cpp-14
+#6 14.47 cpp-14-aarch64-linux-gnu cpp-aarch64-linux-gnu gcc-14
+#6 14.47 gcc-14-aarch64-linux-gnu gcc-aarch64-linux-gnu libasan8 libatomic1
+#6 14.47 libbinutils libcc1-0 libctf-nobfd0 libctf0 libgcc-14-dev libgomp1
+#6 14.47 libgprofng0 libhwasan0 libisl23 libitm1 libjansson4 liblsan0 libmpc3
+#6 14.47 libmpfr6 libsframe1 libtsan2 libubsan1
+#6 14.47 Suggested packages:
+#6 14.47 binutils-doc gprofng-gui binutils-gold cpp-doc gcc-14-locales cpp-14-doc
+#6 14.47 gcc-multilib make manpages-dev autoconf automake libtool flex bison gdb
+#6 14.47 gcc-doc gcc-14-doc gdb-aarch64-linux-gnu
+#6 14.47 Recommended packages:
+#6 14.47 libc6-dev | libc-dev libc6-dev libc-dev
+#6 14.69 The following NEW packages will be installed:
+#6 14.69 binutils binutils-aarch64-linux-gnu binutils-common cpp cpp-14
+#6 14.69 cpp-14-aarch64-linux-gnu cpp-aarch64-linux-gnu gcc gcc-14
+#6 14.69 gcc-14-aarch64-linux-gnu gcc-aarch64-linux-gnu libasan8 libatomic1
+#6 14.69 libbinutils libcc1-0 libctf-nobfd0 libctf0 libgcc-14-dev libgomp1
+#6 14.69 libgprofng0 libhwasan0 libisl23 libitm1 libjansson4 liblsan0 libmpc3
+#6 14.69 libmpfr6 libsframe1 libtsan2 libubsan1
+#6 15.02 0 upgraded, 30 newly installed, 0 to remove and 0 not upgraded.
+#6 15.02 Need to get 45.2 MB of archives.
+#6 15.02 After this operation, 182 MB of additional disk space will be used.
+#6 15.02 Get:1 http://deb.debian.org/debian trixie/main arm64 libsframe1 arm64 2.44-3 [77.8 kB]
+#6 15.18 Get:2 http://deb.debian.org/debian trixie/main arm64 binutils-common arm64 2.44-3 [2509 kB]
+#6 15.65 Get:3 http://deb.debian.org/debian trixie/main arm64 libbinutils arm64 2.44-3 [660 kB]
+#6 15.82 Get:4 http://deb.debian.org/debian trixie/main arm64 libgprofng0 arm64 2.44-3 [668 kB]
+#6 16.00 Get:5 http://deb.debian.org/debian trixie/main arm64 libctf-nobfd0 arm64 2.44-3 [152 kB]
+#6 16.04 Get:6 http://deb.debian.org/debian trixie/main arm64 libctf0 arm64 2.44-3 [84.2 kB]
+#6 16.07 Get:7 http://deb.debian.org/debian trixie/main arm64 libjansson4 arm64 2.14-2+b3 [39.2 kB]
+#6 16.08 Get:8 http://deb.debian.org/debian trixie/main arm64 binutils-aarch64-linux-gnu arm64 2.44-3 [820 kB]
+#6 16.29 Get:9 http://deb.debian.org/debian trixie/main arm64 binutils arm64 2.44-3 [262 kB]
+#6 16.36 Get:10 http://deb.debian.org/debian trixie/main arm64 libisl23 arm64 0.27-1 [601 kB]
+#6 16.52 Get:11 http://deb.debian.org/debian trixie/main arm64 libmpfr6 arm64 4.2.2-1 [685 kB]
+#6 16.71 Get:12 http://deb.debian.org/debian trixie/main arm64 libmpc3 arm64 1.3.1-1+b3 [50.5 kB]
+#6 16.72 Get:13 http://deb.debian.org/debian trixie/main arm64 cpp-14-aarch64-linux-gnu arm64 14.2.0-19 [9169 kB]
+#6 19.17 Get:14 http://deb.debian.org/debian trixie/main arm64 cpp-14 arm64 14.2.0-19 [1276 B]
+#6 19.17 Get:15 http://deb.debian.org/debian trixie/main arm64 cpp-aarch64-linux-gnu arm64 4:14.2.0-1 [4832 B]
+#6 19.17 Get:16 http://deb.debian.org/debian trixie/main arm64 cpp arm64 4:14.2.0-1 [1568 B]
+#6 19.17 Get:17 http://deb.debian.org/debian trixie/main arm64 libcc1-0 arm64 14.2.0-19 [42.2 kB]
+#6 19.17 Get:18 http://deb.debian.org/debian trixie/main arm64 libgomp1 arm64 14.2.0-19 [124 kB]
+#6 19.22 Get:19 http://deb.debian.org/debian trixie/main arm64 libitm1 arm64 14.2.0-19 [24.2 kB]
+#6 19.22 Get:20 http://deb.debian.org/debian trixie/main arm64 libatomic1 arm64 14.2.0-19 [10.1 kB]
+#6 19.22 Get:21 http://deb.debian.org/debian trixie/main arm64 libasan8 arm64 14.2.0-19 [2578 kB]
+#6 19.90 Get:22 http://deb.debian.org/debian trixie/main arm64 liblsan0 arm64 14.2.0-19 [1161 kB]
+#6 20.22 Get:23 http://deb.debian.org/debian trixie/main arm64 libtsan2 arm64 14.2.0-19 [2383 kB]
+#6 20.85 Get:24 http://deb.debian.org/debian trixie/main arm64 libubsan1 arm64 14.2.0-19 [1039 kB]
+#6 21.13 Get:25 http://deb.debian.org/debian trixie/main arm64 libhwasan0 arm64 14.2.0-19 [1442 kB]
+#6 21.52 Get:26 http://deb.debian.org/debian trixie/main arm64 libgcc-14-dev arm64 14.2.0-19 [2359 kB]
+#6 22.16 Get:27 http://deb.debian.org/debian trixie/main arm64 gcc-14-aarch64-linux-gnu arm64 14.2.0-19 [17.7 MB]
+#6 26.86 Get:28 http://deb.debian.org/debian trixie/main arm64 gcc-14 arm64 14.2.0-19 [529 kB]
+#6 26.99 Get:29 http://deb.debian.org/debian trixie/main arm64 gcc-aarch64-linux-gnu arm64 4:14.2.0-1 [1440 B]
+#6 26.99 Get:30 http://deb.debian.org/debian trixie/main arm64 gcc arm64 4:14.2.0-1 [5136 B]
+#6 29.03 Fetched 45.2 MB in 12s (3687 kB/s)
+#6 29.08 Selecting previously unselected package libsframe1:arm64.
+#6 29.08 (Reading database ...
(Reading database ... 5%
(Reading database ... 10%
(Reading database ... 15%
(Reading database ... 20%
(Reading database ... 25%
(Reading database ... 30%
(Reading database ... 35%
(Reading database ... 40%
(Reading database ... 45%
(Reading database ... 50%
(Reading database ... 55%
(Reading database ... 60%
(Reading database ... 65%
(Reading database ... 70%
(Reading database ... 75%
(Reading database ... 80%
(Reading database ... 85%
(Reading database ... 90%
(Reading database ... 95%
(Reading database ... 100%
(Reading database ... 5643 files and directories currently installed.)
+#6 29.11 Preparing to unpack .../00-libsframe1_2.44-3_arm64.deb ...
+#6 29.12 Unpacking libsframe1:arm64 (2.44-3) ...
+#6 29.16 Selecting previously unselected package binutils-common:arm64.
+#6 29.16 Preparing to unpack .../01-binutils-common_2.44-3_arm64.deb ...
+#6 29.16 Unpacking binutils-common:arm64 (2.44-3) ...
+#6 29.50 Selecting previously unselected package libbinutils:arm64.
+#6 29.50 Preparing to unpack .../02-libbinutils_2.44-3_arm64.deb ...
+#6 29.51 Unpacking libbinutils:arm64 (2.44-3) ...
+#6 29.59 Selecting previously unselected package libgprofng0:arm64.
+#6 29.59 Preparing to unpack .../03-libgprofng0_2.44-3_arm64.deb ...
+#6 29.59 Unpacking libgprofng0:arm64 (2.44-3) ...
+#6 29.68 Selecting previously unselected package libctf-nobfd0:arm64.
+#6 29.68 Preparing to unpack .../04-libctf-nobfd0_2.44-3_arm64.deb ...
+#6 29.69 Unpacking libctf-nobfd0:arm64 (2.44-3) ...
+#6 29.71 Selecting previously unselected package libctf0:arm64.
+#6 29.72 Preparing to unpack .../05-libctf0_2.44-3_arm64.deb ...
+#6 29.72 Unpacking libctf0:arm64 (2.44-3) ...
+#6 29.75 Selecting previously unselected package libjansson4:arm64.
+#6 29.75 Preparing to unpack .../06-libjansson4_2.14-2+b3_arm64.deb ...
+#6 29.76 Unpacking libjansson4:arm64 (2.14-2+b3) ...
+#6 29.78 Selecting previously unselected package binutils-aarch64-linux-gnu.
+#6 29.78 Preparing to unpack .../07-binutils-aarch64-linux-gnu_2.44-3_arm64.deb ...
+#6 29.79 Unpacking binutils-aarch64-linux-gnu (2.44-3) ...
+#6 30.13 Selecting previously unselected package binutils.
+#6 30.13 Preparing to unpack .../08-binutils_2.44-3_arm64.deb ...
+#6 30.14 Unpacking binutils (2.44-3) ...
+#6 30.21 Selecting previously unselected package libisl23:arm64.
+#6 30.21 Preparing to unpack .../09-libisl23_0.27-1_arm64.deb ...
+#6 30.21 Unpacking libisl23:arm64 (0.27-1) ...
+#6 30.29 Selecting previously unselected package libmpfr6:arm64.
+#6 30.29 Preparing to unpack .../10-libmpfr6_4.2.2-1_arm64.deb ...
+#6 30.30 Unpacking libmpfr6:arm64 (4.2.2-1) ...
+#6 30.35 Selecting previously unselected package libmpc3:arm64.
+#6 30.35 Preparing to unpack .../11-libmpc3_1.3.1-1+b3_arm64.deb ...
+#6 30.35 Unpacking libmpc3:arm64 (1.3.1-1+b3) ...
+#6 30.37 Selecting previously unselected package cpp-14-aarch64-linux-gnu.
+#6 30.37 Preparing to unpack .../12-cpp-14-aarch64-linux-gnu_14.2.0-19_arm64.deb ...
+#6 30.37 Unpacking cpp-14-aarch64-linux-gnu (14.2.0-19) ...
+#6 31.22 Selecting previously unselected package cpp-14.
+#6 31.23 Preparing to unpack .../13-cpp-14_14.2.0-19_arm64.deb ...
+#6 31.23 Unpacking cpp-14 (14.2.0-19) ...
+#6 31.25 Selecting previously unselected package cpp-aarch64-linux-gnu.
+#6 31.25 Preparing to unpack .../14-cpp-aarch64-linux-gnu_4%3a14.2.0-1_arm64.deb ...
+#6 31.25 Unpacking cpp-aarch64-linux-gnu (4:14.2.0-1) ...
+#6 31.28 Selecting previously unselected package cpp.
+#6 31.28 Preparing to unpack .../15-cpp_4%3a14.2.0-1_arm64.deb ...
+#6 31.28 Unpacking cpp (4:14.2.0-1) ...
+#6 31.30 Selecting previously unselected package libcc1-0:arm64.
+#6 31.31 Preparing to unpack .../16-libcc1-0_14.2.0-19_arm64.deb ...
+#6 31.31 Unpacking libcc1-0:arm64 (14.2.0-19) ...
+#6 31.33 Selecting previously unselected package libgomp1:arm64.
+#6 31.33 Preparing to unpack .../17-libgomp1_14.2.0-19_arm64.deb ...
+#6 31.33 Unpacking libgomp1:arm64 (14.2.0-19) ...
+#6 31.36 Selecting previously unselected package libitm1:arm64.
+#6 31.36 Preparing to unpack .../18-libitm1_14.2.0-19_arm64.deb ...
+#6 31.36 Unpacking libitm1:arm64 (14.2.0-19) ...
+#6 31.38 Selecting previously unselected package libatomic1:arm64.
+#6 31.38 Preparing to unpack .../19-libatomic1_14.2.0-19_arm64.deb ...
+#6 31.39 Unpacking libatomic1:arm64 (14.2.0-19) ...
+#6 31.41 Selecting previously unselected package libasan8:arm64.
+#6 31.41 Preparing to unpack .../20-libasan8_14.2.0-19_arm64.deb ...
+#6 31.41 Unpacking libasan8:arm64 (14.2.0-19) ...
+#6 31.66 Selecting previously unselected package liblsan0:arm64.
+#6 31.66 Preparing to unpack .../21-liblsan0_14.2.0-19_arm64.deb ...
+#6 31.67 Unpacking liblsan0:arm64 (14.2.0-19) ...
+#6 31.79 Selecting previously unselected package libtsan2:arm64.
+#6 31.79 Preparing to unpack .../22-libtsan2_14.2.0-19_arm64.deb ...
+#6 31.79 Unpacking libtsan2:arm64 (14.2.0-19) ...
+#6 32.21 Selecting previously unselected package libubsan1:arm64.
+#6 32.21 Preparing to unpack .../23-libubsan1_14.2.0-19_arm64.deb ...
+#6 32.21 Unpacking libubsan1:arm64 (14.2.0-19) ...
+#6 32.32 Selecting previously unselected package libhwasan0:arm64.
+#6 32.32 Preparing to unpack .../24-libhwasan0_14.2.0-19_arm64.deb ...
+#6 32.32 Unpacking libhwasan0:arm64 (14.2.0-19) ...
+#6 32.49 Selecting previously unselected package libgcc-14-dev:arm64.
+#6 32.49 Preparing to unpack .../25-libgcc-14-dev_14.2.0-19_arm64.deb ...
+#6 32.50 Unpacking libgcc-14-dev:arm64 (14.2.0-19) ...
+#6 32.89 Selecting previously unselected package gcc-14-aarch64-linux-gnu.
+#6 32.90 Preparing to unpack .../26-gcc-14-aarch64-linux-gnu_14.2.0-19_arm64.deb ...
+#6 32.90 Unpacking gcc-14-aarch64-linux-gnu (14.2.0-19) ...
+#6 35.24 Selecting previously unselected package gcc-14.
+#6 35.25 Preparing to unpack .../27-gcc-14_14.2.0-19_arm64.deb ...
+#6 35.27 Unpacking gcc-14 (14.2.0-19) ...
+#6 35.45 Selecting previously unselected package gcc-aarch64-linux-gnu.
+#6 35.45 Preparing to unpack .../28-gcc-aarch64-linux-gnu_4%3a14.2.0-1_arm64.deb ...
+#6 35.46 Unpacking gcc-aarch64-linux-gnu (4:14.2.0-1) ...
+#6 35.58 Selecting previously unselected package gcc.
+#6 35.58 Preparing to unpack .../29-gcc_4%3a14.2.0-1_arm64.deb ...
+#6 35.59 Unpacking gcc (4:14.2.0-1) ...
+#6 35.72 Setting up binutils-common:arm64 (2.44-3) ...
+#6 35.73 Setting up libctf-nobfd0:arm64 (2.44-3) ...
+#6 35.74 Setting up libgomp1:arm64 (14.2.0-19) ...
+#6 35.75 Setting up libsframe1:arm64 (2.44-3) ...
+#6 35.76 Setting up libjansson4:arm64 (2.14-2+b3) ...
+#6 35.77 Setting up libmpfr6:arm64 (4.2.2-1) ...
+#6 35.77 Setting up libmpc3:arm64 (1.3.1-1+b3) ...
+#6 35.78 Setting up libatomic1:arm64 (14.2.0-19) ...
+#6 35.79 Setting up libubsan1:arm64 (14.2.0-19) ...
+#6 35.81 Setting up libhwasan0:arm64 (14.2.0-19) ...
+#6 35.82 Setting up libasan8:arm64 (14.2.0-19) ...
+#6 35.83 Setting up libtsan2:arm64 (14.2.0-19) ...
+#6 35.84 Setting up libbinutils:arm64 (2.44-3) ...
+#6 35.85 Setting up libisl23:arm64 (0.27-1) ...
+#6 35.86 Setting up libcc1-0:arm64 (14.2.0-19) ...
+#6 35.88 Setting up liblsan0:arm64 (14.2.0-19) ...
+#6 35.89 Setting up libitm1:arm64 (14.2.0-19) ...
+#6 35.92 Setting up libctf0:arm64 (2.44-3) ...
+#6 35.93 Setting up binutils-aarch64-linux-gnu (2.44-3) ...
+#6 35.93 Setting up libgprofng0:arm64 (2.44-3) ...
+#6 35.94 Setting up cpp-14-aarch64-linux-gnu (14.2.0-19) ...
+#6 35.95 Setting up libgcc-14-dev:arm64 (14.2.0-19) ...
+#6 35.96 Setting up binutils (2.44-3) ...
+#6 35.97 Setting up cpp-aarch64-linux-gnu (4:14.2.0-1) ...
+#6 35.98 Setting up cpp-14 (14.2.0-19) ...
+#6 35.99 Setting up cpp (4:14.2.0-1) ...
+#6 36.04 Setting up gcc-14-aarch64-linux-gnu (14.2.0-19) ...
+#6 36.05 Setting up gcc-aarch64-linux-gnu (4:14.2.0-1) ...
+#6 36.06 Setting up gcc-14 (14.2.0-19) ...
+#6 36.08 Setting up gcc (4:14.2.0-1) ...
+#6 36.19 Processing triggers for libc-bin (2.41-12+deb13u1) ...
+#6 DONE 37.2s
+
+#7 [3/8] RUN groupadd -r appgroup && useradd -r -g appgroup -d /home/appuser -m -s /sbin/nologin appuser
+#7 DONE 0.9s
+
+#8 [4/8] WORKDIR /app
+#8 DONE 0.0s
+
+#9 [5/8] COPY requirements.txt .
+#9 DONE 0.0s
+
+#10 [6/8] RUN python -m pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt
+#10 2.404 Requirement already satisfied: pip in /usr/local/lib/python3.13/site-packages (25.3)
+#10 2.935 Collecting pip
+#10 3.570 Downloading pip-26.0-py3-none-any.whl.metadata (4.7 kB)
+#10 3.663 Downloading pip-26.0-py3-none-any.whl (1.8 MB)
+#10 4.116 ββββββββββββββββββββββββββββββββββββββββ 1.8/1.8 MB 6.2 MB/s 0:00:00
+#10 4.143 Installing collected packages: pip
+#10 4.143 Attempting uninstall: pip
+#10 4.148 Found existing installation: pip 25.3
+#10 4.325 Uninstalling pip-25.3:
+#10 5.017 Successfully uninstalled pip-25.3
+#10 6.206 Successfully installed pip-26.0
+#10 6.207 WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
+#10 7.596 Collecting Flask==3.1.0 (from -r requirements.txt (line 1))
+#10 7.863 Downloading flask-3.1.0-py3-none-any.whl.metadata (2.7 kB)
+#10 7.957 Collecting Werkzeug>=3.1 (from Flask==3.1.0->-r requirements.txt (line 1))
+#10 8.021 Downloading werkzeug-3.1.5-py3-none-any.whl.metadata (4.0 kB)
+#10 8.101 Collecting Jinja2>=3.1.2 (from Flask==3.1.0->-r requirements.txt (line 1))
+#10 8.169 Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB)
+#10 8.244 Collecting itsdangerous>=2.2 (from Flask==3.1.0->-r requirements.txt (line 1))
+#10 8.308 Downloading itsdangerous-2.2.0-py3-none-any.whl.metadata (1.9 kB)
+#10 8.393 Collecting click>=8.1.3 (from Flask==3.1.0->-r requirements.txt (line 1))
+#10 8.452 Downloading click-8.3.1-py3-none-any.whl.metadata (2.6 kB)
+#10 8.517 Collecting blinker>=1.9 (from Flask==3.1.0->-r requirements.txt (line 1))
+#10 8.586 Downloading blinker-1.9.0-py3-none-any.whl.metadata (1.6 kB)
+#10 8.738 Collecting MarkupSafe>=2.0 (from Jinja2>=3.1.2->Flask==3.1.0->-r requirements.txt (line 1))
+#10 8.814 Downloading markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl.metadata (2.7 kB)
+#10 8.884 Downloading flask-3.1.0-py3-none-any.whl (102 kB)
+#10 9.027 Downloading blinker-1.9.0-py3-none-any.whl (8.5 kB)
+#10 9.091 Downloading click-8.3.1-py3-none-any.whl (108 kB)
+#10 9.172 Downloading itsdangerous-2.2.0-py3-none-any.whl (16 kB)
+#10 9.237 Downloading jinja2-3.1.6-py3-none-any.whl (134 kB)
+#10 9.321 Downloading markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl (24 kB)
+#10 9.391 Downloading werkzeug-3.1.5-py3-none-any.whl (225 kB)
+#10 9.445 Installing collected packages: MarkupSafe, itsdangerous, click, blinker, Werkzeug, Jinja2, Flask
+#10 9.907
+#10 9.910 Successfully installed Flask-3.1.0 Jinja2-3.1.6 MarkupSafe-3.0.3 Werkzeug-3.1.5 blinker-1.9.0 click-8.3.1 itsdangerous-2.2.0
+#10 9.910 WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager, possibly rendering your system unusable. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv. Use the --root-user-action option if you know what you are doing and want to suppress this warning.
+#10 DONE 10.4s
+
+#11 [7/8] COPY app.py .
+#11 DONE 0.0s
+
+#12 [8/8] RUN chown -R appuser:appgroup /app
+#12 DONE 0.2s
+
+#13 exporting to image
+#13 exporting layers
+#13 exporting layers 7.3s done
+#13 exporting manifest sha256:21510e1ea3021e5b4b871880b88467040fecda34575b62215d2630d96ea9df55 done
+#13 exporting config sha256:ae057c1a0b4faee60ec0d57053519e7d81aba7a32b4d66fe0cab58a5685a8a75 done
+#13 exporting attestation manifest sha256:261ce7665a093eebbd77c9bd681f591e7320e2d11e1277d4c0d1aa66fc026584 0.0s done
+#13 exporting manifest list sha256:0bdb7eed5b1a2d0c94182973a6883de99afdb9efbe26b1156d7c8d17b5469845 done
+#13 naming to docker.io/library/devops-info-service:lab02 done
+#13 unpacking to docker.io/library/devops-info-service:lab02
+#13 unpacking to docker.io/library/devops-info-service:lab02 1.6s done
+#13 DONE 9.0s
+
+View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/10yr646xr8e4bz24kpr8ynrq5
diff --git a/app_python/docs/docker_hub_url.txt b/app_python/docs/docker_hub_url.txt
new file mode 100644
index 0000000000..16f82597b7
--- /dev/null
+++ b/app_python/docs/docker_hub_url.txt
@@ -0,0 +1 @@
+https://hub.docker.com/r//devops-info-service
diff --git a/app_python/docs/image_info.txt b/app_python/docs/image_info.txt
new file mode 100644
index 0000000000..b832dda900
--- /dev/null
+++ b/app_python/docs/image_info.txt
@@ -0,0 +1,2 @@
+REPOSITORY TAG SIZE
+devops-info-service lab02 457MB
diff --git a/app_python/docs/lab02_curl_health.json b/app_python/docs/lab02_curl_health.json
new file mode 100644
index 0000000000..eeb74562ca
--- /dev/null
+++ b/app_python/docs/lab02_curl_health.json
@@ -0,0 +1,5 @@
+{
+ "status": "healthy",
+ "timestamp": "2026-02-04T21:01:34.750688+00:00",
+ "uptime_seconds": 24
+}
diff --git a/app_python/docs/lab02_curl_main.json b/app_python/docs/lab02_curl_main.json
new file mode 100644
index 0000000000..0071699bbd
--- /dev/null
+++ b/app_python/docs/lab02_curl_main.json
@@ -0,0 +1,40 @@
+{
+ "endpoints": [
+ {
+ "description": "Service information",
+ "method": "GET",
+ "path": "/"
+ },
+ {
+ "description": "Health check",
+ "method": "GET",
+ "path": "/health"
+ }
+ ],
+ "request": {
+ "client_ip": "192.168.65.1",
+ "method": "GET",
+ "path": "/",
+ "user_agent": "curl/8.7.1"
+ },
+ "runtime": {
+ "current_time": "2026-02-04T21:01:22.363918+00:00",
+ "timezone": "UTC",
+ "uptime_human": "0 hours, 0 minutes",
+ "uptime_seconds": 11
+ },
+ "service": {
+ "description": "DevOps course info service",
+ "framework": "Flask",
+ "name": "devops-info-service",
+ "version": "1.0.0"
+ },
+ "system": {
+ "architecture": "aarch64",
+ "cpu_count": 8,
+ "hostname": "92865388df9f",
+ "platform": "Linux",
+ "platform_version": "6.10.14-linuxkit",
+ "python_version": "3.13.11"
+ }
+}
diff --git a/app_python/docs/push_output.txt b/app_python/docs/push_output.txt
new file mode 100644
index 0000000000..ec1ed7b854
--- /dev/null
+++ b/app_python/docs/push_output.txt
@@ -0,0 +1,288 @@
+The push refers to repository [docker.io/versceana/devops-info-service]
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+0a9eaad638a4: Waiting
+3ea009573b47: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+a6866fe8c3d2: Waiting
+97fc85b49690: Waiting
+a442129b4195: Waiting
+fe9a90620d58: Waiting
+88747c24f5a3: Waiting
+8a73290aa36d: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+88747c24f5a3: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+d878836394b3: Waiting
+e0486f87d018: Waiting
+e4d6198a1cf3: Waiting
+557e876784bf: Waiting
+0a9eaad638a4: Waiting
+d878836394b3: Waiting
+97fc85b49690: Pushed
+8a73290aa36d: Pushed
+e0486f87d018: Pushed
+88747c24f5a3: Pushed
+e4d6198a1cf3: Pushed
+0a9eaad638a4: Pushed
+d878836394b3: Pushed
+fe9a90620d58: Pushed
+a442129b4195: Pushed
+a6866fe8c3d2: Pushed
+3ea009573b47: Pushed
+557e876784bf: Pushed
+lab02: digest: sha256:0bdb7eed5b1a2d0c94182973a6883de99afdb9efbe26b1156d7c8d17b5469845 size: 856
diff --git a/app_python/docs/screenshots/01-startup.png b/app_python/docs/screenshots/01-startup.png
new file mode 100644
index 0000000000..abbf4e9388
Binary files /dev/null and b/app_python/docs/screenshots/01-startup.png differ
diff --git a/app_python/docs/screenshots/02-main-endpoint.png b/app_python/docs/screenshots/02-main-endpoint.png
new file mode 100644
index 0000000000..220d7e9846
Binary files /dev/null and b/app_python/docs/screenshots/02-main-endpoint.png differ
diff --git a/app_python/docs/screenshots/03-health-check.png b/app_python/docs/screenshots/03-health-check.png
new file mode 100644
index 0000000000..a328ee0f36
Binary files /dev/null and b/app_python/docs/screenshots/03-health-check.png differ
diff --git a/app_python/requirements.txt b/app_python/requirements.txt
new file mode 100644
index 0000000000..505159fd45
--- /dev/null
+++ b/app_python/requirements.txt
@@ -0,0 +1,2 @@
+Flask==3.1.0
+pytest==8.3.4
diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/app_python/tests/test_app.py b/app_python/tests/test_app.py
new file mode 100644
index 0000000000..4bc23a3020
--- /dev/null
+++ b/app_python/tests/test_app.py
@@ -0,0 +1,45 @@
+import pytest
+import sys
+import os
+sys.path.insert(0, os.path.abspath(os.path.dirname(__file__) + "/.."))
+from app import app
+
+@pytest.fixture
+def client():
+ app.config['TESTING'] = True
+ with app.test_client() as client:
+ yield client
+
+def test_main_endpoint_status(client):
+ """Test that main endpoint returns 200"""
+ rv = client.get('/')
+ assert rv.status_code == 200
+
+def test_main_endpoint_json_structure(client):
+ """Test that main endpoint contains all required fields"""
+ rv = client.get('/')
+ json_data = rv.get_json()
+ assert 'service' in json_data
+ assert 'system' in json_data
+ assert 'runtime' in json_data
+ assert 'request' in json_data
+ assert 'endpoints' in json_data
+ assert json_data['service']['name'] == 'devops-info-service'
+ assert json_data['service']['framework'] == 'Flask'
+
+def test_health_endpoint(client):
+ """Test /health returns healthy status"""
+ rv = client.get('/health')
+ assert rv.status_code == 200
+ json_data = rv.get_json()
+ assert json_data['status'] == 'healthy'
+ assert 'timestamp' in json_data
+ assert 'uptime_seconds' in json_data
+
+def test_404_error(client):
+ """Test non-existent endpoint returns 404 JSON"""
+ rv = client.get('/nonexistent')
+ assert rv.status_code == 404
+ json_data = rv.get_json()
+ assert 'error' in json_data
+ assert json_data['error'] == 'Not Found'