Career OS

Continuous Deployment

The last manual step in software is also the most dangerous one: a tired engineer SSHing into a server at 11pm, copying a JAR, restarting the service, and hoping. Continuous Deployment deletes that step — a green build deploys itself, the same way every time, with a recorded history and a way back. This module ends with the real proof: the very page you’re reading was deployed by a pipeline you can read line by line.

The Goal

By the end of this module you can:

  • Distinguish Continuous Delivery from Continuous Deployment and say which one fits a small team
  • Deploy a Spring Boot app to a free tier (Railway or Render) without touching a server by hand
  • Handle secrets the right way — never in YAML, always injected — and explain the mindset behind it
  • Set up separate environments so a bad deploy can’t take down what users depend on
  • Roll back a broken release fast, and know which rollback strategy fits which situation
  • Read this site’s real GitHub Pages deploy workflow and explain every key

The Lesson

CD is the natural end of the pipeline

You’ve built the pipeline up across this track: every push gets built and tested, and a green checkmark means safe to merge. Continuous Deployment is the last link — when the checkmark is green, the code ships automatically. No human runs the deploy.

There’s a fork in the road, and the names get confused constantly:

TermWhat happens on a green buildWho decides to release
Continuous DeliveryCode is built, tested, and ready to deploy — packaged and waitingA human clicks “deploy”
Continuous DeploymentCode is built, tested, and deployed to production automaticallyNobody — green means live

Both abbreviate to “CD,” which is why interviewers ask you to disambiguate. Delivery keeps a human gate before production; Deployment removes it. For a beginner project, Delivery is the honest default — auto-deploy to a staging environment, one manual click to production — until your test suite is trustworthy enough that you’d bet the live service on it. This site uses full Continuous Deployment to its single environment, which is fine precisely because it’s a static site with no data to corrupt.

flowchart LR
    A[Push to main] --> B[Build]
    B --> C[Test]
    C --> D{Green}
    D -->|Delivery| E[Ready - human clicks deploy]
    D -->|Deployment| F[Auto-deploys to production]
    D -->|red| G[Stop - nothing ships]

Deploy to a free tier the no-server way

You have zero budget, so the goal is a real internet-reachable deploy that costs nothing. The easiest honest path for a Spring Boot + Postgres app is a Platform-as-a-Service: Railway or Render. You connect your GitHub repo, the platform watches your branch, and on every push it builds and runs your app and gives it a URL. There is no server for you to patch, no Nginx to configure — that’s the whole point of a PaaS. This is the path week 9 walks you through hands-on; deploy splitease-api there following month 3 — week 9.

flowchart LR
    A[git push] --> B[Railway or Render detects the push]
    B --> C[Builds the app from your repo]
    C --> D[Injects env vars and secrets]
    D --> E[Starts the app, runs health check]
    E --> F[App live at a public URL]

The mapping to “real” cloud, so the concept transfers: a PaaS is doing for you what you’d otherwise wire up on AWS — a container running your app, a managed Postgres beside it, environment variables, a load balancer with health checks. Start on the PaaS because it’s free and fast; understand it maps to the same pieces everywhere.

Secrets — never in YAML, ever

Your app needs a database password, a JWT signing secret, maybe an API key. The single rule that protects all of them: a secret never appears in a file you commit. Your workflow YAML, your application.yml, your code — all of it goes into a public Git repo. Paste a real password there and it’s exposed to the entire internet, and Git history means deleting it later doesn’t help — it’s in every clone forever.

So secrets live outside the repo and get injected at deploy time:

Where the secret livesHow the app gets it
GitHub repo → Settings → Secrets${{ secrets.DB_PASSWORD }} in the workflow, never the literal value
Railway/Render dashboard → VariablesSet in the platform UI, exposed to the app as an environment variable
AWS Secrets Manager / SSMApp fetches at startup with an IAM role

The pattern is always the same: the file references the secret by name; the platform substitutes the real value at runtime. This is the exact mindset from Spring Boot — security & JWT: the signing secret is read from configuration, never hardcoded, so it can differ per environment and never touches source control.

