diff --git a/README.md b/README.md index 2478da8..08224f3 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,24 @@ docker-compose logs -f docker-compose down ``` +### Testnet Compose Deployment + +For testnet deployment readiness, use the dedicated compose file: + +```bash +# Build and run the testnet stack +docker compose -f docker-compose.testnet.yml up -d --build + +# Validate backend health +curl http://localhost:3002/health + +# Check service status +docker compose -f docker-compose.testnet.yml ps + +# Stop testnet stack +docker compose -f docker-compose.testnet.yml down +``` + ### Services | Service | Port | Description | diff --git a/backend/.dockerignore b/backend/.dockerignore index 679c3de..59e68b1 100644 --- a/backend/.dockerignore +++ b/backend/.dockerignore @@ -7,7 +7,6 @@ dist build # Prisma -prisma/migrations prisma/dev.db # Environment files diff --git a/backend/Dockerfile b/backend/Dockerfile index 70ab67a..89a5105 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,44 +1,63 @@ -# Stage 1: Build -FROM node:20-alpine AS builder +# ----------------------------------------------------------------------------- +# Stage 1: Shared base image +# ----------------------------------------------------------------------------- +FROM node:20-alpine AS base WORKDIR /app -# Copy package files -COPY package*.json ./ +# ----------------------------------------------------------------------------- +# Stage 2: Install dependencies once (cached by package lock changes) +# ----------------------------------------------------------------------------- +FROM base AS deps + +ENV NODE_ENV=development -# Install dependencies +# Copy only dependency manifests first to maximize Docker cache reuse +COPY package*.json ./ RUN npm ci -# Copy source code -COPY . . +# ----------------------------------------------------------------------------- +# Stage 3: Build application and prune dev dependencies +# ----------------------------------------------------------------------------- +FROM deps AS build -# Build TypeScript -RUN npm run build +# Copy application source after dependency installation to preserve cache layers +COPY . . -# Stage 2: Production -FROM node:20-alpine AS production +# Build TypeScript output and generate Prisma client artifacts. +# Afterwards prune dev dependencies to keep runtime image smaller. +RUN npm run build \ + && npx prisma generate \ + && npm prune --omit=dev \ + && npm cache clean --force -WORKDIR /app +# ----------------------------------------------------------------------------- +# Stage 4: Runtime image (small, secure, production-only) +# ----------------------------------------------------------------------------- +FROM base AS runtime -# Copy package files -COPY package*.json ./ +ENV NODE_ENV=production -# Install production dependencies only -RUN npm ci --only=production +# Runtime utilities: +# - dumb-init for proper signal handling and zombie reaping +# - wget for container health checks +RUN apk add --no-cache dumb-init wget -# Copy built files from builder -COPY --from=builder /app/dist ./dist -COPY --from=builder /app/prisma ./prisma +# Copy only runtime assets from build stage +COPY --from=build /app/package*.json ./ +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/dist ./dist +COPY --from=build /app/prisma ./prisma -# Generate Prisma client -RUN npx prisma generate +# Use non-root user provided by official Node image +USER node -# Expose port +# Expose backend port EXPOSE 3002 -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ +# Health check endpoint used by orchestrators and compose +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3002/health || exit 1 -# Start the application -CMD ["npm", "start"] +ENTRYPOINT ["dumb-init", "--"] +CMD ["node", "dist/index.js"] diff --git a/docker-compose.testnet.yml b/docker-compose.testnet.yml new file mode 100644 index 0000000..cd608e2 --- /dev/null +++ b/docker-compose.testnet.yml @@ -0,0 +1,97 @@ +services: + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: anchorpoint-backend-testnet + ports: + - "3002:3002" + - "9464:9464" + environment: + - NODE_ENV=production + - PORT=3002 + - DATABASE_URL=file:/app/data/testnet.db + - REDIS_URL=redis://redis:6379 + - STELLAR_NETWORK=testnet + - STELLAR_PASSPHRASE=Test SDF Network ; September 2015 + - QUEUE_CONCURRENCY=5 + - JAEGER_ENDPOINT=http://jaeger:14268/api/traces + - PROMETHEUS_METRICS_PORT=9464 + - OTEL_SERVICE_NAME=anchorpoint-backend-testnet + - OTEL_RESOURCE_ATTRIBUTES=service.name=anchorpoint-backend-testnet,environment=testnet + volumes: + - backend-testnet-data:/app/data + depends_on: + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3002/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 20s + restart: unless-stopped + networks: + - anchorpoint-testnet-network + + redis: + image: redis:7-alpine + container_name: anchorpoint-redis-testnet + command: ["redis-server", "--appendonly", "yes"] + ports: + - "6379:6379" + volumes: + - redis-testnet-data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 5 + start_period: 10s + restart: unless-stopped + networks: + - anchorpoint-testnet-network + + jaeger: + image: jaegertracing/all-in-one:latest + container_name: anchorpoint-jaeger-testnet + ports: + - "16686:16686" + - "14268:14268" + - "14250:14250" + - "6831:6831/udp" + - "6832:6832/udp" + environment: + - COLLECTOR_OTLP_ENABLED=true + restart: unless-stopped + networks: + - anchorpoint-testnet-network + + prometheus: + image: prom/prometheus:latest + container_name: anchorpoint-prometheus-testnet + ports: + - "9090:9090" + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus-testnet-data:/prometheus + command: + - "--config.file=/etc/prometheus/prometheus.yml" + - "--storage.tsdb.path=/prometheus" + - "--storage.tsdb.retention.time=200h" + - "--web.enable-lifecycle" + restart: unless-stopped + networks: + - anchorpoint-testnet-network + +volumes: + backend-testnet-data: + driver: local + redis-testnet-data: + driver: local + prometheus-testnet-data: + driver: local + +networks: + anchorpoint-testnet-network: + driver: bridge diff --git a/infra/k8s/cert-manager/README.md b/infra/k8s/cert-manager/README.md index 7e58c00..a7bc983 100644 --- a/infra/k8s/cert-manager/README.md +++ b/infra/k8s/cert-manager/README.md @@ -32,6 +32,26 @@ # curl -v https://your-hostname.example.com # ============================================================================= +NGINX ingress controller configuration for this cert-manager setup now lives in: + +- `infra/k8s/ingress-nginx/values-testnet.yaml` +- `infra/k8s/ingress-nginx/anchorpoint-testnet-ingress.yaml` + +Apply ingress after cert-manager and certificate resources are ready so TLS secrets +can be attached without repeated reconciliation errors: + +```bash +helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx +helm repo update + +helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \ + --namespace ingress-nginx --create-namespace \ + -f infra/k8s/ingress-nginx/values-testnet.yaml + +kubectl apply -f infra/k8s/ingress-nginx/anchorpoint-testnet-ingress.yaml +kubectl get ingress -n anchorpoint-testnet +``` + # ============================================================================= # Implementation Notes: # ============================================================================= diff --git a/infra/k8s/ingress-nginx/README.md b/infra/k8s/ingress-nginx/README.md new file mode 100644 index 0000000..b9426ce --- /dev/null +++ b/infra/k8s/ingress-nginx/README.md @@ -0,0 +1,49 @@ +# NGINX Ingress Controller Setup (Testnet) + +This directory contains the NGINX ingress-controller configuration and ingress +resource required for AnchorPoint testnet routing. + +## Files + +- `values-testnet.yaml`: Helm values for installing `ingress-nginx` +- `anchorpoint-testnet-ingress.yaml`: Ingress routing for `api.anchorpoint-testnet.example.com` + +## Manual QA Steps + +1. Install or upgrade ingress-nginx with testnet values: + ```bash + helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx + helm repo update + helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \ + --namespace ingress-nginx --create-namespace \ + -f infra/k8s/ingress-nginx/values-testnet.yaml + ``` + +2. Verify controller pods and external service: + ```bash + kubectl get pods -n ingress-nginx + kubectl get svc -n ingress-nginx + ``` + +3. Apply AnchorPoint ingress resource: + ```bash + kubectl apply -f infra/k8s/ingress-nginx/anchorpoint-testnet-ingress.yaml + ``` + +4. Verify ingress status and routing: + ```bash + kubectl get ingress -n anchorpoint-testnet + kubectl describe ingress anchorpoint-api-ingress -n anchorpoint-testnet + ``` + +5. Validate TLS and endpoint: + ```bash + kubectl get secret anchorpoint-api-tls -n anchorpoint-testnet + curl -v https://api.anchorpoint-testnet.example.com/health + ``` + +6. Roll back if needed: + ```bash + kubectl delete -f infra/k8s/ingress-nginx/anchorpoint-testnet-ingress.yaml + helm uninstall ingress-nginx -n ingress-nginx + ``` diff --git a/infra/k8s/ingress-nginx/anchorpoint-testnet-ingress.yaml b/infra/k8s/ingress-nginx/anchorpoint-testnet-ingress.yaml new file mode 100644 index 0000000..c86ba83 --- /dev/null +++ b/infra/k8s/ingress-nginx/anchorpoint-testnet-ingress.yaml @@ -0,0 +1,38 @@ +# ============================================================================= +# AnchorPoint Testnet Ingress +# ============================================================================= +# Routes external HTTPS traffic from the NGINX ingress controller to the +# AnchorPoint testnet backend service. +# ============================================================================= +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: anchorpoint-api-ingress + namespace: anchorpoint-testnet + labels: + app: anchorpoint + component: ingress + environment: testnet + annotations: + cert-manager.io/cluster-issuer: anchorpoint-staging-issuer + nginx.ingress.kubernetes.io/ssl-redirect: "true" + nginx.ingress.kubernetes.io/hsts: "true" + nginx.ingress.kubernetes.io/hsts-max-age: "31536000" + nginx.ingress.kubernetes.io/hsts-include-subdomains: "true" +spec: + ingressClassName: nginx + tls: + - hosts: + - api.anchorpoint-testnet.example.com + secretName: anchorpoint-api-tls + rules: + - host: api.anchorpoint-testnet.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: anchorpoint-worker-svc + port: + number: 3002 diff --git a/infra/k8s/ingress-nginx/values-testnet.yaml b/infra/k8s/ingress-nginx/values-testnet.yaml new file mode 100644 index 0000000..5a6d60f --- /dev/null +++ b/infra/k8s/ingress-nginx/values-testnet.yaml @@ -0,0 +1,50 @@ +# ============================================================================= +# NGINX Ingress Controller Helm Values (Testnet) +# ============================================================================= +# Install command: +# helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx +# helm repo update +# helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \ +# --namespace ingress-nginx --create-namespace \ +# -f infra/k8s/ingress-nginx/values-testnet.yaml +# ============================================================================= + +controller: + replicaCount: 2 + + ingressClassResource: + name: nginx + enabled: true + default: true + controllerValue: k8s.io/ingress-nginx + + ingressClass: nginx + watchIngressWithoutClass: false + + service: + type: LoadBalancer + externalTrafficPolicy: Local + annotations: {} + + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + + metrics: + enabled: true + + config: + use-forwarded-headers: "true" + enable-real-ip: "true" + proxy-body-size: "20m" + ssl-redirect: "true" + hsts: "true" + hsts-max-age: "31536000" + hsts-include-subdomains: "true" + +defaultBackend: + enabled: true