$ duy_
cd ../projects

July 2025 — Sept 2025 · Team of 2 — infrastructure, delivery and deployment

Coffee E-Commerce Platform

Polyglot commerce stack — Angular storefront, Spring Boot core, .NET services and a WinForms back office — behind one Traefik edge.

stack

  • Angular
  • Java Spring Boot
  • ASP.NET Core
  • .NET WinForms
  • SQL Server
  • Redis
  • Docker Compose
  • Traefik
  • Cloudflare Tunnel
  • GitHub Actions
  • Prometheus
  • Grafana
  • Alertmanager
  • AWS S3
  • AWS CloudFront

what it does

  • Four runtimes and a desktop client kept in sync against one SQL Server, routed through a single Traefik edge.
  • Zero inbound ports open — the whole stack is published through a Cloudflare Tunnel with Let's Encrypt TLS at Traefik.
  • Path-filtered GitHub Actions pipelines per component, so a storefront change never rebuilds the Java backend.
  • Prometheus + Alertmanager rules for the runtime, plus scheduled jobs for low-stock alerts and financial reports.

Team repository, hosted on my teammate's account. My share was the infrastructure, delivery and deployment layers — Traefik and the tunnel edge, the per-component GitHub Actions pipelines, the Compose topology, and the Prometheus / Grafana / Alertmanager stack.

architecture — coffee e-commerce

Four bands. Edge: a browser reaches a Cloudflare Tunnel, which reaches Traefik for TLS termination and path routing; the same browser fetches product images and JavaScript bundles directly from S3 and CloudFront. Application: Traefik routes to an Angular storefront, a .NET OTP service and a Spring Boot core handling catalog, cart and orders; a report-worker runs scheduled low-stock and financial jobs with no ingress of its own. Data: Spring Boot writes to SQL Server and Redis, the report-worker writes to SQL Server, and a WinForms back-office desktop client calls the REST API. Delivery: path-filtered GitHub Actions build per-component images, push them, and deploy over SSH to the Docker Compose host, with Prometheus, Grafana and Alertmanager watching the result.

The interesting constraint is the top-left corner: the host has no inbound ports. Everything reaches Traefik through an outbound tunnel, so the edge is the only thing that had to be reasoned about.

A coffee retail platform where the interesting problem was not any single service — it was keeping four runtimes, a desktop back office and a shared database coherent, and shipping the whole thing from one repository.

The shape of it

ComponentRuntimeJob
angularNode → NginxCustomer storefront
backEndJava 17 / Spring BootCatalogue, cart, orders
OTP-serviceASP.NET CorePhone/email verification
DashboardASP.NET Core + WinFormsAdmin API, stock worker, desktop back office
report-worker.NETScheduled financial reports
ReverseProxyTraefikTLS termination and routing

Behind those: SQL Server for relational state, Redis for sessions and cache. Product media is served from S3 through CloudFront rather than out of the application container, which took image traffic off the origin entirely.

Edge: no open ports

Traefik terminates TLS with Let’s Encrypt certificates and routes by host rule to each service. Public reachability comes from a Cloudflare Tunnel, so the host runs with no inbound ports open at all — the tunnel dials out, and there is no listening surface for anyone to scan.

That decision also removed the usual “which port is this service on again” problem from local development: the same Traefik config and the same hostnames work in Compose and in production.

Delivery

Each component has its own GitHub Actions workflow, filtered on paths so a change under App/angular/** never triggers a Maven build. Java builds cache ~/.m2, Angular builds cache the npm store, and test results are published back onto the pull request.

Operations

App/Monitoring/ carries a Prometheus config, Grafana provisioning and Alertmanager rules — the operational half of the project, and the part that turned “is it up?” into a dashboard instead of an SSH session. On the business side, scheduled jobs raise low-stock alerts and generate financial reports without anyone opening the desktop client.

Security posture

Authentication, session handling and secure data handling were reviewed against the OWASP Top 10, and the Dockerfiles use pinned base images with non-root users. The WinForms back office authorises per form, so an operator’s role decides which screens open at all rather than which buttons look disabled.

Decisions and trade-offs

DecisionThe obvious alternativeWhy, and what it cost
Cloudflare TunnelPort forwarding plus dynamic DNSThe host ends up with zero inbound ports and no listening surface to scan, which is a much stronger statement than “the firewall is configured correctly”. The cost is honest: the tunnel is a single vendor on the critical path of every request, and if it is down, so are we.
One Traefik config for dev and prodA separate local setup with published portsThe same hostnames and routing rules work in Compose and in production, so “works on my machine” stops being a category of bug. The cost is that local development needs the proxy running before anything is reachable.
Path-filtered workflows per componentOne pipeline that builds everythingA CSS change in the storefront has no business triggering a Maven build; the feedback loop stays in minutes rather than tens of minutes. The cost is six workflow files that drift if nobody keeps them aligned.
S3 + CloudFront for product mediaServe images from the application containerA single-host deployment has no headroom to spare, and image traffic is the easiest thing to move off it. The cost is a second place where content lives, and a cache to invalidate on upload.
One SQL Server for all servicesA database per serviceTwo people, one semester. Shared relational state kept orders, stock and reporting coherent without distributed-transaction work we had no time to do properly. The cost is the obvious one — a schema change ripples through four runtimes at once.
Four runtimesConsolidate on one stackThis was not a greenfield choice; the split came from the team and the coursework. I would not design it this way again. What made it worth doing was that the interesting problem moved to my side of the fence: making a polyglot mess build, deploy and be observable from one repository.

decisions

4 calls
  1. A Cloudflare Tunnel over port forwarding with dynamic DNS

    The host has no static public IP, and more importantly this leaves no listening port for anyone to find. The tunnel dials out; nothing dials in.

  2. Traefik over Nginx

    Label-based discovery from the Compose file meant no separate vhost config per service, and the same hostnames work in local development and in production.

  3. One workflow per component over a single pipeline for the repository

    A storefront CSS change should not trigger a Maven build. Path filters keep feedback fast enough that people actually wait for it.

  4. S3 + CloudFront for product media over serving images from the application container

    Image traffic is most of the bytes and none of the logic. Moving it off the origin took that load off the same process handling checkout.