Guía completa — EMPRESA_JOSESAID en DigitalOcean

Dominio: josesaid.lat · Aplicación: ui-ms (Spring Boot, Java 21) · Docker Compose

Ruta de la petición: Navegador → DNS → Nginx → Apache → Docker → Spring Boot

0. Arquitectura y flujo de una petición

Navegador
https://app.josesaid.lat
→
DNS
app → IP del Droplet
→
Nginx
0.0.0.0:80 / 443
→
Apache
127.0.0.1:8090
→
Docker
127.0.0.1:8098
→
ui-ms
Spring Boot · 8080

Qué pasa en cada salto

1
Navegador → DNS. El navegador pide la IP de app.josesaid.lat. El registro A devuelve la IP pública del Droplet.
2
Navegador → Nginx. La petición llega al puerto 80 (o 443 con HTTPS) del Droplet. Nginx lee la cabecera Host y selecciona el bloque server_name app.josesaid.lat.
3
Nginx → Apache. Nginx hace proxy_pass http://127.0.0.1:8090 y agrega X-Real-IP, X-Forwarded-For, X-Forwarded-Proto y X-Forwarded-Host.
4
Apache → Docker. Apache escucha solo en localhost, selecciona el VirtualHost app.josesaid.lat y hace ProxyPass a http://127.0.0.1:8098/, conservando el Host original.
5
Docker → contenedor. Docker Engine recibe la conexión en el 8098 del host (publicado solo en 127.0.0.1) y la envía al 8080 del contenedor ui-ms.
6
Spring Boot. La aplicación procesa la petición. Con forward-headers-strategy: framework toma la IP real del cliente y si la conexión original fue HTTP o HTTPS.
7
Respuesta. La respuesta regresa por la misma cadena, en orden inverso, hasta el navegador.
Por qué esta arquitectura: solo Nginx está expuesto a internet. Apache y Docker escuchan en localhost, así que no pueden recibir tráfico directo desde fuera. Apache queda como punto de configuración adicional para otros web servers o reglas (mod_rewrite, autenticación, cabeceras).
Costo de la capa Apache: añade un salto más. Si en el futuro ui-ms es la única aplicación, puedes quitar Apache y hacer que Nginx apunte directo a 127.0.0.1:8098 sin tocar Docker.

1. Crear la cuenta de DigitalOcean

Los nombres de botones de la consola de DigitalOcean pueden cambiar con el tiempo. Si algo no coincide, busca la función por nombre en el menú.

1
Entra a cloud.digitalocean.com/registrations/new y regístrate con tu correo o con Google/GitHub.
2
Confirma el correo electrónico desde el mensaje que te llega.
3
Agrega un método de pago. DigitalOcean puede pedir una tarjeta de crédito o débito y, en algunos casos, una verificación adicional.
4
Activa la autenticación de dos factores en Settings → Security. Usa una app de autenticación (Google Authenticator, 1Password, Authy).
5
Guarda los códigos de recuperación de 2FA en un gestor de contraseñas. Sin ellos, perder el teléfono puede bloquear tu cuenta.
Crea también un equipo o revisa el proyecto por defecto en el panel. Todo lo que crees (Droplets, firewalls, DNS) queda asociado a un proyecto; ponle un nombre como EMPRESA_JOSESAID para encontrarlo fácil.

2. Crear la llave SSH

Usaremos llaves SSH en lugar de contraseñas para entrar al servidor. Hazlo en tu computadora local.

2.1 Generar el par de llaves

# macOS / Linux (en la terminal)
ssh-keygen -t ed25519 -C "josesaid-droplet" -f ~/.ssh/id_ed25519_do

# Te pedirá una passphrase. Ponla y guárdala en tu gestor de contraseñas.

2.2 Ver la llave pública

cat ~/.ssh/id_ed25519_do.pub

Copia toda la línea que empieza con ssh-ed25519.

2.3 Registrarla en DigitalOcean

1
En el panel, ve a Settings → Security → SSH Keys → Add SSH Key.
2
Pega la llave pública y dale un nombre como laptop-josesaid.
Nunca compartas la llave privada (~/.ssh/id_ed25519_do, sin .pub). Solo la llave pública se registra en DigitalOcean.

3. Crear el Droplet

