Skip to main content

Command Palette

Search for a command to run...

From Code to Secure Deployment: Building Jerney with DevSecOps

Updated
β€’8 min readβ€’View as Markdown
A
I'm a Computer Science student passionate about Cloud, DevOps, I enjoy building production-ready systems with AWS, Docker, Kubernetes, Terraform, and CI/CD. This blog documents my journey, projects, and lessons as I work toward becoming a global DevOps engineer.

Containerizing a full-stack application, hardening Docker images, securing Kubernetes workloads, and preparing the project for automated DevSecOps delivery.


πŸš€ Introduction

This week, I moved from learning individual DevSecOps tools to applying them to a real full-stack application.

I worked on Jerney, a Gen-Z-style blog platform built with:

  • βš›οΈ React + Vite frontend

  • 🟒 Node.js + Express backend

  • 🐘 PostgreSQL database

  • 🐳 Docker

  • ☸️ Kubernetes

  • ☁️ AWS

  • πŸ—οΈ Terraform

  • πŸ” DevSecOps security practices

The goal wasn't simply to make the application run.

The goal was to ask:

How can I build, containerize, deploy and secure an application using modern DevSecOps practices?


πŸ—οΈ The Application

Jerney is a three-tier blog platform:

                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                 β”‚    Frontend     β”‚
                 β”‚ React + Vite    β”‚
                 β”‚     Nginx       β”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                          β–Ό
                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                 β”‚     Backend     β”‚
                 β”‚ Node + Express  β”‚
                 β”‚     :5000       β”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                          β–Ό
                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                 β”‚   PostgreSQL    β”‚
                 β”‚     :5432       β”‚
                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The application supports:

  • Creating posts

  • Editing posts

  • Deleting posts

  • Comments

  • Health checks

  • REST APIs

But the interesting part for me this week was everything around the application.


🐳 Containerizing the Application

I created separate Docker images for the frontend and backend.

For the frontend, I used a multi-stage Docker build.

FROM node:20-alpine AS build

WORKDIR /app

COPY package.json package-lock.json* ./
RUN npm ci && npm cache clean --force

COPY . .
RUN npm run build

The production image then uses Nginx:

FROM nginx:1.27-alpine AS production

This means the final image doesn't need the Node.js build environment.

The basic flow becomes:

Source Code
     ↓
Node Build Image
     ↓
npm run build
     ↓
Static Files
     ↓
Nginx Production Image

I also configured the frontend container to run Nginx as a non-root user, rather than running the web server with root privileges.


πŸ” Hardening the Backend Container

I applied similar security principles to the Node.js backend.

Instead of running the application as root, I created a dedicated user:

RUN addgroup -S appgroup && adduser -S appuser -G appgroup

USER appuser

I also used:

RUN apk --no-cache add dumb-init

and:

ENTRYPOINT ["dumb-init", "--"]

This addresses the PID 1 problem and provides better signal handling inside the container.

The resulting container follows a few important principles:

❌ Root user
        ↓
βœ… Non-root user

❌ Build dependencies in runtime
        ↓
βœ… Multi-stage build

❌ Unnecessary privileges
        ↓
βœ… Reduced privileges

☸️ Deploying Jerney with Kubernetes

I also created Kubernetes manifests for the application.

The deployment contains:

Jerney Namespace
       β”‚
       β”œβ”€β”€ PostgreSQL
       β”‚      └── EBS Persistent Volume
       β”‚
       β”œβ”€β”€ Backend
       β”‚      β”œβ”€β”€ 2 replicas
       β”‚      └── ClusterIP Service
       β”‚
       └── Frontend
              β”œβ”€β”€ 2 replicas
              └── NodePort Service

The Kubernetes configuration includes the namespace, PostgreSQL deployment, persistent storage, backend/frontend deployments and services. :chatgpt-content-reference{index="0"} :chatgpt-content-reference{index="1"} :chatgpt-content-reference{index="2"}


πŸ’Ύ Persistent Storage for PostgreSQL

Running a database inside Kubernetes introduces a different problem:

What happens to the data if the PostgreSQL pod disappears?

For the lab, I used an EBS-backed PersistentVolumeClaim.

PostgreSQL
    ↓
PVC
    ↓
EBS gp3

The StorageClass uses encrypted gp3 volumes and a Retain reclaim policy. :chatgpt-content-reference{index="3"}

The database then mounts the persistent storage at:

/var/lib/postgresql/data

This helped me understand the difference between stateless application containers and stateful workloads.


πŸ” Kubernetes Security

This was one of the most important parts of the project.

I didn't want the Kubernetes deployment to simply be:

Frontend β†’ Backend β†’ Database

I wanted explicit security boundaries.


1. Non-root Containers

The backend runs with:

runAsNonRoot: true
runAsUser: 1000

and:

allowPrivilegeEscalation: false

while also dropping Linux capabilities.

The frontend follows a similar non-root model. :chatgpt-content-reference{index="4"} :chatgpt-content-reference{index="5"}


2. Service Account Token Protection

The frontend, backend and database don't need to communicate with the Kubernetes API directly.

So I disabled automatic service-account token mounting:

automountServiceAccountToken: false

This follows the principle:

Don't give a workload permissions or credentials it doesn't need.


🌐 Network Policies

I also implemented Kubernetes NetworkPolicies.

The intended traffic flow is:

Frontend
   β”‚
   β”‚ :5000
   β–Ό
Backend
   β”‚
   β”‚ :5432
   β–Ό
