Saltar al contenido principal

Deployment en un VPS con Coolify

Código Completo
El resultado final del código desarrollado en este documento se puede encontrar en el monorepo de GitHub springboot-demo-projects, bajo el tag vps-coolify-deployment.

Deployar a tu propio VPS con Coolify te da más control y evita las limitaciones de cold starts de los tiers gratuitos de PaaS. Sin embargo, este documento no va a entrar demasiado en detalle sobre cómo configurar un VPS + dominio + instancia de Coolify en sí.

Para eso, te recomiendo chequear:

Una vez que tenés tu instancia de Coolify up and running, deployemos tu aplicación Spring Boot.

¿Por qué un VPS?

Hay un montón de formas de hostear una aplicación web en 2026. Clusters de Kubernetes manejados, funciones serverless, proveedores PaaS con tiers gratuitos generosos, lo que se te ocurra. Acá te dejo un mapa aproximado del panorama:

Scroll to zoom • Drag corner to resize
¿Dónde ves tu proyecto en los próximos años?
                ^
                |
Necesito        |  HYPER-SCALERS            :  WRAPPERS DE AWS
alcance global  |  - control masivo         :  - "vibecodeé algo y
y cinco 9 de    |  - lista abrumadora de   :    lo quiero compartir"
confiabilidad   |    servicios              :  - dashboards >> código
y disponibilidad|  - requiere conocimiento  :  - ¿qué es un container?
                |    experto                :
                |                           :
                |  [AWS] [Azure] [GCloud]   :  [Vercel] [Sevalla]
                |                           :  [Railway] [Render]
                |...........................:............................
                |                           :
                |  DEVELOPER CLOUD          :  HOSTS TRADICIONALES
                |  - alto control sobre OS  :  - setup fácil
                |  - pricing simple         :  - buen soporte
                |  - enfocado en VMs core   :  - menos flexibilidad
                |                           :    de configuración
                |                           :
                |  [DigitalOcean]           :  [Hostinger]
                |  [Hetzner]                :  [DreamHost] [A2 Hosting]
No creo que     |                           :
llegue a 100k   |                           :
usuarios        +---------------------------------------------------------->
activos pronto
                Soy proficiente en  <----------->   Preferiría no
                sudo su apt update                  meterme mucho con eso

                ¿Qué tan cómodo estás con comandos de Linux,
                        security patching, etc?

Para este proyecto, vamos con un VPS en el cuadrante "Developer Cloud". La razón es simple: es lo más fácil de explicar, y lo más fácil de razonar. Tenés una caja Linux, te conectás por SSH, corrés tus containers. No hay una capa de abstracción escondiendo lo que pasa.

Esa simplicidad viene con tradeoffs. Al elegir un solo VPS, estás resignando cosas que los sistemas de producción del mundo real suelen necesitar:

  • Load balancing entre múltiples instancias
  • Rolling deployments con zero downtime
  • Auto-scaling cuando el tráfico se dispara
  • Multi-region para redundancia y disponibilidad global
  • Bases de datos manejadas con backups automáticos y failover

No necesitás nada de eso ahora. Estás aprendiendo cómo funciona el deployment, no arquitectando para millones de usuarios. Un solo VPS con Docker Compose es más que suficiente, y cuando llegue el momento de escalar, vas a entender los fundamentos lo suficientemente bien como para saber de qué estás escalando.

Visión general de la arquitectura

Acá te muestro exactamente qué pasa cuando hacés push de código a la branch main:

Scroll to zoom • Drag corner to resize

La idea clave acá es que Coolify solo deploya si GitHub Actions pasa. Esto previene que código roto llegue a producción. GitHub Actions maneja el heavy lifting de compilar y testear a través de las tres implementaciones de lenguaje, mientras Coolify se enfoca puramente en la orquestación del deployment.

Alternativa: enfoque con Docker registry

