DEPLOY.md
4,478 bytes
| 1 | # Deploy — travel.rasmusj.com |
|---|---|
| 2 | |
| 3 | Production deploy on the Hetzner VPS. Caddy (already running in `/opt/caddy/`) terminates |
| 4 | HTTPS and reverse-proxies to this app's container over the shared `web` Docker network. |
| 5 | |
| 6 | ``` |
| 7 | Internet → Caddy (:80/:443, HTTPS) → [web network] → csweb-travel (:8080) |
| 8 | ↓ [internal network] |
| 9 | db (Postgres, no public port) |
| 10 | ``` |
| 11 | |
| 12 | ## Files in this repo |
| 13 | |
| 14 | | File | Purpose | |
| 15 | |------|---------| |
| 16 | | `Dockerfile` | .NET 10 SDK build → aspnet runtime. Source stays under `SplitApp/`, so COPY paths are unchanged. | |
| 17 | | `docker-compose.prod.yml` | Production stack. No public ports; secrets from `.env`. | |
| 18 | | `.env.example` | Template. Copy to `.env` and fill with strong values. | |
| 19 | | `.env` | Real secrets — **gitignored, never commit.** Create on the server. | |
| 20 | |
| 21 | ## Stage 1 — manual first deploy |
| 22 | |
| 23 | ### 1. Deploy key (read-only access to the private repo) |
| 24 | |
| 25 | ```bash |
| 26 | ssh <user>@<server-ip> |
| 27 | ssh-keygen -t ed25519 -C "csweb-travel-deploy" -f ~/.ssh/csweb-travel -N "" |
| 28 | cat ~/.ssh/csweb-travel.pub |
| 29 | ``` |
| 30 | |
| 31 | Add the printed public key to the GitHub repo → **Settings → Deploy keys → Add deploy key** |
| 32 | (read-only is enough). Then make git use it: |
| 33 | |
| 34 | ```bash |
| 35 | cat >> ~/.ssh/config <<'EOF' |
| 36 | |
| 37 | Host github-csweb-travel |
| 38 | HostName github.com |
| 39 | User git |
| 40 | IdentityFile ~/.ssh/csweb-travel |
| 41 | IdentitiesOnly yes |
| 42 | EOF |
| 43 | ``` |
| 44 | |
| 45 | ### 2. Clone |
| 46 | |
| 47 | ```bash |
| 48 | cd /opt/projects |
| 49 | git clone git@github-csweb-travel:<your-user>/<repo>.git csweb-travel |
| 50 | cd csweb-travel |
| 51 | ``` |
| 52 | |
| 53 | ### 3. Secrets |
| 54 | |
| 55 | Create `.env` in the repo root. Use the values prepared locally, or regenerate: |
| 56 | |
| 57 | ```bash |
| 58 | cat > .env <<EOF |
| 59 | POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=' | cut -c1-32) |
| 60 | JWT_KEY=$(openssl rand -base64 48 | tr -d '\n') |
| 61 | SEED_ADMIN_PASSWORD=Adm!n.$(openssl rand -base64 9 | tr -d '/+=')9 |
| 62 | EOF |
| 63 | chmod 600 .env |
| 64 | cat .env # note the SEED_ADMIN_PASSWORD — that's the admin@taltech.ee login |
| 65 | ``` |
| 66 | |
| 67 | ### 4. Build & start |
| 68 | |
| 69 | ```bash |
| 70 | docker compose -f docker-compose.prod.yml up -d --build |
| 71 | docker compose -f docker-compose.prod.yml logs -f app # watch migrate + seed |
| 72 | ``` |
| 73 | |
| 74 | > ⚠️ The .NET build is the heaviest step. On a 4 GB server it should fit, but if the build |
| 75 | > gets OOM-killed, add temporary swap: |
| 76 | > `sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile` |
| 77 | |
| 78 | Confirm the container is on the `web` network and healthy: |
| 79 | |
| 80 | ```bash |
| 81 | docker network inspect web --format '{{range .Containers}}{{.Name}} {{end}}' |
| 82 | # should list: caddy ... csweb-travel |
| 83 | ``` |
| 84 | |
| 85 | ### 5. Caddy |
| 86 | |
| 87 | Append to `/opt/caddy/Caddyfile`: |
| 88 | |
| 89 | ``` |
| 90 | travel.rasmusj.com { |
| 91 | reverse_proxy csweb-travel:8080 |
| 92 | } |
| 93 | ``` |
| 94 | |
| 95 | > Note: target is `csweb-travel` (the container_name), not `web`. A unique name avoids |
| 96 | > DNS collisions once other apps join the shared `web` network. |
| 97 | |
| 98 | Reload (no downtime for other sites): |
| 99 | |
| 100 | ```bash |
| 101 | cd /opt/caddy |
| 102 | docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile |
| 103 | # or, if the Caddyfile is bind-mounted and that fails: |
| 104 | docker compose restart caddy |
| 105 | ``` |
| 106 | |
| 107 | ### 6. Test |
| 108 | |
| 109 | ```bash |
| 110 | curl -I https://travel.rasmusj.com # expect 200/302/308, valid Let's Encrypt cert |
| 111 | ``` |
| 112 | |
| 113 | Open https://travel.rasmusj.com and log in: `admin@taltech.ee` / `<SEED_ADMIN_PASSWORD from .env>`. |
| 114 | |
| 115 | ## Updating later |
| 116 | |
| 117 | ```bash |
| 118 | cd /opt/projects/csweb-travel |
| 119 | git pull |
| 120 | docker compose -f docker-compose.prod.yml up -d --build |
| 121 | ``` |
| 122 | |
| 123 | Migrations run automatically on boot (`MigrateDatabase=true`). Seeding is idempotent |
| 124 | (guards on existing rows), so it won't duplicate data on restart. |
| 125 | |
| 126 | ## Security notes |
| 127 | |
| 128 | - **DB password & JWT key** live only in `.env` (gitignored) and are injected as env vars. |
| 129 | `JWT__Key` overrides the placeholder in `appsettings.json`. |
| 130 | - **Seed admin password** is read from `SEED_ADMIN_PASSWORD` on first boot |
| 131 | (`AppDataInit.SeedIdentity`). This replaces the well-known course default `Kala.12345`, |
| 132 | so the public admin login is strong from the very first deploy. |
| 133 | - The other seeded demo accounts (`user@`, `alice@`, …) still use `Kala.12345`. They are |
| 134 | non-admin demo users. Remove them from `InitialData.cs` if you don't want demo logins. |
| 135 | - Postgres has **no published port** — it's reachable only by the app over `internal`. |
| 136 | - To rotate any secret: edit `.env`, then `docker compose -f docker-compose.prod.yml up -d`. |
| 137 | (Changing `SEED_ADMIN_PASSWORD` after first boot has no effect — the user already exists; |
| 138 | change that password through the app instead.) |
| 139 | ``` |
| 140 | |