PostgreSQL

The database only accepts traffic from the backend:

Backend β†’ PostgreSQL     βœ…
Frontend β†’ PostgreSQL    ❌

Similarly, the backend only accepts traffic from the frontend. :chatgpt-content-reference{index="6"}

This was a good practical demonstration of zero-trust networking inside Kubernetes.


🩺 Health Checks

I also added Kubernetes liveness and readiness probes.

For the backend:

GET /api/health

is used to determine whether the application is healthy and ready to receive traffic. :chatgpt-content-reference{index="7"}

The frontend similarly exposes / for its health checks.

This introduced another important production concept:

Running isn't the same as being healthy.


πŸ”‘ Secrets

I also worked with Kubernetes Secrets for database configuration.

The application consumes database credentials through:

Kubernetes Secret
       ↓
Environment Variables
       ↓
Backend

However, this also reinforced an important security lesson:

Base64-encoded Kubernetes Secrets are not the same thing as encrypted secrets.

For a production GitOps workflow, I would move toward External Secrets Operator + AWS Secrets Manager/Vault, rather than committing secret values to Git.

The manifest itself even notes External Secrets Operator or Sealed Secrets as the production direction. :chatgpt-content-reference{index="8"}


πŸ›‘οΈ DevSecOps Practices Around the Project

The project wasn't limited to Kubernetes security.

I also applied the security practices I had been learning throughout the week:

Git Security

  • .gitignore

  • Gitleaks

  • Pre-commit secret scanning

  • Branch protection

  • CODEOWNERS

  • Dependabot

Infrastructure Security

  • Terraform

  • Checkov

  • IaC security scanning

Application Security

  • SAST

  • SCA

  • DAST

  • SonarQube

  • pip-audit

  • OWASP ZAP

Container Security

  • Non-root containers

  • Multi-stage builds

  • Minimal images

  • .dockerignore

  • Runtime hardening

Kubernetes Security

  • RBAC

  • NetworkPolicies

  • Security contexts

  • Kyverno

  • Secrets management

  • Resource requests/limits

  • Liveness/readiness probes


☁️ AWS + Terraform

The project also has an infrastructure/deployment side using AWS and Terraform.

The repository separates the standard application from the DevSecOps implementation, with the devops branch containing the Docker, Kubernetes, Terraform, CI/CD and security work.

That separation helped me think about the project in two different layers:

Application
    ↓
Build & Package
    ↓
Infrastructure
    ↓
Deployment
    ↓
Security
    ↓
Operations

Instead of treating DevOps as simply "deploying the application."


⚠️ One Security Lesson I Caught

While working through the deployment setup, I also came across something important:

The EC2 setup script currently contains database credentials directly in the script.

That's exactly the kind of thing that DevSecOps practices are supposed to catch.

So the next improvement is to replace hardcoded credentials with a proper secret-management approach such as:

AWS Secrets Manager
        ↓
Deployment / Application
        ↓
Runtime Secret Injection

This was a useful reminder that having security tools in a project doesn't automatically make the project secure.

Security requires continuously reviewing the actual implementation.


🧠 What I Learned

This project helped connect many of the concepts I've been learning individually.

Previously, I was learning:

Docker
Kubernetes
Terraform
AWS
Security
CI/CD

as separate technologies.

Now I'm starting to see them as one system:

                    Git
                     β”‚
                     β–Ό
              Security Checks
                     β”‚
                     β–Ό
              Docker Build
                     β”‚
                     β–Ό
               Image Scan
                     β”‚
                     β–Ό
                    ECR
                     β”‚
                     β–Ό
              Kubernetes / EKS
                     β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β–Ό                     β–Ό
      Frontend                Backend
                                β”‚
                                β–Ό
                            PostgreSQL

With security controls running throughout the pipeline.


πŸš€ What's Next?

The next step is to take what I've built manually and automate the entire workflow.

My target DevSecOps pipeline is:

Git Push
   ↓
GitHub Actions
   ↓
Gitleaks
   ↓
SAST
   ↓
SCA
   ↓
Checkov
   ↓
Docker Build
   ↓
Container Scan
   ↓
Push Image
   ↓
Deploy
   ↓
Kubernetes
   ↓
Policy + Network Security
   ↓
Monitoring

Eventually, I want to connect this with GitOps using Argo CD, along with Prometheus and Grafana for Kubernetes observability.


πŸ”§ Tech Stack

Frontend: React, Vite, Nginx
Backend: Node.js, Express
Database: PostgreSQL
Containers: Docker
Orchestration: Kubernetes / EKS
Cloud: AWS
IaC: Terraform
CI/CD: GitHub Actions
Security: Gitleaks, Checkov, SonarQube, pip-audit, OWASP ZAP, Kyverno
Container Security: Non-root, multi-stage builds, runtime hardening
Kubernetes Security: RBAC, NetworkPolicy, SecurityContext
Secrets: Kubernetes Secrets, Vault / External Secrets concepts


Final Takeaway

This week wasn't about adding another tool to my DevOps stack.

It was about changing the way I think about deployments.

Instead of:

Build β†’ Deploy β†’ Done

I'm moving toward:

Build β†’ Test β†’ Scan β†’ Secure β†’ Deploy β†’ Monitor β†’ Improve

That's the mindset I'm trying to develop as I move deeper into DevOps, Cloud and DevSecOps engineering. πŸ”β˜οΈπŸš€

Github Link : https://github.com/AyushChoudhary6/jerney.git