From db5ca09c1b09cdfb5a7b263bd444481a78ed4e30 Mon Sep 17 00:00:00 2001 From: Kurt Ferreira Date: Sat, 11 Apr 2026 13:57:02 +0200 Subject: [PATCH] first commit --- .woodpecker.yml | 25 +++++++++++++++++++ Dockerfile | 14 +++++++++++ job.hcl | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 20 +++++++++++++++ src/index.js | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 189 insertions(+) create mode 100644 .woodpecker.yml create mode 100644 Dockerfile create mode 100644 job.hcl create mode 100644 package.json create mode 100644 src/index.js diff --git a/.woodpecker.yml b/.woodpecker.yml new file mode 100644 index 0000000..defdc79 --- /dev/null +++ b/.woodpecker.yml @@ -0,0 +1,25 @@ +steps: + - name: build-and-push + image: woodpeckerci/plugin-docker-buildx + settings: + repo: "registry.af-east-1.dc.koldsoftware.com/${CI_REPO_OWNER}/nodejs-app" + registry: "registry.af-east-1.dc.koldsoftware.com" + tags: + - "${CI_COMMIT_SHA:0:8}" + - latest + username: + from_secret: registry_username + password: + from_secret: registry_password + + - name: deploy + image: hashicorp/nomad:latest + commands: + - nomad job run + -var="version=${CI_COMMIT_SHA:0:8}" + -var="domain=af-east-1.dc.koldsoftware.com" + job.hcl + environment: + NOMAD_ADDR: "http://192.168.2.106:4646" + when: + branch: main diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5efa372 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +# Stage 1: install production dependencies +FROM node:20-alpine AS deps +WORKDIR /app +COPY package*.json ./ +RUN npm install --omit=dev + +# Stage 2: final image — no dev tools, no npm cache +FROM node:20-alpine +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY src/ ./src/ +EXPOSE 3000 +USER node +CMD ["node", "src/index.js"] diff --git a/job.hcl b/job.hcl new file mode 100644 index 0000000..7dc0a92 --- /dev/null +++ b/job.hcl @@ -0,0 +1,65 @@ +variables { + version = "latest" + domain = "homelab.local" + count = 1 +} + +job "nodejs-app" { + datacenters = ["*"] + type = "service" + + update { + max_parallel = 1 + min_healthy_time = "10s" + healthy_deadline = "3m" + auto_revert = true + } + + group "nodejs-app" { + count = var.count + + network { + port "http" { to = 3000 } + } + + task "nodejs-app" { + driver = "docker" + + config { + image = "registry.${var.domain}/kurt/nodejs-app:${var.version}" + ports = ["http"] + } + + env { + NODE_ENV = "production" + APP_VERSION = var.version + PORT = "3000" + } + + resources { + cpu = 256 + memory = 256 + } + } + + service { + name = "nodejs-app" + port = "http" + provider = "nomad" + + tags = [ + "traefik.enable=true", + "traefik.http.routers.nodejs-app.rule=Host(`nodejs-app.${var.domain}`)", + "traefik.http.routers.nodejs-app.tls=true", + "traefik.http.routers.nodejs-app.tls.certresolver=letsencrypt", + ] + + check { + type = "http" + path = "/health" + interval = "10s" + timeout = "2s" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6c49e41 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "nodejs-app", + "version": "0.1.0", + "description": "Example Node.js app for homelab-dc", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "nodemon src/index.js" + }, + "dependencies": { + "express": "^4.19.2", + "prom-client": "^15.1.0" + }, + "devDependencies": { + "nodemon": "^3.1.0" + }, + "engines": { + "node": ">=20" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..390189d --- /dev/null +++ b/src/index.js @@ -0,0 +1,65 @@ +'use strict' + +const express = require('express') +const promClient = require('prom-client') + +const app = express() +const PORT = process.env.PORT || 3000 + +// Prometheus metrics +const collectDefaultMetrics = promClient.collectDefaultMetrics +collectDefaultMetrics({ prefix: 'nodejs_app_' }) + +const httpRequestDuration = new promClient.Histogram({ + name: 'http_request_duration_seconds', + help: 'Duration of HTTP requests in seconds', + labelNames: ['method', 'route', 'status'], + buckets: [0.01, 0.05, 0.1, 0.5, 1, 5], +}) + +const httpRequestTotal = new promClient.Counter({ + name: 'http_requests_total', + help: 'Total number of HTTP requests', + labelNames: ['method', 'route', 'status'], +}) + +// Metrics middleware +app.use((req, res, next) => { + const end = httpRequestDuration.startTimer() + res.on('finish', () => { + const route = req.route ? req.route.path : req.path + const labels = { method: req.method, route, status: res.statusCode } + end(labels) + httpRequestTotal.inc(labels) + }) + next() +}) + +app.use(express.json()) + +app.get('/metrics', async (_req, res) => { + res.set('Content-Type', promClient.register.contentType) + res.end(await promClient.register.metrics()) +}) + +app.get('/health', (_req, res) => { + res.json({ status: 'ok' }) +}) + +app.get('/', (_req, res) => { + res.json({ + message: 'Hello from Node.js!', + env: process.env.NODE_ENV || 'development', + version: process.env.APP_VERSION || 'unknown', + }) +}) + +app.listen(PORT, '0.0.0.0', () => { + console.log(`Server listening on port ${PORT}`) +}) + +// Graceful shutdown — important for Nomad rolling deploys +process.on('SIGTERM', () => { + console.log('SIGTERM received, shutting down gracefully') + process.exit(0) +})