docs: update README to match deployed reality
- Live URL set to https://savesave.duckdns.org and repo URL added - Compute row + cost table updated for VM.Standard.E2.1.Micro (was A1.Flex) - Explain the A1 → E2.1.Micro switch (Out of host capacity) and the local Vite build + 2 GB swap mitigations for the 1 GB RAM limit - Frontend service description updated for single-stage Dockerfile - New section 2.5 (Ports) and 2.6 (Deployment workflow) for defense talking points - Prerequisites mention Node 18+ for local Vite build and Git Bash on Windows - OCI quota check switched to E2.1.Micro limit name - .gitattributes added to repo layout
This commit is contained in:
parent
2f665a6977
commit
1ac8f3e8ab
62
README.md
62
README.md
@ -2,7 +2,9 @@
|
||||
|
||||
**Author:** Gigi Saji
|
||||
**Course exam project:** public cloud web application deployment
|
||||
**Live URL:** ("https://savesave.duckdns.org")
|
||||
**Live URL:** https://savesave.duckdns.org
|
||||
**Cloud:** Oracle Cloud Infrastructure — `eu-zurich-1` / `Cekz:EU-ZURICH-1-AD-1`
|
||||
**Repo:** `git@git.kemt.fei.tuke.sk:gs839gf/Sk1-exam.git`
|
||||
|
||||
---
|
||||
|
||||
@ -27,7 +29,13 @@ It is a **single-user** app — there is no login, no signup. The UI is built wi
|
||||
|
||||
**Oracle Cloud Infrastructure (OCI)** — region `eu-zurich-1`, availability domain `Cekz:EU-ZURICH-1-AD-1`.
|
||||
|
||||
I chose OCI because the **Always Free tier** includes a generous ARM-based compute instance (`VM.Standard.A1.Flex` — up to 4 OCPUs and 24 GB RAM total, never expires) and 200 GB of block storage, which is more than enough to host the entire stack at zero cost forever. No other major public cloud (AWS, Azure, GCP) offers a comparable always-free compute slice.
|
||||
I chose OCI because the **Always Free tier** offers two compute shapes that never expire:
|
||||
- `VM.Standard.A1.Flex` — ARM Ampere, up to 4 OCPU / 24 GB total
|
||||
- `VM.Standard.E2.1.Micro` — x86 AMD, 1 OCPU / 1 GB, up to 2 instances
|
||||
|
||||
plus 200 GB of block storage and 10 TB/month of egress. No other major public cloud (AWS, Azure, GCP) offers a comparable always-free compute slice.
|
||||
|
||||
This deployment runs on **`VM.Standard.E2.1.Micro`** (x86_64). My first attempt targeted `A1.Flex` (1 OCPU / 6 GB) but the Zurich A1 pool was at capacity (`Out of host capacity` from the launch API), so I switched to the smaller x86 shape. The 1 GB RAM limit is handled by (a) **building the Vite frontend bundle locally** on my laptop and shipping the static `dist/` to the VM (the Dockerfile is single-stage `nginx + COPY dist`) and (b) creating a **2 GB swap file** on the VM during bootstrap.
|
||||
|
||||
### 2.2 Cloud resources
|
||||
|
||||
@ -38,7 +46,9 @@ I chose OCI because the **Always Free tier** includes a generous ARM-based compu
|
||||
| Internet Gateway (`sk1-ig`) | IGW | Egress + ingress for the public subnet |
|
||||
| Route Table (`sk1-rt`) | Default route `0.0.0.0/0 → sk1-ig` | Routes outbound traffic |
|
||||
| Security List (`sk1-seclist`) | Stateful firewall | Allows TCP 22, 80, 443 inbound; all outbound |
|
||||
| Compute Instance (`sk1-expense-tracker`) | `VM.Standard.A1.Flex`, 1 OCPU, 6 GB RAM, 50 GB boot, Ubuntu 22.04 aarch64 | Runs Docker + the four containers |
|
||||
| Compute Instance (`sk1-expense-tracker`) | `VM.Standard.E2.1.Micro`, 1 OCPU, 1 GB RAM, 50 GB boot, Ubuntu 22.04 x86_64 | Runs Docker + the four containers. 2 GB swap added at bootstrap so Postgres init / Docker installs don't OOM. |
|
||||
|
||||
**Public IP of the live deployment:** `140.86.210.113` → `savesave.duckdns.org`.
|
||||
|
||||
The existing VM (`gymsys-server`) is on a different shape and is **not touched** by any script in this project.
|
||||
|
||||
@ -49,7 +59,7 @@ The app runs as a Docker Compose stack with four services:
|
||||
| Service | Image | Role |
|
||||
|---|---|---|
|
||||
| `caddy` | `caddy:2-alpine` | Reverse proxy. Terminates HTTPS via Let's Encrypt, routes `/api/*` to backend and everything else to frontend, writes JSON access logs |
|
||||
| `frontend` | custom (multi-stage `node:20-alpine` → `nginx:alpine`) | Serves the built React static bundle on port 80 (internal only) |
|
||||
| `frontend` | custom (single-stage `nginx:alpine` + pre-built `dist/`) | Serves the React static bundle on port 80 (internal only). The bundle is built on the laptop by `prepare-app.sh` before upload — the VM never runs Vite. |
|
||||
| `backend` | custom (`node:20-alpine`) | Express API listening on port 3000 (internal only). Endpoints: `GET /api/expenses`, `GET /api/expenses/summary`, `POST /api/expenses`, `DELETE /api/expenses/:id`, `GET /health` |
|
||||
| `db` | `postgres:16-alpine` | PostgreSQL database, writes to the `postgres_data` named volume; runs `init.sql` once on first start |
|
||||
|
||||
@ -75,6 +85,35 @@ Internet ──:443──► caddy ──/───► frontend:80 (static Rea
|
||||
|
||||
DNS is provided by **DuckDNS** (free dynamic DNS). `prepare-app.sh` updates the A record to point at the VM's public IP **before** starting Caddy, so Let's Encrypt's HTTP-01 challenge succeeds on the first try.
|
||||
|
||||
### 2.5 Ports
|
||||
|
||||
| Port | Where | Purpose |
|
||||
|---|---|---|
|
||||
| 22 | host (VM) | SSH — used by `prepare-app.sh`, `backup.sh`, `view-logs.sh` |
|
||||
| 80 | host (VM) → `sk1-caddy` | HTTP — ACME HTTP-01 challenge + redirect to 443 |
|
||||
| 443 | host (VM) → `sk1-caddy` | HTTPS — public traffic for `savesave.duckdns.org` |
|
||||
| 3000 | `sk1-backend` (internal) | Express API; Caddy proxies `/api/*` to it |
|
||||
| 80 | `sk1-frontend` (internal) | nginx serving the React bundle |
|
||||
| 5432 | `sk1-db` (internal) | Postgres; only `sk1-backend` connects |
|
||||
|
||||
**Only Caddy publishes host ports.** The other three containers have no `ports:` block in `docker-compose.yml`, so they are reachable only on the internal Docker bridge `appnet`, by service name. The attack surface from the internet is exactly SSH (key-only) + HTTP + HTTPS, enforced at two layers (OCI security list + VM iptables).
|
||||
|
||||
### 2.6 Deployment workflow (what `prepare-app.sh` actually does)
|
||||
|
||||
1. **Load + validate** `.env`, auto-generate `POSTGRES_PASSWORD` if blank.
|
||||
2. **Resolve** tenancy/compartment OCID from `~/.oci/config`, then the latest Ubuntu 22.04 x86_64 image for `E2.1.Micro`.
|
||||
3. **Create or reuse** the networking objects — VCN, internet gateway, route table, security list (TCP 22/80/443 in), subnet. Reuses by display name, so re-runs are idempotent.
|
||||
4. **Launch the VM** (`E2.1.Micro`, 50 GB boot, x86_64 Ubuntu image, public IP) and wait for `RUNNING`.
|
||||
5. **Update DuckDNS** A record → new VM IP **before** Caddy starts (so Let's Encrypt validation works first time).
|
||||
6. **Wait for SSH** (up to 5 min of 10 s retries).
|
||||
7. **Bootstrap the VM**: create a 2 GB swap file, install Docker + Compose plugin, open ports 80/443 with idempotent iptables rules, save iptables.
|
||||
8. **Build the frontend locally** (`npm install` + `npm run build`) — Vite needs more RAM than the Micro shape has, so it runs on the laptop.
|
||||
9. **Tar + scp** the source + pre-built `dist/` to `/opt/sk1/` on the VM (preserves `logs/` and `backups/` from any previous deploy).
|
||||
10. **`docker compose up -d --build`** on the VM — builds the two custom images, pulls postgres/caddy/nginx, starts all 4 containers in dependency order (db → backend → frontend → caddy).
|
||||
11. **Caddy auto-issues** the Let's Encrypt cert via ACME HTTP-01 (~30 s) and stores it in the `caddy_data` named volume.
|
||||
|
||||
Total wall-clock time from `./prepare-app.sh` to live HTTPS: **~6 minutes**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Cost analysis — 1000 users/day, 50 GB data, one year
|
||||
@ -112,10 +151,10 @@ This is a hypothetical paid-tier sizing exercise. The **actual deployment of thi
|
||||
|
||||
| Resource | Free tier limit | This app uses | Annual cost |
|
||||
|---|---|---|---|
|
||||
| A1.Flex compute | 4 OCPU + 24 GB total | 1 OCPU + 6 GB | $0.00 |
|
||||
| E2.1.Micro compute | 2 instances (1 OCPU + 1 GB each) | 1 instance (1 OCPU + 1 GB) | $0.00 |
|
||||
| Block storage | 200 GB tenancy-wide | 50 GB (boot volume) | $0.00 |
|
||||
| Egress | 10 TB/month | < 1 GB/month | $0.00 |
|
||||
| DuckDNS DNS | Free | 1 hostname | $0.00 |
|
||||
| DuckDNS DNS | Free | 1 hostname (`savesave.duckdns.org`) | $0.00 |
|
||||
| Let's Encrypt cert | Free | 1 cert | $0.00 |
|
||||
| **Total** | | | **$0.00 / year** |
|
||||
|
||||
@ -132,10 +171,11 @@ sk1/
|
||||
├── .env.example ← template for secrets (committed)
|
||||
├── .env ← real secrets (gitignored, never committed)
|
||||
├── .gitignore ← keeps secrets, build artifacts, logs out of git
|
||||
├── .gitattributes ← forces LF line endings on .sh/Dockerfile (cross-platform safety)
|
||||
├── README.md ← this file
|
||||
│
|
||||
├── frontend/
|
||||
│ ├── Dockerfile ← multi-stage: build React → serve with nginx
|
||||
│ ├── Dockerfile ← single-stage: nginx:alpine + COPY pre-built dist/
|
||||
│ ├── nginx.conf ← nginx config (SPA fallback to index.html)
|
||||
│ ├── package.json ← React + Vite + Tailwind deps
|
||||
│ ├── tsconfig.json ← TypeScript config
|
||||
@ -251,21 +291,23 @@ Each entry includes timestamp, client IP, request method/path, response status,
|
||||
|
||||
### 8.1 Prerequisites on your laptop
|
||||
|
||||
- **OS:** Linux / macOS / WSL2 (the scripts use Bash and Unix tools)
|
||||
- **OS:** Linux / macOS / WSL2 / Git Bash on Windows (the scripts use Bash and Unix tools)
|
||||
- **OCI CLI:** installed and configured (`oci setup config`). Test with `oci iam region list`.
|
||||
- **Node.js 18+ and npm:** required locally because `prepare-app.sh` builds the Vite frontend bundle on your laptop before uploading. Verify with `node --version`.
|
||||
- **`curl`, `openssl`, `ssh`, `scp`, `tar`, `gzip`:** standard Unix tools, all needed by `prepare-app.sh`. `jq` is optional — `scripts/view-logs.sh` uses it for pretty-printing if available, otherwise it falls back to `cat`.
|
||||
- **SSH key pair:** an Ed25519 or RSA key pair. Path is set in `.env`.
|
||||
|
||||
### 8.2 OCI account requirements
|
||||
|
||||
- A tenancy with at least one Always Free A1.Flex slice available — `1 OCPU + 6 GB RAM`. Check with:
|
||||
- A tenancy with at least one Always Free `VM.Standard.E2.1.Micro` slot free (limit is 2). Check the host capacity in your AD with:
|
||||
```bash
|
||||
oci limits resource-availability get \
|
||||
--compartment-id <tenancy-ocid> \
|
||||
--service-name compute \
|
||||
--limit-name standard-a1-core-count \
|
||||
--limit-name vm-standard-e2-1-micro-count \
|
||||
--availability-domain Cekz:EU-ZURICH-1-AD-1
|
||||
```
|
||||
*(`VM.Standard.A1.Flex` works too if you have ARM capacity available — see the change required in the `--shape` argument of `prepare-app.sh`. The Zurich A1 pool is often capacity-constrained, which is why this project defaults to the more reliable x86 `E2.1.Micro`.)*
|
||||
- At least 50 GB free in the 200 GB Always Free block-storage pool.
|
||||
|
||||
### 8.3 External services
|
||||
|
||||
Loading…
Reference in New Issue
Block a user