From ad6b6b12cd3883cffdb620dc3c8c3ed602a288ec Mon Sep 17 00:00:00 2001 From: Marlon Silva Date: Sun, 7 Jun 2026 15:08:57 -0300 Subject: [PATCH] Adiciona Dockerfile, docker-compose e manifestos Kubernetes - src/Dockerfile: imagem multi-stage node:22-alpine, usuario nao-root e healthcheck - src/.dockerignore: reduz o contexto de build - docker-compose.yml: app + PostgreSQL com healthcheck e volume persistente - k8s/: namespace, configmap, secret, StatefulSet do Postgres e Deployment/Service/Ingress da app Co-Authored-By: Claude Opus 4.8 --- docker-compose.yml | 34 +++++++++++++++++ k8s/00-namespace.yaml | 6 +++ k8s/01-configmap.yaml | 13 +++++++ k8s/02-secret.yaml | 18 +++++++++ k8s/03-postgres-statefulset.yaml | 64 ++++++++++++++++++++++++++++++++ k8s/04-postgres-service.yaml | 17 +++++++++ k8s/05-app-deployment.yaml | 54 +++++++++++++++++++++++++++ k8s/06-app-service.yaml | 18 +++++++++ k8s/07-app-ingress.yaml | 25 +++++++++++++ src/.dockerignore | 8 ++++ src/Dockerfile | 28 ++++++++++++++ 11 files changed, 285 insertions(+) create mode 100644 docker-compose.yml create mode 100644 k8s/00-namespace.yaml create mode 100644 k8s/01-configmap.yaml create mode 100644 k8s/02-secret.yaml create mode 100644 k8s/03-postgres-statefulset.yaml create mode 100644 k8s/04-postgres-service.yaml create mode 100644 k8s/05-app-deployment.yaml create mode 100644 k8s/06-app-service.yaml create mode 100644 k8s/07-app-ingress.yaml create mode 100644 src/.dockerignore create mode 100644 src/Dockerfile diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..14ce1995b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +services: + db: + image: postgres:16-alpine + environment: + POSTGRES_DB: kubedevnews + POSTGRES_USER: kubedevnews + POSTGRES_PASSWORD: Pg#123 + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U kubedevnews -d kubedevnews"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + app: + build: ./src + ports: + - "8081:8080" + environment: + DB_HOST: db + DB_PORT: 5432 + DB_DATABASE: kubedevnews + DB_USERNAME: kubedevnews + DB_PASSWORD: Pg#123 + DB_SSL_REQUIRE: "false" + depends_on: + db: + condition: service_healthy + restart: unless-stopped + +volumes: + pgdata: diff --git a/k8s/00-namespace.yaml b/k8s/00-namespace.yaml new file mode 100644 index 000000000..a2bf4a125 --- /dev/null +++ b/k8s/00-namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: kube-news + labels: + app.kubernetes.io/part-of: kube-news diff --git a/k8s/01-configmap.yaml b/k8s/01-configmap.yaml new file mode 100644 index 000000000..19070c8ff --- /dev/null +++ b/k8s/01-configmap.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: kube-news-config + namespace: kube-news + labels: + app.kubernetes.io/part-of: kube-news +data: + # Aponta para o Service do PostgreSQL (DNS interno do cluster) + DB_HOST: "postgres" + DB_PORT: "5432" + DB_DATABASE: "kubedevnews" + DB_SSL_REQUIRE: "false" diff --git a/k8s/02-secret.yaml b/k8s/02-secret.yaml new file mode 100644 index 000000000..c20391941 --- /dev/null +++ b/k8s/02-secret.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Secret +metadata: + name: kube-news-secret + namespace: kube-news + labels: + app.kubernetes.io/part-of: kube-news +type: Opaque +# stringData permite escrever os valores em texto puro (o Kubernetes +# converte para base64 automaticamente). Em produção, considere usar +# um gerenciador de segredos (Sealed Secrets, External Secrets, Vault...). +stringData: + DB_USERNAME: "kubedevnews" + DB_PASSWORD: "Pg#123" + # Credenciais consumidas pelo container do Postgres + POSTGRES_DB: "kubedevnews" + POSTGRES_USER: "kubedevnews" + POSTGRES_PASSWORD: "Pg#123" diff --git a/k8s/03-postgres-statefulset.yaml b/k8s/03-postgres-statefulset.yaml new file mode 100644 index 000000000..080f56670 --- /dev/null +++ b/k8s/03-postgres-statefulset.yaml @@ -0,0 +1,64 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: postgres + namespace: kube-news + labels: + app.kubernetes.io/name: postgres + app.kubernetes.io/part-of: kube-news +spec: + serviceName: postgres + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: postgres + template: + metadata: + labels: + app.kubernetes.io/name: postgres + app.kubernetes.io/part-of: kube-news + spec: + containers: + - name: postgres + image: postgres:16-alpine + ports: + - name: postgres + containerPort: 5432 + envFrom: + - secretRef: + name: kube-news-secret + env: + # O Postgres grava os dados em /var/lib/postgresql/data; usar um + # subdiretório evita conflito com o lost+found do volume montado. + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: pgdata + mountPath: /var/lib/postgresql/data + readinessProbe: + exec: + command: ["pg_isready", "-U", "kubedevnews", "-d", "kubedevnews"] + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + livenessProbe: + exec: + command: ["pg_isready", "-U", "kubedevnews", "-d", "kubedevnews"] + initialDelaySeconds: 30 + periodSeconds: 15 + timeoutSeconds: 5 + resources: + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" + volumeClaimTemplates: + - metadata: + name: pgdata + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 2Gi diff --git a/k8s/04-postgres-service.yaml b/k8s/04-postgres-service.yaml new file mode 100644 index 000000000..190e89531 --- /dev/null +++ b/k8s/04-postgres-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres + namespace: kube-news + labels: + app.kubernetes.io/name: postgres + app.kubernetes.io/part-of: kube-news +spec: + # Acesso apenas interno ao cluster (a app o consome via DNS "postgres") + type: ClusterIP + selector: + app.kubernetes.io/name: postgres + ports: + - name: postgres + port: 5432 + targetPort: 5432 diff --git a/k8s/05-app-deployment.yaml b/k8s/05-app-deployment.yaml new file mode 100644 index 000000000..2345197b7 --- /dev/null +++ b/k8s/05-app-deployment.yaml @@ -0,0 +1,54 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: kube-news + namespace: kube-news + labels: + app.kubernetes.io/name: kube-news + app.kubernetes.io/part-of: kube-news +spec: + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/name: kube-news + template: + metadata: + labels: + app.kubernetes.io/name: kube-news + app.kubernetes.io/part-of: kube-news + spec: + containers: + - name: kube-news + # >>> Substitua pela sua imagem publicada no registry <<< + image: marlonanderson/kube-news:1.0 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + # Variáveis não sensíveis vêm do ConfigMap e as sensíveis do Secret + envFrom: + - configMapRef: + name: kube-news-config + - secretRef: + name: kube-news-secret + readinessProbe: + httpGet: + path: /ready + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 20 + periodSeconds: 15 + timeoutSeconds: 5 + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "256Mi" diff --git a/k8s/06-app-service.yaml b/k8s/06-app-service.yaml new file mode 100644 index 000000000..004cb15a6 --- /dev/null +++ b/k8s/06-app-service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: kube-news + namespace: kube-news + labels: + app.kubernetes.io/name: kube-news + app.kubernetes.io/part-of: kube-news +spec: + # ClusterIP + Ingress é o padrão mais portável entre nuvens. + # Para expor diretamente via balanceador da nuvem, troque para LoadBalancer. + type: ClusterIP + selector: + app.kubernetes.io/name: kube-news + ports: + - name: http + port: 80 + targetPort: http diff --git a/k8s/07-app-ingress.yaml b/k8s/07-app-ingress.yaml new file mode 100644 index 000000000..4cf632213 --- /dev/null +++ b/k8s/07-app-ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: kube-news + namespace: kube-news + labels: + app.kubernetes.io/name: kube-news + app.kubernetes.io/part-of: kube-news + annotations: + # Ajuste conforme o seu controller (nginx, traefik, alb, gce...). + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + ingressClassName: nginx + rules: + # >>> Troque pelo domínio que você usará na nuvem <<< + - host: kube-news.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: kube-news + port: + number: 80 diff --git a/src/.dockerignore b/src/.dockerignore new file mode 100644 index 000000000..52bc2e39f --- /dev/null +++ b/src/.dockerignore @@ -0,0 +1,8 @@ +node_modules +npm-debug.log +.git +.gitignore +Dockerfile +.dockerignore +*.md +.env diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 000000000..e2bcf90be --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,28 @@ +# syntax=docker/dockerfile:1 + +# ---- Stage de dependências ---- +FROM node:22-alpine AS deps +WORKDIR /usr/src/app +COPY package.json package-lock.json ./ +RUN npm ci --omit=dev + +# ---- Stage final ---- +FROM node:22-alpine +ENV NODE_ENV=production +WORKDIR /usr/src/app + +# Dependências já resolvidas +COPY --from=deps /usr/src/app/node_modules ./node_modules + +# Código da aplicação (static/, views/, models/, etc.) +COPY . . + +# Executa como usuário não-root +USER node + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD node -e "require('http').get('http://localhost:8080/health',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" + +CMD ["node", "server.js"]