Topic #51Advanced7 min read

CI/CD & Containerization

A GitLab pipeline builds, tests, and deploys; Docker multi-stage ships a tiny Nginx image.

#ci-cd#docker#gitlab#deployment#nginx

A typical GitLab pipeline has three sequential stages: build, test, and deploy. The build stage runs npm ci and npm run build, saving dist/ as an artifact passed to later stages. The test stage runs lint and unit tests. The deploy stage (gated to the main branch via only: [main]) syncs dist/ to S3 and invalidates the CloudFront cache so users get the new assets immediately.

For containerized deployment, a multi-stage Dockerfile keeps the final image small. The first stage uses node:20-alpine to install dependencies and build the app; the second stage copies only the resulting dist/ into a lightweight nginx:alpine image to serve. The bulky Node toolchain and node_modules never reach production — the runtime image contains only static files plus Nginx.

SPA routing gotcha: CloudFront, S3, and Nginx don't know about your client-side routes. A deep link like /expenses/42 hits the server, which has no such file and returns 404. The fix is to route all unmatched paths back to index.html so the client router can take over — configure a CloudFront custom error response that serves index.html, or add try_files $uri /index.html in the Nginx config.

Backend Analogy

The GitLab stages mirror a Maven/Jenkins pipeline: build (mvn package) -> test (mvn verify) -> deploy (push artifact). The multi-stage Docker build is the same trick you'd use for a Java service — compile in a JDK image, then copy just the JAR into a slim JRE image — so build tools don't bloat the runtime image.

Key Insights
  • Pipeline stages run in order; build's dist/ artifact is handed to test and deploy stages.
  • Gate deploys with only: [main] so feature branches build and test but never ship.
  • Multi-stage Docker: build in node:20-alpine, then copy only dist/ into nginx:alpine — the Node toolchain never reaches production.
  • SPA deep links 404 on static hosts unless you fall back to index.html (CloudFront error page or Nginx try_files $uri /index.html).

Worked Code

.gitlab-ci.yml — build, test, deploy pipeline
YAML
# .gitlab-ci.yml — build, test, deploy pipeline
stages: [build, test, deploy]

build:
  stage: build
  image: node:20-alpine
  script:
    - npm ci
    - npm run build
  artifacts:
    paths: [dist/]

test:
  stage: test
  image: node:20-alpine
  script:
    - npm ci
    - npm run lint
    - npm run test -- --run

deploy:
  stage: deploy
  image: amazon/aws-cli
  script:
    - aws s3 sync dist/ s3://$S3_BUCKET --delete
    - aws cloudfront create-invalidation --distribution-id $CF_DIST_ID --paths '/*'
  only: [main]
  environment: production
Dockerfile (multi-stage, containerized deployment)
Shell
# Dockerfile (for containerized deployment)
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

Interview-Ready Q&A

Build installs deps with npm ci and runs npm run build, publishing dist/ as an artifact. Test runs lint and unit tests against the code. Deploy — gated to main — syncs dist/ to S3 with --delete to remove stale files and issues a CloudFront invalidation so the CDN serves the new assets. Stages run sequentially and a failure stops the pipeline before deploy.

Things to Remember
  • 1Stages run in order: build -> test -> deploy; pass dist/ as an artifact and gate deploy with only: [main].
  • 2Multi-stage Docker: build in node:20-alpine, copy only dist/ into nginx:alpine for a tiny runtime image.
  • 3Fix SPA 404s on deep links with index.html fallback (CloudFront error page or Nginx try_files $uri /index.html).

References & Further Reading