If the Operating Systems and Networks courses felt intense to you, hold on tight because DevOps and Cloud Computing has been the crown jewel; a true technical marathon that completely changes your perspective. Here, it is no longer valid to say *"it works on my machine"*. If it is not automated, if it doesn't pass the linter, if it is not monitored in real time... it simply doesn't exist.
I want to share with you the entire journey covered throughout this semester at UOC-Jesuïtes (2026), breaking down the three major practical "Products" we had to sweat, configure, and defend. From writing code in Go and packaging it in minimalist containers, to setting up a complete CI/CD pipeline that automates code reviews on GitHub, and finally deploying a cloud infrastructure monitored to the last byte. Let's go!
1. Product 1: Preparing the Environment (Go 1.25 + Docker Multi-Stage)
The first major challenge was to lay the foundation of the DevOps philosophy: isolation, consistency, and efficiency. Instead of fighting with heavy local configurations that end up cluttering the operating system, we set out to develop and package a web microservice using Go 1.25 and Docker.
Since it was our first contact with a real container environment, we decided to approach the development collaboratively but in an organized way. We centralized all technical documentation in a group Notion workspace and structured the GitHub repository with individual folders for each of us (`alex`, `andrey`, `davidov`).
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
// Port configurable through an environment variable (useful in Docker)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
mux := http.NewServeMux()
// 1) Serve the /static/ folder (image)
fs := http.FileServer(http.Dir("./static"))
mux.Handle("/static/", http.StripPrefix("/static/", fs))
// 2) Main endpoint
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Best practices: allowed method and content-type
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Product 1 - UOC</title>
</head>
<body style="text-align:center;">
<h1>I am a UOC student (davidov@uoc.edu)</h1>
<p>Product 1: Go + Docker environment</p>
<img src="/static/uoc.jpg" alt="UOC Image" width="900">
</body>
</html>
`)
})
// 3) Optional extra endpoint: health check
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprint(w, "OK")
})
srv := &http.Server{
Addr: ":" + port,
Handler: logRequest(mux),
}
log.Printf("Server listening on http://localhost:%s", port)
log.Fatal(srv.ListenAndServe())
}
// Simple logging middleware
func logRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
The Efficient Container Approach (Multi-Stage Build)
One of the most common mistakes when starting with Docker is to generate gigantic images (hundreds of megabytes or even gigabytes) that include the entire SDK, compilers, and debugging tools in the production image. To solve this, we implemented a professional Multi-Stage Build strategy.
The idea is simple:
1. Build Phase (Builder): We use a robust Go image based on Alpine to compile the executable binary.
2. Final Image (Runtime): We copy only the resulting executable to a completely clean and ultra-lightweight Alpine Linux image. The final result is an image of only 5MB!
Here is the exact and polished structure of our production `Dockerfile`:
# STAGE 1: Compilation (here we define the executable)
# GO version and working directory
FROM golang:1.25 AS builder
WORKDIR /app
# Copy dependency management files
COPY go.mod ./
RUN go mod download
# Copy the rest of the code and create the "server" file (HEAVY BUILD)
# This “debug” image is not intended for deployment; it is the code in its “raw” state
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server main.go
# STAGE 2: Final image (the lightweight executable/runtime sent to production)
# This is the microservice designed and optimized for deployment, minimal and efficient
# Alpine is a “mini Linux” distribution of around 5 MB to which we add our service code
FROM alpine:3.20
WORKDIR /app
# We only bring what is necessary from the previous stage: libraries, dependencies and
# minimal code, as well as static files (fonts, scripts, images, CSS…)
COPY --from=builder /app/server /app/server
COPY --from=builder /app/static /app/static
# Inform that the container will use port 8081 (the port defined for GO)
EXPOSE 8081
ENTRYPOINT ["/app/server"]
Local Environment Verification
By deploying this microservice locally mapping port 8081, we managed to spin up our web application in a matter of milliseconds with minimal resource consumption. The execution and isolation were perfect, proving that we could compile code without needing the SDK installed directly on the host machine.
Download: Product 1 - Go + Docker Environment
2. Product 2: The Heart of CI/CD (AWS = Jenkins + Minikube + GitHub Integration)
The climax and true "headache" of the course came with Product 2. We bid farewell to the controlled local environment and moved to the real cloud infrastructure on Amazon Web Services (AWS) to deploy our service in an EC2 instance, installing Jenkins, Minikube, and everything necessary to start our cloud development.
With the application already containerized, the logical next step was to automate the code lifecycle. In Product 2 we delved into the true Continuous Integration and Continuous Deployment (CI/CD) workflow by connecting Jenkins with GitHub Webhooks and orchestrating the local deployment using Kubernetes (Minikube).
We worked strictly with individual branches (`alex-branch`, `andrey-branch`, `davidov-branch`) and completely banned direct commits to `main`. Every change had to be automatically audited by the machine before a human (me! 😉) gave it the green light.
The Pipeline Validation Flow
We designed an automated `Jenkinsfile` that reacted immediately to any `git push` on the development branches. The flow followed these rigorous steps:
1. Linting Phase (Validation): Jenkins analyzes the syntax and structure of the codebase (HTML and Go). To test the system and verify it worked, we simulated a real error by introducing a poorly closed paragraph `<p>` tag. Jenkins jumped in immediately! It marked the commit with a red X in the GitHub interface and automatically blocked any possibility of merging.
2. Pull Request Creation: If the code passes the linter cleanly (green check), the developer knows their code is "good" and opens a *Pull Request* to the `main` branch.
3. Review and Merge: The repository administrator visually evaluates the status of the pipeline on GitHub. If they see Jenkins' green check, they approve the merge.
4. Automatic Deployment in Kubernetes: The merge triggers the final phase of the pipeline on the main branch (`main`). Jenkins compiles the final version, generates the Docker image, tags it as `latest`, and automatically updates the pods in our Minikube cluster, exposing the service on port 8081.

Summary of the Team Workflow Architecture
| Role / Who | Action Performed | Environment / Where it happens |
|---|---|---|
| Developer | Writes code, commits, and `git push` | On their personal branch (`davidov-branch`) |
| Jenkins Server | Automates Linter and static tests | Notifies with Check or X directly on GitHub |
| Developer | Requests merge via *Pull Request* | GitHub web interface |
| GitHub Admin | Reviews automation checks and performs *Merge* | Central GitHub repository |
| Jenkins (Final) | Global build, Docker Build, and Deploy | Deployment of Pods/Services in Minikube |
Download: PIPELINE ADMIN GITHUB
Download: Product 2 - CI/CD Jenkins + Minikube
3. Product 3: Cloud Infrastructure and Active Observability (AWS + Prometheus + Grafana + Loki)
Now let's see that everything runs efficiently ;) let's monitor...
Observability is key in production: Pure metrics without visualization or alerts without logical thresholds do not prevent downtime. The combination of Prometheus for metrics and Loki for logs builds a fast diagnosis.
Metrics Collection with Prometheus and Node Exporter
To know with surgical precision what is happening on the server, we installed Node Exporter directly on the EC2 instance operating system. This agent collects raw hardware metrics (CPU usage, network, disk reads, memory). Subsequently, we configured Prometheus to scrape those data at specific intervals.

Definitive `prometheus.yml` configuration file:
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "alert_rules.yml"
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9091"]
- job_name: "node"
static_configs:
- targets: ["localhost:9100"] # Node Exporter listening endpoint
Intelligent Alert Management
Having pretty graphs is useless if you have to watch them 24 hours a day. We designed a robust alert rules file (`alert_rules.yml`) with critical thresholds calculated through mathematical expressions so the system warns us if the infrastructure is in danger:
groups:
- name: server_alerts
rules:
- alert: ServerDown
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Server down"
description: "Prometheus cannot contact the monitoring target. The web service is offline!"
- alert: HighCPU
expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
for: 5m
labels:
severity: warning
annotations:
summary: "CPU above 80%"
description: "High and sustained processor usage detected on the EC2 cloud instance."
- alert: HighMemory
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 85
for: 5m
labels:
severity: warning
annotations:
summary: "Memory above 85%"
description: "Available RAM is reaching critical limits. Danger of OOM Killer."
The Control Panel: Grafana and Log Analysis with Loki + Promtail
With metrics flowing to Prometheus, we connected them as a data source in Grafana, designing a unified interactive visual Dashboard to see the machine's state at a single glance.

However, metrics only tell you *when* something fails, but not *why*. To solve the other half of the observability problem, we implemented Loki (the log aggregator) alongside Promtail (the agent responsible for reading the microservice log files in real time). Thanks to this integration, we could perform complete forensic audits: if we saw an anomalous CPU spike in the Grafana panels, we could select that exact time range and see the detailed error lines our Go application had registered at that very second.
Download: Product 3 - Prometheus + Grafana + Loki
Conclusion and Final Reflections
This course has been a true reality immersion. DevOps is not about using modern tools just because they are fashionable; it is about creating solid, automated, reproducible, and secure bridges between software development and systems operations. I went from managing simple containers locally to understanding advanced orchestration and how to sleep peacefully at night thanks to a proactive monitoring stack in the cloud.
If you have read all the way to this point of this "technical wall of text", I mean it: you deserve an award! 🏆 Since I love sharing knowledge and want you to see how we defended all of this before the committee, I leave you here access to the complete video of our project presentation.
Do you have any questions about how to configure your Jenkinsfile, manage Prometheus mathematical alerts, or deal with AWS EC2 security port policies? Leave a comment below and let's debug or run a `git log` together!