[{"Value":"","Discard":false,"Expires":9999999999}]
La entrada Introduction to KEDA: Event-Driven Autoscaling for Kubernetes se publicó primero en CloudArch.
]]>This is where KEDA (Kubernetes Event-Driven Autoscaling) comes in. KEDA is a lightweight, open-source component that integrates with Kubernetes to provide event-driven autoscaling. Unlike traditional Horizontal Pod Autoscalers (HPAs) that only scale based on CPU or memory usage, KEDA can scale your workloads based on external metrics or events, such as:
With KEDA, your applications can react instantly to real-world workloads without over-provisioning resources. It allows you to run microservices cost-effectively while maintaining responsiveness.
In this guide, we’ll build a realistic event-driven microservice that processes jobs from a Redis queue. The scenario mirrors what many production systems face:
By the end of this guide, you will have:
This project is valuable because it lets you experience a real-world autoscaling scenario. Most production systems have unpredictable workloads, and learning how KEDA responds to events gives you a deep understanding of:
Even if your real application is more complex (with multiple queues or different event sources), this example provides a solid foundation to implement production-ready, cost-efficient, and resilient services.
What you’ll learn:
Make sure you have the following locally installed and working on your machine:
If any of those are missing, install them first. For example on Debian/Ubuntu: sudo snap install kubectl –classic (or use your distro’s package manager). I will not assume any particular OS beyond these tools being available.
# Start minikube with 3 nodes (one control-plane + 2 workers), adjust memory/
CPUs if needed
$ minikube start --nodes=3 --driver=docker --memory=4096 --cpus=2
# Verify nodes come up
$ kubectl get nodesNotes: –nodes=3 creates 1 control-plane and 2 worker nodes by default. If you already have the cluster running, skip the minikube start step. Make sure kubectl context points to your minikube cluster: kubectl config current-context .
This command will install Helm for you in your local machine. Please, refer to this other post if you wanna learn more about it.
# Add KEDA Helm repo and update
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
# Install KEDA into namespace 'keda'
helm install keda kedacore/keda --namespace keda --create-namespace
# Wait for KEDA pods to become ready
kubectl -n keda get podsWhy Helm? Helm is the easiest official way to install KEDA and its CRDs. KEDA installs CRDs that are required for ScaledObject and ScaledJob resources.
I’ll explain each file before showing it. All manifests are designed to run in the default namespace for simplicity. You can change namespace fields if you prefer.
# redis-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
labels:
app: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:7.0-alpine
ports:
- containerPort: 6379
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "250m"
memory: "256Mi"
---
apiVersion: v1
kind: Service
metadata:
name: redis
labels:
app: redis
spec:
ports:
- port: 6379
targetPort: 6379
protocol: TCP
selector:
app: redis
type: ClusterIP
As we should know already, we are gonna deploy this in our cluster with:
kubectl apply -f redis-deployment.yamlTo be sure the redis service is client, we can test the connectivity with a redis client:
kubectl run -i --tty redis-client --image=redis:7.0-alpine --restart=Never
-- sh
# inside pod shell run: redis-cli -h redis ping
# should reply PONGWe’ll create a tiny Python worker that continuously polls a Redis list (named job-queue ) with BRPOP and “processes” messages (here, just sleep and echo ). Just to replicate some functionality of reading from Redis. The purpouse of this guide is not the logic of the code itself but it’s realibility on production environments.
In real life your worker would do meaningful job processing.
# worker.py
import time
import os
import redis
REDIS_HOST = os.getenv('REDIS_HOST', 'redis')
REDIS_PORT = int(os.getenv('REDIS_PORT', '6379'))
LIST_NAME = os.getenv('LIST_NAME', 'job-queue')
r = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, decode_responses=True)
print('Worker started, connecting to', REDIS_HOST)
while True:
try:
# BRPOP blocks until an item is available
item = r.brpop(LIST_NAME, timeout=5)
if item:
# item is (list_name, value)
value = item[1]
print('Processing', value)
# simulate processing
time.sleep(2)
print('Done', value)
else:
# nothing to do, sleep to avoid tight loop
time.sleep(1)
except Exception as e:
print('Worker error:', e)
time.sleep(2)# Dockerfile.worker
FROM python:3.11-slim
WORKDIR /app
COPY worker.py .
RUN pip install --no-cache-dir redis
CMD ["python","/app/worker.py"]By default if we create the image, it will be in our local registry and Minikube won’t see it. To solve this problem we can enable an addon called registry which will allow us to create a registry in Minikube.
minikube addons enable registryWe need to do a port-forward to forward the data to the registry:
kubectl port-forward -n kube-system service/registry 5000:80And now, in other terminal to not terminate our tunnel, we need to build and push the image so it can be used within the cluster:
docker build -t keda-worker:latest -f Dockerfile.worker .
docker tag keda-worker:latest localhost:5000/keda-worker:latest
docker push localhost:5000/keda-worker:latestAnd now we need to create the deployment of our service using the brand new image:
# worker-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: keda-worker
labels:
app: keda-worker
spec:
replicas: 0 # start with 0 so we can see KEDA scale up from zero
selector:
matchLabels:
app: keda-worker
template:
metadata:
labels:
app: keda-worker
spec:
containers:
- name: worker
image: localhost:5000/keda-worker:latest
imagePullPolicy: Always
env:
- name: REDIS_HOST
value: "redis"
- name: REDIS_PORT
value: "6379"
- name: LIST_NAME
value: "job-queue"
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "256Mi"Please, pay attention that we stated that we want a total of 0 replicas since the design of this is to only run when Keda allows it, saving us computing time and resources in our cluster.
kubectl apply -f worker-deployment.yamlNow that we have Redis ready to have message and our service ready to start reading those messages when it’s needed, we need to create a ScaledObject which will tell our service when it’s time to work, create instances and do its job.
# scaledobject-redis.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: keda-worker-scaledobject
labels:
app: keda-worker
spec:
scaleTargetRef:
name: keda-worker
pollingInterval: 5 # how often KEDA checks Redis (seconds)
cooldownPeriod: 30 # how long to wait after scale down before next check
minReplicaCount: 0
maxReplicaCount: 10
triggers:
- type: redis
metadata:
address: "redis.default.svc:6379"
listName: "job-queue"
listLength: "5" # scale target: number of items -> triggers scale
activationListLength: "1" # minimum backlog to activate scaling
Fields explained:
Please, pay special attention at triggers.type since that value is unique for the kind of job we are doing. Keda provides us with an extensive list of different triggers we can use depending on what we want to observe in order to scale our services. You can have a complete list at their official site.
Keda and Redis are on different namespaces on our cluster, when specifying the address make sure you are pointing correctly to Redis. If you don’t use
defaultnamespace as I did, it will be different thanredis.default.svc:6379
kubectl apply -f scaledobject-redis.yamlFinally, we are going to create a cronjob that will just generate some load to replicate a real user, and will just push a message to redis every minute so we can emulate the whole flow automatically and don’t hit any button neither waiting for an user.
# producer-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: job-producer
spec:
schedule: "*/1 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: producer
image: redis:7.0-alpine
command: [ "sh", "-c" ]
args:
- |
now=$(date +%s)
echo "Producing job-$now"
redis-cli -h redis rpush job-queue job-$now
sleep 1kubectl apply -f producer-cronjob.yamlFor this purpose I have splited my terminal into 3 sections, so I can see everything at one glance. First I have to sections of the same size: right and left.
On the right size I will be running the logs of the Scaled Object to see how it’s being triggered everytime it sees there is 1 or more messages in the queue
kubectl logs -f -n keda keda-operator-f948b6c4-ln9chAnd then the left side I will have it splitted into two parts again. In the upper part I will be checking how many messages do I have on the list
watch 'kubectl exec -it deploy/redis -- redis-cli llen job-queue'And at the botton I will be watching the number of pods from my deplotments
watch 'kubectl get deploy'
There we will be able to see how every minute the list has a new message, a pod of the worker is being created and the message is deleted. Reading all the logs in the right side.
By this way, we could have a service that only will be running when it’s needed. But also, this can be replicated and configured to just scale up or down services based on some triggers to ensure we are always giving the desired availability and reliability.
kubectl delete -f producer-cronjob.yaml
kubectl delete -f scaledobject-redis.yaml
kubectl delete -f worker-deployment.yaml
kubectl delete -f redis-deployment.yaml
helm uninstall keda -n keda
kubectl delete namespace kedaYou can see and download all the code on GitHub https://github.com/JoaquinJimenezGarcia/LearningKeda
La entrada Introduction to KEDA: Event-Driven Autoscaling for Kubernetes se publicó primero en CloudArch.
]]>La entrada 🧠 Run Your Own LLM Locally with Ollama and Docker se publicó primero en CloudArch.
]]>That’s exactly what Ollama allows you to do.
In this post, I’ll show you how to deploy your own LLM locally using Ollama on Docker, step by step — from installation to using it via API. We’ll also explore why it can be a game-changer for privacy, experimentation, and control.
Why Run an LLM Locally?While cloud-based LLMs like ChatGPT or Gemini are powerful and easy to use, they come with trade-offs:
Data Privacy: Anything you send to those services is processed in the cloud. Running your own model locally ensures that all your prompts, code, and data stay on your machine.
Customization: You can tweak system prompts, memory, or even fine-tune models without limitations.
Offline Access: No internet? No problem. You can still use the model.
Cost Control: No API fees or subscriptions — just your local hardware doing the work.This approach is perfect for developers, researchers, and hobbyists who want to experiment safely with AI on their own terms.
What Is Ollama?Ollama is a lightweight runtime that lets you run open-source LLMs (like Llama 3, Mistral, Phi, or Gemma) with a single command.
It exposes a REST API compatible with the OpenAI format, meaning you can integrate it easily with existing tools or scripts.
Think of it as “Docker for models” — you pull, run, and interact with them locally.
RequirementsBefore starting, make sure you have:
docker --version)
Step 1: Run Ollama in DockerOpen a terminal and pull the official Ollama image:
docker pull ollama/ollamaThen start the container:
docker run -d \
--name ollama \
-v ollama:/root/.ollama \
-p 11434:11434 \
ollama/ollamaollama:/root/.ollama stores your downloaded models.11434 exposes Ollama’s REST API locally.If you have an NVIDIA GPU, add --gpus=all to use hardware acceleration.
Step 2: Pull a ModelOnce the container is up, let’s download a model.
For this example, we’ll use Mistral, a solid open-source model known for good reasoning and small size (~4 GB):
docker exec -it ollama ollama pull mistralYou can also try Llama 3 or Gemma later.
Check your installed models:
docker exec -it ollama ollama list
Step 3: Chat in the CLIStart a local chat session:
docker exec -it ollama ollama run mistralExample:
>>> Hello, what can you do?
I can summarize text, answer questions, or help you write code — all locally on your machine!To exit, press Ctrl + C.
Step 4: Use the APIOllama exposes an endpoint compatible with OpenAI’s API at http://localhost:11434/api/generate.
Let’s test it with curl:
curl http://localhost:11434/api/generate -d '{
"model": "mistral",
"prompt": "Write a haiku about Docker."
}'You’ll get a JSON response similar to:
{"response":"Containers afloat / Isolation in motion / Cloud in a small box"}
Step 5: Connect from PythonYou can even use the OpenAI client library — just point it to Ollama’s local endpoint:
pip install openaiThen create a small Python script:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
response = client.chat.completions.create(
model="mistral",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain what Docker is in one sentence."}
]
)
print(response.choices[0].message.content)Run it, and you’ll see the response generated locally — no external API involved.
Best Practices
Keep models on a dedicated volume: This avoids re-downloading large files every time you restart Docker.
Clean unused models: docker exec -it ollama ollama rm <model>
Optimize with GPU: Ollama automatically uses GPU if available (CUDA or Metal).
Stay offline: If you want full privacy, disable internet access for the container. The model works entirely locally.
Monitor resources: Some models can use several GBs of RAM — use docker stats to watch usage.
Which Model Is 100% Private?If privacy is your top priority, I recommend Mistral 7B:
Open-source and licensed for local use
Excellent performance on general tasks
Does not send data anywhere
Works well even without GPU
Around 4 GB on diskYou can pull it with:
docker exec -it ollama ollama pull mistralThis model runs entirely on your machine — no telemetry, no cloud, no data collection.
Bonus: Expose Ollama on Your Local NetworkIf you want to connect from another device on your LAN (e.g. from a laptop or tablet), run:
docker run -d \
--name ollama \
-v ollama:/root/.ollama \
-p 0.0.0.0:11434:11434 \
ollama/ollamaThen access it from another device using your local IP, e.g.:
http://192.168.1.100:11434/api/generate
ConclusionRunning an LLM locally gives you control, privacy, and freedom.
With Ollama on Docker, you can deploy models like Mistral or Llama 3 in minutes, use them offline, and even integrate them into your own tools or scripts.
No subscriptions, no data leaks — just you, your machine, and your AI.
La entrada 🧠 Run Your Own LLM Locally with Ollama and Docker se publicó primero en CloudArch.
]]>La entrada Monitoring Docker with Prometheus: Gain Full Visibility into Your Containers se publicó primero en CloudArch.
]]>As organizations increasingly rely on containerized applications, ensuring their performance, stability, and reliability becomes a critical task. Docker makes it easy to package and deploy applications, but without proper monitoring, you may miss vital insights into how your containers are behaving. Issues such as resource exhaustion, unexpected crashes, or networking bottlenecks can quickly escalate if left unnoticed.
This is where Prometheus, an open-source monitoring and alerting toolkit, comes into play. Prometheus is designed to collect, store, and query time-series metrics, making it an excellent fit for containerized environments. When paired with Docker, it gives you the ability to:
By setting up Prometheus to monitor Docker, you establish a foundation for observability that not only improves day-to-day operations but also builds confidence in your system’s resilience. In this post, we will walk through how to configure Prometheus to collect Docker metrics and show you how this setup can be the backbone of a reliable monitoring strategy.
Nowadays, getting a Prometheus instance up and running is simpler than it sounds. Thanks to Docker itself we can get our instance deployed and ready to be queried in seconds. Let’s see an example of a docker-compose file.
# docker-compose.yml
version: "3.8"
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: unless-stopped
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
volumes:
prometheus_data:We can create also a small configuration for starting collecting Prometheus own metrics
# prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]Now if we run docker compose up -d we will have our instance serving traffic on port 9090
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6416f7540370 prom/prometheus:latest "/bin/prometheus --c…" 9 seconds ago Up 9 seconds 0.0.0.0:9090->9090/tcp, [::]:9090->9090/tcp prometheus
Now it’s important to understand that we are gonna monitor docker itself, not the applications running with Docker.
Docker has a new feature to expose metrics on Prometheus-like format of the Docker server without being force to expose the whole Docker API, which is a win on security. To do that, we need to enable metrics-addr: on the deamon.json.
Thanks to that, Prometheus will be able to read and store our metrics.
# /etc/docker/daemon.json
{
"metrics-addr": "0.0.0.0:9323"
}After adding that, we will need to restart docker.
Now that we are exposing the metrics, we would be able to see them on the specified port and /metrics path
$ curl localhost:9323/metrics
# HELP builder_builds_failed_total Number of failed image builds
# TYPE builder_builds_failed_total counter
builder_builds_failed_total{reason="build_canceled"} 0
builder_builds_failed_total{reason="build_target_not_reachable_error"} 0
builder_builds_failed_total{reason="command_not_supported_error"} 0
builder_builds_failed_total{reason="dockerfile_empty_error"} 0
builder_builds_failed_total{reason="dockerfile_syntax_error"} 0
builder_builds_failed_total{reason="error_processing_commands_error"} 0
builder_builds_failed_total{reason="missing_onbuild_arguments_error"} 0
builder_builds_failed_total{reason="unknown_instruction_error"} 0
# HELP builder_builds_triggered_total Number of triggered image builds
# TYPE builder_builds_triggered_total counter
builder_builds_triggered_total 0
# HELP engine_daemon_container_actions_seconds The number of seconds it takes to process each container action
# TYPE engine_daemon_container_actions_seconds histogram
engine_daemon_container_actions_seconds_bucket{action="changes",le="0.005"} 1
engine_daemon_container_actions_seconds_bucket{action="changes",le="0.01"} 1
engine_daemon_container_actions_seconds_bucket{action="changes",le="0.025"} 1
engine_daemon_container_actions_seconds_bucket{action="changes",le="0.05"} 1
engine_daemon_container_actions_seconds_bucket{action="changes",le="0.1"} 1
engine_daemon_container_actions_seconds_bucket{action="changes",le="0.25"} 1
engine_daemon_container_actions_seconds_bucket{action="changes",le="0.5"} 1
engine_daemon_container_actions_seconds_bucket{action="changes",le="1"} 1
engine_daemon_container_actions_seconds_bucket{action="changes",le="2.5"} 1
engine_daemon_container_actions_seconds_bucket{action="changes",le="5"} 1
engine_daemon_container_actions_seconds_bucket{action="changes",le="10"} 1Now that we have Prometheus up and running and Docker exporting its metrics, it’s time to tell Prometheus were to look and scrape for the metrics, so later we can navigate through them.
In order to tell Prometheus where are the metrics, we need to modify the prometheus.yml file that we created during the first steps. We need to create a new job and specify the address. Our file should look like this now:
# prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
- job_name: 'DockerStats'
static_configs:
- targets: ['172.17.0.1:9323']Note 1: Even on the
curlcommand we specified the/metricspath, here it’s not needed. By default, Prometheus will scrape on that path.
Note 2: Please, be careful of the target. 127.0.0.1 or
localhostwon’t work. Metrics are exposed on the docker bridge network which you can get from network interfacedocker0ip a | grep docker0 7: docker0: mtu 1500 qdisc noqueue state DOWN group default inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
Now we cand go to our Prometheus instance and we will see our job scraping the metrics correctly on /targets path.

