Jenkins puede construir, probar y publicar imágenes Docker en cada cambio del repositorio si describes el proceso en un Jenkinsfile. En este tutorial prepararás un servidor Jenkins en Ubuntu 24.04 para usar Docker, crearás una pequeña aplicación de ejemplo con un Dockerfile multietapa y escribirás un pipeline declarativo que ejecuta los tests dentro de la imagen, la analiza con Trivy y la publica en Docker Hub.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 GB de RAM.
- Un usuario no root con privilegios
sudo. - Jenkins instalado desde el repositorio oficial y accesible por navegador, con los plugins sugeridos del asistente inicial (incluyen Pipeline, Git y Credentials Binding).
- Docker Engine instalado en el mismo servidor desde el repositorio oficial de Docker.
- Una cuenta de Docker Hub y un repositorio Git (GitHub, GitLab o similar) donde subir el código de ejemplo.
Paso 1: Permitir que Jenkins use Docker
Jenkins se ejecuta con el usuario de sistema jenkins, que por defecto no puede hablar con el daemon de Docker. Añádelo al grupo docker:
sudo usermod -aG docker jenkins
El cambio de grupo solo se aplica a procesos nuevos, así que reinicia Jenkins:
sudo systemctl restart jenkins
Comprueba que el usuario jenkins puede ejecutar comandos de Docker:
sudo -u jenkins docker version --format '{{.Server.Version}}'
Deberías ver la versión del servidor Docker, por ejemplo:
28.3.3
Advertenciapertenecer al grupo
dockerequivale en la práctica a tener acceso root en el servidor. Usa este esquema en un servidor Jenkins dedicado y limita quién puede crear o modificar trabajos.
Paso 2: Crear la aplicación de ejemplo
El pipeline necesita un repositorio con un Dockerfile y algo que probar. En tu equipo, crea un directorio para el proyecto:
mkdir myapp && cd myapp
Crea una aplicación Flask mínima con un endpoint de salud:
nano app.py
from flask import Flask
app = Flask(__name__)
@app.get("/health")
def health():
return {"status": "ok"}
Añade un test que la compruebe:
nano test_app.py
from app import app
def test_health():
client = app.test_client()
response = client.get("/health")
assert response.status_code == 200
assert response.get_json() == {"status": "ok"}
Declara las dependencias de ejecución en requirements.txt:
flask
gunicorn
Y las de desarrollo en requirements-dev.txt:
pytest
Ahora crea el Dockerfile. Usa tres etapas: base instala las dependencias, test ejecuta pytest y runtime es la imagen final, sin herramientas de test y con un usuario sin privilegios:
nano Dockerfile
FROM python:3.12-slim AS base
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
FROM base AS test
COPY requirements-dev.txt test_app.py ./
RUN pip install --no-cache-dir -r requirements-dev.txt && pytest -q
FROM base AS runtime
RUN useradd --create-home appuser
USER appuser
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
BuildKit solo construye las etapas que necesita el objetivo pedido. Por eso el pipeline construirá primero --target test (si pytest falla, falla la construcción) y después la imagen final, que reutiliza la caché de la etapa base.
Sube estos archivos a tu repositorio Git en la rama main. El Jenkinsfile lo añadirás en el paso 4.
Paso 3: Guardar las credenciales de Docker Hub en Jenkins
No escribas contraseñas en el Jenkinsfile. En su lugar, crea un token de acceso en Docker Hub (Account settings > Personal access tokens > Generate new token) con permisos de lectura y escritura, y guárdalo en Jenkins:
- Ve a Manage Jenkins > Credentials > System > Global credentials (unrestricted) > Add Credentials.
- En Kind elige Username with password.
- En Username escribe tu usuario de Docker Hub y en Password pega el token.
- En ID escribe
dockerhuby guarda.
El pipeline hará referencia a la credencial por ese ID. Jenkins enmascara su valor en los logs de la construcción.
Paso 4: Escribir el Jenkinsfile
Crea el archivo Jenkinsfile en la raíz del repositorio. Sustituye your_dockerhub_user por tu usuario de Docker Hub:
nano Jenkinsfile
pipeline {
agent any
environment {
IMAGE = 'your_dockerhub_user/myapp'
TAG = "${env.BUILD_NUMBER}"
}
options {
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds()
}
stages {
stage('Test') {
steps {
sh 'docker build --target test -t "$IMAGE:test-$TAG" .'
}
}
stage('Build') {
steps {
sh 'docker build --target runtime -t "$IMAGE:$TAG" -t "$IMAGE:latest" .'
}
}
stage('Scan') {
steps {
sh '''
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:latest image \
--exit-code 1 --severity CRITICAL --no-progress \
"$IMAGE:$TAG"
'''
}
}
stage('Push') {
steps {
withCredentials([usernamePassword(credentialsId: 'dockerhub',
usernameVariable: 'REG_USER',
passwordVariable: 'REG_PASS')]) {
sh 'echo "$REG_PASS" | docker login -u "$REG_USER" --password-stdin'
sh 'docker push "$IMAGE:$TAG"'
sh 'docker push "$IMAGE:latest"'
}
}
}
}
post {
always {
sh 'docker logout || true'
sh 'docker image rm "$IMAGE:test-$TAG" "$IMAGE:$TAG" "$IMAGE:latest" || true'
}
}
}
Qué hace cada parte:
- Test: construye la etapa
testdel Dockerfile. Si algún test falla,docker builddevuelve error y el pipeline se detiene. - Build: construye la imagen final con dos etiquetas, el número de construcción (inmutable, útil para desplegar y volver atrás) y
latest. - Scan: ejecuta Trivy contra la imagen local y hace fallar el pipeline si encuentra vulnerabilidades de severidad
CRITICAL. - Push:
withCredentialsexpone el usuario y el token solo dentro del bloque. Los comandos van entre comillas simples para que sea el shell, y no Groovy, quien expanda$REG_PASS; así el secreto no aparece en el script generado. - post: cierra la sesión del registro y borra las imágenes locales para no llenar el disco del servidor, aunque el pipeline falle.
Sube el Jenkinsfile al repositorio:
git add Jenkinsfile
git commit -m "Add Jenkins pipeline"
git push origin main
Paso 5: Crear el trabajo de pipeline en Jenkins
- En el panel de Jenkins, pulsa New Item, escribe
myapp, elige Pipeline y pulsa OK. - En la sección Pipeline, cambia Definition a Pipeline script from SCM.
- En SCM elige Git e introduce la URL del repositorio. Si es privado, añade una credencial de Git en Credentials.
- En Branch Specifier escribe
*/mainy deja Script Path comoJenkinsfile. - Guarda y pulsa Build Now.
La primera ejecución tarda más porque descarga las imágenes python:3.12-slim y aquasec/trivy. Abre la construcción y entra en Console Output. Al final deberías ver algo parecido a:
+ docker push your_dockerhub_user/myapp:1
...
1: digest: sha256:3f5c... size: 1994
...
Finished: SUCCESS
La vista Stages muestra las cuatro etapas en verde.
Paso 6: Verificar la imagen publicada
Desde cualquier máquina con Docker, descarga la imagen con la etiqueta del número de construcción y ejecútala:
docker run -d --rm --name myapp-check -p 8000:8000 your_dockerhub_user/myapp:1
Comprueba el endpoint de salud:
curl http://localhost:8000/health
{"status":"ok"}
Detén el contenedor de prueba:
docker stop myapp-check
Para comprobar que el pipeline protege la rama, cambia el test para que falle, haz push y lanza otra construcción: debe detenerse en la etapa Test y no publicar nada.
Paso 7: Construir automáticamente en cada push (opcional)
Pulsar Build Now a mano no escala. Tienes dos opciones:
- Webhook: en la configuración del trabajo, marca el disparador que ofrezca tu plugin de SCM (por ejemplo GitHub hook trigger for GITScm polling con el plugin de GitHub) y crea en el repositorio un webhook hacia
https://your_jenkins_domain/github-webhook/. - Sondeo: marca Poll SCM con una programación como
H/5 * * * *. Jenkins comprobará cambios cada cinco minutos, sin exponer Jenkins a Internet.
Si más adelante usas un trabajo Multibranch Pipeline, puedes limitar la etapa Push a la rama principal añadiendo when { branch 'main' } dentro de esa etapa.
Solución de problemas
permission denied while trying to connect to the Docker daemon socket: el proceso de Jenkins no tiene el grupo docker. Comprueba con id jenkins que aparece el grupo y reinicia Jenkins con sudo systemctl restart jenkins.
docker: not found: Docker no está instalado en el nodo donde se ejecuta el pipeline. Si usas agentes, instala Docker en ellos o restringe el trabajo con una etiqueta (agent { label 'docker' }).
unauthorized: incorrect username or password en el push: el token de Docker Hub ha caducado o no tiene permiso de escritura, o el ID de la credencial no es dockerhub. Genera un token nuevo y actualiza la credencial.
La etapa Scan falla: Trivy ha encontrado vulnerabilidades críticas. Revisa la tabla en el log; normalmente se resuelve actualizando la imagen base o la dependencia afectada.
Conclusión
Tienes un pipeline de Jenkins que construye una imagen Docker, ejecuta los tests dentro de ella, la analiza con Trivy y la publica en Docker Hub solo si todo lo anterior ha ido bien, sin exponer credenciales en el código. Como siguientes pasos, puedes publicar en un registro privado como Harbor cambiando IMAGE y la credencial, añadir análisis de calidad de código con SonarQube o desplegar la imagen etiquetada con el número de construcción en tu servidor de producción.