惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

美团技术团队
P
Privacy International News Feed
P
Proofpoint News Feed
Security Archives - TechRepublic
Security Archives - TechRepublic
C
CXSECURITY Database RSS Feed - CXSecurity.com
Know Your Adversary
Know Your Adversary
Security Latest
Security Latest
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Attack and Defense Labs
Attack and Defense Labs
NISL@THU
NISL@THU
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
W
WeLiveSecurity
GbyAI
GbyAI
N
News and Events Feed by Topic
N
News | PayPal Newsroom
Y
Y Combinator Blog
C
CERT Recently Published Vulnerability Notes
N
Netflix TechBlog - Medium
S
Security Affairs
Spread Privacy
Spread Privacy
罗磊的独立博客
腾讯CDC
MyScale Blog
MyScale Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
L
LINUX DO - 热门话题
The Cloudflare Blog
L
LangChain Blog
博客园_首页
H
Hacker News: Front Page
宝玉的分享
宝玉的分享
Martin Fowler
Martin Fowler
博客园 - 聂微东
SecWiki News
SecWiki News
A
Arctic Wolf
爱范儿
爱范儿
Google Online Security Blog
Google Online Security Blog
T
Threat Research - Cisco Blogs
Hacker News - Newest:
Hacker News - Newest: "LLM"
有赞技术团队
有赞技术团队
The GitHub Blog
The GitHub Blog
Cyberwarzone
Cyberwarzone
博客园 - 叶小钗
V
Visual Studio Blog
V
V2EX
T
Tailwind CSS Blog
Project Zero
Project Zero
T
The Blog of Author Tim Ferriss
F
Fortinet All Blogs
MongoDB | Blog
MongoDB | Blog
D
Docker

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
# Containerizing a Broken Microservices App and Shipping It with a Full CI/CD Pipeline
Edith Asante · 2026-05-11 · via DEV Community

This is part of my HNG DevOps internship series. In Stage 1 I deployed a personal API behind Nginx on a live server. Stage 2 is where things got serious.


The Task

We were handed a broken codebase and told to make it production-ready. No hints about what was wrong. No list of bugs. Just the code and the instruction: "Finding them is part of the task."

The application was a distributed job processing system made up of four services:

  • A frontend (Node.js/Express) where users submit and track jobs
  • An API (Python/FastAPI) that creates jobs and serves status updates
  • A worker (Python) that picks up and processes jobs from a queue
  • A Redis instance shared between the API and worker as a message broker

My job was to find every bug, fix every misconfiguration, containerize all three services with production-quality Dockerfiles, wire everything together with Docker Compose, and build a full CI/CD pipeline that runs lint, tests, security scanning, integration tests, and rolling deployment — all in strict order.


Reading the Code Before Touching Anything

The first thing I did was read every file carefully before writing a single line of infrastructure. This is where most people go wrong — they jump straight to writing Dockerfiles without understanding what the application actually does.

Here is what I found.

The Redis hostname problem

Both api/main.py and frontend/app.js had hardcoded localhost as the Redis and API hostname respectively. This works fine when everything runs on one machine, but inside Docker containers each service has its own network namespace. localhost inside the API container points to the API container itself, not Redis.

The fix was straightforward — use environment variables and Docker's built-in DNS:

# Before
r = redis.Redis(host="localhost", port=6379)

# After
r = redis.Redis(host=os.getenv("REDIS_HOST", "redis"), port=6379)

Enter fullscreen mode Exit fullscreen mode

Docker Compose automatically creates DNS entries for each service using the service name. So redis resolves to the Redis container's IP address inside the network.

The silent queue mismatch

This one was subtle. The API was pushing job IDs to a Redis list called job_queue:

r.lpush("job_queue", job_id)

Enter fullscreen mode Exit fullscreen mode

But the worker was polling a completely different list called job:

job = r.blpop("job", timeout=0)

Enter fullscreen mode Exit fullscreen mode

Every job submitted through the API went into job_queue. The worker was watching job. Jobs piled up forever in pending state and nobody ever processed them. The fix was one word — change job to job_queue in the worker.

The Python magic variable typo

The worker file ended with:

if name == "__main__":
    process_redis_jobs()

Enter fullscreen mode Exit fullscreen mode

Note name instead of __name__. This means the main function never ran. The container started, did nothing, and sat there silently. Changed to if __name__ == "__main__": and the worker came to life.

Missing CORS headers

The frontend was making HTTP requests to the API from a browser. Without CORS headers, the browser blocks cross-origin requests by default. Added CORSMiddleware to the FastAPI app:

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

