Overview
SmartProperty is a property rental platform built as a team project at ESPRIT: a NestJS API, a React/Vite frontend and a MongoDB Atlas database. The team built the application; the DevOps phase was my work.
That meant a pipeline that tests and ships every merged change, monitoring that tells me when something breaks, security checks on every push, and hosting that fits within a $100 student credit.
- API: Docker image on Azure Container Apps (Italy North, "express" environment)
- Frontend: static build on an Azure Storage static website, served through Cloudflare (DNS, TLS, proxy) on the custom domain
- Files: Cloudflare R2 (S3-compatible)
- Database: MongoDB Atlas
- Infrastructure as code: Bicep; CI/CD: GitHub Actions
How a change reaches production
Every push and pull request runs CI on GitHub Actions:
- Type checks, unit tests with coverage, and production builds of the API and the frontend
- SonarQube Cloud analysis (the quality gate is advisory for now)
- Bicep infrastructure templates compiled and linted
A merge to main then deploys both halves, about 7 minutes from merge to verified production:
- Keyless sign-in: GitHub authenticates to Azure with OIDC, so no cloud secret is stored
- The student tenant forbids app registrations, so the federated credential sits on a user-assigned managed identity that only trusts jobs in the repository's "production" environment
- That identity holds three narrowly scoped roles: the container app, its environment's join permission, and the static site's $web container
- The API image is deployed by digest
- The API reports the git commit it was built from; the pipeline waits for that exact commit, then 10 healthy checks in a row, then a real database read
- If any check fails, the pipeline rolls back to the previous image automatically
- The frontend publishes hashed bundles first (cached for a year), other files next and index.html last (never cached), then checks that the live site serves the new bundle
Monitoring
Monitoring is defined as code in Bicep, on Azure Monitor:
- An availability test calls the API from outside Azure every 15 minutes, including a TLS certificate expiry check
- Alerts are emailed to me for: API down, no running replica, a container restart, memory over 80%, and 5 or more logged errors within 15 minutes
- I set the error threshold from a week of real logs: normal running logs 0–2 errors a day
- A Log Analytics workbook shows availability, container metrics, errors grouped by message, and a deploy history built from each container start and the commit it runs
Azure blocks test notifications on student subscriptions, so I fired a deliberate alert to prove the email path end to end. Calibrating the alerts also surfaced a real bug: production email had never been configured.
Security
Security checks run on every push, plus weekly:
- gitleaks secret scanning, CodeQL, dependency scanning, hadolint for Dockerfiles, a Trivy image scan and an SBOM
- Hardened API image: multi-stage, runs as non-root, base image pinned by digest
- Removing npm and yarn from the runtime layer cleared one critical and about a dozen high Trivy findings
- The secret scanner flagged public frontend keys; I handled them with explicit allow markers and a fingerprint ignore file, so the scanner stays strict everywhere else
In the application itself:
- ID and income verification documents moved to a separate private bucket, readable only through signed links that expire after 10 minutes
- Two unsafe upload endpoints removed
- Leaked credentials are being rotated: the storage keys, the Google OAuth secret and the database password are done
- The database now uses a least-privilege user limited to one database
Cost
I measured spending with Cost Management. Reserved CPU and memory are billed even when idle, so the first setup would have used the $100 student credit in under 5 months.
- Resized the API container to the smallest size
- Trimmed monitoring
- Kept one always-warm replica, so visitors never hit a cold start
The bill went from about $20.70 a month to about $8.40, enough to last until the credit expires.
Challenges and decisions
- Infrastructure drifts quietly. The deploy job was set to re-apply a Bicep template that no longer matched production, because Atlas and R2 had replaced the original setup. The first automated deploy would have overwritten live settings, so I removed that step before switching CD on.
- Platform limits shaped the design: region policies, no app registrations, and an environment without blue/green deployments.
- Without blue/green, a deploy can briefly drop requests. The post-deploy checks allow for that, but not for a build that keeps failing.
