From a5f8c54f2b1ac37bd7d2975fa36f9b8fa45a2321 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Tue, 15 Sep 2026 21:48:17 +0330 Subject: [PATCH] docs: explain self-hosting, content import and safe upgrades --- .env.example | 8 ++ DEPLOYMENT.md | 15 ++- SELF_HOSTING.md | 214 +++++++++++++++++++++++++++++++++++++ tasks/portfolio-builder.md | 23 ++++ 4 files changed, 256 insertions(+), 4 deletions(-) create mode 100644 .env.example create mode 100644 SELF_HOSTING.md create mode 100644 tasks/portfolio-builder.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bc480e8 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Exact public origin; use HTTPS behind your reverse proxy in production. +SITE_URL=http://localhost:3000 +# Native development/maintenance database and uploads directory. +DATA_DIR=./data +# Optional. If absent, initialization generates a secret in DATA_DIR/auth-secret. +# BETTER_AUTH_SECRET=replace-with-at-least-32-random-characters +# For compose.selfhost.yaml, directory mounted at /data: +PORTFOLIO_DATA_DIR=./data diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 16f1f9f..ca239e8 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,5 +1,9 @@ # Deployment +For the portfolio builder's first installation, owner commands, persistent +SQLite storage, and backup/restore, see [SELF_HOSTING.md](SELF_HOSTING.md). +Set the public `SITE_URL` in `/runtime.env` before deployment. + The Gitea Actions workflow in `.gitea/workflows/ci-cd.yml` verifies every pull request and every push to `main`. A successful push to `main` builds one verified Docker image on the shared host daemon, smoke-tests it, and activates @@ -8,7 +12,9 @@ that exact commit-tagged image over SSH without uploading source or rebuilding. Release manifests are stored in `/releases/`. Each manifest records the immutable image tag and Gitea run number. The `current` symlink is changed only after the new container is healthy. If startup fails, -the deployment script restarts the previous image without rebuilding it. +the deployment script stops short of automatic rollback: startup may have +migrated the database. Restore the pre-upgrade backup with the matching older +image using the procedure in `SELF_HOSTING.md`. After health succeeds, the Git checkout at `` is fast-forwarded to the exact deployed commit. Deployment refuses to overwrite tracked server-side changes or a checkout that has diverged from `origin/main`. @@ -108,9 +114,10 @@ docker compose \ This legacy path builds `amiirkhl-portfolio:latest` and is intentionally kept outside the normal CI deployment. Existing source-based release directories can -still be started with their original Compose file during migration. Automatic -rollback begins with the first successful image-based release because older -release directories do not contain `release.env` image metadata. +still be started with their original Compose file during migration. Rollback is +manual: verify schema compatibility or restore the pre-upgrade backup together +with its matching application image. Older release directories do not contain +`release.env` image metadata. ## Pipeline timing verification diff --git a/SELF_HOSTING.md b/SELF_HOSTING.md new file mode 100644 index 0000000..0de5b00 --- /dev/null +++ b/SELF_HOSTING.md @@ -0,0 +1,214 @@ +# Portfolio Builder + +One Next.js application, one SQLite file, and local uploads. Each installation +hosts one portfolio with one owner. Saving in `/admin` updates the next public +request without rebuilding. The existing design, animations, and section order +are retained. + +## Run locally + +Use Node.js 22 or newer. From the repository root: + +```sh +npm ci +cp .env.example .env +npm run maintenance -- init +npm run maintenance -- create-owner +npm run dev +``` + +Open `http://localhost:3000/admin`. `create-owner` prompts for an email and a +hidden password (12–128 characters). It refuses to create a second account. +`reset-password` changes the owner's password and revokes all existing sessions. +There is no public registration or email delivery requirement. + +```sh +npm run maintenance -- reset-password +``` + +For automated provisioning only, pass `OWNER_EMAIL` and `OWNER_PASSWORD` through +the process environment. Do not bake them into an image, commit them, or leave +them in a production container's environment after provisioning. + +## Docker on your own server + +The generic Compose file does not depend on the original private Caddy network. +Set `SITE_URL` in `.env` to the exact public origin, including the scheme and any +nonstandard port. Set `PORTFOLIO_DATA_DIR` to an absolute, local disk directory. +The container runs as UID/GID 1001; this directory must be writable by that user. + +```sh +mkdir -p /srv/portfolio/data +sudo chown 1001:1001 /srv/portfolio/data +# .env: SITE_URL=https://portfolio.example.com +# .env: PORTFOLIO_DATA_DIR=/srv/portfolio/data +docker compose -f compose.selfhost.yaml up -d --build +docker compose -f compose.selfhost.yaml exec portfolio-app node maintenance.mjs create-owner +``` + +Compose binds port 3000 to the host's loopback address. Point your existing HTTPS +reverse proxy to `127.0.0.1:3000`. For a local trial, keep +`SITE_URL=http://localhost:3000`. Set `PORT` to change the published host port and +adjust `SITE_URL` accordingly. The proxy must replace incoming forwarded IP +headers with the real client address so authentication rate limiting is reliable. + +The production image includes `maintenance.mjs`; it does not need TypeScript, +Drizzle Kit, npm installs, or a compiler at runtime. Startup applies the committed +SQL migrations and seeds samples only when the database is empty. `/api/health` +checks database readiness without rendering the portfolio. + +Use one application instance with a local filesystem. Do not mount the SQLite +database over NFS/SMB, use an ephemeral serverless disk, or run multiple replicas +against the same data volume. PostgreSQL would be the next step if those become +requirements. + +## Persistent files and configuration + +| Setting/file | Purpose | +| --- | --- | +| `SITE_URL` | Exact origin used for authentication and cross-site request checks | +| `DATA_DIR` | Native runtime directory, default `./data`; Docker uses `/data` | +| `PORTFOLIO_DATA_DIR` | Host bind mount for the generic Compose file | +| `BETTER_AUTH_SECRET` | Optional secret of at least 32 characters | +| `portfolio.sqlite` | Content, owner account, and sessions | +| `uploads/` | Validated images and PDF attachments | +| `auth-secret` | Generated secret when no environment secret was supplied | + +Keep the generated `auth-secret` with the database. When using an environment +secret, preserve it separately through your host's secret-management process. +The Next.js image cache is separate and may be discarded safely. + +## Editing and translations + +- **Site settings:** avatar, contact destinations, social accounts, default + language, and search metadata. Empty optional URLs hide the corresponding link. +- **Profile & sections:** hero/about/footer text, section headings, journey steps, + and visibility. Section order is fixed by the template. +- **Content lists:** projects and case studies, jobs, qualifications, milestones, + and skill groups. Lower position numbers appear first; nested lists have + up/down controls. Entry IDs are stable URL identifiers and cannot be renamed + through the editor after creation. +- **Languages:** add a lowercase language tag such as `de`, `ar`, or `pt-br`, a + native display name, and text direction. English and Persian labels are built + in. Other labels fall back to English until translated. Named placeholders + such as `{title}` and `{current}` must be retained. +- **Media library:** upload JPEG, PNG, WebP, or PDF files, up to 10 MB each. + Images are decoded and converted to WebP; metadata is removed. Image pickers + use the returned dimensions automatically. PDFs are served as downloads. + Referenced uploads cannot be deleted, including references in hidden content + and untranslated records. + +The default language uses `/` and `/projects/example`. Other enabled languages +use paths such as `/fa` and `/fa/projects/example`. Missing portfolio +translations fall back to the default-language content; the editor identifies +missing translations. Translate each section and add metadata for a language +before making it the default. Changing the default changes which language the +root URLs serve. Disabled languages return 404; their saved translations remain. +Fonts use the template's English/Persian fonts and system fallbacks for other +scripts. The admin interface itself is English. + +Changes appear on a fresh page request. An already-open visitor page does not +receive live push updates. Hidden projects are excluded from navigation and +return 404 when accessed directly. Upload URLs are public and immutable, so +upload a replacement rather than overwrite an existing URL. Deleting content +does not recall files already cached by a visitor. + +## Import this repository's original portfolio + +New installations show generic samples. To preserve this repository's original +English/Persian content and project URLs, run the explicit import **before editing +any content**: + +```sh +npm run maintenance -- import-legacy +# Or in Docker: +docker compose -f compose.selfhost.yaml exec portfolio-app node maintenance.mjs import-legacy +``` + +The import is transactional and only replaces untouched samples. It refuses to +run twice or overwrite edits. Creating the owner account does not prevent import. +The source fixture is `src/seed/legacy.json`; startup never imports it implicitly. + +## Backups and restore + +Back up before every upgrade. Use the maintenance command, which acquires the +same lock as content and upload writes, uses SQLite's backup API, copies uploads +and the generated secret, then writes a completion manifest. A backup without +`manifest.json` is incomplete. Do not copy only a live `.sqlite` file: committed +changes may still be in its WAL file. + +```sh +npm run maintenance -- backup /srv/backups/portfolio-2026-09-15 +``` + +The target must be new and outside the data directory. In Docker, mount a host +backup directory at `/backups` in an override file, then run: + +```sh +docker compose -f compose.selfhost.yaml -f compose.backup.yaml exec portfolio-app node maintenance.mjs backup /backups/before-upgrade +``` + +Example `compose.backup.yaml`: + +```yaml +services: + portfolio-app: + volumes: + - /srv/backups/portfolio:/backups +``` + +The mounted backup directory must also be writable by UID 1001. Move completed +backups to separate storage using your existing backup system. No scheduler or +additional service is installed by this project. + +Restore verifies database integrity and that every uploaded file exists. It +restores into a **new directory**, revokes all sessions, and never overwrites a +live database: + +```sh +npm run maintenance -- restore /srv/backups/portfolio-2026-09-15 /srv/portfolio/restored +``` + +Stop the application, update `DATA_DIR` or `PORTFOLIO_DATA_DIR` to the restored +directory, fix ownership if necessary, and start the matching application image. +Keep the old directory until the restored site has been verified. There is no +persistent content cache to invalidate after a restore. + +If a process is forcibly killed during a save or backup, `content.lock` may +remain. Stop all application and maintenance processes, remove only that lock +file from the data directory, then restart. Do not remove it while a write or +backup is running. + +## Upgrades and the existing Gitea deployment + +Build and test a new image, back up, then replace the container using the same +data volume. Never use `docker compose down -v` for an installation you intend +to keep. Migrations are versioned under `migrations/`; generate new migrations +with `npx drizzle-kit generate` during development and commit them. + +The existing `compose.yaml` retains its `caddy_proxy` network and adds a named +`portfolio-data` volume. Create `/runtime.env` containing the real +`SITE_URL=https://your-domain.example`; the release script passes it to Compose. +Keep this file private and outside version control. Existing deployments must +explicitly import the original portfolio into the new data volume before editing. + +Automatic rollback to an older image is disabled after a failed activation: +startup may already have changed the database schema. To roll back, restore the +pre-upgrade backup into a new volume/directory and activate the matching previous +image. Retain both the failed data and the backup for diagnosis. + +## Verification + +```sh +npm run test:content +npm run lint +npm run typecheck +npm run build +npm test +npm run test:e2e +``` + +Browser tests provision a temporary owner in a temporary data directory. Their +credentials are test fixtures, not production defaults. The container smoke test +checks generic startup, packaged legacy import, localized pages, and image +optimization. Run `scripts/smoke-container.sh IMAGE_TAG` against the built image. diff --git a/tasks/portfolio-builder.md b/tasks/portfolio-builder.md new file mode 100644 index 0000000..875c2aa --- /dev/null +++ b/tasks/portfolio-builder.md @@ -0,0 +1,23 @@ +# Portfolio builder implementation + +Approved architecture: one Next.js Node application, SQLite/Drizzle, Better Auth, +and persistent local uploads. One owner, save publishes, arbitrary languages, +default language at root, existing design and fixed section order. + +- [x] Database, migrations, validated content schemas and generic seed +- [x] Owner provisioning, recovery, sessions and protected management APIs +- [x] Database-backed public rendering, languages and dynamic project URLs +- [x] Structured admin forms, media management and translation editing +- [x] Current-site import, container startup, backup/restore and deployment docs +- [ ] Integration, browser, production build and container verification + +Acceptance: owners can change every portfolio field and public label without +editing source or rebuilding. Save changes fresh public HTML; private APIs remain +protected. Container replacement and backup restoration retain data and media. + +Verification recorded 2026-09-15: production build and all 24 Node tests passed; +browser suite passed 13 checks, with the mobile-only native-sharing check skipped +on desktop. Content/security tests passed again before committing. Container +verification remains open: dependency installation required a native compiler; +the Docker build stage now includes Python, make, and g++, but the image and +replacement-persistence smoke check have not yet been rerun successfully.