Building a Secure and Scalable Voting App with Docker, Docker Compose, and DevSecOps

Greetings! 👋 I'm Priyadarshi Ranjan, a dedicated DevOps Engineer embarking on an enriching journey. Join me as I delve into the dynamic realms of cloud computing and DevOps through insightful blogs and updates. 🛠️ My focus? Harnessing AWS services, optimizing CI/CD pipelines, and mastering infrastructure as code. Whether you're peers, interns, or curious learners, let's thrive together in the vibrant DevOps ecosystem. 🌐 Connect with me for engaging discussions, shared insights, and mutual growth opportunities. Let's embrace the learning curve and excel in the dynamic realm of AWS and DevOps technology!
Introduction
In today’s world, creating and deploying applications quickly and securely is crucial. Imagine you're tasked with building a simple voting application where users can vote between two options. The architecture involves multiple technologies: a front-end web app in Python, a Redis database to collect votes, a .NET worker to process and store votes, a Postgres database for persistent storage, and a Node.js app to display results in real time. To make this application scalable, secure, and easy to deploy, we’ll use Docker, Docker Compose, and implement DevSecOps practices using tools like SonarQube, Trivy, and OWASP.
Project Breakdown
Let’s break down the components of our project:
Front-End Web App (Python): This is the interface users interact with to cast their votes. It's built with Python and provides two options for users to choose from.
Redis Database: Redis is an in-memory data store used here to temporarily collect and store the votes as they come in.
.NET Worker: This worker consumes the votes from Redis, processes them, and stores them in a Postgres database.
Postgres Database: A relational database that persists the voting data. It's backed by a Docker volume, ensuring data is not lost when containers are restarted.
Node.js Web App: Displays the results of the voting in real time, giving users immediate feedback on the current standings.
Docker & Docker Compose: Docker allows us to containerize each component, ensuring they run consistently across different environments. Docker Compose is used to define and manage multi-container Docker applications, making deployment straightforward.
DevSecOps with SonarQube, Trivy, OWASP, and Docker Hub: We’ll integrate security checks and quality assurance into our deployment pipeline. SonarQube will analyze code quality, Trivy will scan for vulnerabilities in Docker images, and OWASP tools will help us secure the application. Finally, we’ll push the secure images to Docker Hub.
Real-Life Example: Voting on Your Favorite Pet
To make this more relatable, let’s imagine a voting app where users can vote between two pets: Cats and Dogs.
Front-End Web App: Built in Python using Flask. Users visit the app and choose between “Cats” and “Dogs.”
Redis: As soon as a user votes, the vote is sent to Redis.
.NET Worker: This worker fetches the vote from Redis, processes it, and stores it in a Postgres database.
Postgres: Stores the total votes for each option, ensuring this data is safe even if the server restarts.
Node.js App: Displays the current voting results, updating in real time as new votes come in.
Step-by-Step Implementation
Creating Dockerfiles for Voting, Worker, and Result Apps: The first step is to containerize the key components of our application by creating Dockerfiles for each of them.
Voting App (Python): The voting app allows users to cast their votes. Below is a simple Dockerfile for the Flask-based voting app.
# Using official python runtime base image FROM python:3.9-slim # add curl for healthcheck RUN apt-get update \ && apt-get install -y --no-install-recommends \ curl \ && rm -rf /var/lib/apt/lists/* # Set the application directory WORKDIR /app # Install our requirements.txt COPY requirements.txt /app/requirements.txt RUN pip install -r requirements.txt # Copy our code from the current folder to /app inside the container COPY . . # Make port 80 available for links and/or publish EXPOSE 80 # Define our command to be run when launching the container CMD ["gunicorn", "app:app", "-b", "0.0.0.0:80", "--log-file", "-", "--access-logfile", "-", "--workers", "4", "--keep-alive", "0"]Worker (.NET): The worker processes votes from Redis and stores them in the Postgres database.
FROM mcr.microsoft.com/dotnet/sdk:6.0 as builder WORKDIR /Worker COPY src/Worker/Worker.csproj . RUN dotnet restore COPY src/Worker/ . RUN dotnet publish -c Release -o /out Worker.csproj # app image FROM mcr.microsoft.com/dotnet/core/runtime:3.1 #COPY --from=builder /out . WORKDIR /app ENTRYPOINT ["dotnet", "Worker.dll"] COPY --from=builder /out .Result App (Node.js): The result app shows the real-time voting results.
FROM node:18-slim # add curl for healthcheck RUN apt-get update -y #&& apt-get install -y --no-install-recommends curl \ RUN rm -rf /var/lib/apt/list/* # Add Tini for proper init of signals #ENV TINI_VERSION v0.19.0 #ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini #RUN chmod +x /tini WORKDIR /app # have nodemon available for local dev use (file watching) RUN npm install -g nodemon COPY package*.json ./ RUN npm ci \ && npm cache clean --force \ && mv /app/node_modules /node_modules COPY . . #ENV PORT 80 EXPOSE 80 CMD [ "node", "server.js"]
Setting Up Docker Compose: Docker Compose allows us to define all the services in a
docker-compose.ymlfile. This file describes how the containers interact, which ports to expose, and how to network them together.version: '3.9' services: vote: build: ./vote container_name: vote ports: - '2000:80' depends_on: - redis redis: image: redis container_name: redis ports: - '6379:6379' worker: build: ./worker container_name: worker depends_on: - redis - postgres postgres: image: postgres container_name: postgres ports: - '5432:5432' environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres result: build: ./result container_name: result ports: - '4000:4000' depends_on: - postgresIntegrating DevSecOps:
SonarQube: Set up SonarQube to analyze code quality. SonarQube scans your codebase, providing insights into potential bugs, code smells, and security vulnerabilities.
docker run -d -p 9000:9000 -- name sonar-server sonar:latest-community



