Skip to content

Latest commit

Β 

History

29 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Voting APP

A simple distributed application running across multiple Docker containers. This solution uses Python, Node.js, RabbitMQ, Redis, Postgres, Prometheus, Grafana, RedisInsight and various technology

This isn't meant to be a perfectly architected production system β€” it's a hands-on example of the moving parts you'll encounter in a real distributed app (caches, queues, persistent data, tracing, metrics) and how they fit together (all are stateless now)

Getting started

Installation

Download Docker Desktop for Mac or Windows. Docker Compose will be automatically installed. On Linux, make sure you have the latest version of Compose.

On the recent downloads, kubernets comes prebuild in the Docker Desktop

sudo apt update
sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg lsb-release
sudo mkdir -p /etc/apt/keyrings

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker --version
docker compose version

To verify

sudo usermod -aG docker $USER
newgrp docker

sudo systemctl enable docker
sudo systemctl start docker
sudo systemctl status docker

Run the app in Docker Compose

docker compose up

Once the containers are up, you should be able to reach the voting frontend and the results app on their configured ports, along with Flower, RedisInsight, Prometheus, and Grafana for monitoring.

containers

Components Used

  • Frontend web app where users vote between two options Python Flask App
  • Celery Worker – Consumes votes from RabbitMQ and stores them in PostgreSQL.
    • RabbitMQ – Message broker that queues incoming votes.
    • Redis – Celery backend for task state and metadata.
  • PostgreSQL – Persistent database for storing votes.
  • Displays live voting results by polling the database every 2 seconds. Node.js App
  • Flower – Web UI for monitoring Celery workers and tasks.
  • Redis Insight – GUI for managing and inspecting Redis.
  • Redis Exporter – Exposes Redis metrics to Prometheus.
  • PostgreSQL Exporter – Exposes PostgreSQL metrics to Prometheus.
  • OpenTelemetry – Collects distributed traces across services.
  • Jaeger – Visualizes distributed traces.
  • Prometheus – Collects metrics from applications and exporters.
  • Grafana – Visualizes metrics and dashboards.

Repository layout

  • vote/ β€” Python Flask voting frontend
  • worker/ β€” Celery worker that consumes votes
  • result/ β€” Node.js results app
  • postgres/ β€” PostgreSQL configuration
  • prometheus/ β€” Prometheus configuration
  • otel/ β€” OpenTelemetry collector configuration
  • compose.yaml β€” Docker Compose definition for the whole stack

How it works

  1. A Python (Flask) web app lets users vote between two options.
  2. Each vote is pushed onto a RabbitMQ queue.
  3. A Celery worker consumes votes from RabbitMQ and writes them to PostgreSQL, using Redis as its task/result backend.
  4. A Node.js app polls the database every 2 seconds and displays live results.

Note: the app only accepts one vote per client browser β€” casting another vote overwrites the previous one.

Architectural Diagrams

flowchart TD

subgraph group_runtime["Vote Processing"]
  node_vote_ui["Vote Browser UI<br/>web UI<br/>[index.html]"]
  node_vote_service["Flask Vote Service<br/>Python write API<br/>[app.py]"]
  node_rabbitmq["RabbitMQ<br/>task broker<br/>[amqp.deploy.yml]"]
  node_worker["Celery Worker<br/>Python worker<br/>[worker.py]"]
end

subgraph group_data["State &amp; Results"]
  node_redis[("Redis<br/>Celery backend<br/>[redis.deploy.yml]")]
  node_postgres[("PostgreSQL<br/>system of record")]
  node_postgres_storage[("Postgres Volume<br/>persistent storage<br/>[postgres.pvc.yml]")]
  node_db_init["Database Initialization<br/>SQL schema<br/>[init.sql]"]
  node_results_ui["Live Results UI<br/>web UI<br/>[index.html]"]
  node_results_service["Node Results Service<br/>read API<br/>[server.js]"]
end

subgraph group_observability["Observability"]
  node_otel_collector["OpenTelemetry Collector<br/>trace collector"]
  node_jaeger["Jaeger<br/>trace viewer<br/>[jager.deploy.yml]"]
  node_metrics_exporters["Redis &amp; Postgres Exporters<br/>metrics exporters"]
  node_prometheus["Prometheus<br/>metrics collector<br/>[prometheus.yml]"]
  node_grafana["Grafana<br/>metrics dashboard<br/>[grafana.deploy.yml]"]