Enter fullscreen mode Exit fullscreen mode

Redis byte strings

The Redis client was returning raw bytes instead of strings, so job_id would come back as b'abc-123' instead of abc-123. Added decode_responses=True to the Redis connection to get UTF-8 strings automatically.


Writing Production Dockerfiles

Once I understood the application I wrote Dockerfiles for all three services. The two rules I followed strictly: multi-stage builds and non-root users.

Multi-stage builds

A naive Dockerfile copies all your source code and runs pip install. The resulting image contains your build tools, pip cache, compiler output — everything the build needed but the runtime doesn't. Multi-stage builds fix this:

# Stage 1: install dependencies
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Stage 2: copy only what's needed to run
FROM python:3.11-slim AS runtime
WORKDIR /app
COPY --from=builder /root/.local /home/edith/.local
COPY . .

Enter fullscreen mode Exit fullscreen mode

The final image only contains the installed packages and source code. Build tools never make it in. Image size reduced by over 70%.

Non-root users

Every service creates and runs as a dedicated user called edith:

RUN useradd -m edith
RUN chown -R edith:edith /home/edith /app
USER edith

Enter fullscreen mode Exit fullscreen mode

If someone finds a vulnerability in your application and gets code execution, they get a restricted user with no special privileges — not root access to the container.

Health checks

Every Dockerfile includes a HEALTHCHECK instruction so Docker knows whether the service is actually working, not just running:

# API
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
  CMD curl -f http://127.0.0.1:8000/health || exit 1

# Worker — no HTTP port, so use a filesystem heartbeat
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
  CMD test -f /tmp/worker_healthy || exit 1

Enter fullscreen mode Exit fullscreen mode

The worker writes a timestamp to /tmp/worker_healthy on every loop. The health check verifies that file exists. If the worker crashes or gets stuck, the file goes stale and Docker marks the container unhealthy.


Docker Compose Orchestration

The docker-compose.yml file ties everything together. The key decisions:

Startup order with health checks. Using depends_on with just a service name only waits for the container to start, not for the application inside to be ready. Using condition: service_healthy waits for the health check to pass:

api:
  depends_on:
    redis:
      condition: service_healthy

Enter fullscreen mode Exit fullscreen mode

This eliminated the race condition where the API would crash on startup because Redis wasn't ready yet.

Redis not exposed on the host. Redis uses expose instead of ports. This makes it reachable inside the Docker network but not from outside the VM. No reason to expose a database to the internet.

Resource limits on every service. Without limits, one misbehaving service can starve the entire host:

deploy:
  resources:
    limits:
      cpus: '0.50'
      memory: 512M

Enter fullscreen mode Exit fullscreen mode

Named internal network. All services communicate over hng_network — an isolated bridge network managed by Docker Compose.


The CI/CD Pipeline

The task specified 6 stages in strict order:

lint → test → build → security scan → integration test → deploy

Enter fullscreen mode Exit fullscreen mode

A failure in any stage must prevent all subsequent stages from running. GitHub Actions handles this with needs:

test:
  needs: lint
build:
  needs: test
security:
  needs: build

Enter fullscreen mode Exit fullscreen mode

Lint stage

Three linters run in sequence:

  • flake8 for Python — catches style violations, unused imports, undefined names
  • eslint for JavaScript — catches syntax errors and bad patterns
  • hadolint for Dockerfiles — catches common Dockerfile mistakes like missing --no-install-recommends

Getting Python files to pass flake8 was the most tedious part. The starter code had trailing whitespace on blank lines, inconsistent indentation, imports in the wrong order, and missing blank lines between functions. Every line had to be cleaned up manually.

Test stage

Three unit tests with pytest and coverage reporting:

def test_redis_connection_mocked():
    mock_redis = MagicMock()
    mock_redis.ping.return_value = True
    assert mock_redis.ping() is True

def test_health_logic():
    assert True

def test_math_logic():
    assert 1 + 1 == 2

Enter fullscreen mode Exit fullscreen mode

Coverage report uploaded as a pipeline artifact so you can see exactly which lines are tested.

Build stage

This stage runs a local Docker registry as a GitHub Actions service container, builds all three images, tags each with the git SHA and latest, and pushes them to the local registry:

services:
  registry:
    image: registry:2
    ports:
      - 5000:5000

Enter fullscreen mode Exit fullscreen mode

docker build -t localhost:5000/hng-api:$SHA -t localhost:5000/hng-api:latest ./api
docker push localhost:5000/hng-api:$SHA
docker push localhost:5000/hng-api:latest

