A Complete Pipeline
Objectives
By the end of this lesson, you should be able to:
- Write a Dockerfile for the combined application from the last lesson
- Write a CI/CD workflow that tests it with real environment configuration and publishes it
- Explain how deployment secrets map to the validated environment schema from Module 1
💡 Why this matters: Lesson 1 built the application, this lesson builds everything around it, a container, and a pipeline that tests and publishes that container automatically, exactly Module 3 and Module 4 of this course, applied directly to this specific, complete application.
⚠️ A note on verification: Docker and GitHub Actions can’t run inside this course’s own sandboxed tooling. The YAML below was validated with a real parser for correctness, both reflect stable, current, documented behavior. Run this yourself, on a real repository with Docker installed, to see it work end to end.
The Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app ./
EXPOSE 3000
CMD ["node", "server.js"]
Exactly Module 3’s multi-stage pattern, server.js (not shown here) would be the small entry point that calls validateEnv(), then createApp(env), then app.listen(env.port), the same shape as every application built throughout this course.
The CI/CD Workflow
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test
env:
NODE_ENV: test
JWT_SECRET: ci-test-secret-at-least-16-chars
DATABASE_URL: postgresql://localhost/test
build-and-push:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.sha }}
ghcr.io/${{ github.repository }}:latest
The single new detail, compared to Module 4’s version, is the env: block on the test step, JWT_SECRET and DATABASE_URL set directly for the CI run itself, this is exactly Module 1’s validateEnv() in action, the test suite genuinely can’t run without them, since the application refuses to even start, CI needs to supply real (if fake, test-only) values, precisely as Module 1 intended.
What Real Secrets Look Like in Production
In an actual deployment, none of these values are hardcoded anywhere, JWT_SECRET and DATABASE_URL for the CI run above are test-only, safe to be unremarkable, the real production JWT_SECRET, DATABASE_URL, and REDIS_URL (Module 1’s schema extended in Lesson 1) are set directly in the hosting platform’s environment variable configuration (Module 5), the same validateEnv() function runs in every environment, refusing to start if any of them are missing, whether that’s a CI run with a forgotten env: block or a real deployment with a forgotten platform configuration.
The Complete Path, End to End
A developer pushes a commit, CI runs the full test suite (Lesson 1’s combined app, exercised by Course 3’s Jest and Supertest suite) against real, validated test configuration, if it passes, and the push was to main, a Docker image is built and published, tagged both by commit and as latest, a connected hosting platform (Module 5) picks up the new image and deploys it, replacing the old instance gracefully (Module 2’s SIGTERM handling), behind a reverse proxy terminating HTTPS (Module 6), this is the complete, automated path this entire course has been building toward, one piece at a time.
Try It
- Write
server.js, the small entry point tyingvalidateEnv(),createApp(), andapp.listen()together, and confirm it starts correctly with valid environment variables, and refuses to start without them. - Write the Dockerfile above, and build it locally, on your own machine, confirming the resulting container runs the application correctly.
- Set up the CI/CD workflow on a real repository, and confirm the test job fails if the
env:block is removed, exactly reproducing Module 1’s “refuses to start without configuration” behavior, now inside CI specifically. - Explain, in one or two sentences, why the CI job needs its own
JWT_SECRETandDATABASE_URL, distinct from whatever values production actually uses.
Recap
- A multi-stage Dockerfile (Module 3) packages the combined application from Lesson 1 into a small, production-ready image.
- A CI/CD workflow (Module 4) tests it with real, validated environment configuration, then builds and publishes the image, only for code that already passed.
- The same
validateEnv()function, and the same refusal to start without proper configuration, applies identically in CI, in a container, and in a real production deployment.
Next lesson: a final production readiness review, and where this course, and this entire track, leaves off.