end

subgraph group_operations["Operations"]
  node_compose["Local Compose<br/>orchestration<br/>[compose.yaml]"]
  node_kubernetes["Kubernetes Manifests<br/>deployment"]
  node_flower["Flower<br/>Celery operations UI<br/>[flower.deploy.yml]"]
  node_redisinsight["RedisInsight<br/>Redis operations UI"]
  node_pgadmin["pgAdmin<br/>Postgres operations UI<br/>[pgadmin.deploy.yml]"]
end

node_compose -.->|"runs locally"| node_vote_service
node_compose -.->|"runs locally"| node_results_service
node_kubernetes -.->|"deploys"| node_rabbitmq
node_kubernetes -.->|"deploys"| node_postgres
node_vote_ui -->|"submits vote"| node_vote_service
node_vote_service -->|"queues vote task"| node_rabbitmq
node_rabbitmq -->|"delivers task"| node_worker
node_worker -->|"stores task state"| node_redis
node_worker -->|"persists vote"| node_postgres
node_postgres_storage -->|"persists data"| node_postgres
node_db_init -.->|"initializes schema"| node_postgres
node_results_service -->|"polls totals"| node_postgres
node_results_ui -->|"reads live totals"| node_results_service
node_results_service -.->|"exports traces"| node_otel_collector
node_otel_collector -->|"forwards traces"| node_jaeger
node_redis -.->|"metrics"| node_metrics_exporters
node_postgres -.->|"metrics"| node_metrics_exporters
node_metrics_exporters -->|"scraped by"| node_prometheus
node_prometheus -->|"dashboard data"| node_grafana
node_flower -.->|"monitors"| node_worker
node_redisinsight -.->|"administers"| node_redis
node_pgadmin -.->|"administers"| node_postgres

click node_compose "https://github.com/muthukamalan/votingapp/blob/main/compose.yaml"
click node_vote_ui "https://github.com/muthukamalan/votingapp/blob/main/vote/templates/index.html"
click node_vote_service "https://github.com/muthukamalan/votingapp/blob/main/vote/app.py"
click node_rabbitmq "https://github.com/muthukamalan/votingapp/blob/main/k8s/manifests/amqp/amqp.deploy.yml"
click node_worker "https://github.com/muthukamalan/votingapp/blob/main/worker/worker.py"
click node_redis "https://github.com/muthukamalan/votingapp/blob/main/k8s/manifests/redis/redis.deploy.yml"
click node_postgres "https://github.com/muthukamalan/votingapp/blob/main/k8s/manifests/postgres/postgres.deploy.yml"
click node_postgres_storage "https://github.com/muthukamalan/votingapp/blob/main/k8s/manifests/postgres/postgres.pvc.yml"
click node_db_init "https://github.com/muthukamalan/votingapp/blob/main/postgres/init.sql"
click node_results_ui "https://github.com/muthukamalan/votingapp/blob/main/result/public/index.html"
click node_results_service "https://github.com/muthukamalan/votingapp/blob/main/result/server.js"
click node_otel_collector "https://github.com/muthukamalan/votingapp/blob/main/otel/otel-collector-config.yaml"
click node_jaeger "https://github.com/muthukamalan/votingapp/blob/main/k8s/manifests/jager/jager.deploy.yml"
click node_prometheus "https://github.com/muthukamalan/votingapp/blob/main/prometheus/prometheus.yml"
click node_grafana "https://github.com/muthukamalan/votingapp/blob/main/k8s/manifests/grafana/grafana.deploy.yml"
click node_flower "https://github.com/muthukamalan/votingapp/blob/main/k8s/manifests/flower/flower.deploy.yml"
click node_redisinsight "https://github.com/muthukamalan/votingapp/blob/main/k8s/manifests/redisinsights/redisinsights.deploy.yml"
click node_pgadmin "https://github.com/muthukamalan/votingapp/blob/main/k8s/manifests/pgadmin/pgadmin.deploy.yml"

