En esta lección
Lo que vas a lograr
- Instalar Dokploy en un VPS y darle dominio con HTTPS
- Empaquetar tu app con Docker en una imagen ligera
- Desplegar sola cada vez que subes a main, con GitHub Actions
Antes de empezar
- Un VPS con Linux y acceso por SSH como root (2 GB de RAM mínimo)
- Un dominio donde puedas crear registros DNS
- Una cuenta de GitHub y otra de Docker Hub
- Una app que se construya con npm run build (Astro, Vite o similar)
Al terminar este tutorial vas a tener tu app corriendo en tu propio servidor, con dominio y HTTPS.
Cada cambio que subas a main se publicará solo. Es el mismo flujo con el que publicamos
este sitio, academy.lopandsolutions.com.
Qué vamos a construir
- Un VPS con Dokploy, un panel que administra tus apps con Docker y les pone HTTPS.
- Una app en Dokploy apuntando a tu dominio.
- Dos flujos de trabajo en GitHub Actions: uno prueba y construye tu código; el otro arma una imagen de Docker, la sube a Docker Hub y le pide a Dokploy que la despliegue.
¿Por qué construir la imagen en GitHub y no en el servidor? Porque el servidor solo corre apps: si la construcción falla, falla en GitHub y tu sitio sigue arriba con la versión anterior.
Paso 1: Prepara el servidor
Según la documentación oficial, tu VPS necesita como mínimo:
- 2 GB de RAM y 30 GB de disco. Es el mínimo para el panel; suma lo que necesiten tus apps.
- Un sistema probado por Dokploy: Ubuntu 24.04, 23.10, 22.04, 20.04 o 18.04; Debian 12, 11 o 10; Fedora 40; o CentOS 9 u 8.
Deja abiertos estos puertos en el firewall del proveedor:
| Puerto | Para qué |
|---|---|
| 22 | SSH |
| 80 y 443 | Tus sitios (HTTP y HTTPS), los atiende Traefik |
| 3000 | El panel de Dokploy, solo mientras le pones dominio |
Paso 2: Instala Dokploy
Conéctate por SSH y ejecuta el instalador oficial:
curl -sSL https://dokploy.com/install.sh | shAl terminar, abre http://IP-DE-TU-SERVIDOR:3000 y crea la cuenta de administrador.
Lo primero que hacemos en Lopand es darle al panel su propio subdominio con HTTPS
(por ejemplo panel.tu-dominio.com). Cuando ya entres por ese dominio, cierra el acceso por IP y puerto:
docker service update --publish-rm "published=3000,target=3000,mode=host" dokployPaso 3: Prepara tu app con un Dockerfile
Dokploy corre contenedores, así que tu app necesita un Dockerfile. Para un sitio estático
(Astro, Vite, etc.) usamos dos etapas: una construye con Node y la otra sirve el resultado con nginx.
La imagen final no lleva Node ni node_modules; en uno de nuestros proyectos bajó de unos 500 MB a unos 50 MB.
Necesitas tres archivos en la raíz del proyecto. Además, npm ci exige que tengas package-lock.json.
FROM node:22-alpine AS construccionWORKDIR /appCOPY package.json package-lock.json ./RUN npm ciCOPY . .RUN npm run build
FROM nginx:1.29-alpineCOPY nginx.conf /etc/nginx/conf.d/default.confCOPY --from=construccion /app/dist /usr/share/nginx/htmlEXPOSE 80HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=5 \ CMD wget -q --spider http://127.0.0.1/ || exit 1server { listen 80; server_name _; root /usr/share/nginx/html; index index.html;
location / { try_files $uri/index.html $uri $uri/ =404; }
error_page 404 /404.html;}El .dockerignore evita mandarle a Docker lo que no hace falta:
node_modulesdist.gitUna lección que nos costó un despliegue: en el
HEALTHCHECKusa127.0.0.1, nolocalhost. Dentro del contenedor,localhosttambién resuelve a la dirección IPv6::1. Este nginx no escucha ahí, así que la revisión de salud falla siempre y el contenedor queda marcado como no saludable.
Paso 4: Crea la app y apunta tu dominio
-
En tu proveedor de DNS crea un registro A con tu subdominio (por ejemplo
app.tu-dominio.com) apuntando a la IP del servidor. -
En Dokploy crea un proyecto y, dentro, una aplicación. En General → Provider elige Docker y escribe el nombre de tu imagen en Docker Hub (por ejemplo
tu-usuario/mi-app). Si tu imagen es privada, llena también Username y Password con tu usuario de Docker Hub y un token de acceso; si es pública, déjalos vacíos.
En General → Provider elige Docker y escribe el nombre completo de tu imagen. -
En la pestaña Domains pulsa Add Domain: tu subdominio, ruta
/, puerto 80 (el de nginx dentro del contenedor), HTTPS activado y certificado Let’s Encrypt.
Así queda el dominio de academy.lopandsolutions.com: puerto 80, HTTPS y certificado de Let's Encrypt. -
Copia el ID de la aplicación: es el último segmento de la URL del panel cuando estás dentro de ella (
…/services/application/<ID>). Lo vas a necesitar en GitHub.
Paso 5: Despliegue automático desde GitHub
Usamos dos flujos de trabajo. El de CI prueba y construye en cada push. El de CD solo corre
cuando el CI terminó bien en main, así que nunca se despliega código que no pasó las pruebas.
name: CI
on: push: branches: [main] pull_request: branches: [main]
jobs: ci: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: npm - run: npm ci - run: npm run buildSi tu proyecto tiene pruebas, agrega aquí un paso - run: npm test.
name: CD
on: workflow_run: workflows: ["CI"] types: [completed] branches: [main]
jobs: deploy: if: github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.workflow_run.head_sha }} - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64 push: true tags: tu-usuario/mi-app:build-${{ github.run_number }} - name: Desplegar en Dokploy env: API: https://panel.tu-dominio.com/api KEY: ${{ secrets.DOKPLOY_API_KEY }} APP: ${{ vars.DOKPLOY_APP_ID }} IMAGEN: tu-usuario/mi-app:build-${{ github.run_number }} run: | : "${APP:?Falta la variable DOKPLOY_APP_ID}" : "${KEY:?Falta el secreto DOKPLOY_API_KEY}" curl --fail-with-body -sS -X POST "$API/application.update" \ -H "x-api-key: $KEY" -H "Content-Type: application/json" \ -d "{\"applicationId\":\"$APP\",\"dockerImage\":\"$IMAGEN\"}" curl --fail-with-body -sS -X POST "$API/application.deploy" \ -H "x-api-key: $KEY" -H "Content-Type: application/json" \ -d "{\"applicationId\":\"$APP\"}"Detalles que evitan horas de depuración:
workflows: ["CI"]se refiere alname:del otro flujo, no al nombre del archivo. Si no coinciden, el CD nunca arranca y GitHub no te avisa.cd.ymltiene que estar en tu rama principal. Los flujos que se disparan conworkflow_runsolo corren desde la rama por defecto del repositorio.tu-usuarioen la etiqueta es el mismo queDOCKERHUB_USERNAME(o una organización donde tengas permiso). Si no, Docker Hub rechaza el push condenied.- Una etiqueta nueva en cada despliegue (
build-123, nolatest). Con una etiqueta que no cambia, Docker puede no volver a bajar la imagen y tu contenedor arranca con la versión vieja. El nombre da igual mientras cambie: en la captura de arriba veslatest-5, que es la forma que usamos en Lopand. --fail-with-bodyencurl. Sin él, un error de la API de Dokploy deja el flujo en verde.- Las dos primeras líneas de
rundetienen el despliegue con un mensaje claro si olvidaste dar de alta la variable o el secreto.
Da de alta los secretos y la variable desde la terminal, dentro de la carpeta de tu repositorio
(o agrega -R tu-usuario/tu-repo a cada comando). Cada gh secret set te pide el valor:
gh secret set DOCKERHUB_USERNAMEgh secret set DOCKERHUB_TOKENgh secret set DOKPLOY_API_KEYgh variable set DOKPLOY_APP_ID --body "ID-DE-TU-APP"La llave de Dokploy se genera en el panel, en Profile → API/CLI Keys → Generate New Key
(la ruta es /settings/profile). Dale un nombre que diga para qué es, por ejemplo github-actions-mi-app,
y si quieres, una fecha de vencimiento en Expiration para obligarte a rotarla.

La API vive en
https://panel.tu-dominio.com/api y se autentica con el encabezado x-api-key.
Paso 6: Comprueba que quedó
Haz un push a main. Primero corre el CI y, cuando termina bien, arranca el CD:
gh run list --workflow=CD --limit 1gh run watchcurl -sI https://app.tu-dominio.com | head -1Si ves HTTP/2 200 (o HTTP/1.1 200 OK), tu app ya está en producción con HTTPS.
Desde ahora, cada push a main la actualiza sola.
Conclusión
Ya tienes un servidor propio con Dokploy, tu app empaquetada en una imagen ligera y un despliegue
que corre solo cada vez que subes a main, sin pasar código que no se construyó.
En la siguiente lección de la ruta hacemos el CI más rápido y usamos ejecutores propios para no gastar los minutos del plan de GitHub.
Escribimos sobre lo que construimos para nuestros clientes y nuestros juegos. Si algo no te funcionó, escríbenos y lo corregimos.