docs: explain self-hosting, content import and safe upgrades
This commit is contained in:
8
.env.example
Normal file
8
.env.example
Normal 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
|
||||
@@ -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 `<DEPLOY_PATH>/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 `<DEPLOY_PATH>/releases/<commit-sha>`. 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 `<DEPLOY_PATH>` 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
|
||||
|
||||
|
||||
214
SELF_HOSTING.md
Normal file
214
SELF_HOSTING.md
Normal 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.
|
||||
23
tasks/portfolio-builder.md
Normal file
23
tasks/portfolio-builder.md
Normal 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.
|
||||
Reference in New Issue
Block a user