# CORRECT — references a secret by name
env:
  DB_PASSWORD: ${{ secrets.DB_PASSWORD }}

# CATASTROPHIC — a real secret committed to a public repo, exposed forever
env:
  DB_PASSWORD: SplitEase_Prod_2026!

The classic, career-denting trap: committing an AWS key or DB password “just to test it,” then force-pushing to hide it. Force-push doesn’t help — bots scrape public repos for keys within minutes. The only real fix is to rotate (regenerate) the leaked secret immediately. Treat every secret as if it’s already being watched.

Environments — don’t test in production

An environment is a separate running copy of your app — same code, different config and data. The standard set:

EnvironmentPurposeWho hits it
developmentYour laptopYou
stagingA production-like copy for final checksThe team, automated tests
productionThe real thingReal users

Each gets its own database and its own secrets. A bug discovered in staging never touched a real user’s data. The deploy flow becomes: green build → auto-deploy to staging → smoke-test → promote to production. GitHub Actions models this with an environment: key on a job, which can require a manual approval before the job runs — that’s your human gate for Continuous Delivery.

The beginner mistake is one environment doing double duty — “I’ll just test against prod, it’s only me.” The day it isn’t only you, a test write corrupts real data. Separate environments are cheap insurance you set up before you need them.

Rollback — the way back

Deploys go wrong. The measure of a mature pipeline isn’t that it never breaks — it’s how fast you recover. Know these three by name:

StrategyHow it worksBest for
Redeploy previous versionPoint the platform back at the last good build/commitThe simple, universal first move
Blue-greenTwo identical environments; flip traffic to the old one instantlyZero-downtime, instant rollback
RollingReplace instances a few at a time; halt and reverse if errors spikeLarge fleets, gradual exposure

On Railway/Render, rollback is literally a button: select the previous deployment and “redeploy.” Because every deploy is tied to a Git commit, “roll back” means “deploy the commit before the bad one” — which is also why your Git history and good commit discipline pay off at the worst moment. The one-line interview distinction: blue-green flips between two whole environments instantly; rolling swaps instances gradually within one.

Worked example — how THIS site deploys

This is the payoff: the site you’re reading right now is a Docusaurus site deployed to GitHub Pages by a real workflow, .github/workflows/deploy.yml. Every push to main rebuilds and republishes it in about 70 seconds. Here it is, read key by key — this is genuine Continuous Deployment to a single environment.

name: Deploy to GitHub Pages

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: true

jobs:
  build:
    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 run build
      - uses: actions/upload-pages-artifact@v3
        with:
          path: build

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - id: deployment
        uses: actions/deploy-pages@v4
KeyWhat it does, and why it’s here
on: push: branches: [main]The trigger — every merge to main deploys. This is the “continuous” in CD
workflow_dispatchLets you run the deploy manually from the Actions tab too — useful when a build is fine but needs a re-run
permissions: pages: write, id-token: writeLeast privilege — grants this workflow exactly the rights to publish Pages and nothing more. No secret token is stored; GitHub mints a short-lived one
concurrency: group: pages, cancel-in-progressIf two pushes land close together, cancel the older in-flight deploy so the newest wins — no racing deploys clobbering each other
build jobCheckout → set up Node → npm ci (clean install) → npm run build → upload the built site as an artifact. Pure CI: compile the site
needs: buildThe deploy job waits for build. If the build fails, nothing deploys — fail-fast protects production
environment: github-pagesThe named environment, with the live URL surfaced in the Actions run
actions/deploy-pages@v4Takes the uploaded artifact and publishes it to GitHub Pages — the actual go-live step

Notice what’s not here: no password, no API key, no server IP. Pages auth uses a short-lived id-token minted per run instead of a stored secret — the secrets rule taken to its conclusion. And rollback is built in: a bad deploy is fixed by pushing a revert commit (or re-running an older successful run), and ~70 seconds later the site is good again. Every concept in this module, working, in a file you can open in your own repo right now.

Check The Concept