1
En el panel, selecciona Create → Droplets.
2
Región: elige la más cercana a tus usuarios. Desde México, las regiones de Norteamérica (Nueva York, San Francisco o Toronto) suelen tener menor latencia. Revisa la lista disponible en tu cuenta.
3
Imagen: Ubuntu 24.04 (LTS) x64. Es la versión con soporte a largo plazo y la que usa esta guía.
4
Plan: Basic → Regular. Para una aplicación Spring Boot con Docker, Nginx y Apache, el mínimo recomendado es 2 GB de RAM y 1 vCPU. Si el build de Maven en el servidor falla por memoria, sube a 4 GB.
5
Autenticación: selecciona SSH Key y marca la llave que registraste en la sección 2. No uses contraseña.
6
Hostname: empresa-josesaid-01.
7
Opciones adicionales: activa Monitoring (gratuito). Los Backups cuestan un extra mensual; actívalos si quieres copias automáticas del disco.
8
Haz clic en Create Droplet y espera a que el estado sea Running.

3.1 Anotar la IP

Copia la IPv4 pública del Droplet desde la lista de Droplets. La usarás en el DNS y en el acceso SSH. En esta guía la llamaremos IP_DEL_DROPLET.

Las siguientes secciones usan TU_IP para tu IP pública actual. Consíguela con curl ifconfig.me desde tu computadora.

4. Primer acceso y endurecimiento del servidor

4.1 Entrar como root por primera vez

ssh -i ~/.ssh/id_ed25519_do root@IP_DEL_DROPLET

La primera vez te preguntará si confías en el host. Escribe yes.

4.2 Actualizar el sistema

apt update && apt upgrade -y
reboot

Espera un minuto y vuelve a entrar.

4.3 Crear un usuario de despliegue

adduser deploy
usermod -aG sudo deploy

4.4 Copiar la llave SSH al nuevo usuario

mkdir -p /home/deploy/.ssh
cp /root/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys

4.5 Probar el acceso con el nuevo usuario

Antes de cerrar la sesión de root, abre otra terminal en tu computadora y prueba:

ssh -i ~/.ssh/id_ed25519_do deploy@IP_DEL_DROPLET
sudo whoami    # debe responder: root
No deshabilites el acceso por contraseña ni el login de root hasta que el acceso como deploy con llave funcione. Si lo haces antes, puedes quedarte sin acceso al servidor.

4.6 Deshabilitar login de root y contraseñas

Solo ejecútalo cuando el paso 4.5 haya funcionado:

sudo nano /etc/ssh/sshd_config

Asegúrate de que estas líneas tengan estos valores:

PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
sudo sshd -t && sudo systemctl restart ssh

A partir de ahora entra solo con ssh -i ~/.ssh/id_ed25519_do deploy@IP_DEL_DROPLET.

4.7 Zona horaria

sudo timedatectl set-timezone America/Mexico_City
timedatectl

Los logs y el cron usarán tu hora local. Si prefieres UTC para los logs de producción, omite este paso.

5. Swap y ajustes del sistema

Un Droplet de 2 GB puede quedarse sin memoria durante el build de Maven o al arrancar la JVM. Un archivo de swap evita que el sistema se caiga.

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h

En free -h debes ver la línea Swap con 2 GB.

5.1 Ajustar la agresividad del swap

echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swappiness.conf
sudo sysctl -p /etc/sysctl.d/99-swappiness.conf

6. Instalar Docker Engine

Usamos el repositorio oficial de Docker, no el paquete de Ubuntu, que suele estar desactualizado.

6.1 Dependencias y clave del repositorio

sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

6.2 Agregar el repositorio e instalar

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

6.3 Permitir a tu usuario usar Docker sin sudo

sudo usermod -aG docker deploy

Cierra la sesión SSH y vuelve a entrar para que el grupo tome efecto.

6.4 Verificar

docker --version
docker compose version
docker run --rm hello-world

Si hello-world imprime un mensaje, Docker funciona.

6.5 Limitar el tamaño de los logs de contenedores

Evita que los logs de Docker llenen el disco.

sudo nano /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}
sudo systemctl restart docker

7. Registros DNS

En el proveedor donde administras josesaid.lat, crea estos registros apuntando a IP_DEL_DROPLET:

TipoNombre (host)ValorTTL
A@ (josesaid.lat)IP_DEL_DROPLET3600
AwwwIP_DEL_DROPLET3600
AappIP_DEL_DROPLET3600