classDef toneNeutral fill:#f8fafc,stroke:#334155,stroke-width:1.5px,color:#0f172a
classDef toneBlue fill:#dbeafe,stroke:#2563eb,stroke-width:1.5px,color:#172554
classDef toneAmber fill:#fef3c7,stroke:#d97706,stroke-width:1.5px,color:#78350f
classDef toneMint fill:#dcfce7,stroke:#16a34a,stroke-width:1.5px,color:#14532d
classDef toneRose fill:#ffe4e6,stroke:#e11d48,stroke-width:1.5px,color:#881337
classDef toneIndigo fill:#e0e7ff,stroke:#4f46e5,stroke-width:1.5px,color:#312e81
classDef toneTeal fill:#ccfbf1,stroke:#0f766e,stroke-width:1.5px,color:#134e4a
class node_vote_ui,node_vote_service,node_rabbitmq,node_worker toneBlue
class node_redis,node_postgres,node_postgres_storage,node_db_init,node_results_ui,node_results_service toneAmber
class node_otel_collector,node_jaeger,node_metrics_exporters,node_prometheus,node_grafana toneMint
class node_compose,node_kubernetes,node_flower,node_redisinsight,node_pgadmin toneRose
Loading

Examples UI

  1. Voting & Results app page

app page

  1. Celery celery

  2. Rabbit MQ rabbitmq

  3. Postgresql psql

  4. Jager UI trace

  5. Prometheus

metrics

service discovery

  1. Redis Insights

insights

  1. Grafana

Getting started with K8S

kubectl apply -f -R k8s/manifests/  # --recursive
#find k8s/manifests -type f \( -name "*.yml" -o -name "*.yaml" \) -print0 | xargs -0 kubectl apply -f

alt text

Verify the rollout

kubectl get pods -w
kubectl get deployments
kubectl get svc

View Via Headlamp

helm repo add headlamp https://kubernetes-sigs.github.io/headlamp/
helm install my-headlamp headlamp/headlamp --namespace kube-system
kubectl create token my-headlamp --namespace kube-system
export POD_NAME=$(kubectl get pods --namespace kube-system -l "app.kubernetes.io/name=headlamp,app.kubernetes.io/instance=my-headlamp" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace kube-system $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application"
kubectl --namespace kube-system port-forward $POD_NAME 8080:$CONTAINER_PORT
Service Role Tech
vote Frontend where users cast a vote between two options Python / Flask
worker Consumes votes off the queue and writes them to Postgres Python / Celery
result Polls the DB and shows live results Node.js
RabbitMQ Message broker queuing incoming votes RabbitMQ
Redis Celery result backend / task metadata Redis
PostgreSQL Durable store for vote results PostgreSQL
Flower Web UI for monitoring Celery workers/tasks Flower
Redis Insight GUI for inspecting Redis Redis Insight
Redis / Postgres Exporters Expose DB & cache metrics Prometheus exporters
OpenTelemetry + Jaeger Distributed tracing across services OTel Collector, Jaeger
Prometheus + Grafana Metrics collection & dashboards Prometheus, Grafana

alt text

helm uninstall my-headlamp  -n kube-system
helm repo rm headlamp

🌐 Accessing the App

Check how each service is exposed (ClusterIP, NodePort, or LoadBalancer) and grab the matching port:

kubectl get svc

NodePort / LoadBalancer: hit <node-or-external-ip>:<node-port> directly.

ClusterIP (no external access configured): port-forward for a quick local look:

kubectl port-forward svc/vote   8080:80    # cast a vote
kubectl port-forward svc/result 8081:80    # watch live results
kubectl port-forward svc/flower 5555:5555  # Celery task monitoring
kubectl port-forward svc/grafana 3000:3000 # dashboards

πŸ“ˆ Scaling

Because vote, worker, and result are independent Deployments, you can scale each based on its actual bottleneck (usually worker, since it does the DB writes):

kubectl scale deployment worker --replicas=3
kubectl get pods -l app=worker

🧹 Tear Down

kubectl delete -f k8s/manifests/ -R
Notes

The voting application only accepts one vote per client browser. If registers additional votes, it'll be overwritten.

This isn't an example of a properly architected perfectly designed distributed app... it's just a simple example of the various types of pieces and languages you might see (caches, queues, persistent data, etc), and how to deal with them in Docker Compose and Kubernetes at a basic level.

Credits

License

This project is licensed under the MIT License.