La arquitectura de arriba hace que Coolify buildee las imágenes Docker en el VPS mismo. Hay otro patrón común donde GitHub Actions buildea las imágenes, las pushea a un container registry (como GitHub Container Registry, Docker Hub, o AWS ECR), y Coolify solo descarga las imágenes pre-buildeadas:

Scroll to zoom • Drag corner to resize

Ambos enfoques son válidos. Acá te muestro cómo se comparan:

Coolify buildea (esta guía)Basado en registry
Uso de recursos del VPSMayor: la compilación ocurre en tu servidorMenor: tu VPS solo descarga y ejecuta imágenes
Minutos de CIMenos: CI solo corre testsMás: CI también buildea y pushea imágenes Docker
Complejidad del pipelineMás simple: menos partes móviles, sin credenciales de registry que manejarMás setup: auth del registry, estrategia de tagging de imágenes, políticas de limpieza
Consistencia del buildLos builds ocurren en el hardware del VPS, que puede diferir del CILas imágenes son idénticas en todos lados, buildeadas una vez en un entorno controlado
Velocidad de deploymentMás lento: Coolify compila desde el código fuente cada vezMás rápido: Coolify solo descarga una imagen lista para correr
RollbacksRequiere un rebuild desde un commit anteriorDescargás un tag de imagen anterior, casi instantáneo
Debugging de buildsMás difícil: los logs de build están en Coolify, separados del CIMás fácil: todo está en un solo lugar (GitHub Actions)
Cuándo considerar un registry

Si tu VPS tiene recursos limitados (1-2 GB de RAM), buildear aplicaciones Java con Gradle en él puede ser doloroso. Mover el build a GitHub Actions (que tiene 7 GB de RAM en runners gratuitos) y pushear imágenes pre-buildeadas a un registry es una solución práctica.

Esta guía usa el enfoque de "Coolify buildea" porque es más simple de configurar y no requiere manejar credenciales de registry ni políticas de retención de imágenes. Para un proyecto chico o un setup de aprendizaje, la menor complejidad vale la pena el tradeoff.

Configuración del repositorio

Ejemplo de Monorepo

Esta guía demuestra deployar un monorepo que contiene tres proyectos Spring Boot (implementaciones Java, Kotlin y Groovy de la misma API). Si bien la configuración es más compleja que un repo de proyecto único, los conceptos principales se mantienen. Si estás trabajando con un repositorio más simple de proyecto único, vas a saltear la configuración de Docker Compose y deployar un solo servicio.

Antes de configurar Coolify, necesitás preparar tu repositorio con configuraciones de Docker y CI/CD. Acá te muestro un resumen de los archivos nuevos y modificados:

Archivos a Crear/Modificar
File Tree
springboot-demo-projects/
├── .github/
│ └── workflows/
│ └── ci-cd.yml
├── docker-compose.yml
├── .gitignore
├── spring_java/
│ ├── Dockerfile
│ └── settings-docker.gradle
├── spring_kotlin/
│ ├── Dockerfile
│ └── settings-docker.gradle
└── spring_groovy/
├── Dockerfile
└── settings-docker.gradle

docker-compose.yml

Este archivo orquesta los tres servicios Spring Boot:

docker-compose.yml

services:
spring-java:
build:
context: .
dockerfile: spring_java/Dockerfile
container_name: spring-java
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped

spring-kotlin:
build:
context: .
dockerfile: spring_kotlin/Dockerfile
container_name: spring-kotlin
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped

spring-groovy:
build:
context: .
dockerfile: spring_groovy/Dockerfile
container_name: spring-groovy
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped

Cada servicio:

  • Build desde su propio Dockerfile ubicado en el respectivo directorio del módulo
  • Mapea el puerto 8080 al puerto interno de Spring Boot (8080). El puerto externo coincide porque Coolify maneja el routing
  • Activa el profile prod para settings optimizados de producción
  • Incluye un healthcheck usando Spring Boot Actuator para asegurar que el contenedor esté realmente listo
  • Restart automático si el contenedor crashea

