docs: explain self-hosting, content import and safe upgrades

This commit is contained in:
2026-09-15 21:48:17 +03:30
parent c17b4b57b2
commit a5f8c54f2b
4 changed files with 256 additions and 4 deletions

8
.env.example Normal file
View File

@@ -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

View File

@@ -1,5 +1,9 @@
# Deployment # 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 `<DEPLOY_PATH>/runtime.env` before deployment.
The Gitea Actions workflow in `.gitea/workflows/ci-cd.yml` verifies every pull 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 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 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 `<DEPLOY_PATH>/releases/<commit-sha>`. Each Release manifests are stored in `<DEPLOY_PATH>/releases/<commit-sha>`. Each
manifest records the immutable image tag and Gitea run number. The `current` 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, 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 `<DEPLOY_PATH>` is fast-forwarded to After health succeeds, the Git checkout at `<DEPLOY_PATH>` is fast-forwarded to
the exact deployed commit. Deployment refuses to overwrite tracked server-side the exact deployed commit. Deployment refuses to overwrite tracked server-side
changes or a checkout that has diverged from `origin/main`. 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 This legacy path builds `amiirkhl-portfolio:latest` and is intentionally kept
outside the normal CI deployment. Existing source-based release directories can outside the normal CI deployment. Existing source-based release directories can
still be started with their original Compose file during migration. Automatic still be started with their original Compose file during migration. Rollback is
rollback begins with the first successful image-based release because older manual: verify schema compatibility or restore the pre-upgrade backup together
release directories do not contain `release.env` image metadata. with its matching application image. Older release directories do not contain
`release.env` image metadata.
## Pipeline timing verification ## Pipeline timing verification

214
SELF_HOSTING.md Normal file
View File

@@ -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 `<DEPLOY_PATH>/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.

View File

@@ -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.