How This Shows Up At Work

  • The Friday-night deploy that broke prod. Manual deploys fail at the worst time because they depend on one tired person remembering twelve steps. The team that automated it — green build deploys itself, rollback is one button — sleeps through the incident the other team is firefighting.
  • The leaked key incident. A junior commits an API key “just to test,” the security bot files an alert within minutes, and the whole team scrambles to rotate it. Knowing before you do it that secrets are injected, never committed, is the difference between a calm engineer and an incident.
  • The “it worked in staging” save. A change passes every test in staging, then a config difference surfaces in production. The post-incident lesson is environment parity — and the engineer who set up separate environments with their own config looks prescient.
  • The interview pair. “Delivery vs Deployment?” and “How do you handle secrets?” are near-guaranteed for any role touching deploys. Clean, specific answers (human gate vs auto, inject-never-commit, rotate on leak) mark you as someone who’s actually shipped.

Build This

You’ll deploy splitease-api to a free PaaS, then read and reason about this site’s real deploy workflow.

  1. Follow month 3 — week 9 to deploy splitease-api to Railway or Render: connect the GitHub repo, add a managed Postgres, set the connection string and JWT secret as platform variables (not in any file), and let it build and start.

  2. Confirm it’s live: hit the public URL the platform gives you with a real request.

curl https://your-app.up.railway.app/api/health
{"status":"UP"}
  1. Push a trivial change (edit a string in a response) and watch the platform auto-deploy it. Time it — note how long from git push to the change being live. That gap is your deploy latency.

  2. Use rollback for real. In the platform’s deployments list, find the previous deployment and click redeploy/rollback. Confirm the old version is live again. You’ve now recovered a deploy without a single manual file copy.

  3. Open .github/workflows/deploy.yml in this site’s repo and read it against the Lesson table. Point at: the trigger, the needs: build dependency, the concurrency block, and the permissions. Confirm there is no secret value anywhere in the file.

  4. Break it on purpose — fail-fast. On a branch, introduce a deliberate build error in the Docusaurus site (e.g. a broken internal link, which onBrokenLinks: throw rejects). Push and watch the build job go red. Confirm the deploy job is skipped entirely — needs: build stopped a broken build from ever publishing. Fix it.

Interview Practice

Asked constantly for backend and DevOps-adjacent roles at Indian product companies. Say your answer aloud, then check.

1. What's the difference between Continuous Delivery and Continuous Deployment?

Both are “CD.” Continuous Delivery means every green build is automatically built, tested, and packaged so it’s ready to deploy — but a human decides when to release to production. Continuous Deployment removes that human gate: a green build goes live automatically. Delivery suits teams that want a final manual check before production; Deployment suits teams with enough test confidence to ship on green. I’d start a small project on Delivery — auto-deploy to staging, one click to prod — and move to full Deployment once the test suite earns that trust.

2. How do you handle secrets in a CI/CD pipeline?

A secret never goes in a committed file — not the workflow YAML, not application config, not code — because the repo (and its entire history) can leak. Secrets live outside the repo: GitHub Secrets, the deploy platform’s variable store, or a secrets manager like AWS Secrets Manager. The file references them by name (${{ secrets.DB_PASSWORD }}) and the platform injects the real value at runtime. Each environment has its own values. If a secret ever leaks, you rotate it immediately — deleting the commit doesn’t undo the exposure.

3. What makes a good deployment pipeline?

It’s automated (no manual steps to forget), fast (quick feedback, quick deploys), and fails fast — a broken build or test deploys nothing. It has separate environments so production is protected, secrets injected not committed, and a quick, reliable rollback. Every deploy is tied to a commit so you have a clear history and can always go back. The goal is that shipping is boring and recovery is fast.

4. Blue-green vs rolling deployment — one line each.

Blue-green keeps two identical full environments and flips all traffic from the old (blue) to the new (green) at once, giving instant rollback by flipping back. Rolling replaces instances a few at a time within one environment, exposing the new version gradually and halting if errors spike. Blue-green is instant but needs double the resources; rolling is resource-efficient but exposes users to the new version sooner.

5. How would you roll back a bad deploy?

