chore: simplify standalone Compose setup
This commit is contained in:
18
.env.example
18
.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
|
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
|
||||||
|
|||||||
138
SELF_HOSTING.md
138
SELF_HOSTING.md
@@ -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.
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
services:
|
|
||||||
portfolio-app:
|
|
||||||
image: amiirkhl-portfolio:latest
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
target: runtime
|
|
||||||
10
compose.caddy.yaml
Normal file
10
compose.caddy.yaml
Normal 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
|
||||||
@@ -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:
|
|
||||||
40
compose.yaml
40
compose.yaml
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user