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.
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.
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
| Component | Runtime | Job |
|---|---|---|
angular | Node → Nginx | Customer storefront |
backEnd | Java 17 / Spring Boot | Catalogue, cart, orders |
OTP-service | ASP.NET Core | Phone/email verification |
Dashboard | ASP.NET Core + WinForms | Admin API, stock worker, desktop back office |
report-worker | .NET | Scheduled financial reports |
ReverseProxy | Traefik | TLS 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
| Decision | The obvious alternative | Why, and what it cost |
|---|---|---|
| Cloudflare Tunnel | Port forwarding plus dynamic DNS | The 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 prod | A separate local setup with published ports | The 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 component | One pipeline that builds everything | A 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 media | Serve images from the application container | A 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 services | A database per service | Two 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 runtimes | Consolidate on one stack | This 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-
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.
-
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.
-
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.
-
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.