chore: simplify standalone Compose setup

This commit is contained in:
2026-09-16 10:18:31 +03:30
parent 76ab2d1078
commit f45efdc667
6 changed files with 118 additions and 110 deletions

View File

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

View File

@@ -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 `<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.
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.

View File

@@ -1,7 +0,0 @@
services:
portfolio-app:
image: amiirkhl-portfolio:latest
build:
context: .
dockerfile: Dockerfile
target: runtime

10
compose.caddy.yaml Normal file
View File

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

View File

@@ -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:

View File

@@ -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}