20. Docker y Despliegue
🐳 20.1 Docker Básico
Section titled “🐳 20.1 Docker Básico”Dockerfile simple
Section titled “Dockerfile simple”FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]Dockerfile multi-stage (optimizado)
Section titled “Dockerfile multi-stage (optimizado)”# Etapa 1: BuildFROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
# Copiar archivos de MavenCOPY pom.xml .COPY .mvn .mvnCOPY mvnw .
# Descargar dependencias (cacheadas si pom.xml no cambia)RUN ./mvnw dependency:go-offline
# Copiar código fuenteCOPY src src
# CompilarRUN ./mvnw package -DskipTests
# Etapa 2: RuntimeFROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# Crear usuario no-rootRUN addgroup -S spring && adduser -S spring -G springUSER spring:spring
# Copiar JAR desde builderCOPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s CMD wget -q --spider http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]Dockerfile con layers (Spring Boot 2.3+)
Section titled “Dockerfile con layers (Spring Boot 2.3+)”FROM eclipse-temurin:21-jre-alpine AS builderWORKDIR /appCOPY target/*.jar app.jarRUN java -Djarmode=layertools -jar app.jar extract
FROM eclipse-temurin:21-jre-alpineWORKDIR /app
RUN addgroup -S spring && adduser -S spring -G springUSER spring:spring
COPY --from=builder /app/dependencies/ ./COPY --from=builder /app/spring-boot-loader/ ./COPY --from=builder /app/snapshot-dependencies/ ./COPY --from=builder /app/application/ ./
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]Comandos Docker
Section titled “Comandos Docker”# Construir imagendocker build -t mi-app:1.0.0 .
# Ejecutar contenedordocker run -d -p 8080:8080 --name mi-app mi-app:1.0.0
# Con variables de entornodocker run -d -p 8080:8080 -e SPRING_PROFILES_ACTIVE=prod -e SPRING_DATASOURCE_URL=jdbc:mysql://host:3306/db mi-app:1.0.0
# Ver logsdocker logs -f mi-app
# Entrar al contenedordocker exec -it mi-app sh🐙 20.2 Docker Compose
Section titled “🐙 20.2 Docker Compose”Aplicación con MySQL
Section titled “Aplicación con MySQL”version: '3.8'
services:app: build: . ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=docker - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/miapp - SPRING_DATASOURCE_USERNAME=root - SPRING_DATASOURCE_PASSWORD=secret depends_on: db: condition: service_healthy networks: - app-network restart: unless-stopped
db: image: mysql:8 environment: - MYSQL_ROOT_PASSWORD=secret - MYSQL_DATABASE=miapp volumes: - mysql_data:/var/lib/mysql - ./init.sql:/docker-entrypoint-initdb.d/init.sql ports: - "3306:3306" healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s timeout: 5s retries: 5 networks: - app-network
networks:app-network: driver: bridge
volumes:mysql_data:Stack completo con Redis y RabbitMQ
Section titled “Stack completo con Redis y RabbitMQ”version: '3.8'
services:app: build: . ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=docker - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/miapp - SPRING_DATA_REDIS_HOST=redis - SPRING_RABBITMQ_HOST=rabbitmq depends_on: - db - redis - rabbitmq networks: - backend
db: image: postgres:15-alpine environment: - POSTGRES_DB=miapp - POSTGRES_USER=postgres - POSTGRES_PASSWORD=secret volumes: - postgres_data:/var/lib/postgresql/data networks: - backend
redis: image: redis:7-alpine command: redis-server --appendonly yes volumes: - redis_data:/data networks: - backend
rabbitmq: image: rabbitmq:3-management-alpine ports: - "5672:5672" - "15672:15672" environment: - RABBITMQ_DEFAULT_USER=guest - RABBITMQ_DEFAULT_PASS=guest volumes: - rabbitmq_data:/var/lib/rabbitmq networks: - backend
nginx: image: nginx:alpine ports: - "80:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - ./certs:/etc/nginx/certs:ro depends_on: - app networks: - backend
networks:backend:
volumes:postgres_data:redis_data:rabbitmq_data:Perfil Docker
Section titled “Perfil Docker”# application-docker.propertiesspring.datasource.url=jdbc:mysql://db:3306/miappspring.datasource.username=rootspring.datasource.password=secret
spring.data.redis.host=redisspring.data.redis.port=6379
spring.rabbitmq.host=rabbitmqspring.rabbitmq.port=5672🚀 20.3 CI/CD con GitHub Actions
Section titled “🚀 20.3 CI/CD con GitHub Actions”Build y test
Section titled “Build y test”# .github/workflows/ci.ymlname: CI
on:push: branches: [main, develop]pull_request: branches: [main]
jobs:build: runs-on: ubuntu-latest
steps: - uses: actions/checkout@v4
- name: Set up JDK 21 uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' cache: maven
- name: Build with Maven run: mvn clean verify
- name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: file: target/site/jacoco/jacoco.xmlBuild y push a Docker Hub
Section titled “Build y push a Docker Hub”# .github/workflows/docker.ymlname: Docker Build & Push
on:push: tags: - 'v*'
jobs:docker: runs-on: ubuntu-latest
steps: - uses: actions/checkout@v4
- name: Set up JDK 21 uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' cache: maven
- name: Build JAR run: mvn clean package -DskipTests
- 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_PASSWORD }}
- name: Extract version id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Build and push uses: docker/build-push-action@v5 with: context: . push: true tags: | miusuario/mi-app:${{ steps.version.outputs.VERSION }} miusuario/mi-app:latest cache-from: type=gha cache-to: type=gha,mode=maxDeploy a servidor
Section titled “Deploy a servidor”# .github/workflows/deploy.ymlname: Deploy
on:workflow_run: workflows: ["Docker Build & Push"] types: [completed]
jobs:deploy: runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps: - name: Deploy to server uses: appleboy/ssh-action@v1.0.0 with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SERVER_SSH_KEY }} script: | cd /opt/mi-app docker-compose pull docker-compose up -d docker system prune -f☸️ 20.4 Kubernetes
Section titled “☸️ 20.4 Kubernetes”Deployment
Section titled “Deployment”# k8s/deployment.yamlapiVersion: apps/v1kind: Deploymentmetadata:name: mi-applabels: app: mi-appspec:replicas: 3selector: matchLabels: app: mi-apptemplate: metadata: labels: app: mi-app spec: containers: - name: mi-app image: miusuario/mi-app:latest ports: - containerPort: 8080 env: - name: SPRING_PROFILES_ACTIVE value: "kubernetes" - name: SPRING_DATASOURCE_URL valueFrom: secretKeyRef: name: db-secrets key: url - name: SPRING_DATASOURCE_PASSWORD valueFrom: secretKeyRef: name: db-secrets key: password resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m" livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 10 periodSeconds: 5Service y Ingress
Section titled “Service y Ingress”# k8s/service.yamlapiVersion: v1kind: Servicemetadata:name: mi-app-servicespec:selector: app: mi-appports: - port: 80 targetPort: 8080type: ClusterIP
---# k8s/ingress.yamlapiVersion: networking.k8s.io/v1kind: Ingressmetadata:name: mi-app-ingressannotations: nginx.ingress.kubernetes.io/rewrite-target: / cert-manager.io/cluster-issuer: letsencrypt-prodspec:ingressClassName: nginxtls: - hosts: - api.midominio.com secretName: mi-app-tlsrules: - host: api.midominio.com http: paths: - path: / pathType: Prefix backend: service: name: mi-app-service port: number: 80ConfigMap y Secrets
Section titled “ConfigMap y Secrets”# k8s/configmap.yamlapiVersion: v1kind: ConfigMapmetadata:name: mi-app-configdata:SPRING_PROFILES_ACTIVE: "kubernetes"SERVER_PORT: "8080"LOGGING_LEVEL_ROOT: "INFO"
---# k8s/secrets.yamlapiVersion: v1kind: Secretmetadata:name: db-secretstype: OpaquestringData:url: "jdbc:postgresql://postgres:5432/miapp"username: "postgres"password: "supersecret"🌐 20.5 Despliegue en Cloud
Section titled “🌐 20.5 Despliegue en Cloud”AWS Elastic Beanstalk
Section titled “AWS Elastic Beanstalk”# .elasticbeanstalk/config.ymlbranch-defaults:main: environment: mi-app-prodglobal:application_name: mi-appdefault_region: us-east-1workspace_type: Application
# Procfileweb: java -jar target/mi-app.jar --server.port=5000Google Cloud Run
Section titled “Google Cloud Run”# Desplegar a Cloud Rungcloud run deploy mi-app --image gcr.io/mi-proyecto/mi-app:latest --platform managed --region us-central1 --allow-unauthenticated --set-env-vars SPRING_PROFILES_ACTIVE=gcp --memory 512Mi --cpu 1 --min-instances 0 --max-instances 10Azure Container Apps
Section titled “Azure Container Apps”# Crear Container Appaz containerapp create --name mi-app --resource-group mi-rg --environment mi-env --image miregistry.azurecr.io/mi-app:latest --target-port 8080 --ingress external --min-replicas 1 --max-replicas 5 --env-vars SPRING_PROFILES_ACTIVE=azure📦 20.6 Configuración de Producción
Section titled “📦 20.6 Configuración de Producción”application-prod.properties
Section titled “application-prod.properties”# Servidorserver.port=8080server.shutdown=gracefulspring.lifecycle.timeout-per-shutdown-phase=30s
# Base de datosspring.datasource.url=${DATABASE_URL}spring.datasource.hikari.maximum-pool-size=20spring.datasource.hikari.minimum-idle=5
# JPAspring.jpa.hibernate.ddl-auto=validatespring.jpa.show-sql=falsespring.jpa.open-in-view=false
# Logginglogging.level.root=WARNlogging.level.com.miapp=INFO
# Actuatormanagement.endpoints.web.exposure.include=health,info,prometheusmanagement.endpoint.health.probes.enabled=true
# Seguridadserver.ssl.enabled=trueserver.ssl.key-store=classpath:keystore.p12server.ssl.key-store-password=${SSL_PASSWORD}server.ssl.key-store-type=PKCS12Variables de entorno
Section titled “Variables de entorno”# .env.exampleDATABASE_URL=jdbc:postgresql://localhost:5432/miappDATABASE_USERNAME=postgresDATABASE_PASSWORD=secretREDIS_HOST=localhostREDIS_PORT=6379JWT_SECRET=mi-clave-super-secreta-de-256-bitsSSL_PASSWORD=keystorepassword🔒 20.7 Seguridad en Despliegue
Section titled “🔒 20.7 Seguridad en Despliegue”Escaneo de vulnerabilidades
Section titled “Escaneo de vulnerabilidades”# Escanear imagen Dockerdocker scan mi-app:latest
# Con Trivytrivy image mi-app:latest
# En CI/CD- name: Run Trivy vulnerability scanneruses: aquasecurity/trivy-action@masterwith: image-ref: 'mi-app:latest' format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH'Secrets management
Section titled “Secrets management”# Con HashiCorp Vaultspring.cloud.vault.uri=https://vault.miempresa.comspring.cloud.vault.token=${VAULT_TOKEN}spring.cloud.vault.kv.enabled=truespring.cloud.vault.kv.backend=secret
# Con AWS Secrets Managerspring.config.import=aws-secretsmanager:mi-app-secrets📊 20.8 Monitoreo en Producción
Section titled “📊 20.8 Monitoreo en Producción”Stack de observabilidad
Section titled “Stack de observabilidad”version: '3.8'
services:prometheus: image: prom/prometheus volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml ports: - "9090:9090"
grafana: image: grafana/grafana ports: - "3000:3000" environment: - GF_SECURITY_ADMIN_PASSWORD=admin volumes: - grafana_data:/var/lib/grafana
loki: image: grafana/loki ports: - "3100:3100"
volumes:grafana_data:prometheus.yml
Section titled “prometheus.yml”global:scrape_interval: 15s
scrape_configs:- job_name: 'spring-app' metrics_path: '/actuator/prometheus' static_configs: - targets: ['app:8080']
🐝