Skip to content

feat: production container, Render deployment, and cutover runbook (Hosting Interval E) - #50

Merged
quangshuynh merged 5 commits into
mainfrom
feat/production-deployment
Sep 27, 2026
Merged

quangshuynh merged 5 commits into
mainfrom
feat/production-deployment

Conversation

@quangshuynh

Copy link
Copy Markdown
Owner

Hosting Interval E, repository part: everything needed to deploy the one private Flipper instance. Nothing is deployed by this PR, and no real data is involved.

What it adds

  • Dockerfile: python:3.13-slim-bookworm (the version CI tests) with the pinned requirements.txt. Runs one Uvicorn process (web.app:app, no workers, no reload, --no-proxy-headers, graceful shutdown) as non-root uid 10001. Image defaults fail closed: FLIPPER_WEB_SECURITY_MODE=hosted, FLIPPER_DATA_DIR=/var/data/flipper, FLIPPER_CREDENTIAL_BACKEND=file. Includes openssh-sftp-server and /root/.ssh so Render's SSH/scp -s can move a verified backup at cutover. The image runs no SSH server of its own.
  • .dockerignore: an allow-list. .env, databases, attachments, credentials, backups, context.md, .claude/, tests, docs, and .git cannot enter the image.
  • deploy/entrypoint.sh: refuses to start unless the data directory is on a mounted volume, so SQLite can never land on ephemeral disk. Gives the root-owned disk to the app user, sets umask 077, drops root with setpriv, and execs the command.
  • deploy/flipper-cli (flipper on PATH): runs the CLI as the app user inside the container.
  • deploy/smoke.py: read-only HTTPS security smoke covering headers, HSTS, CSP, no-store, sign-in redirects, generic wrong password, __Host-/Secure/HttpOnly/SameSite cookie, CSRF origin checks, the public eBay route, hidden framework docs, and sign-out.
  • render.yaml: Blueprint for a new flipper-app service only. Starter plan, numInstances: 1, 1 GB disk at /var/data, health check GET /login, deploys main only after CI passes, and every secret sync: false.
  • CI container job: builds the image, inspects it for data and secrets, proves it refuses to start without a volume or secrets, runs the smoke with synthetic secrets, checks the non-root uid, proves persistence across restart and container replacement, and rehearses backup, verify, restore, and verify-restore on the volume.
  • Docs: docs/operations/deployment.md covers architecture, container, Render, environment and secrets, synthetic first deploy, real-data cutover, equivalence, authority transition, eBay reconnect, backups and disaster recovery, rollback, logs, and a phone checklist. Cross-links in the web-security, backup, and configuration docs and the README.

Deliberately unchanged

  • The existing eBay account-deletion service (flipper-zyui.onrender.com, standalone ebay.compliance:app) is not in the Blueprint and keeps working. Both services coexist, and the eBay developer account is untouched.
  • No new routes. /login is already public and serves as the health check, so the route classification table is unchanged.
  • No schema change (v14). No changes to accounting, Deal Score, Q/S/C allocation, backup fingerprint semantics, CSP, CSRF, or trusted-host checks. No WAL. No PWA work.

Verification

  • pytest: 802 passed, 4 skipped (the Blueprint path test is POSIX-only and runs in CI).
  • ruff check . and ruff format --check . pass; mkdocs build --strict passes; git diff --check is clean; tracked-ignored hygiene passes.
  • Local Docker, synthetic data only:
    • The image builds from a clean git archive of the tree.
    • Without a volume: exit 65. With a volume but no secrets: exit 3, and no database is created.
    • The smoke passes, and an untrusted Host gets 400.
    • PID 1 runs as uid 10001. Database, attachment, and credential file are owner-only (600/700).
    • The fingerprint, attachment hash, and credential file survive both a restart and replacement with a rebuilt image.
    • Backup → verify → restore → verify-restore passes, credentials are excluded, and a restore onto the active directory is refused.
    • The cutover switch was rehearsed: pointing FLIPPER_DATA_DIR at the restored directory and restarting gives equivalent data.
    • At 390×844, Dashboard, Deals, Research History, Inventory and detail, Sales, Insights, Settings, and the add-opportunity form showed no page overflow and no input under 16px. The table scrolls inside its container, attachments load, and sign-out works.
    • Access logs contain only method, path, and status; no password, hash, session secret, cookie, or token.

After merge (human actions)

Creating the Render service needs the owner's Render account (a paid Starter plan plus a disk, about USD 7.25/month) and an owner password chosen by the owner. The real-data cutover needs separate explicit approval. See the runbook.

One non-root Uvicorn process in a python:3.13-slim image. The build context
is an allow-list, so no database, attachment, credential, backup, .env, or
context file can enter the image. The image defaults to hosted security mode
and a /var/data/flipper data directory; the entrypoint refuses to start unless
that directory is on a mounted volume, hands the root-owned disk to the app
user, and drops privileges before exec.

render.yaml defines only the new flipper-app service: one instance, a 1 GB
persistent disk, GET /login as the health check, deploys of main after CI
passes, and every secret as sync:false. The existing eBay compliance service
is not part of the Blueprint.
Checks headers, HSTS, CSP, no-store, sign-in redirects, generic wrong
password, __Host- cookie flags, CSRF origin checks, the public eBay route,
hidden framework docs, and sign-out without changing any data. The password
comes from the environment or a no-echo prompt and is never printed.
Builds the image, inspects it for data and secrets, proves it refuses to
start without a volume or hosted secrets, runs the security smoke, checks the
unprivileged user, proves persistence across restart and container
replacement, and rehearses backup/verify/restore on the volume. Nothing
deploys and no production secret is used.
@gitguardian

gitguardian Bot commented Sep 27, 2026

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
37670708 Triggered Generic High Entropy Secret 55fbd53 tests/test_deployment.py View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@quangshuynh
quangshuynh merged commit aebd0a6 into main Sep 27, 2026
3 checks passed
@quangshuynh
quangshuynh deleted the feat/production-deployment branch September 27, 2026 17:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant