Skip to content

Build & Deploy

Build / scripts

{
  "scripts": {
    "test":  "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon server.js",
    "dev":   "nodemon server.js"
  }
}

Both start and dev invoke nodemon. As elsewhere in the platform, production presumably overrides this via PM2 node server.js (verify with the ops owner).

Dockerfile (multi-stage, optimised)

A well-crafted multi-stage Dockerfile is committed. Highlights:

# Stage 1: Builder
FROM node:20.19.2-slim AS builder
ENV NODE_ENV=production \
    YARN_CACHE_FOLDER=/tmp/.yarn-cache \
    YARN_NETWORK_CONCURRENCY=8
WORKDIR /usr/src/app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile --production=true --prefer-offline --link-duplicates && \
    find node_modules -type d \( -name "test*" -o -name "example*" -o -name "docs" -o -name ".git" \) -prune -exec rm -rf {} + && \
    find node_modules -type f \( -name "*.md" -o -name "*.ts" -not -path "*/typescript/*" -o -name "LICENSE*" -o -name "CHANGELOG*" -o -name "*.o" \) -delete && \
    yarn cache clean

# Stage 2: Runtime
FROM node:20.19.2-slim
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y --no-install-recommends \
    nginx libnss3 libexpat1 fontconfig && \
    apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY .env conf.js favicon.ico ./
COPY nginx.conf /etc/nginx/nginx.conf
COPY *.js ./
COPY routes/ ./routes/
COPY modules/ ./modules/
COPY helper/ ./helper/
COPY actions/ ./actions/
COPY conf/ ./conf/
RUN echo '#!/bin/bash\nnginx -g "daemon off;" &\nnode server.js' > /entrypoint.sh && \
    chmod +x /entrypoint.sh
EXPOSE 80 8080
CMD ["/entrypoint.sh"]

Notable:

  • Two-stage: builder strips test files / docs / LICENSE / CHANGELOG / .o files from node_modules to slim the runtime image
  • Runtime libs for Puppeteer: libnss3, libexpat1, fontconfig are present — needed for headless Chrome
  • nginx in same container: not a sidecar; nginx is launched alongside node via the entrypoint script. nginx + node share the container's network and file system.
  • .env is COPY'd into the image: this means the .env file must exist in the build context and is baked into the image. Anyone who pulls the image can read the secrets. This is a finding — see security.md. Better: pass .env as a runtime mount (docker run -v), or use Docker secrets / env vars on the orchestrator.

nginx.conf

In the repo. Not inspected in detail in this audit; should be audited for:

  • Reverse-proxy target (likely 127.0.0.1:5002 or similar)
  • HSTS / X-Frame-Options / Referrer-Policy / X-Content-Type-Options headers
  • Body-size limit
  • Suppressed server header

push.sh — manual Docker → ECR image push

push.sh is not an SSH/git-pull script — it builds and pushes a Docker image to ECR via deploy_image.py:

#!/bin/bash
python deploy_image.py \
  --image-tag "dapi" \
  --repository-uri 255061853867.dkr.ecr.us-west-2.amazonaws.com/designers \
  --dockerfile /home/sanjai/designer-api
  • ECR repos (two on the designer side): designers — the designer-api service image that push.sh targets — and designers-crons for the designer cron/worker fleet (57 jobs + 6 bots). Both are in account 255061853867, region us-west-2, separate from the someli-api crons repos (dev_crons / uat / live_crons). See ../DEPLOYMENT-OVERVIEW.md §1. Verify with ops how the designers-crons image is built/pushed — push.sh only covers designers.
  • Tag: dapi, overwritten on every push — no image versioning (see D-7).
  • Hardcoded workstation path /home/sanjai/designer-api — releases break for anyone but sanjai (truck factor of 1; same antipattern as someli-api's push.sh).
  • deploy_image.py authenticates to ECR via boto3, runs docker build, tags, and pushes. The ECS task definition / service that consumes the designers image lives in AWS, not in this repo — verify with ops which cluster/service runs it.

No CI

  • No Jenkinsfile
  • No .github/workflows/ — unlike someli-platform, there is no automated ECS pipeline (uat_app_ecs.yml) here; the image push is fully manual via push.sh.
  • someli-api has both Jenkins and GitHub Actions (for the Lightsail dev path); designer-api has neither in repo.

This is a gap — see D-2.

PM2 management

No ecosystem.config.js. With 57 jobs + 6 bots + the main server, the deploy box must somehow pm2 start each one. Either:

  • The team does this manually on first deploy and PM2 saves the list via pm2 save / pm2 startup
  • There's an external ops repo with PM2 manifests
  • Everything runs inside server.js's process via the require() of the bots (which also kicks off their crons) — would mean PM2 only manages one process

Audit recommendation: commit an ecosystem.config.js. If everything is in one process, document it; if there are many processes, declare them.

Health checks

No /health endpoint. nginx may have its own health-check that just verifies the upstream responds. Verify.

Rollback

  • Docker: docker run <previous-tag> (if tags are kept)
  • Bare-metal: git checkout <previous-sha> && pm2 restart

Neither is documented in repo.

ID Recommendation Effort
D-1 Remove .env COPY from Dockerfile; use runtime mount or orchestrator env vars Small
D-2 Commit an automated ECS deploy workflow modelled after someli-platform/.github/workflows/uat_app_ecs.yml (OIDC → ECR login → build/push SHA-tagged image to designers → render+deploy task definition) instead of the manual push.sh Small
D-3 Commit ecosystem.config.js with all processes declared Small
D-4 Add a /health endpoint Trivial
D-5 Audit nginx.conf for security headers Small
D-6 Document the rollback runbook Trivial
D-7 Tag Docker images with the git SHA so rollback is well-defined Small