Dockerfiles

Cada módulo obtiene su propio Dockerfile optimizado:

spring_java/Dockerfile
FROM gradle:jdk21-alpine AS build
WORKDIR /home/gradle/src

COPY --chown=gradle:gradle spring_java/settings-docker.gradle ./settings.gradle
COPY --chown=gradle:gradle gradle ./gradle
COPY --chown=gradle:gradle gradlew ./
COPY --chown=gradle:gradle spring_java ./spring_java

RUN gradle :spring_java:bootJar -x test -x spotlessApply -x spotlessCheck --no-daemon

FROM eclipse-temurin:21-jre-alpine
RUN apk add --no-cache curl
WORKDIR /app
COPY --from=build /home/gradle/src/spring_java/build/libs/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

Algunos detalles importantes sobre este build multi-stage:

  • Stage 1 (Build): Usa la imagen full de Gradle con JDK 21 para compilar la aplicación. Saltamos tests y formateo de Spotless porque esos ya corrieron en GitHub Actions.
  • settings-docker.gradle trick: Cada módulo tiene un archivo de settings mínimo que solo se incluye a sí mismo. Esto previene que Docker intente buildear el monorepo entero cuando solo queremos un servicio.
  • Stage 2 (Runtime): Usa una imagen slim solo JRE de Alpine para una superficie de ataque más pequeña y deployments más rápidos.
  • curl installation: Requerido para que funcione el healthcheck.

settings-docker.gradle

Cada módulo necesita un archivo de settings standalone para builds de Docker:

spring_java/settings-docker.gradle
rootProject.name = 'springboot-demo-projects'

include 'spring_java'

Esta configuración mínima le dice a Gradle que solo buildee el módulo específico, evitando compilación innecesaria del monorepo entero durante la fase de build de Docker.

Excepción de Gradle Wrapper en .gitignore

gradle/wrapper/gradle-wrapper.jar actualmente es necesario para el deployment con Docker Compose porque:

  1. Configuración actual de Docker: Cada Dockerfile usa gradlew para buildear los JARs durante el proceso de build de Docker.
  2. Builds multi-stage: La etapa de build depende de Gradle para compilar y empaquetar las aplicaciones.
  3. Personalización de settings: Los Dockerfiles usan archivos de Gradle settings personalizados.

Excluilo de ser ignorado:

.gitignore
# ...
!gradle/wrapper/gradle-wrapper.jar

.github/workflows/ci-cd.yml

El workflow de GitHub Actions orquesta el pipeline CI/CD:

.github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build-and-test:
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: gradle

- name: Make gradlew executable
run: chmod +x gradlew

- name: Build & test all modules
run: ./gradlew clean build

- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports
path: '**/build/reports/**'

deploy:
needs: build-and-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'

steps:
- name: Trigger Coolify deployment
run: |
curl -X GET "https://coolify.pollito.tech/api/v1/deploy?uuid=${{ secrets.COOLIFY_DEPLOY_UUID }}&force=false" \
--header "Authorization: Bearer ${{ secrets.COOLIFY_API_TOKEN }}" \
--fail-with-body

Este workflow tiene dos jobs:

  1. build-and-test: Corre en cada push y pull request. Compila los tres módulos, corre tests y sube reportes de test como artifacts.
  2. deploy: Solo corre en pushes a main después de que build-and-test tenga éxito. Dispara el webhook de deployment de Coolify.

El workflow usa dos secrets (COOLIFY_DEPLOY_UUID y COOLIFY_API_TOKEN) que vas a configurar después en GitHub.

Configuración de Coolify + GitHub Actions

Ahora que tu repositorio está listo, configuremos Coolify para que trabaje con GitHub Actions.

