Deploy — travel.rasmusj.com
Production deploy on the Hetzner VPS. Caddy (already running in /opt/caddy/) terminates
HTTPS and reverse-proxies to this app's container over the shared web Docker network.
Internet → Caddy (:80/:443, HTTPS) → [web network] → csweb-travel (:8080)
↓ [internal network]
db (Postgres, no public port)
Files in this repo
| File | Purpose |
|---|---|
Dockerfile |
.NET 10 SDK build → aspnet runtime. Source stays under SplitApp/, so COPY paths are unchanged. |
docker-compose.prod.yml |
Production stack. No public ports; secrets from .env. |
.env.example |
Template. Copy to .env and fill with strong values. |
.env |
Real secrets — gitignored, never commit. Create on the server. |
Stage 1 — manual first deploy
1. Deploy key (read-only access to the private repo)
ssh <user>@<server-ip>
ssh-keygen -t ed25519 -C "csweb-travel-deploy" -f ~/.ssh/csweb-travel -N ""
cat ~/.ssh/csweb-travel.pub
Add the printed public key to the GitHub repo → Settings → Deploy keys → Add deploy key (read-only is enough). Then make git use it:
cat >> ~/.ssh/config <<'EOF'
Host github-csweb-travel
HostName github.com
User git
IdentityFile ~/.ssh/csweb-travel
IdentitiesOnly yes
EOF
2. Clone
cd /opt/projects
git clone git@github-csweb-travel:<your-user>/<repo>.git csweb-travel
cd csweb-travel
3. Secrets
Create .env in the repo root. Use the values prepared locally, or regenerate:
cat > .env <<EOF
POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=' | cut -c1-32)
JWT_KEY=$(openssl rand -base64 48 | tr -d '\n')
SEED_ADMIN_PASSWORD=Adm!n.$(openssl rand -base64 9 | tr -d '/+=')9
EOF
chmod 600 .env
cat .env # note the SEED_ADMIN_PASSWORD — that's the admin@taltech.ee login
4. Build & start
docker compose -f docker-compose.prod.yml up -d --build
docker compose -f docker-compose.prod.yml logs -f app # watch migrate + seed
⚠️ The .NET build is the heaviest step. On a 4 GB server it should fit, but if the build gets OOM-killed, add temporary swap:
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile
Confirm the container is on the web network and healthy:
docker network inspect web --format '{{range .Containers}}{{.Name}} {{end}}'
# should list: caddy ... csweb-travel
5. Caddy
Append to /opt/caddy/Caddyfile:
travel.rasmusj.com {
reverse_proxy csweb-travel:8080
}
Note: target is
csweb-travel(the container_name), notweb. A unique name avoids DNS collisions once other apps join the sharedwebnetwork.
Reload (no downtime for other sites):
cd /opt/caddy
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
# or, if the Caddyfile is bind-mounted and that fails:
docker compose restart caddy
6. Test
curl -I https://travel.rasmusj.com # expect 200/302/308, valid Let's Encrypt cert
Open https://travel.rasmusj.com and log in: admin@taltech.ee / <SEED_ADMIN_PASSWORD from .env>.
Updating later
cd /opt/projects/csweb-travel
git pull
docker compose -f docker-compose.prod.yml up -d --build
Migrations run automatically on boot (MigrateDatabase=true). Seeding is idempotent
(guards on existing rows), so it won't duplicate data on restart.
Security notes
- DB password & JWT key live only in
.env(gitignored) and are injected as env vars.JWT__Keyoverrides the placeholder inappsettings.json. - Seed admin password is read from
SEED_ADMIN_PASSWORDon first boot (AppDataInit.SeedIdentity). This replaces the well-known course defaultKala.12345, so the public admin login is strong from the very first deploy. - The other seeded demo accounts (
user@,alice@, …) still useKala.12345. They are non-admin demo users. Remove them fromInitialData.csif you don't want demo logins. - Postgres has no published port — it's reachable only by the app over
internal. - To rotate any secret: edit
.env, thendocker compose -f docker-compose.prod.yml up -d. (ChangingSEED_ADMIN_PASSWORDafter first boot has no effect — the user already exists; change that password through the app instead.)