From f45efdc667bdbc5469888b20240664b29be63377 Mon Sep 17 00:00:00 2001 From: Amirhossein Khalili Date: Wed, 16 Sep 2026 10:18:31 +0330 Subject: [PATCH] chore: simplify standalone Compose setup --- .env.example | 18 ++++-- SELF_HOSTING.md | 138 ++++++++++++++++++++++++------------------ compose.build.yaml | 7 --- compose.caddy.yaml | 10 +++ compose.selfhost.yaml | 15 ----- compose.yaml | 40 +++++------- 6 files changed, 118 insertions(+), 110 deletions(-) delete mode 100644 compose.build.yaml create mode 100644 compose.caddy.yaml delete mode 100644 compose.selfhost.yaml diff --git a/.env.example b/.env.example index bc480e8..bce47ea 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,16 @@ -# Exact public origin; use HTTPS behind your reverse proxy in production. +# Public origin. Set your HTTPS origin when using a reverse proxy. SITE_URL=http://localhost:3000 -# Native development/maintenance database and uploads directory. +PORT=3000 +# Set 0.0.0.0 to expose the port beyond this machine. +BIND_ADDRESS=127.0.0.1 +# Native development only; Docker always stores data at /data. DATA_DIR=./data -# Optional. If absent, initialization generates a secret in DATA_DIR/auth-secret. +# These defaults preserve volumes from the previous production deployment. +PORTFOLIO_DATA_VOLUME=portfolio_portfolio-data +PORTFOLIO_CACHE_VOLUME=portfolio_next-image-cache +# Optional; otherwise generated once and kept in the data volume. # BETTER_AUTH_SECRET=replace-with-at-least-32-random-characters -# For compose.selfhost.yaml, directory mounted at /data: -PORTFOLIO_DATA_DIR=./data +# Override if your server needs a registry mirror. +NPM_REGISTRY=https://registry.npmjs.org/ +# Optional existing Caddy network (Linux server path separator): +# COMPOSE_FILE=compose.yaml:compose.caddy.yaml diff --git a/SELF_HOSTING.md b/SELF_HOSTING.md index 688cdee..803ad29 100644 --- a/SELF_HOSTING.md +++ b/SELF_HOSTING.md @@ -32,42 +32,42 @@ 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. +With Docker Engine and Compose v2 installed, clone/pull this repository, copy +`.env.example` to `.env`, set your public `SITE_URL`, then run: ```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 +docker compose up --build -d ``` -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 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: -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. +```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 Docker build compiles libvips and Sharp from source for baseline CPUs. This -supports VPS hosts that lack the features required by Sharp's prebuilt native -and WebAssembly binaries. The first build takes longer; Docker caches the image -libraries for later application changes. Image processing is checked before -Next.js compilation and again in the final runtime image. When upgrading Sharp, -keep the libvips source version in the Dockerfile at or above Sharp's minimum. +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. -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. +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. ## Persistent files and configuration @@ -75,7 +75,8 @@ requirements. | --- | --- | | `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 | +| `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 | @@ -165,7 +166,7 @@ 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 +docker compose -f compose.yaml -f compose.backup.yaml exec portfolio-app node maintenance.mjs backup /backups/before-upgrade ``` Example `compose.backup.yaml`: @@ -199,35 +200,56 @@ 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 +## Upgrading an existing installation -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. +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. -The existing `compose.yaml` retains its `caddy_proxy` network and adds a named -`portfolio-data` volume. Create `/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. +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: -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 +```yaml +services: + portfolio-app: + volumes: + - /absolute/path/to/existing/data:/data ``` -Browser tests provision a temporary owner in a temporary data directory. Their -credentials are test fixtures, not production defaults. The optional container smoke test -checks generic startup, owner commands, uploads, and persistence. Run `scripts/smoke-container.sh IMAGE_TAG` against the built image. +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. + +## 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. diff --git a/compose.build.yaml b/compose.build.yaml deleted file mode 100644 index fc138f7..0000000 --- a/compose.build.yaml +++ /dev/null @@ -1,7 +0,0 @@ -services: - portfolio-app: - image: amiirkhl-portfolio:latest - build: - context: . - dockerfile: Dockerfile - target: runtime diff --git a/compose.caddy.yaml b/compose.caddy.yaml new file mode 100644 index 0000000..aa5421d --- /dev/null +++ b/compose.caddy.yaml @@ -0,0 +1,10 @@ +# Optional: COMPOSE_FILE=compose.yaml:compose.caddy.yaml in the server's .env. +services: + portfolio-app: + networks: + - default + - caddy_proxy +networks: + caddy_proxy: + external: true + name: caddy_proxy diff --git a/compose.selfhost.yaml b/compose.selfhost.yaml deleted file mode 100644 index 572573f..0000000 --- a/compose.selfhost.yaml +++ /dev/null @@ -1,15 +0,0 @@ -services: - portfolio-app: - build: . - image: portfolio-builder:local - restart: unless-stopped - ports: - - "127.0.0.1:${PORT:-3000}:3000" - environment: - SITE_URL: ${SITE_URL:-http://localhost:3000} - DATA_DIR: /data - volumes: - - ${PORTFOLIO_DATA_DIR:-./data}:/data - - next-image-cache:/app/.next/cache -volumes: - next-image-cache: diff --git a/compose.yaml b/compose.yaml index 62a120d..4b20b2b 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,34 +1,24 @@ +name: portfolio services: portfolio-app: - image: ${PORTFOLIO_IMAGE:-amiirkhl-portfolio:latest} - container_name: portfolio-app + image: portfolio:local + build: + context: . + target: runtime + args: + NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org/} restart: unless-stopped + ports: + - "${BIND_ADDRESS:-127.0.0.1}:${PORT:-3000}:3000" environment: - SITE_URL: ${SITE_URL:-https://portfolio.example.com} + SITE_URL: ${SITE_URL:-http://localhost:3000} DATA_DIR: /data - healthcheck: - test: - [ - "CMD", - "node", - "-e", - "fetch('http://127.0.0.1:3000/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))", - ] - interval: 2s - timeout: 2s - start_period: 3s - retries: 30 + BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-} volumes: - - next-image-cache:/app/.next/cache - portfolio-data:/data - networks: - - caddy_proxy - -networks: - caddy_proxy: - external: true - name: caddy_proxy - + - next-image-cache:/app/.next/cache volumes: - next-image-cache: portfolio-data: + name: ${PORTFOLIO_DATA_VOLUME:-portfolio_portfolio-data} + next-image-cache: + name: ${PORTFOLIO_CACHE_VOLUME:-portfolio_next-image-cache}