Fastest universal move: redeploy the previous known-good version — on a PaaS that’s a one-click “redeploy previous deployment,” and since deploys map to commits it’s effectively deploying the commit before the bad one. With blue-green I’d flip traffic back to the old environment instantly. The prerequisite is that each deploy is versioned and tied to a commit, and that I haven’t run an irreversible database migration without a plan — schema rollbacks need their own thought.

6. Why do you need separate staging and production environments?

So changes are verified against a production-like environment — its own database, its own config — before real users and real data are exposed. A bug or a test write that hits staging never harms a customer. Testing directly in production is how a stray operation corrupts live data. Environments are cheap to set up and save you from an expensive incident.

7. Walk me through how a static site like a docs portal gets deployed automatically.

A workflow triggers on every push to the main branch. A build job checks out the code, installs dependencies cleanly, and runs the site build into static files, which it uploads as an artifact. A deploy job that depends on the build job then publishes that artifact to the host — for GitHub Pages it’s the deploy-pages action publishing to Pages. Because deploy needs build, a failed build deploys nothing. Auth uses a short-lived token minted per run, so no secret is stored, and rollback is a revert commit that rebuilds in about a minute.

Where to Practice

ResourceWhat to doHow long
docs.github.com (Pages → custom GitHub Actions workflow)Read the official “Publishing with a custom workflow” guide alongside this site’s deploy.yml30 min
docs.github.com (Actions → using secrets)Read “Using secrets in GitHub Actions” and add one secret to a test repo20 min
docs.github.com (Actions → environments)Read “Using environments for deployment” — note the manual-approval option20 min

Check Yourself

  1. In one line each, what’s the difference between Continuous Delivery and Continuous Deployment?
  2. Why must a secret never appear in your workflow YAML, and where does it go instead?
  3. You committed a key to a public repo and force-pushed to remove it. What’s the only real fix?
  4. What does a separate staging environment protect you from?
  5. Name the three rollback strategies and the one-line difference between blue-green and rolling.
  6. In this site’s deploy.yml, what does needs: build guarantee, and why does that matter?
  7. Why does the Pages deploy store no secret token, and what does it use instead?
Answers
  1. Delivery: green builds are auto-built/tested and ready, a human clicks to release. Deployment: green builds go to production automatically, no human gate.
  2. The repo (and its full history) can leak — committing a secret exposes it to everyone forever. It goes in GitHub Secrets / the platform’s variable store / a secrets manager, referenced by name and injected at runtime.
  3. Rotate (regenerate) the leaked secret immediately. Force-push doesn’t help — bots scrape public repos within minutes and the key was already public.
  4. A bug or test write found in staging never touches real users or real production data, because staging has its own database and config.
  5. Redeploy previous version, blue-green, rolling. Blue-green flips all traffic between two whole environments instantly; rolling swaps instances gradually within one environment.
  6. The deploy job runs only after build succeeds — so a failed build deploys nothing. It’s fail-fast protecting the live site from a broken build.
  7. GitHub mints a short-lived id-token per run (with id-token: write permission) instead of a stored long-lived token — the inject-never-store secrets rule taken to its conclusion.

Explain it out loud: Open this site’s deploy.yml and explain it to an empty chair as if onboarding a new teammate — the trigger, why deploy needs build, what concurrency prevents, why there’s no password anywhere, and how you’d roll back a bad deploy. If you stumble on “why no secret is stored,” re-read the secrets section.

Why AI Can’t Do This For You

AI will generate a deploy workflow that looks right — and the most dangerous version is the one that quietly hardcodes a password in the YAML because that’s a common pattern in its training data. It can’t see that your repo is public, that the key it just inlined is your real production database password, or that you skipped the staging environment that would’ve caught last week’s outage. The judgment calls — Delivery or Deployment for this team, what belongs in a secret store, how fast you can actually roll back — are decisions about your risk, not text to autocomplete.

The way you earn that judgment is by deploying something real to a free tier, leaking nothing, rolling back a deploy on purpose, and reading a working pipeline like this site’s until every key makes sense. A prompt can hand you YAML; it can’t hand you the instinct that stops you pasting a secret into a public repo at midnight.

Module done? Add it to today’s tracker

Saves your progress on this device.