Files
my-protfolio/SELF_HOSTING.md
Amirhossein Khalili bc49f0ed31
Some checks failed
Test and deploy / Verify production build (push) Failing after 15m8s
fix: support image processing on CPUs without x86-64-v2
2026-09-15 22:42:06 +03:30

220 lines
9.7 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
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.
Sharp uses native image processing where supported and includes a WebAssembly
fallback for servers without the required x86-64 CPU features. The fallback
preserves uploads and image optimization, but can use more CPU time. Keep
`@img/sharp-wasm32` at the same version as `sharp` when upgrading.
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.