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 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 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 # BETTER_AUTH_SECRET=replace-with-at-least-32-random-characters
# For compose.selfhost.yaml, directory mounted at /data: # Override if your server needs a registry mirror.
PORTFOLIO_DATA_DIR=./data 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 ## Docker on your own server
The generic Compose file does not depend on the original private Caddy network. With Docker Engine and Compose v2 installed, clone/pull this repository, copy
Set `SITE_URL` in `.env` to the exact public origin, including the scheme and any `.env.example` to `.env`, set your public `SITE_URL`, then run:
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 ```sh
mkdir -p /srv/portfolio/data docker compose up --build -d
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 The default named volumes are created with the required ownership automatically.
reverse proxy to `127.0.0.1:3000`. For a local trial, keep The application initializes SQLite and its authentication secret, but **never
`SITE_URL=http://localhost:3000`. Set `PORT` to change the published host port and creates an owner on startup**. When you want admin access, run:
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, ```sh
Drizzle Kit, npm installs, or a compiler at runtime. Startup applies the committed docker compose exec portfolio-app node maintenance.mjs create-user
SQL migrations and seeds samples only when the database is empty. `/api/health` # create-owner remains an equivalent command.
checks database readiness without rendering the portfolio. docker compose exec portfolio-app node maintenance.mjs reset-password
```
The Docker build compiles libvips and Sharp from source for baseline CPUs. This The commands prompt for credentials. No owner credentials are needed in `.env`.
supports VPS hosts that lack the features required by Sharp's prebuilt native Open `/admin` after creating the owner. For updates, pull and repeat the same
and WebAssembly binaries. The first build takes longer; Docker caches the image Compose command. Never use `docker compose down -v` for data you intend to keep.
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.
Use one application instance with a local filesystem. Do not mount the SQLite The default port binds to localhost. Set `BIND_ADDRESS=0.0.0.0` to access it
database over NFS/SMB, use an ephemeral serverless disk, or run multiple replicas remotely, or point your existing HTTPS reverse proxy to `127.0.0.1:3000`.
against the same data volume. PostgreSQL would be the next step if those become Set `PORT` and `SITE_URL` together when using a different port. The proxy should
requirements. 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 ## Persistent files and configuration
@@ -75,7 +75,8 @@ requirements.
| --- | --- | | --- | --- |
| `SITE_URL` | Exact origin used for authentication and cross-site request checks | | `SITE_URL` | Exact origin used for authentication and cross-site request checks |
| `DATA_DIR` | Native runtime directory, default `./data`; Docker uses `/data` | | `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 | | `BETTER_AUTH_SECRET` | Optional secret of at least 32 characters |
| `portfolio.sqlite` | Content, owner account, and sessions | | `portfolio.sqlite` | Content, owner account, and sessions |
| `uploads/` | Validated images and PDF attachments | | `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: backup directory at `/backups` in an override file, then run:
```sh ```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`: 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 file from the data directory, then restart. Do not remove it while a write or
backup is running. 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 Keep your existing data volume: the default `portfolio_portfolio-data` matches
data volume. Never use `docker compose down -v` for an installation you intend the previous deployment with project name `portfolio`. If yours differs, set
to keep. Migrations are versioned under `migrations/`; generate new migrations `PORTFOLIO_DATA_VOLUME` to its existing name **before** starting the new Compose
with `npx drizzle-kit generate` during development and commit them. 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 If you previously used the self-hosting bind mount, keep it through a local
`portfolio-data` volume. Create `<DEPLOY_PATH>/runtime.env` containing the real `compose.override.yaml` instead of accidentally starting an empty named volume:
`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: ```yaml
startup may already have changed the database schema. To roll back, restore the services:
pre-upgrade backup into a new volume/directory and activate the matching previous portfolio-app:
image. Retain both the failed data and the backup for diagnosis. volumes:
- /absolute/path/to/existing/data:/data
## 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 Keep its existing UID/GID 1001 ownership. Do not copy a live SQLite database.
credentials are test fixtures, not production defaults. The optional container smoke test Back up before upgrading. To migrate old `/images/...` references, mount your
checks generic startup, owner commands, uploads, and persistence. Run `scripts/smoke-container.sh IMAGE_TAG` against the built image. 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: services:
portfolio-app: portfolio-app:
image: ${PORTFOLIO_IMAGE:-amiirkhl-portfolio:latest} image: portfolio:local
container_name: portfolio-app build:
context: .
target: runtime
args:
NPM_REGISTRY: ${NPM_REGISTRY:-https://registry.npmjs.org/}
restart: unless-stopped restart: unless-stopped
ports:
- "${BIND_ADDRESS:-127.0.0.1}:${PORT:-3000}:3000"
environment: environment:
SITE_URL: ${SITE_URL:-https://portfolio.example.com} SITE_URL: ${SITE_URL:-http://localhost:3000}
DATA_DIR: /data DATA_DIR: /data
healthcheck: BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-}
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
volumes: volumes:
- next-image-cache:/app/.next/cache
- portfolio-data:/data - portfolio-data:/data
networks: - next-image-cache:/app/.next/cache
- caddy_proxy
networks:
caddy_proxy:
external: true
name: caddy_proxy
volumes: volumes:
next-image-cache:
portfolio-data: portfolio-data:
name: ${PORTFOLIO_DATA_VOLUME:-portfolio_portfolio-data}
next-image-cache:
name: ${PORTFOLIO_CACHE_VOLUME:-portfolio_next-image-cache}