From Code to Secure Deployment: Building Jerney with DevSecOps
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
.gitignoreGitleaks
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
.dockerignoreRuntime 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