Trivy: Trivy is used to scan Docker images for vulnerabilities. Before pushing the Docker images to Docker Hub, run Trivy to ensure they are secure.
#install trivy on ubuntu machine sudo apt-get install wget apt-transport-https gnupg wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | sudo tee -a /etc/apt/sources.list.d/trivy.list sudo apt-get update sudo apt-get install trivyOWASP: Use OWASP tools to check for common web vulnerabilities like SQL Injection and Cross-Site Scripting (XSS). These checks should be automated as part of your CI/CD pipeline.
Pushing to Docker Hub: After passing all checks, push the images to Docker Hub.
docker tag python-flask-app:latest your-dockerhub-username/python-flask-app:latest docker push your-dockerhub-username/python-flask-app:latest
Setting Up a DevSecOps Pipeline in Jenkins:
Jenkins Configuration: Jenkins is a widely-used automation server that enables continuous integration and continuous delivery (CI/CD). We’ll set up a Jenkins pipeline to automate the build, test, and deployment processes, integrating our DevSecOps tools.
Pipeline Setup: Define a Jenkins pipeline script (
Jenkinsfile) to automate the steps of building Docker images, scanning for vulnerabilities, running security checks, and pushing the images to Docker Hub.pipeline { agent any environment { SONAR_HM = tool "sonar" } stages { stage('Code Clone from GitHub: Step-1') { steps { echo 'Cloning code from GitHub' git url: 'https://github.com/priyadarshi0811/vote-project-2024.git', branch: 'Devops' } } stage('SonarQube Analysis: Step-2') { steps { withSonarQubeEnv("sonar") { sh "$SONAR_HM/bin/sonar-scanner -Dsonar.projectName=Wanderlust -Dsonar.projectKey=Wanderlust -X" echo 'SonarQube Analysis Done' } } } stage('Sonar Quality Gates: Step-3') { steps { timeout(time: 5, unit: 'MINUTES') { waitForQualityGate abortPipeline: false } echo 'Quality Gates of Code Completed.' } } stage('OWASP: Step-4') { steps { dependencyCheck additionalArguments: '--scan ./', odcInstallation: 'OWASP' dependencyCheckPublisher pattern: '**/dependency-check-report.xml' echo 'OWASP Dependency check completed.' } } stage('Code Build and Test: Step-5') { steps { echo 'Building Docker image' sh 'docker build -t vote:v1 ./vote' echo 'voting Docker image build done.' sh 'docker build -t worker:v2 ./worker' echo 'worker Docker image build done.' sh 'docker build -t result:v3 ./result' echo 'result docker image build' } } stage('Trivy Image Scan: Step-6') { steps { sh 'trivy image vote:v1' sh 'trivy image worker:v2' sh 'trivy image result:v3' echo 'Images Scan Completed.' } } stage('Image Push to Docker Hub: Step-7') { steps { withCredentials([usernamePassword( credentialsId: 'DockerCred', passwordVariable: 'DockerHubpass', usernameVariable: 'DockerHubUser' )]) { echo 'Logging in to Docker Hub' sh 'docker login -u ${DockerHubUser} -p ${DockerHubpass}' echo 'Tagging voting Docker image' sh 'docker tag vote:v1 ${DockerHubUser}/voting-app-2024:voting' echo 'Pushing Backend Docker image to Docker Hub' sh 'docker push ${DockerHubUser}/voting-app-2024:voting' echo 'Tagging worker Docker image' sh 'docker tag worker:v2 ${DockerHubUser}/voting-app-2024:worker' echo 'Pushing Frontend Docker image to Docker Hub' sh 'docker push ${DockerHubUser}/voting-app-2024:worker' echo 'Tagging result Docker image' sh 'docker tag result:v3 ${DockerHubUser}/voting-app-2024:result' echo 'Pushing result Docker image to Docker Hub' sh 'docker push ${DockerHubUser}/voting-app-2024:result' echo 'Voting, worker, and result Docker images pushed to DockerHub successfully!' } } } stage('Deploy with Docker Compose: Step-8') { steps { echo 'Deploying with Docker Compose' sh 'docker-compose down' sh 'docker-compose up -d' } } } }Pipeline Stages:
Build: The pipeline first builds the Docker images for the voting, worker, and result apps.
Code Quality Analysis: SonarQube scans the code to ensure it meets quality standards.
Security Scanning: Trivy scans the Docker images for known vulnerabilities.
OWASP Dependency Check: Checks the project dependencies for known vulnerabilities using OWASP Dependency Check.
Push to Docker Hub: If all checks pass, the Docker images are pushed to Docker Hub.
setup pipeline on jenkins create a new job





install owasp in jenkins

setup pipeline

select github hook for webhook

write a pipeline scripts

pipeline sucessfull run



Conclusion
By creating Dockerfiles for the voting, worker, and result apps, we’ve containerized our application, making it easier to manage and deploy. Using Docker Compose, we’ve streamlined the process of running our multi-container application. Setting up a DevSecOps pipeline in Jenkins allows us to automate the build, test, and deployment process while integrating security checks at every stage. With tools like SonarQube, Trivy, and OWASP, we ensure our application is secure and robust. Whether you’re building a simple voting app or a complex system, these practices will help you deploy confidently and securely.
Connect and Follow Me on Socials




