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)
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 versionTo verify
sudo usermod -aG docker $USER
newgrp docker
sudo systemctl enable docker
sudo systemctl start docker
sudo systemctl status dockerdocker compose upOnce 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.
- 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.
vote/β Python Flask voting frontendworker/β Celery worker that consumes votesresult/β Node.js results apppostgres/β PostgreSQL configurationprometheus/β Prometheus configurationotel/β OpenTelemetry collector configurationcompose.yamlβ Docker Compose definition for the whole stack
- A Python (Flask) web app lets users vote between two options.
- Each vote is pushed onto a RabbitMQ queue.
- A Celery worker consumes votes from RabbitMQ and writes them to PostgreSQL, using Redis as its task/result backend.
- 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.
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 & 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 & 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
- Voting & Results app page
- Redis Insights
- Grafana
kubectl apply -f -R k8s/manifests/ # --recursive
#find k8s/manifests -type f \( -name "*.yml" -o -name "*.yaml" \) -print0 | xargs -0 kubectl apply -fkubectl get pods -w
kubectl get deployments
kubectl get svchelm 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 |
helm uninstall my-headlamp -n kube-system
helm repo rm headlampCheck how each service is exposed (ClusterIP, NodePort, or LoadBalancer) and grab the matching port:
kubectl get svcNodePort / 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 # dashboardsBecause 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=workerkubectl delete -f k8s/manifests/ -RThe 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.
This project is licensed under the MIT License.