Paso 1: Crear el recurso de Coolify

  1. Andá a tu proyecto en Coolify y clickeá + Add Resource.

    Coolify Pollito Tech Project P4wk08wwcw0g888ogk4w4kcw Environment O8cckkoowsw0cgo84owsgsg0 2026 02 06 18_11_48 3fdaf6361d579f2a21476743c9f6dddb
  2. Bajo Git Based, seleccioná Private Repository (with GitHub App).

    Coolify Pollito Tech Project P4wk08wwcw0g888ogk4w4kcw Environment O8cckkoowsw0cgo84owsgsg0 New 0fc323715ed22ce6d1a6b306695f4c4c
  3. Seleccioná tu GitHub App conectada.

    Coolify Pollito Tech Project P4wk08wwcw0g888ogk4w4kcw Environment O8cckkoowsw0cgo84owsgsg0 New 2026 02 06 20_52_51 9d63c91a76159877271e4501195bc8bc
  4. Configurá la aplicación:

    • Repository: Seleccioná tu repositorio (ej. springboot-demo-projects)
    • Branch: main
    • Build Pack: Docker Compose
    • Base Directory: /
    • Docker Compose Location: /docker-compose.yml
    La Extensión del Archivo Importa

    Asegurate de que la ubicación de Docker Compose coincida con tu extensión de archivo real. Es fácil confundir .yml y .yaml. Coolify va a fallar en encontrar el archivo si no coinciden exactamente.

    Coolify Pollito Tech Project P4wk08wwcw0g888ogk4w4kcw Environment O8cckkoowsw0cgo84owsgsg0 New 2026 02 06 20_53_57 91b5c6be8b25e44990499eb750702bac

Paso 2: Configurar los settings generales

Una vez creada, andá a la pestaña Configuration y seteá tus servicios:

  1. Domains: Agregá tus dominios custom:

    • https://sakila-java.your-domain.com:8080
    • https://sakila-kotlin.your-domain.com:8080
    • https://sakila-groovy.your-domain.com:8080
    Configuración de Puertos

    Esto es crucial: Cada servicio está configurado para exponer el puerto 8080. El reverse proxy de Coolify va a manejar el routing de tráfico a cada contenedor basado en el nombre de dominio. Si encontrás problemas de routing, chequeá la documentación de Coolify para más detalles.

  2. Docker Compose Editor: Verificá que el contenido de tu docker-compose.yml se muestre correctamente.

    Coolify Pollito Tech Project P4wk08wwcw0g888ogk4w4kcw Environment O8cckkoowsw0cgo84owsgsg0 Application Mooo884gwccg0wosk88g8csw 2026 02 06 20_58_48 0df4f9e1c349aa285c88c386e2861231

Paso 3: Deshabilitar Auto Deploy

Navegá a la pestaña Advanced y desmarcá Auto Deploy.

Deshabilitar Auto Deploy

Crítico: Debés deshabilitar Auto Deploy. Si se deja habilitado, Coolify va a deployar en cada git push, bypassing tu pipeline CI de GitHub Actions. Esto derrota el propósito de tener tests como gate de tus deployments.

Coolify Pollito Tech Project P4wk08wwcw0g888ogk4w4kcw Environment O8cckkoowsw0cgo84owsgsg0 Application Mooo884gwccg0wosk88g8csw Advanced 2026 02 06 20_54_38 567cd2cddcc8d8ed90b7f3153dc0d7ef

Paso 4: Obtener el Deploy Webhook URL

Navegá a la pestaña Webhooks para encontrar tu deployment trigger URL:

Coolify Pollito Tech Project P4wk08wwcw0g888ogk4w4kcw Environment O8cckkoowsw0cgo84owsgsg0 Application Mooo884gwccg0wosk88g8csw Webhooks 2026 02 06 20_59_06 F682dd6f50c3038575b8184043ac8359

Copiá el Deploy Webhook URL. Se ve así:

https://coolify.your-domain.com/api/v1/deploy?uuid=YOUR_UUID&force=false

El parámetro de query uuid es el identificador único para tu recurso de Coolify. Lo vas a guardar en GitHub como el secret COOLIFY_DEPLOY_UUID.

