Combined flat-alert + flat-apply with authenticated web UI
Previous attempt mapped GIT_COMMIT=${SOURCE_COMMIT:-dev} in compose.
That backfired: Coolify parses the compose, sees ${SOURCE_COMMIT},
auto-registers it as a user-defined env var with my "dev" default,
and then *skips* its own SHA injection — because the registration
guard is "only inject if user hasn't defined it." Result: container
got SOURCE_COMMIT=dev and footer kept showing build dev.
Drop the compose reference entirely so Coolify's auto-injection
takes over, and read SOURCE_COMMIT in settings.py (with GIT_COMMIT
kept as the local-dev override). One-time cleanup of the orphan
SOURCE_COMMIT rows in the Coolify DB was done out-of-band.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|---|---|---|
| alert | ||
| apply | ||
| web | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| README.md | ||
lazyflat
Combined deployment of flat-alert (reliable scraper) and flat-apply (experimental auto‑applier) behind a single authenticated web UI.
Architecture
Three isolated containers on one internal Docker network:
┌─────────┐ POST /internal/flats ┌─────────┐ POST /apply ┌─────────┐
│ alert │ ────────────────────────► │ web │ ────────────────► │ apply │
│ scraper │ │ UI+DB │ │ browser │
└─────────┘ └─────────┘ └─────────┘
▲
│ HTTPS (Coolify / Traefik)
│
user (auth)
alert/— scrapes inberlinwohnen.de (unchanged logic) and posts each discovered flat toweb. Zero external dependencies onapply, soapplycrashes can never bring the alerter down.apply/— FastAPI wrapper around the experimental Playwright applier. Only accepts requests with a sharedX-Internal-Api-Key. Not exposed publicly.web/— FastAPI + Jinja + HTMX dashboard. The only public service. Owns the SQLite database, auth, and orchestration.
Safety / isolation
Because apply/ is still experimental, the system is hardened around it:
| Control | Behavior |
|---|---|
| Separate container | A crashed apply does not take alert or web with it. |
| Internal-only network | apply is not reachable from the internet; requires internal API key. |
Default mode manual |
New flats are just shown in the UI; apply runs only on click. |
| Circuit breaker | N consecutive apply failures auto-disable further apply calls. |
| Kill switch | One-click button in the UI that blocks all apply activity. |
SUBMIT_FORMS=False |
Default for apply — runs the full flow without final submit. |
| Audit log | Every auth event, mode change, and apply is recorded. |
Web security
- Argon2id password hashes (
argon2-cffi), constant-time compare. - Session cookie: signed with
itsdangerous,HttpOnly,Secure,SameSite=Strict. - CSRF: synchronizer token bound to the session on every state-changing form.
- Login rate limit (in-memory, per-IP).
- Strict
Content-Security-Policy,X-Frame-Options: DENY,noindex. /internal/*endpoints gated by a sharedINTERNAL_API_KEYand never exposed by Coolify.
Deployment on Coolify
- Create repo: push this monorepo to
ssh://git@git.moritz.run:2222/moritz/lazyflat.git. - New Coolify resource → Docker Compose → point it at this repo. Coolify
will read
docker-compose.ymland deploy all three services on one network. - Domain: set
flat.lab.moritz.runon thewebservice only. Coolify (Traefik) handles TLS. Do not set a domain onalertorapply. - Secrets: paste the environment variables from
.env.exampleinto Coolify's env UI. At minimum you need:AUTH_USERNAME,AUTH_PASSWORD_HASHSESSION_SECRET,INTERNAL_API_KEYGMAPS_API_KEY,BERLIN_WOHNEN_USERNAME,BERLIN_WOHNEN_PASSWORD- personal info + WBS for
apply
- Generate the password hash:
python -c "from argon2 import PasswordHasher; print(PasswordHasher().hash('<your-password>'))" - Generate secrets:
python -c "import secrets; print(secrets.token_urlsafe(48))" # SESSION_SECRET python -c "import secrets; print(secrets.token_urlsafe(48))" # INTERNAL_API_KEY - First apply launch should stay in manual mode with
SUBMIT_FORMS=Falseuntil each provider is verified end-to-end.
Local development
cp .env.example .env
# fill in AUTH_PASSWORD_HASH, SESSION_SECRET, INTERNAL_API_KEY, creds
docker compose up -d --build
# open http://localhost:8000 (set COOKIE_SECURE=false for plain http!)
To also expose the web port locally, add ports: ["8000:8000"] under web: in
docker-compose.yml (the Coolify production compose doesn't publish host ports).