That enables us for a vast options to explore our metrics and get to know better the state of our Docker service. As for example, we could use Prometheus as a Dataset on Grafana to visualize all the stats and a more beautiful way, and even create alerts based on thresholds.
La entrada Monitoring Docker with Prometheus: Gain Full Visibility into Your Containers se publicó primero en CloudArch.
]]>La entrada 🩺 Docker HEALTHCHECK: Is Your App Really Alive Inside the Container? se publicó primero en CloudArch.
]]>

Welcome to the world of Docker HEALTHCHECK — a super underrated feature that can make or break your reliability game. Today we’ll dive into:
Why HEALTHCHECK is essential
Real risks of skipping it
How Docker HEALTHCHECK Works
How to add it to your Dockerfiles
Two practical test cases (healthy vs unhealthy)
Why You Should CareLet’s be honest. We often celebrate when our container is “up and running” — but that just means the process inside hasn’t crashed. It doesn’t tell us if:



Without a healthcheck, Docker assumes everything is okay. That’s dangerous in production, but also in dev: it gives you a false sense of security.
Healthchecks add real visibility — if your app doesn’t behave as expected, Docker will mark it as unhealthy, and tools like Docker Swarm or Kubernetes can act accordingly (restarts, scaling, etc.).
How Docker HEALTHCHECK Works — Under the HoodWhen you add a HEALTHCHECK instruction in your Dockerfile, you’re telling the Docker engine to periodically run a command inside the container to determine its health status. Here’s how it works step by step:
1. The HEALTHCHECK InstructionExample:
HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
CMD curl -f http://localhost:5000/health || exit 1You’re defining:
| Option | Meaning |
|---|---|
CMD | The actual command to run inside the container. It must exit with 0 for healthy, non-zero for unhealthy. |
--interval | How often to run the health check (default: 30s). |
--timeout | How long to wait before the command is considered failed (default: 30s). |
--retries | Number of consecutive failures before the container is marked unhealthy (default: 3). |
2. Docker Monitors Using a Background Healthcheck ManagerWhen you start a container that has a HEALTHCHECK, Docker spawns a lightweight internal timer per container. This timer schedules and executes the CMD at the interval you define.
It’s all handled by the Docker daemon, which adds a health state entry to the container’s metadata.
3. Exit Codes Determine HealthDocker executes the healthcheck command inside the container, and uses its exit code to decide the result:
| Exit Code | Meaning |
|---|---|
0 | Healthy ![]() |
1 | Unhealthy ![]() |
>1 | Unhealthy ![]() |
CMD not found or fails to run? Still counts as unhealthy.
Docker tracks the consecutive failures, and once the retry limit is reached, the container is marked as unhealthy.
4. Status Stored in Container MetadataYou can view this with:
docker inspect --format='{{json .State.Health}}' [container_name] | jqIt shows:
Status: starting, healthy, or unhealthyFailingStreak: how many times it failed consecutivelyLog: recent healthcheck attempts with timestamps and outputsDocker updates this metadata in real-time, and you can consume it via:
docker ps, docker inspect)/containers/id/json)
5. No Magic, Just Smart LogicDocker doesn’t inject anything magical into your container. It simply:
curl, wget, etc.)But this tiny mechanism becomes powerful when combined with:
--restart=on-failure)
A Note About “Starting”After the container boots, healthchecks begin after a default grace period of 0s (can be configured). During this period, the container status shows as:
"Status": "starting"Once the first successful check is done, status becomes healthy. If it fails N times, it becomes unhealthy.
What Healthchecks DON’T Do
They do not stop or restart containers by themselves
They don’t directly affect container networking or DNS
They don’t send alerts unless you wire them to an external system
Adding a HEALTHCHECK to Your DockerfileIt’s simple! Here’s the syntax:
HEALTHCHECK --interval=10s --timeout=3s --retries=3 CMD curl -f http://localhost:5000/health || exit 1This checks every 10 seconds if the /health endpoint returns a success. If it fails 3 times in a row, the container becomes unhealthy.
Let’s Test It in ActionWe’ll create two test containers:
Healthy AppThis one includes a proper /health endpoint that always returns 200 OK.
Dockerfile:
FROM python:3.11-slim
ENV DEBIAN_FRONTEND=noninteractive
WORKDIR /app
COPY app.py .
# Install curl
RUN apt-get update && \
apt-get install -y curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
RUN pip install flask
EXPOSE 5000
HEALTHCHECK --interval=10s CMD curl -f http://127.0.0.1:5000/health || exit 1
CMD ["python", "app.py"]
app.py:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def home():
return "All good!"
@app.route('/health')
def health():
return "OK", 200
app.run(host="0.0.0.0", port=5000)
Build and run:
docker build -t healthy-app .healthtest
docker run -d --namehealthy-apphealthtest
docker inspect --format='{{.State.Health.Status}}'
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
55e9b6f148a9 healthcheck_test "python app.py" 18 seconds ago Up 18 seconds (healthy) 0.0.0.0:5000->5000/tcp, [::]:5000->5000/tcp healthtest
You’ll get: healthy
Unhealthy AppNow let’s break the /health endpoint.
Modified app.py:
@app.route('/health')
def health():
return "Error", 500Build and run again:
docker build -t unhealthy-app .
docker run -d --name broken unhealthy-app
docker inspect --format='{{.State.Health.Status}}' broken
Result: unhealthy
You’ll also see the logs showing failed healthcheck attempts:
$docker inspect broken | jq '.[].State.Health.Log'
[
{
"Start": "2025-06-05T19:26:07.323262742+02:00",
"End": "2025-06-05T19:26:07.367595028+02:00",
"ExitCode": 1,
"Output": " % Total % Received % Xferd Average Speed Time Time Time Current\n Dload Upload Total Spent Left Speed\n\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r 0 5 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\ncurl: (22) The requested URL returned error: 500\n"
},
{
"Start": "2025-06-05T19:26:17.369661511+02:00",
"End": "2025-06-05T19:26:17.408770486+02:00",
"ExitCode": 1,
"Output": " % Total % Received % Xferd Average Speed Time Time Time Current\n Dload Upload Total Spent Left Speed\n\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r 0 5 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\ncurl: (22) The requested URL returned error: 500\n"
},
{
"Start": "2025-06-05T19:26:27.409488914+02:00",
"End": "2025-06-05T19:26:27.450101106+02:00",
"ExitCode": 1,
"Output": " % Total % Received % Xferd Average Speed Time Time Time Current\n Dload Upload Total Spent Left Speed\n\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r 0 5 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\ncurl: (22) The requested URL returned error: 500\n"
},
{
"Start": "2025-06-05T19:26:37.450803223+02:00",
"End": "2025-06-05T19:26:37.492805511+02:00",
"ExitCode": 1,
"Output": " % Total % Received % Xferd Average Speed Time Time Time Current\n Dload Upload Total Spent Left Speed\n\r 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\r 0 5 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0\ncurl: (22) The requested URL returned error: 500\n"
}
]
What’s the Impact?| Scenario | Behavior |
|---|---|
| No HEALTHCHECK | Docker marks container as healthy by default |
| HEALTHCHECK passes | Container state = healthy ![]() |
| HEALTHCHECK fails | Container state = unhealthy ![]() |
Why it matters:
Final ThoughtsA HEALTHCHECK is like a pulse check for your app 
Just because a container runs doesn’t mean your service is okay.
Whether you’re in local development or scaling in production, a tiny HEALTHCHECK line in your Dockerfile can save you hours of debugging and nights of firefighting.
So go ahead — make your containers honest.
Docker HEALTHCHECK is:
Bonus tip: Want to auto-restart unhealthy containers?
Add this when running your container:
docker run --restart=on-failure ...La entrada 🩺 Docker HEALTHCHECK: Is Your App Really Alive Inside the Container? se publicó primero en CloudArch.
]]>La entrada 🐳 Docker Monitoring: Keeping an Eye on Your Containers from the Start se publicó primero en CloudArch.
]]>
The Power of Built-in ToolsDocker provides built-in commands that offer valuable insights into container performance:
docker stats
This command displays real-time metrics for your running containers, including CPU usage, memoryconsumption, and network I/O.
docker logs
Access the logs of a container to monitor its output and diagnose issues.
These commands are straightforward and require no additional setup, making them ideal for quick checks during development.
Testing with docker.io/spkane/train-os:latestTo see these tools in action, let’s use the docker.io/spkane/train-os:latest image, which simulates system stress and is perfect for testing monitoring setups.
Run the container:
$ docker container run --rm -d --name stress docker.io/spkane/train-os:latest stress -v --cpu 2 --io 1 --vm 2 --vm-bytes 128M --timeout 60s
Unable to find image 'spkane/train-os:latest' locally
latest: Pulling from spkane/train-os
d4df0db66c89: Pull complete
19c5d5a1e2b2: Pull complete
2b25593057c7: Pull complete
0355d914b0bb: Pull complete
Digest: sha256:5acc35b4325d348c8ce6843f6751f62de6e83e518f94f5abe29d0f3ac0fb54be
Status: Downloaded newer image for spkane/train-os:latest
45e7f21918af3000a67d8f78bdfc6601d059160af9429304fca616b75e6036acMonitor with docker stats:
$ docker container stats stress --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
b75e1302b035 stress 429.59% 119.9MiB / 31.07GiB 0.38% 5.82kB / 126B 0B / 0B 6You’ll observe metrics like CPU and memory usage updating in real-time. We are using `–no-stream` to just have a brief output of the current state. Otherwise, it will be running being updated the values each few seconds.
View logs:
$ docker logs stress
stress: info: [1] dispatching hogs: 2 cpu, 1 io, 2 vm, 0 hdd
stress: dbug: [1] using backoff sleep of 15000us
stress: dbug: [1] setting timeout to 60s
stress: dbug: [1] --> hogcpu worker 2 [7] forked
stress: dbug: [1] --> hogio worker 1 [8] forked
stress: dbug: [1] --> hogvm worker 2 [9] forked
Accessing Metrics via Docker APIFor more advanced monitoring or integration with custom tools, you can access container stats directly through the Docker API:
$ curl --no-buffer -X GET --unix-socket /var/run/docker.sock http://docker/containers/stress/stats | head -n 1 | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0{
"name": "/stress",
"id": "54370040079f3f7c3c6fd8608968050569b1141412c255949a0a72161f4a326a",
"read": "2025-06-04T19:45:21.220812324Z",
"preread": "0001-01-01T00:00:00Z",
"pids_stats": {
"current": 6,
"limit": 37968
},
"blkio_stats": {
"io_service_bytes_recursive": [
{
"major": 259,
"minor": 0,
"op": "read",
"value": 0
},
{
"major": 259,
"minor": 0,
"op": "write",
"value": 0
}
],This command fetches real-time statistics for the stress container in JSON format, which can be parsed and utilized by various monitoring solutions. That helps us to build our own monitoring solutions too, so if we run a budget environment or want to have control over all our stack we can easily control how our containers behave.
Note that curl is not making a TCP/IP call, we are directly hearing over the unix socket exposed for docker --unix-socket /var/run/docker.sock. That socket exports throught the Docker API /stats/ all needed parameters.
Choosing the Right Monitoring Approach| Scenario | Recommended Approach |
|---|---|
| Development & Testing | docker stats and docker logs |
| Custom Integrations | Docker API via curl |
| Production & Large Deployments | Grafana, Prometheus, etc. |
For small-scale applications or during development, Docker’s built-in tools are often sufficient. They provide immediate insights without the complexity of setting up external monitoring systems. However, as your application scales, integrating more robust solutions like Grafana and Prometheus becomes beneficial for long-term monitoring and alerting.
ConclusionMonitoring doesn’t have to be complex. Starting with Docker’s native tools allows for quick and effective oversight of your containers. As your needs grow, you can seamlessly transition to more comprehensive solutions. Remember, the key is to implement monitoring early to ensure smooth and efficient container operations.
La entrada 🐳 Docker Monitoring: Keeping an Eye on Your Containers from the Start se publicó primero en CloudArch.
]]>La entrada Improve docker build speed se publicó primero en CloudArch.
]]>
Taking into consideration that every Docker image is based into layers which can be cache’d for later re-use them and decrease the build time, we need to remember that if we modify a layer in our Dockerfile, the consequent layers will need to be rebuilt again, increasing the time. That’s why we can have two approaches.
To improve docker build speed, we need to make sure we can reuse the maximum number of layers cache’d, so the rebuild time will be the minimul. For example, if we need to install some dependencies or updates some packeges, the latest we do it, the better. Look at the following example:
FROM python:3.11
RUN pip install gunicorn
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
ENV PORT 8080
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 app:appIn that case, we are installing a dependency, doing some steps, installing the requirements and then setting up an environment variable. Some of those layers will be rebuild without a real need to do so, like for example, the ENV PORT 8080 that’s a layer that will be rebuild. In this concrete case is only one and very light, but in bigger Dockerfiles it could mean much more.
A way to force more layers to be reused, could be moving the installation to the very end, right before executing CMD. In that way, we are making sure we are re-using most of the layers from time to time and saving time.
FROM python:3.11
COPY . /app
WORKDIR /app
RUN pip install gunicorn
RUN pip install -r requirements.txt
ENV PORT 8080
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 app:appNow we would be saving some compiling time.
By doing directory caching, Docker is able to “remember” where something is located and use it in case it exists to build faster the image. It can bind a special layer in build time with your requirements and unmount it before the snapshot is made. This is often used to handle directories where tools like Linux software installers (apt, apk, dnf, etc) and language dependency managers (npm, bundler, pip, etc) are.
To use this feature, we need to make sure that we have buildkit enabled by running export DOCKER_BUILDKIT=1
Following the previous Dockerfile, if we add a new dependency to the requirements.txt file, it will need to download again all the dependecies, but why if we already have done that in a previous build? This inefficiency results from the builder seeing that we have made a change that impacts this layer and therefore completely re-creates the layer, so we lose that cache, even though we had it stored in the image layer.
In order to improve this situation and save time during compilation, we can mount the directory where pip typically saves the dependencies and mount it as a cache layer to make Docker look at that directory before downloading anything and only download the new dependencies. To do that we must add a little modification to the pip install line. But also we need to add a new line at the top of the file.
#syntax=docker/dockerfile:1
FROM python:3.11
COPY . /app
WORKDIR /app
RUN pip install gunicorn
RUN --mount=type=cache,target=/root/.cache pip install -r requirements.txt
ENV PORT 8080
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 app:appThe first header line we added is needed to tell Docker we are going to use a newer version of the Dockerfile frontent, which provides us with access to BuildKit’s new features. However the second line is actually mounting a caching layer into the container at /root/.cache for the duration of this step to build. It will remove the content of the directory from the resulting image.
In this way, it will check in consecutive builds which dependencies are already installed to save us some time to download again the same.
Did you like what you read? Don’t forget to read more articles about Docker in our blog.
La entrada Improve docker build speed se publicó primero en CloudArch.
]]>La entrada Make our Docker images lighter se publicó primero en CloudArch.
]]>
We are not going to define and talk about what exactly are the layers but as a very basic notion, Docker images are built by layers and these layers are the instructions written in the Docker file. Every instruction (line in a Dockerfile) is a layer and it has some weight depending on what’s doing.
# One layer, which also contains layers since it's an image
FROM docker.io/fedora
# Another layer
RUN dnf install -y httpd
# Another layer
CMD ["/usr/sbin/httpd", "-DFOREGROUND"]Layers are immutable and additive, meaning that once a Layer has done its job, it cannot be deleted or modified.
The main goal here would be to use a very minimal base image, that only contains what we need. Nothing else. Usually you won’t find a base image that contains everything, so probably you may end up by adding some extra layers like installing some packages like in the previous example.
If we build the previous image, we would see how Docker is building layer by layer and storing in into the cache for its previous usage.
terrorsys@Andromeda:~/TestLayers$ docker build -t testlayers .
[+] Building 39.3s (6/6) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 124B 0.0s
=> [internal] load metadata for docker.io/library/fedora:latest 3.2s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/2] FROM docker.io/library/fedora:latest@sha256:5ce8497aeea599bf6b54ab3979133923d82aaa4f6ca5 4.7s
=> => resolve docker.io/library/fedora:latest@sha256:5ce8497aeea599bf6b54ab3979133923d82aaa4f6ca5 0.0s
=> => sha256:5e22da79803c567fceb0e255f1168977259525a4279cb518016a60df025412fb 2.00kB / 2.00kB 0.0s
=> => sha256:d4df0db66c89d7e6225ce9d3597a045fb95c020f3174af1830df88a37a871db8 80.12MB / 80.12MB 3.7s
=> => sha256:5ce8497aeea599bf6b54ab3979133923d82aaa4f6ca5ced1812611b197c79eb0 1.25kB / 1.25kB 0.0s
=> => sha256:a0f4dffd30e0af6e53f57533e79a9e32699d37d8e850132ff89f612d6ea8a300 529B / 529B 0.0s
=> => extracting sha256:d4df0db66c89d7e6225ce9d3597a045fb95c020f3174af1830df88a37a871db8 1.0s
=> [2/2] RUN dnf install -y httpd 30.5s
=> exporting to image 0.7s
=> => exporting layers 0.7s
=> => writing image sha256:28bff713c5bc28c097fbae424ce9f4bb87226596736f04f7eb215aa92dbb4078 0.0s
=> => naming to docker.io/library/testlayers 0.0s Finally we can see how the image is built and inspect the layers to see their weight.
terrorsys@Andromeda:~/TestLayers$ docker images | grep testlayer
testlayers latest 28bff713c5bc 8 seconds ago 386MB
terrorsys@Andromeda:~/TestLayers$ docker image history testlayers
IMAGE CREATED CREATED BY SIZE COMMENT
28bff713c5bc 16 seconds ago CMD ["/usr/sbin/httpd" "-DFOREGROUND"] 0B buildkit.dockerfile.v0
<missing> 16 seconds ago RUN /bin/sh -c dnf install -y httpd # buildk… 164MB buildkit.dockerfile.v0
<missing> 3 months ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 3 months ago /bin/sh -c #(nop) ADD file:b8701dca3d7c8dad1… 222MB
<missing> 11 months ago /bin/sh -c #(nop) ENV DISTTAG=f40container … 0B
<missing> 3 years ago /bin/sh -c #(nop) LABEL maintainer=Clement … 0B Basically, we can see there layers coming from the base image, and the layers we added. But how about if we decrease the size of it to make the image lighter?
As we can see, since we are using “dnf install”, we are also caching so many other data we don’t really need. Accordingly on Fedora configuration, we can simply remove it by running “dnf clean all”.
However, as we saw recently, layers are additive and immutable, meaning that once the layer has been written and cached, we cannot modify it. It won’t matter if we add a new layer with that step, it won’t make any change.
Nevertheless, we can squash these three steps in an unique layer, so when the step finishes and save on cache, the size will be much lower. We can do that by modifying our Dockerfile.
FROM docker.io/fedora
RUN dnf install -y httpd && \
dnf clean all
CMD ["/usr/sbin/httpd", "-DFOREGROUND"]Basically we can use “&&” to say “run this second command only if the first one was successfully done” and then by “\” we indicate a line jump, just to see our Dockerfile much more clear.
Once we have created again our image, we can see now that the size it’s 303 MB instead of 386 MB and the layer went from 164 MB to 81.3 MB. This may not seem like a huge improvement but take into consideration this two scenarios:
Another point here is the base image we are using to create our own. This one runs a Fedora-based image. But since we only want to run Apache, we can find in the Docker registry another lighter base image which already contains Apache: https://hub.docker.com/r/nimmis/alpine-apache This image for example is an Alpine (much lighter than Fedora) which already has the desired package.
So our Dockerfile would look like the following:
FROM nimmis/alpine-apache
CMD ["/usr/sbin/httpd", "-DFOREGROUND"]Once we compiled it, we can see that the size of our image went from 303 MB to only 20.7 MB. This is much lighter size, which will improve our disk usage and network traffic to pull/push images. It’s ideal for production environments or other critical environments where we prioritize network and disk.
terrorsys@Andromeda:~/TestLayers$ docker images | grep testlayer
testlayers latest 311bb5eeeb68 2 years ago 20.7MBIf you liked this post, don’t forget to comment and read other posts regarding Docker.
La entrada Make our Docker images lighter se publicó primero en CloudArch.
]]>La entrada How to make Docker images more secure se publicó primero en CloudArch.
]]>
When we are writing our Dockerfile, Docker is using by default the root user to run the commands declared to create every layer in our image. Also, other times he copy and paste other Dockerfile templates which implicitly are using the root user by declaring this line:
USER rootEven this is redundant because Docker already uses it as default is not a good practice to keep that user. Instead, we should have our own user created for our specific purpose with only the needed permissions.
Even the Docker containers have certain level of isolation, we cannot forget Docker containers are still sharing the same kernel with the host, so using the root user in the wrong hands could end in a disaster.
The root user is not intended for ordinary tasks and should not be used for running our apps.
The best practice to follow is to create a new user and a new group for our service, and assignt to it the right permissions at system level.
In order to create the user, we can run the following layers on our Dockerfile:
# Create a custom user with UID 1234 and GID 1234
RUN groupadd -g 1234 customgroup && \
useradd -m -u 1234 -g customgroup customuser
# Switch to the custom user
USER customuserDid you like this post? Don’t forget to read other related posts, leave your comment and ask for more content!
La entrada How to make Docker images more secure se publicó primero en CloudArch.
]]>