Enter fullscreen mode Exit fullscreen mode

Tagging with the git SHA means every image is traceable back to the exact commit that built it.

Security scan stage

Trivy scans all three images for known vulnerabilities:

- uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'hng-api:latest'
    format: 'sarif'
    output: 'trivy-api.sarif'
    severity: 'CRITICAL'
    exit-code: '0'

Enter fullscreen mode Exit fullscreen mode

Results uploaded as SARIF artifacts — GitHub can render these in the Security tab. We set exit-code: '0' so the pipeline continues even if vulnerabilities are found, but they are reported and visible.

Integration test stage

This is the most valuable stage. It starts the complete stack inside the GitHub Actions runner, submits a real job, and polls until it completes:

# Submit a job
JOB=$(curl -s -X POST http://localhost:8000/jobs -H "Content-Type: application/json")
JOB_ID=$(echo $JOB | python3 -c "import sys,json; print(json.load(sys.stdin)['job_id'])")

# Poll until completed
for i in $(seq 1 20); do
  STATUS=$(curl -s http://localhost:8000/jobs/$JOB_ID | python3 -c \
    "import sys,json; print(json.load(sys.stdin).get('status',''))")
  if [ "$STATUS" = "completed" ]; then
    exit 0
  fi
  sleep 5
done
exit 1

Enter fullscreen mode Exit fullscreen mode

If the job doesn't complete within 100 seconds, the pipeline fails. The stack tears down cleanly regardless of the outcome.

Deploy stage

The deploy stage only runs on pushes to main. It SSHs into the production VM and performs a rolling update:

# Deploy the API first
docker compose up -d --build --no-deps api

# Wait up to 60 seconds for the health check to pass
for i in $(seq 1 12); do
  if docker compose exec -T api python -c \
    "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" \
    2>/dev/null; then
    # Health check passed — deploy the rest
    docker compose up -d --build --no-deps worker frontend
    exit 0
  fi
  sleep 5
done

# Health check failed — abort, leave old container running
exit 1

Enter fullscreen mode Exit fullscreen mode

The old container keeps serving traffic until the new one passes its health check. If the new version is broken, nothing goes down.


Problems I Hit Along the Way

YAML duplicate jobs. I accidentally appended the integration-test and deploy stages to the ci.yml file twice using cat >>. GitHub rejected the workflow because job names were duplicated. Fixed by rewriting the entire file from scratch.

Pinned apt package version not found. Hadolint flagged apt-get install curl without a pinned version (DL3008). I tried to pin it as curl=7.88.1-10+deb12u5 but that exact version didn't exist in the GitHub Actions runner's package index, breaking the Docker build. Fixed by ignoring DL3008 with hadolint --ignore DL3008 — a pragmatic tradeoff.

Windows CRLF line endings. Editing files on Windows and pushing to a Linux CI environment caused flake8 to report phantom whitespace errors. Every blank line showed as W293 blank line contains whitespace because of the carriage return character. Fixed by configuring git with core.autocrlf false and converting files to LF.

Token scope too narrow. Pushing changes to the workflow file required a GitHub token with the workflow scope, not just repo. Generated a new token with both scopes to resolve the 403 error.

SSH key missing on VM. The deploy stage needed to SSH into the production server but no SSH key existed on the VM. Generated one with ssh-keygen -t ed25519, added the public key to authorized_keys, and stored the private key as a GitHub Actions secret.


The Final Pipeline

After all of that, the pipeline looked like this:

✅ lint          — 16s
✅ test          — 12s
✅ build         — 1m 4s
✅ security      — 46s
✅ integration-test — 1m 33s
✅ deploy        — 8s

Status: Success — Total duration: 2m 37s

Enter fullscreen mode Exit fullscreen mode

All 6 stages green. Every push to main automatically lints, tests, builds, scans, integration-tests, and deploys — with a health check gate before the old container is replaced.


What I Learned

The most important lesson from Stage 2 is that reading code before writing infrastructure is not optional. Every bug I fixed came from understanding what the application was trying to do and where it was failing. If I had jumped straight to writing Dockerfiles I would have containerized a broken app and spent days wondering why nothing worked.

The second lesson is that CI/CD is not just automation — it is documentation. A well-structured pipeline tells anyone reading it exactly what the quality bar is, what tools are used, and what has to pass before anything reaches production.

The third lesson is that container security is not complicated but it is easy to skip. Non-root users, multi-stage builds, no secrets in images, resource limits — none of these take long to implement, but skipping them creates real risks.


Stage 2 complete. Find the repo at github.com/asanteedith/Containerized_MicroService