Paso 5: Generar API Token

  1. Andá a Keys & TokensAPI Tokens en la sidebar izquierda.

  2. Si la API está deshabilitada, habilitala en Settings primero.

  3. Creá un nuevo token:

    • Description: github-actions
    • Permissions: Solo marcar deploy
    Permisos Mínimos

    Solo otorgá el permiso deploy. El workflow de GitHub Actions solo necesita disparar deployments, no leer datos sensibles o modificar recursos.

    Coolify Pollito Tech Security Api Tokens 2026 02 06 21_31_39 4d5e985bb3ef9f25c4958976ddf3bbf7
  4. Copiá el token generado. Lo vas a guardar en GitHub como el secret COOLIFY_API_TOKEN.

Paso 6: Configurar los secrets de GitHub

Andá a tu repositorio de GitHub → SettingsSecrets and variablesActions, y agregá estos repository secrets:

  • COOLIFY_API_TOKEN: El token de API que acabás de generar

  • COOLIFY_DEPLOY_UUID: El UUID del webhook URL (el valor del parámetro de query uuid)

    Github FranBec Springboot Demo Projects Settings Secrets Actions 2026 02 07 02_59_45 5cdd2940fc3961c7de09e3190a66de70

Verificación

Hacé push de tus cambios a la branch main y mirá la magia suceder:

  1. Chequeá la ejecución de GitHub Actions para confirmar que el pipeline CI funciona:

    • Tu workflow debería dispararse y mostrar ambos jobs build-and-test y deploy
    • Buscá el checkmark verde indicando éxito
    Github FranBec Springboot Demo Projects Actions Runs 21766853844 2026 02 06 21_51_30 0ec53bcde9e41109bfe3a9fe207515b1
  2. Después de un deployment exitoso (usualmente 3-5 minutos), lo vas a ver en la pestaña Deployments:

    Coolify Pollito Tech Project P4wk08wwcw0g888ogk4w4kcw Environment O8cckkoowsw0cgo84owsgsg0 Application Mooo884gwccg0wosk88g8csw Deployment 2026 02 06 21_51_58 23eac7b92b7e93c5855fa754b4a363f0
  3. Finalmente, testeá tu API deployada:

Terminal
curl -s https://sakila-java.pollito.tech/api/films/42 | jq                                                    
{
"instance": "/api/films/42",
"status": 200,
"timestamp": "2026-02-07T19:12:08.752369341Z",
"trace": "b4e26474-1dd7-4af9-9865-21c056a43b34",
"data": {
"description": "A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies",
"id": 42,
"language": "English",
"lengthMinutes": 86,
"rating": "PG",
"releaseYear": 2006,
"title": "ACADEMY DINOSAUR"
}
}

curl -s https://sakila-kt.pollito.tech/api/films/42 | jq
{
"instance": "/api/films/42",
"status": 200,
"timestamp": "2026-02-07T19:12:16.4206552Z",
"trace": "58cf1f78-b195-41d4-8f43-0fc8b823899a",
"data": {
"id": 42,
"title": "ACADEMY DINOSAUR",
"description": "A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies",
"releaseYear": 2006,
"rating": "PG",
"lengthMinutes": 86,
"language": "English"
}
}

curl -s https://sakila-groovy.pollito.tech/api/films/42 | jq
{
"instance": "/api/films/42",
"status": 200,
"timestamp": "2026-02-07T19:12:26.069297914Z",
"trace": "005d057a-cc83-47b5-9962-1bbc44862b03",
"data": {
"description": "A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies",
"id": 42,
"language": "English",
"lengthMinutes": 86,
"rating": "PG",
"releaseYear": 2006,
"title": "ACADEMY DINOSAUR"
}
}

¡Felicitaciones! Configuraste exitosamente un pipeline CI/CD listo para producción para tu monorepo Spring Boot. Ahora cada push a main va a correr tests automáticamente antes de deployar, dándote confianza de que producción siempre esté en un estado funcional.