Si prefieres administrar el DNS en DigitalOcean, crea el dominio en Networking → Domains y apunta los nameservers del registrador a DigitalOcean. Los registros son los mismos.

7.1 Verificar la propagación

dig +short josesaid.lat
dig +short app.josesaid.lat

Ambos deben devolver IP_DEL_DROPLET. Si no, espera y vuelve a intentar.

8. Firewall (UFW)

Ejecuta esto dentro del Droplet, como deploy.

Antes de activar UFW, confirma que tu IP (TU_IP) está correcta. Si UFW bloquea tu SSH, pierdes el acceso y tendrás que usar la consola web de DigitalOcean.
sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing

# SSH solo desde tu IP
sudo ufw allow from TU_IP to any port 22 proto tcp

# Web público
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

sudo ufw enable
sudo ufw status verbose
Alternativa: DigitalOcean ofrece Cloud Firewalls en Networking → Firewalls. Puedes usarlo además de UFW como una segunda capa. Si lo usas, aplica las mismas reglas: SSH desde tu IP y HTTP/HTTPS abiertos.
Qué no debe quedar abierto: 8080, 8090 y 8098. Están ligados a 127.0.0.1 y no deben ser accesibles desde internet.

9. Mapa de puertos

PuertoEnlaceServicioExposición
220.0.0.0SSHSolo tu IP
800.0.0.0Nginx (HTTP)Público
4430.0.0.0Nginx (HTTPS, tras Certbot)Público
8090127.0.0.1ApacheSolo localhost
8098127.0.0.1Docker → ui-ms (127.0.0.1:8098:8080)Solo localhost
8080 (dentro del contenedor)red interna de DockerSpring Boot (ui-ms)No se publica al host

10. Subir el código y preparar ui-ms

10.1 Clonar el repositorio

Si el repositorio es privado, crea una llave de deploy o un token en GitHub. La forma más simple es una llave SSH de solo lectura:

# En el Droplet, como deploy
ssh-keygen -t ed25519 -C "droplet-deploy" -f ~/.ssh/id_ed25519_github
cat ~/.ssh/id_ed25519_github.pub

Agrega esa llave pública en GitHub: Repositorio → Settings → Deploy keys → Add deploy key. Deja Allow write access desmarcado.

cat >> ~/.ssh/config <<'EOF'
Host github.com
  IdentityFile ~/.ssh/id_ed25519_github
  IdentitiesOnly yes
EOF
chmod 600 ~/.ssh/config

git clone git@github.com:TU_USUARIO/EMPRESA_JOSESAID.git ~/EMPRESA_JOSESAID
cd ~/EMPRESA_JOSESAID
git log --oneline -n 5

10.2 Estructura esperada

~/EMPRESA_JOSESAID/
└── ui-ms/
    ├── Dockerfile
    ├── docker-compose.yml
    ├── .env                 ← NO va en git; se crea en el servidor
    ├── pom.xml
    └── src/
Si tu repositorio no tiene esta estructura, ajusta las rutas de este documento. El Dockerfile y el docker-compose.yml deben estar en la misma carpeta que pom.xml.

10.3 Verificar que Spring Boot compila localmente

Antes de dockerizar, verifica en tu computadora que el proyecto compila con Java 21:

cd ui-ms
./mvnw -B clean package -DskipTests
ls target/*.jar

11. Dockerfile de ui-ms

Archivo: ui-ms/Dockerfile

Este Dockerfile hace un build multi-etapa. La primera etapa compila con Maven y la segunda solo copia el JAR a una imagen liviana de Java 21.

# ---- Etapa 1: build con Maven ----
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /src

# Cachear dependencias primero para acelerar builds siguientes
COPY pom.xml .
RUN mvn -B -q dependency:go-offline

COPY src ./src
RUN mvn -B -q clean package -DskipTests

# ---- Etapa 2: runtime ----
FROM eclipse-temurin:21-jre
WORKDIR /app

# Usuario sin privilegios
RUN groupadd --system app && useradd --system --gid app --uid 10001 app

COPY --from=build /src/target/*.jar /app/app.jar

USER app
EXPOSE 8080

# Límite de memoria de la JVM: 75% del límite del contenedor
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError"

ENTRYPOINT ["sh", "-c", "exec java $JAVA_OPTS -jar /app/app.jar"]
Por qué exec: sin él, la JVM no recibe las señales de Docker (SIGTERM) y los apagados tardan 10 segundos o fallan.
Si el proyecto no usa el wrapper mvnw o tiene varios módulos Maven, el paso COPY pom.xml cambia. Compártelo para ajustarlo.

12. docker-compose.yml y variables de entorno

12.1 Compose de ui-ms

Archivo: ui-ms/docker-compose.yml

services:
  ui-ms:
    build:
      context: .
      dockerfile: Dockerfile
    image: empresa/ui-ms:latest
    container_name: ui-ms
    restart: unless-stopped
    env_file:
      - .env
    environment:
      SERVER_PORT: 8080
    ports:
      - "127.0.0.1:8098:8080"   # Solo localhost: Apache es el único que llega aquí
    mem_limit: 1g
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
Sobre mem_limit: 1g: ajústalo según el plan del Droplet. En un Droplet de 2 GB, 1 GB para ui-ms deja espacio para Nginx, Apache y el sistema. Con MaxRAMPercentage=75, la JVM usará como máximo unos 768 MB de heap.

12.2 Archivo .env con las variables reales

Archivo: ui-ms/.env (en el servidor, nunca en git)

# Crear el archivo y restringir permisos
cd ~/EMPRESA_JOSESAID/ui-ms
nano .env
chmod 600 .env
# ---- Perfil de Spring ----
SPRING_PROFILES_ACTIVE=PERFIL_REAL_DEL_PROYECTO

# ---- Base de datos ----
SPRING_DATASOURCE_URL=jdbc:postgresql://HOST:5432/NOMBRE_BD
SPRING_DATASOURCE_USERNAME=USUARIO_BD
SPRING_DATASOURCE_PASSWORD=PASSWORD_BD

# ---- Seguridad JWT (RS256 / JWKS) ----
VARIABLES_JWT_REALES

# ---- Eureka / servicios ----
EUREKA_URL_Y_OTROS_SERVICIOS
Los valores entre marcadores amarillos son placeholders. Reemplázalos con las variables reales de tu proyecto. Nunca subas el .env a git ni lo pegues en un chat. Si lo compartiste por error, rota las credenciales.

12.3 Qué pasa con el perfil

El valor de SPRING_PROFILES_ACTIVE debe coincidir con el nombre de un archivo application-PERFIL.yml existente en el proyecto. Si no existe, Spring usará solo application.yml.

13. Spring Boot: cabeceras reenviadas y Actuator

13.1 Leer las cabeceras de los proxies

Archivo: ui-ms/src/main/resources/application.yml (o el perfil de producción)

server:
  port: ${SERVER_PORT:8080}
  forward-headers-strategy: framework

Sin esta propiedad, request.getRemoteAddr() devuelve 127.0.0.1 y los redirects pueden salir en http:// en lugar de https://.

13.2 Actuator para el healthcheck

Si el proyecto no tiene Actuator, agrega la dependencia en pom.xml:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Y exponer solo lo necesario:

management:
  endpoints:
    web:
      exposure:
        include: health
  endpoint:
    health:
      probes:
        enabled: true
No expongas env, beans ni heapdump en producción. Pueden filtrar secretos.

13.3 Verificar

curl -s http://127.0.0.1:8098/actuator/health

Debe responder {"status":"UP"}.

14. Apache: proxy hacia Docker

14.1 Instalar y habilitar módulos

sudo apt install -y apache2
sudo a2enmod proxy proxy_http headers
sudo a2dismod -f status 2>/dev/null || true

14.2 Cambiar el puerto de escucha

Archivo: /etc/apache2/ports.conf

Listen 127.0.0.1:8090

Así Apache no escucha en el 80 público (que es de Nginx) y solo acepta conexiones locales.

14.3 Desactivar el sitio por defecto

sudo a2dissite 000-default.conf

14.4 Crear el VirtualHost

Archivo: /etc/apache2/sites-available/app.josesaid.lat.conf

<VirtualHost 127.0.0.1:8090>
    ServerName app.josesaid.lat

    ProxyRequests Off
    ProxyPreserveHost On

    ProxyPass        / http://127.0.0.1:8098/ retry=0 timeout=120
    ProxyPassReverse / http://127.0.0.1:8098/

    ErrorLog  ${APACHE_LOG_DIR}/app.josesaid.lat-error.log
    CustomLog ${APACHE_LOG_DIR}/app.josesaid.lat-access.log combined
</VirtualHost>
Cabeceras: Apache no sobrescribe X-Forwarded-*. Las recibe de Nginx y las reenvía a Docker. Apache agrega su propia IP a X-Forwarded-For; por eso Spring debe leer la primera IP de la lista, que es la del cliente original.

14.5 Activar y validar

sudo a2ensite app.josesaid.lat.conf
sudo apache2ctl configtest
sudo systemctl restart apache2
ss -ltnp | grep 8090

configtest debe responder Syntax OK, y ss debe mostrar 127.0.0.1:8090.

15. Nginx: josesaid.lat y app.josesaid.lat

15.1 Instalar Nginx

sudo apt install -y nginx
sudo rm -f /etc/nginx/sites-enabled/default

15.2 Sitio principal: josesaid.lat

Archivo: /etc/nginx/sites-available/josesaid.lat

server {
    listen 80;
    listen [::]:80;
    server_name josesaid.lat www.josesaid.lat;

    root /var/www/josesaid.lat/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

15.3 Aplicación: app.josesaid.lat → Apache

Archivo: /etc/nginx/sites-available/app.josesaid.lat

server {
    listen 80;
    listen [::]:80;
    server_name app.josesaid.lat;

    client_max_body_size 20m;

    location / {
        proxy_pass http://127.0.0.1:8090;
        proxy_http_version 1.1;

        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host  $host;

        proxy_connect_timeout 60s;
        proxy_send_timeout    120s;
        proxy_read_timeout    120s;
    }
}

15.4 Crear directorios y activar

sudo mkdir -p /var/www/josesaid.lat/html /var/www/app.josesaid.lat/html
sudo chown -R deploy:deploy /var/www/josesaid.lat /var/www/app.josesaid.lat

sudo ln -s /etc/nginx/sites-available/josesaid.lat /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/app.josesaid.lat /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

nginx -t debe responder syntax is ok y test is successful.

16. Página de bienvenida

Archivo: /var/www/josesaid.lat/html/index.html

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>josesaid.lat</title>
</head>
<body>
    <h1>Bienvenido a mi página web</h1>
</body>
</html>

17. Pruebas por niveles

Ejecuta las pruebas en orden. Cada nivel depende del anterior.

17.1 Nivel 1: servicios del sistema

systemctl is-active nginx apache2 docker
ss -ltnp | grep -E ':(80|8090|8098)\b'

Esperado: active tres veces; 0.0.0.0:80 de Nginx y 127.0.0.1:8090 y 127.0.0.1:8098.

17.2 Nivel 2: cada capa por separado

# Docker → Spring Boot
curl -I http://127.0.0.1:8098/

# Apache → Docker
curl -I -H "Host: app.josesaid.lat" http://127.0.0.1:8090/

# Nginx → Apache → Docker
curl -I -H "Host: app.josesaid.lat" http://127.0.0.1/

# Nginx → página de bienvenida
curl -I -H "Host: josesaid.lat" http://127.0.0.1/

17.3 Nivel 3: desde internet

curl -I http://josesaid.lat
curl -I http://app.josesaid.lat

17.4 Nivel 4: puertos internos no públicos

Desde tu computadora (no desde el Droplet):

curl -m 5 http://IP_DEL_DROPLET:8090/   # debe fallar
curl -m 5 http://IP_DEL_DROPLET:8098/   # debe fallar

17.5 Nivel 5: IP real del cliente

tail -n 3 /var/log/apache2/app.josesaid.lat-access.log
docker logs --tail 20 ui-ms

Apache registra 127.0.0.1 (Nginx). Spring Boot debe registrar tu IP pública.

18. HTTPS con Certbot

Ejecuta esto solo cuando la sección 17 pase completa.

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d josesaid.lat -d www.josesaid.lat -d app.josesaid.lat \
  --redirect --agree-tos -m TU_CORREO --no-eff-email
sudo certbot renew --dry-run

Certbot agrega listen 443 ssl, la redirección de HTTP a HTTPS y el timer de renovación. Nginx sigue enviando X-Forwarded-Proto $scheme, que ahora será https; Spring Boot lo lee gracias a la sección 13.

curl -I https://josesaid.lat
curl -I https://app.josesaid.lat
sudo systemctl list-timers | grep certbot

19. Actualizar la aplicación

Procedimiento para publicar una nueva versión de ui-ms:

cd ~/EMPRESA_JOSESAID
git pull --ff-only
cd ui-ms
docker compose build --no-cache ui-ms
docker compose up -d ui-ms
docker compose ps
docker logs --tail 50 ui-ms
curl -s http://127.0.0.1:8098/actuator/health
--ff-only evita merges automáticos en el servidor. Si falla, hay cambios locales que revisar antes de desplegar.

19.1 Limpiar imágenes viejas

docker image prune -f

Borra solo las imágenes sin etiqueta que quedan de builds anteriores.

20. Solución de problemas

SíntomaCausa probableQué revisar
502 Bad Gateway en NginxApache no escucha en 8090systemctl status apache2 y ss -ltnp | grep 8090
503 o error de conexión desde Apacheui-ms no responde en 8098curl -I http://127.0.0.1:8098/ y docker logs ui-ms
El contenedor se reinicia en bucleFalta una variable del .env o no conecta a la BDdocker logs ui-ms busca Caused by
OutOfMemoryError o el contenedor se caeMemoria insuficientefree -h, docker stats y subir el plan o mem_limit
El build de Maven falla en el servidorMemoria insuficiente durante la compilaciónVerifica el swap (sección 5) o compila localmente y sube el JAR
Redirects a http:// con HTTPSSpring no lee X-Forwarded-ProtoConfirma forward-headers-strategy: framework
Spring ve 127.0.0.1 como IP del clienteFalta forward-headers-strategySección 13
Timeout desde internetUFW o el Cloud Firewall bloquea 80/443, o el DNS no propagóufw status verbose y dig +short app.josesaid.lat
No puedes entrar por SSHUFW bloqueó tu IP o cambió tu IP públicaConsola web de DigitalOcean (Recovery Console) para corregir UFW
nginx -t fallaError de sintaxis o archivo duplicadoEl mensaje de error y ls -la /etc/nginx/sites-enabled/
permission denied al usar DockerNo cerraste sesión tras agregar el grupoCierra sesión SSH y vuelve a entrar

Comandos de diagnóstico

sudo tail -n 50 /var/log/nginx/error.log
sudo tail -n 50 /var/log/apache2/app.josesaid.lat-error.log
docker logs --tail 100 ui-ms
docker stats --no-stream
free -h
df -h
sudo ss -ltnp

21. Rollback

Para volver a una versión anterior de ui-ms:

cd ~/EMPRESA_JOSESAID
git log --oneline -n 10          # identifica el commit anterior
git checkout COMMIT_ANTERIOR
cd ui-ms
docker compose build ui-ms
docker compose up -d ui-ms
Después del rollback, vuelve a la rama principal con git checkout main y git pull cuando decidas desplegar otra versión. No dejes el servidor en un commit desconocido.

Desactivar la app sin borrar nada

cd ~/EMPRESA_JOSESAID/ui-ms
docker compose stop ui-ms

sudo a2dissite app.josesaid.lat.conf
sudo systemctl restart apache2
Si ya activaste HTTPS, no borres /etc/letsencrypt. Solo elimina el bloque listen 443 del archivo de Nginx que quieras revertir.

22. Checklist final

☐
Cuenta: DigitalOcean con 2FA y método de pago.
☐
Llave SSH: creada localmente y registrada en DigitalOcean.
☐
Droplet: Ubuntu 24.04, 2 GB RAM mínimo, autenticación con llave SSH.
☐
Endurecimiento: usuario deploy, login de root y contraseñas deshabilitados.
☐
Swap: 2 GB activos.
☐
Docker: Docker Engine y Compose instalados; deploy en el grupo docker.
☐
DNS: josesaid.lat, www y app apuntan al Droplet.
☐
Firewall: SSH solo desde tu IP; 80 y 443 abiertos; 8090 y 8098 cerrados.
☐
Código: repositorio clonado con deploy key de solo lectura; .env creado con chmod 600.
☐
ui-ms: docker compose up -d levanta el contenedor; /actuator/health responde UP.
☐
Spring Boot: forward-headers-strategy: framework configurado.
☐
Apache: escucha en 127.0.0.1:8090 y redirige a 127.0.0.1:8098.
☐
Nginx: josesaid.lat sirve la bienvenida; app.josesaid.lat hace proxy_pass a Apache.
☐
Pruebas: sección 17 completa, incluidos los puertos internos cerrados desde internet.
☐
HTTPS: certificados emitidos y renovación probada.