Files
my-protfolio/SELF_HOSTING.md

270 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
With Docker Engine and Compose v2 installed, clone/pull this repository, copy
`.env.example` to `.env`, set your public `SITE_URL`, then run:
```sh
docker compose up --build -d
```
The default named volumes are created with the required ownership automatically.
The application initializes SQLite and its authentication secret, but **never
creates an owner on startup**. When you want admin access, run:
```sh
docker compose exec portfolio-app node maintenance.mjs create-user
# create-owner remains an equivalent command.
docker compose exec portfolio-app node maintenance.mjs reset-password
```
The commands prompt for credentials. No owner credentials are needed in `.env`.
Open `/admin` after creating the owner. For updates, pull and repeat the same
Compose command. Never use `docker compose down -v` for data you intend to keep.
The default port binds to localhost. Set `BIND_ADDRESS=0.0.0.0` to access it
remotely, or point your existing HTTPS reverse proxy to `127.0.0.1:3000`.
Set `PORT` and `SITE_URL` together when using a different port. The proxy should
replace forwarded IP headers with the actual client address.
For the existing Caddy Docker network, set
`COMPOSE_FILE=compose.yaml:compose.caddy.yaml` in the Linux server's `.env`.
Caddy must already belong to `caddy_proxy` and proxy to `portfolio-app:3000`.
Standalone installations do not need that network or override.
Use a single application instance and local storage. SQLite is not intended for
multiple replicas or a network filesystem. Startup applies committed migrations.
The image includes `maintenance.mjs`, so maintenance needs no npm installation.
Baseline CPU support for image processing is retained; native compilation is
cached separately and can make the first build slower. UI edits reuse dependency
and native-library layers. Docker builds only the application, including Next.js
TypeScript validation, and packages it; lint, audits, tests, and image probes do
not run in the production build. Postbuild packages assets once.
## 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_VOLUME` | Persistent Docker volume name; preserve it on upgrades |
| `PORTFOLIO_CACHE_VOLUME` | Docker image-cache volume name |
| `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. Flags are automatic; optionally set
`Flag country` to a lowercase country code (for example `gb` or `us`) to override
the inferred region. Unknown regions show a globe. 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 private portfolio content
Personal content is supplied separately, never embedded in the application image.
A version-1 bundle contains `bundle.json` with `content` (settings, languages,
sections, entries) and `files` (original URL, relative file path, SHA-256, MIME,
width, height). Keep the bundle outside the Git checkout.
```sh
npm run maintenance -- import-legacy /absolute/path/to/private-bundle
```
The import validates the bundle before writing, preserves project IDs, copies
images into persistent uploads, and replaces only untouched sample content.
It refuses repeated imports or edited portfolios. Creating an owner is independent.
For an existing, edited installation, migrate only references to the old bundled
images before upgrading away from those assets:
```sh
npm run maintenance -- migrate-legacy-media /absolute/path/to/private-bundle
```
This operation preserves content and existing uploads and is safe to repeat.
Back up first. For Docker, mount the external bundle read-only at `/import` and
run `node maintenance.mjs import-legacy /import` (or `migrate-legacy-media`).
Do not copy the bundle into the image or repository. Git history is unchanged.
`create-user` is an alias for `create-owner`; neither runs at application startup.
## 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.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.
## Upgrading an existing installation
Keep your existing data volume: the default `portfolio_portfolio-data` matches
the previous deployment with project name `portfolio`. If yours differs, set
`PORTFOLIO_DATA_VOLUME` to its existing name **before** starting the new Compose
configuration. Preserve the existing authentication secret, including any
`BETTER_AUTH_SECRET` you previously supplied. Copy `SITE_URL` from `runtime.env`
into `.env`. Enable the optional Caddy override when retaining that network.
If you previously used the self-hosting bind mount, keep it through a local
`compose.override.yaml` instead of accidentally starting an empty named volume:
```yaml
services:
portfolio-app:
volumes:
- /absolute/path/to/existing/data:/data
```
Keep its existing UID/GID 1001 ownership. Do not copy a live SQLite database.
Back up before upgrading. To migrate old `/images/...` references, mount your
external private bundle read-only in the same override:
```yaml
services:
portfolio-app:
volumes:
- /absolute/path/to/private-bundle:/import:ro
```
Combine both mounts in one `volumes` list if using both. Build the new image,
then run the migration against the existing volume before replacing the server:
```sh
docker compose build
docker compose run --rm --no-deps portfolio-app node maintenance.mjs migrate-legacy-media /import
docker compose up -d
```
This is a one-time content migration, not a requirement for fresh installations.
If startup fails after a schema migration, restore a pre-upgrade backup with the
matching older image. Do not automatically downgrade the image over newer data.
See `DEPLOYMENT.md` for the simplified Gitea workflow.
## Loading feedback
Initial rendering and language changes use a full-page indicator. Case-study
opening and next/previous navigation keep the drawer and its close control while
showing a loading indicator inside it. Indicators follow navigation/rendering
state without artificial delays or waiting for off-screen images and 3D scenes.
They follow the current theme, honor reduced motion, and expose localized status
labels under Languages in the editor.
## Build checks
```sh
npm run typecheck
npm run build
```
The production build includes TypeScript validation. Browser tests and other
optional test suites are not part of deployment.