This episode builds a CI/CD pipeline: continuous integration with testing and linting, continuous deployment with Docker images, GitHub Actions as a real example, and the blue-green and canary strategies.

Deploying manually is an invitation to disaster: a missed step, out-of-sync environments, or untested code running in production. CI/CD turns that into an automated flow: every change is tested, then deployed without human hands.
Episode 30 covers the CI/CD pipeline for WebSocket applications: what continuous integration does, how continuous deployment works with Docker images, an example GitHub Actions pipeline, and release strategies such as blue-green and canary.
Every code change passes a series of automated checks before it is considered worthy.
bun install --frozen-lockfile
bun run lint
bun run test
bun run buildbun run test runs all the tests from episode 26. A strong CI blocks merges when a test fails, a lint error appears, or the build breaks — so bugs are found in minutes, not after reaching production.
Beyond lint and build, a WebSocket pipeline should also run:
CD builds a Docker image, pushes it to a registry, then deploys it to the target environment.
docker build -t registry.example.com/ws-server:1.4.0 .
docker push registry.example.com/ws-server:1.4.0A version-tagged image is an immutable artifact that can be rolled back. The latest tag is not recommended for production because it obscures which version is actually running.
Deploy the new image to the cluster, let Kubernetes handle the rollout.
kubectl set image deployment/ws-server \
ws-server=registry.example.com/ws-server:1.4.0kubectl set image triggers a rolling update. The health check from episode 28 determines whether a new pod is considered healthy before the old one is terminated.
GitHub Actions automates the whole flow in one file.
name: deploy
on:
push:
branches: [main]
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- run: bun install --frozen-lockfile
- run: bun run lint
- run: bun run test
deploy:
needs: ci
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: registry.example.com
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASS }}
- run: docker build -t registry.example.com/ws-server:1.4.0 .
- run: docker push registry.example.com/ws-server:1.4.0needs: ci ensures the deploy job only runs if ci passes. Registry secrets are stored in GitHub Secrets, not written in the file.
Blue-green runs two environments: the old version (blue) and the new version (green).
kubectl apply -f deployment-green.yaml
kubectl rollout status deployment/ws-server-green
kubectl set service selector ...When green is ready and its health checks pass, traffic is switched all at once. Rollback is as easy as restoring the selector to blue. The consequence: double capacity is needed during the transition.
A canary releases the new version to a small slice of traffic first.
spec:
http:
- route:
- destination:
subset: stable
weight: 90
- destination:
subset: canary
weight: 10weight: 10 routes 10 percent of connections to the canary version. Monitor the error rate and latency (episode 18); if healthy, raise the weight gradually to 100 percent. Canary is safer for WebSocket connections because failures only affect a small fraction of users.
Long-lived WebSocket connections make deployments more complex: old pods cannot be killed outright. The key to success is connection draining: when a pod receives a termination signal, it stops accepting new connections, releases old ones with code 1001 (episode 15), and exits after everyone has moved. Without this, connected users are cut off mid-activity.
Episode 30 automated the code journey: CI blocks bad quality at the door, CD ships tested artifacts to production, and release strategies keep users safe during the transition.
Key takeaways:
In the next episode we cover infrastructure as code: Terraform for the cloud, Helm and GitOps with ArgoCD, and managing secrets and feature flags.