CI/CD & Containerization
A GitLab pipeline builds, tests, and deploys; Docker multi-stage ships a tiny Nginx image.
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.
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.
- 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
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 (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 80Interview-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.
- 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).