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