feat(docker): add fail-closed host profiles

This commit is contained in:
Pi Web Agent
2026-06-27 23:05:40 +00:00
parent 3ef3643f64
commit 2987b6f282
8 changed files with 745 additions and 101 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@jmfederico/pi-web": patch
---
Add fail-closed Docker host profile detection with Linux and Docker Desktop for Mac Compose overrides, and split Docker dev configuration into a user-editable local env file plus generated Compose inputs.
+68 -34
View File
@@ -13,12 +13,11 @@ No prebuilt image or registry is required in either mode.
The Docker setup is for trusted single-user or trusted-admin environments. It is not a sandbox and it is not suitable for untrusted multi-tenant use. The Docker setup is for trusted single-user or trusted-admin environments. It is not a sandbox and it is not suitable for untrusted multi-tenant use.
By design, the runtime containers get deliberate host access so PI WEB agents can work on real server paths: By design, the runtime containers get deliberate host access so PI WEB agents can work on real host paths:
- `/var/run/docker.sock` is mounted into the containers. The Docker socket is root-equivalent on the host. - `/var/run/docker.sock` is mounted into the containers. The Docker socket is root-equivalent on the Docker host.
- `/srv`, `/opt`, and `/home` are mounted read/write. - On native Linux Docker Engine, existing `/home`, `/srv`, and `/opt` paths are mounted read/write, `/` is mounted read-only at `/host` for inspection, and `hostexec` can run explicit commands in the Linux host namespaces.
- `/` is mounted read-only at `/host` for inspection. - On Docker Desktop for Mac, existing `/Users`, `/Volumes`, and `/private` paths are mounted read/write. `hostexec` is disabled because Docker Desktop containers run inside a Linux VM and cannot enter native macOS namespaces.
- `hostexec` can start a temporary privileged helper container and run explicit commands in the host namespaces. Commands run as the container user by default, and `hostexec --root` can still run explicit administrative commands as root.
Only install this on machines where the PI WEB user, the selected workspaces, and the browser/API clients are trusted. Review scripts before piping them to `sh` if you do not already trust this repository. Only install this on machines where the PI WEB user, the selected workspaces, and the browser/API clients are trusted. Review scripts before piping them to `sh` if you do not already trust this repository.
@@ -32,10 +31,15 @@ The web port is bound to `127.0.0.1` by default. Do **not** expose PI WEB direct
Prerequisites: Prerequisites:
- Docker Engine with the Compose plugin (`docker compose`) or `docker-compose`; - one supported Docker host profile:
- native Linux Docker Engine using the local `/var/run/docker.sock`; or
- Docker Desktop for Mac;
- Docker Compose through the `docker compose` plugin or `docker-compose`;
- a user that can talk to the Docker daemon; - a user that can talk to the Docker daemon;
- `curl` or `wget` for the one-liner installer. - `curl` or `wget` for the one-liner installer.
The installer fails closed on unknown or unsupported Docker setups, such as remote Docker contexts, `DOCKER_HOST` overrides outside the supported local Unix socket, rootless/alternate Linux sockets, Docker Desktop for Linux, Colima, or OrbStack. It prints the detected host OS, Docker context, endpoint, `DOCKER_HOST`, socket source, and Docker OS before exiting, and it does not recreate services.
Install or update with the same command: Install or update with the same command:
```bash ```bash
@@ -96,6 +100,8 @@ Common environment variables written to `.env`:
| `PI_WEB_UID`, `PI_WEB_GID` | user/group used by the runtime containers and the image's `pi-web` account | | `PI_WEB_UID`, `PI_WEB_GID` | user/group used by the runtime containers and the image's `pi-web` account |
| `DOCKER_GID` | extra group used for Docker socket access | | `DOCKER_GID` | extra group used for Docker socket access |
| `PI_WEB_DOCKER_DATA_DIR` | persistent data bind mount | | `PI_WEB_DOCKER_DATA_DIR` | persistent data bind mount |
| `PI_WEB_DOCKER_HOST_PROFILE`, `HOSTEXEC_MODE` | detected host profile and host-command capability toggle |
| `PI_WEB_DOCKER_EXTRA_HOST_PATHS` | optional whitespace-separated existing absolute paths to bind-mount read/write at the same path |
| `PI_WEB_BIND_ADDR`, `PI_WEB_PORT` | host bind address and port | | `PI_WEB_BIND_ADDR`, `PI_WEB_PORT` | host bind address and port |
| `PI_WEB_VERSION` | npm version/range for `@jmfederico/pi-web` | | `PI_WEB_VERSION` | npm version/range for `@jmfederico/pi-web` |
| `PI_VERSION` | npm version/range for `@earendil-works/pi-coding-agent` | | `PI_VERSION` | npm version/range for `@earendil-works/pi-coding-agent` |
@@ -106,7 +112,9 @@ Common environment variables written to `.env`:
| `PI_WEB_IMAGE` | local image tag to build and run | | `PI_WEB_IMAGE` | local image tag to build and run |
| `HOSTEXEC_IMAGE` | helper image used by `hostexec` | | `HOSTEXEC_IMAGE` | helper image used by `hostexec` |
Host-derived IDs are refreshed on rerun unless you explicitly override them. User-facing values such as data directory, bind address, port, image names, upload limit, base image, Node.js settings, extra packages, and version pins are preserved from an existing `.env` unless you pass a flag or environment override. Host-derived IDs and the Docker host profile are refreshed on rerun unless you explicitly override the IDs. User-facing values such as data directory, bind address, port, image names, upload limit, extra host paths, base image, Node.js settings, extra packages, and version pins are preserved from an existing `.env` unless you pass a flag or environment override.
The installer also writes a generated `compose.override.yml` in the install directory. Docker Compose loads it automatically for ordinary `docker compose ...` commands run from that directory; re-run the installer instead of editing that generated file by hand.
### Base image and tooling ### Base image and tooling
@@ -204,9 +212,11 @@ If you use a reverse proxy, keep the container bound to localhost or a private a
## `hostexec` examples ## `hostexec` examples
`hostexec [--root] <command...>` is the only host command bridge provided by this Docker setup. It intentionally does not abstract package managers or detect distributions. By default, commands run as the same numeric user/group as the PI WEB container. Use `--root` only for administrative host commands. `hostexec [--root] <command...>` is the native Linux host command bridge provided by this Docker setup. It is enabled only for the `linux-native-docker` profile and intentionally does not abstract package managers or detect distributions. By default, commands run as the same numeric user/group as the PI WEB container. Use `--root` only for administrative host commands.
Run it from a PI WEB session, a PI WEB terminal, or by execing into the runtime container: On Docker Desktop for Mac, `hostexec` exits with a clear disabled message because the Docker daemon and containers run inside a Linux VM, not in native macOS namespaces. Docker CLI and Docker Compose commands still work through the mounted Docker socket.
Run it from a PI WEB session, a PI WEB terminal, or by execing into the runtime container on native Linux:
```bash ```bash
hostexec uname -a hostexec uname -a
@@ -223,7 +233,7 @@ cd ~/.local/share/pi-web-docker
docker compose exec web hostexec uname -a docker compose exec web hostexec uname -a
``` ```
`hostexec` starts a temporary privileged helper container through the mounted Docker socket, enters the host namespaces with `nsenter`, and runs exactly the command you passed. Treat it like privileged host access even when the final command drops back to the container user. On native Linux, `hostexec` starts a temporary privileged helper container through the mounted Docker socket, enters the host namespaces with `nsenter`, and runs exactly the command you passed. Treat it like privileged host access even when the final command drops back to the container user.
## Development Docker setup ## Development Docker setup
@@ -232,24 +242,42 @@ Use this mode when developing PI WEB from this checkout. It bind-mounts the sour
- `sessiond` runs `npm run start:sessiond` as the long-lived owner of Pi agent runtimes; - `sessiond` runs `npm run start:sessiond` as the long-lived owner of Pi agent runtimes;
- `web` runs `npm run dev:web` and `npm run dev:client` so API, plugin, and Vite changes can autoreload without restarting `sessiond`. - `web` runs `npm run dev:web` and `npm run dev:client` so API, plugin, and Vite changes can autoreload without restarting `sessiond`.
From the repository root: From the repository root, use the dev Compose wrapper so the same fail-closed host profile detection is applied as runtime mode:
```bash ```bash
export PI_WEB_UID=$(id -u) ./docker/scripts/docker-compose-dev up --build
export PI_WEB_GID=$(id -g)
export DOCKER_GID=$(stat -c '%g' /var/run/docker.sock)
# Optional; this is also the default dev data path.
export PI_WEB_DOCKER_DATA_DIR=${PI_WEB_DOCKER_DATA_DIR:-$HOME/.local/share/pi-web-docker/data}
mkdir -p "$PI_WEB_DOCKER_DATA_DIR"
docker compose -f docker/compose.dev.yml up --build
``` ```
If you already ran the runtime installer, you can reuse its `.env` so dev mode gets the same UID/GID, Docker group, ports, and data directory: The wrapper creates `.pi-web/docker-compose-dev.local.env` on first run, writes `.pi-web/docker-compose-dev.generated.env` and `.pi-web/docker-compose-dev.host.generated.yml`, then runs Docker Compose with `docker/compose.dev.yml` plus that generated host override. Edit only the `.local.env` file for persistent dev settings; the `.generated.env` and `.host.generated.yml` files are refreshed by the wrapper.
Values used by the wrapper are resolved in this order:
1. current shell environment, for this run only;
2. `.pi-web/docker-compose-dev.local.env`;
3. runtime installer env, usually `$HOME/.local/share/pi-web-docker/.env`;
4. built-in defaults.
If you already ran the runtime installer, dev mode therefore reuses defaults such as UID/GID, Docker group, data directory, extra host paths, image build inputs, upload limit, and bind address unless you set a more specific value in the shell or `.local.env`. If an older `.pi-web/docker-compose-dev.env` exists, the first run copies its dev bind/port values into `.local.env` so previous local exposure settings are easy to see and edit.
To expose the dev API and Vite UI beyond localhost persistently, edit `.pi-web/docker-compose-dev.local.env`:
```dotenv
PI_WEB_DEV_API_BIND_ADDR=0.0.0.0
PI_WEB_DEV_BIND_ADDR=0.0.0.0
```
For temporary overrides, prefix the wrapper command:
```bash ```bash
docker compose --env-file "$HOME/.local/share/pi-web-docker/.env" \ PI_WEB_DEV_API_BIND_ADDR=0.0.0.0 \
-f docker/compose.dev.yml up --build PI_WEB_DEV_BIND_ADDR=0.0.0.0 \
./docker/scripts/docker-compose-dev up -d --build
```
You can run the dev stack in the background with:
```bash
./docker/scripts/docker-compose-dev up -d --build
``` ```
Open the Vite UI at <http://127.0.0.1:8505>. The dev API is published on <http://127.0.0.1:8504>. Open the Vite UI at <http://127.0.0.1:8505>. The dev API is published on <http://127.0.0.1:8504>.
@@ -257,16 +285,16 @@ Open the Vite UI at <http://127.0.0.1:8505>. The dev API is published on <http:/
Useful development commands: Useful development commands:
```bash ```bash
docker compose -f docker/compose.dev.yml ps ./docker/scripts/docker-compose-dev ps
docker compose -f docker/compose.dev.yml logs -f web ./docker/scripts/docker-compose-dev logs -f web
docker compose -f docker/compose.dev.yml restart web ./docker/scripts/docker-compose-dev restart web
docker compose -f docker/compose.dev.yml restart sessiond ./docker/scripts/docker-compose-dev restart sessiond
docker compose -f docker/compose.dev.yml down ./docker/scripts/docker-compose-dev down
``` ```
Restart `sessiond` manually after changes that affect `src/server/sessiond.ts`, daemon ownership, or session-daemon-only code paths. Restarting only `web` is enough for ordinary API/client/plugin development reloads. Restart `sessiond` manually after changes that affect `src/server/sessiond.ts`, daemon ownership, or session-daemon-only code paths. Restarting only `web` is enough for ordinary API/client/plugin development reloads.
The dev setup intentionally has the same Docker socket and broad host mounts as the runtime setup. The same trust warnings apply. The dev setup intentionally has the same Docker socket and profile-specific host mounts as the runtime setup. The same trust warnings apply.
On startup, a short `data-init` service creates the shared `/data` subdirectories and gives them to `PI_WEB_UID:PI_WEB_GID`. This handles the common Flatcar/Docker case where a missing bind-mount directory is created as root by the Docker daemon. Because the image also builds its `pi-web` account with those IDs, rebuild the image if you change `PI_WEB_UID` or `PI_WEB_GID`. On startup, a short `data-init` service creates the shared `/data` subdirectories and gives them to `PI_WEB_UID:PI_WEB_GID`. This handles the common Flatcar/Docker case where a missing bind-mount directory is created as root by the Docker daemon. Because the image also builds its `pi-web` account with those IDs, rebuild the image if you change `PI_WEB_UID` or `PI_WEB_GID`.
@@ -288,14 +316,14 @@ Set `PI_WEB_DOCKER_DATA_DIR=/some/path` for both modes if you want that shared d
Use this shared directory to switch between runtime and dev mode, not to run both at the same time. Stop one Compose stack before starting the other so two session daemons do not share the same socket/state directory concurrently. Use this shared directory to switch between runtime and dev mode, not to run both at the same time. Stop one Compose stack before starting the other so two session daemons do not share the same socket/state directory concurrently.
For sessions to appear under the same workspace in both modes, use the same project path in PI WEB. On Flatcar, prefer host-mounted paths such as `/home/core/<repo>`, `/srv/<project>`, or `/opt/<project>`. The dev container also exposes this checkout as `/workspace` so the PI WEB dev server can run from it, but sessions started against `/workspace` are organized under that different working-directory path and will not line up with runtime sessions for `/home/core/<repo>`. For sessions to appear under the same workspace in both modes, use the same project path in PI WEB. On Linux, prefer host-mounted paths such as `/home/core/<repo>`, `/srv/<project>`, or `/opt/<project>`. On Mac, prefer paths under `/Users/<you>/...`. The dev container also exposes this checkout as `/workspace` so the PI WEB dev server can run from it, but sessions started against `/workspace` are organized under that different working-directory path and will not line up with runtime sessions for the host-mounted path.
When `package-lock.json` changes, rebuild the dev image and recreate the `node_modules` volume so the bind-mounted checkout sees the new dependency tree: When `package-lock.json` changes, rebuild the dev image and recreate the `node_modules` volume so the bind-mounted checkout sees the new dependency tree:
```bash ```bash
docker compose -f docker/compose.dev.yml down ./docker/scripts/docker-compose-dev down
docker volume rm pi-web-dev_node_modules docker volume rm pi-web-dev_node_modules
docker compose -f docker/compose.dev.yml up --build ./docker/scripts/docker-compose-dev up --build
``` ```
## Local checkout validation ## Local checkout validation
@@ -309,11 +337,17 @@ PI_WEB_DOCKER_HOME="$(mktemp -d)" \
sh docker/install.sh sh docker/install.sh
``` ```
For Compose validation: For Compose validation after generating host overrides:
```bash ```bash
docker compose -f docker/compose.yml config tmp_home=$(mktemp -d)
docker compose -f docker/compose.dev.yml config PI_WEB_DOCKER_SKIP_COMPOSE=1 \
PI_WEB_DOCKER_ASSET_DIR="$PWD/docker" \
PI_WEB_DOCKER_HOME="$tmp_home" \
sh docker/install.sh
docker compose -f "$tmp_home/compose.yml" -f "$tmp_home/compose.override.yml" config
./docker/scripts/docker-compose-dev config
docker build --check -f docker/Dockerfile docker docker build --check -f docker/Dockerfile docker
docker build --check -f docker/Dockerfile.dev . docker build --check -f docker/Dockerfile.dev .
``` ```
+14
View File
@@ -38,6 +38,20 @@ if [ "$#" -eq 0 ]; then
exit 64 exit 64
fi fi
hostexec_mode="${HOSTEXEC_MODE:-nsenter}"
case "$hostexec_mode" in
nsenter) ;;
disabled|none)
echo "hostexec: disabled for this Docker host profile" >&2
echo "hostexec: on Docker Desktop for Mac, containers run inside a Linux VM and cannot enter native macOS namespaces" >&2
exit 69
;;
*)
echo "hostexec: unsupported HOSTEXEC_MODE: $hostexec_mode" >&2
exit 64
;;
esac
if ! command -v docker >/dev/null 2>&1; then if ! command -v docker >/dev/null 2>&1; then
echo "hostexec: docker CLI not found in this container" >&2 echo "hostexec: docker CLI not found in this container" >&2
exit 127 exit 127
+1 -16
View File
@@ -18,6 +18,7 @@ x-pi-web-dev-environment: &pi-web-dev-environment
PI_WEB_SESSIOND_SOCKET: /data/pi-web/sessiond.sock PI_WEB_SESSIOND_SOCKET: /data/pi-web/sessiond.sock
PI_CODING_AGENT_DIR: /data/pi-agent PI_CODING_AGENT_DIR: /data/pi-agent
HOSTEXEC_IMAGE: ${HOSTEXEC_IMAGE:-alpine:3.22} HOSTEXEC_IMAGE: ${HOSTEXEC_IMAGE:-alpine:3.22}
HOSTEXEC_MODE: ${HOSTEXEC_MODE:-disabled}
PI_WEB_MAX_UPLOAD_BYTES: ${PI_WEB_MAX_UPLOAD_BYTES:-67108864} PI_WEB_MAX_UPLOAD_BYTES: ${PI_WEB_MAX_UPLOAD_BYTES:-67108864}
NPM_CONFIG_UPDATE_NOTIFIER: "false" NPM_CONFIG_UPDATE_NOTIFIER: "false"
NPM_CONFIG_CACHE: /data/npm-cache NPM_CONFIG_CACHE: /data/npm-cache
@@ -35,22 +36,6 @@ x-pi-web-dev-volumes: &pi-web-dev-volumes
source: node_modules source: node_modules
target: /workspace/node_modules target: /workspace/node_modules
- *pi-web-dev-data-volume - *pi-web-dev-data-volume
- type: bind
source: /var/run/docker.sock
target: /var/run/docker.sock
- type: bind
source: /srv
target: /srv
- type: bind
source: /opt
target: /opt
- type: bind
source: /home
target: /home
- type: bind
source: /
target: /host
read_only: true
services: services:
data-init: data-init:
+1 -16
View File
@@ -21,28 +21,13 @@ x-pi-web-environment: &pi-web-environment
PI_WEB_SESSIOND_SOCKET: /data/pi-web/sessiond.sock PI_WEB_SESSIOND_SOCKET: /data/pi-web/sessiond.sock
PI_CODING_AGENT_DIR: /data/pi-agent PI_CODING_AGENT_DIR: /data/pi-agent
HOSTEXEC_IMAGE: ${HOSTEXEC_IMAGE:-alpine:3.22} HOSTEXEC_IMAGE: ${HOSTEXEC_IMAGE:-alpine:3.22}
HOSTEXEC_MODE: ${HOSTEXEC_MODE:-disabled}
PI_WEB_MAX_UPLOAD_BYTES: ${PI_WEB_MAX_UPLOAD_BYTES:-67108864} PI_WEB_MAX_UPLOAD_BYTES: ${PI_WEB_MAX_UPLOAD_BYTES:-67108864}
x-pi-web-volumes: &pi-web-volumes x-pi-web-volumes: &pi-web-volumes
- type: bind - type: bind
source: ${PI_WEB_DOCKER_DATA_DIR:-./data} source: ${PI_WEB_DOCKER_DATA_DIR:-./data}
target: /data target: /data
- type: bind
source: /var/run/docker.sock
target: /var/run/docker.sock
- type: bind
source: /srv
target: /srv
- type: bind
source: /opt
target: /opt
- type: bind
source: /home
target: /home
- type: bind
source: /
target: /host
read_only: true
services: services:
sessiond: sessiond:
+65 -35
View File
@@ -39,6 +39,12 @@ Options:
--skip-compose Write assets/.env but skip build and service recreate --skip-compose Write assets/.env but skip build and service recreate
-h, --help Show this help -h, --help Show this help
Progressive host setup:
The installer supports native Linux Docker Engine and Docker Desktop for Mac.
Unknown Docker hosts fail closed before services are recreated. Set
PI_WEB_DOCKER_EXTRA_HOST_PATHS to a whitespace-separated list of additional
existing absolute directories to bind-mount at the same path in the containers.
Environment variables with the same names used in .env may also be set before Environment variables with the same names used in .env may also be set before
running the installer, for example: running the installer, for example:
@@ -223,30 +229,6 @@ dotenv_quote() {
printf '"%s"' "$(printf '%s' "$value" | sed 's/[\\"]/\\&/g')" printf '"%s"' "$(printf '%s' "$value" | sed 's/[\\"]/\\&/g')"
} }
detect_docker_gid() {
if [ -S /var/run/docker.sock ]; then
if gid=$(stat -c '%g' /var/run/docker.sock 2>/dev/null); then
printf '%s\n' "$gid"
return 0
fi
if gid=$(stat -f '%g' /var/run/docker.sock 2>/dev/null); then
printf '%s\n' "$gid"
return 0
fi
fi
if command -v getent >/dev/null 2>&1; then
if gid=$(getent group docker | awk -F: 'NR == 1 { print $3 }'); then
if [ -n "$gid" ]; then
printf '%s\n' "$gid"
return 0
fi
fi
fi
printf '0\n'
}
fetch_url() { fetch_url() {
url=$1 url=$1
target=$2 target=$2
@@ -292,13 +274,11 @@ write_asset() {
} }
compose_cmd() { compose_cmd() {
if docker compose version >/dev/null 2>&1; then pi_web_docker_compose "$@"
docker compose "$@" }
elif command -v docker-compose >/dev/null 2>&1; then
docker-compose "$@" run_runtime_compose() {
else compose_cmd -f compose.yml -f compose.override.yml "$@"
die "Docker Compose is required (docker compose plugin or docker-compose)"
fi
} }
if [ -n "${XDG_DATA_HOME:-}" ]; then if [ -n "${XDG_DATA_HOME:-}" ]; then
@@ -332,12 +312,37 @@ else
log "Fetching Docker assets from $asset_base" log "Fetching Docker assets from $asset_base"
fi fi
profile_helper_temp=
cleanup_profile_helper() {
[ -z "$profile_helper_temp" ] || rm -f "$profile_helper_temp"
}
trap cleanup_profile_helper EXIT
if [ -n "$asset_dir" ]; then
profile_helper=$asset_dir/lib/host-profile.sh
[ -f "$profile_helper" ] || die "missing Docker asset: $profile_helper"
else
profile_helper_temp=${TMPDIR:-/tmp}/pi-web-host-profile.$$
fetch_url "$asset_base/lib/host-profile.sh" "$profile_helper_temp"
profile_helper=$profile_helper_temp
fi
# shellcheck source=lib/host-profile.sh
# shellcheck disable=SC1091
. "$profile_helper"
if ! pi_web_docker_host_detect_profile; then
pi_web_docker_host_print_detection_failure
die "refusing to install on an unsupported or unknown Docker host setup"
fi
write_asset Dockerfile 0644 write_asset Dockerfile 0644
write_asset compose.yml 0644 write_asset compose.yml 0644
write_asset .dockerignore 0644 write_asset .dockerignore 0644
write_asset install.sh 0755 write_asset install.sh 0755
write_asset bin/hostexec 0755 write_asset bin/hostexec 0755
write_asset bin/install-opensuse-base 0755 write_asset bin/install-opensuse-base 0755
write_asset lib/host-profile.sh 0644
custom_image_hooks_dir=$install_dir/custom-image.d custom_image_hooks_dir=$install_dir/custom-image.d
mkdir -p "$custom_image_hooks_dir" || die "could not create custom image hooks directory: $custom_image_hooks_dir" mkdir -p "$custom_image_hooks_dir" || die "could not create custom image hooks directory: $custom_image_hooks_dir"
@@ -347,7 +352,9 @@ fi
pi_web_uid=$(value_from_env_or_default PI_WEB_UID "$(id -u)") pi_web_uid=$(value_from_env_or_default PI_WEB_UID "$(id -u)")
pi_web_gid=$(value_from_env_or_default PI_WEB_GID "$(id -g)") pi_web_gid=$(value_from_env_or_default PI_WEB_GID "$(id -g)")
docker_gid=$(value_from_env_or_default DOCKER_GID "$(detect_docker_gid)") docker_gid=$(value_from_env_or_default DOCKER_GID "$(pi_web_docker_host_detect_docker_gid)")
pi_web_host_profile=$PI_WEB_DETECTED_DOCKER_HOST_PROFILE
hostexec_mode=$PI_WEB_DETECTED_HOSTEXEC_MODE
raw_data_dir=$(value_from_env_or_existing_or_default PI_WEB_DOCKER_DATA_DIR "$install_dir/data") raw_data_dir=$(value_from_env_or_existing_or_default PI_WEB_DOCKER_DATA_DIR "$install_dir/data")
data_dir=$(absolute_dir "$(path_from_base "$install_dir" "$raw_data_dir")") || die "could not create data directory" data_dir=$(absolute_dir "$(path_from_base "$install_dir" "$raw_data_dir")") || die "could not create data directory"
@@ -363,10 +370,13 @@ pi_web_extra_zypper_packages=$(value_from_env_or_existing_or_default PI_WEB_EXTR
pi_web_image=$(value_from_env_or_existing_or_default PI_WEB_IMAGE pi-web:local) pi_web_image=$(value_from_env_or_existing_or_default PI_WEB_IMAGE pi-web:local)
hostexec_image=$(value_from_env_or_existing_or_default HOSTEXEC_IMAGE alpine:3.22) hostexec_image=$(value_from_env_or_existing_or_default HOSTEXEC_IMAGE alpine:3.22)
pi_web_max_upload_bytes=$(value_from_env_or_existing_or_default PI_WEB_MAX_UPLOAD_BYTES 67108864) pi_web_max_upload_bytes=$(value_from_env_or_existing_or_default PI_WEB_MAX_UPLOAD_BYTES 67108864)
pi_web_extra_host_paths=$(value_from_env_or_existing_or_default PI_WEB_DOCKER_EXTRA_HOST_PATHS "")
require_non_empty PI_WEB_UID "$pi_web_uid" require_non_empty PI_WEB_UID "$pi_web_uid"
require_non_empty PI_WEB_GID "$pi_web_gid" require_non_empty PI_WEB_GID "$pi_web_gid"
require_non_empty DOCKER_GID "$docker_gid" require_non_empty DOCKER_GID "$docker_gid"
require_non_empty PI_WEB_DOCKER_HOST_PROFILE "$pi_web_host_profile"
require_non_empty HOSTEXEC_MODE "$hostexec_mode"
require_non_empty PI_WEB_DOCKER_DATA_DIR "$data_dir" require_non_empty PI_WEB_DOCKER_DATA_DIR "$data_dir"
require_non_empty PI_WEB_BIND_ADDR "$pi_web_bind_addr" require_non_empty PI_WEB_BIND_ADDR "$pi_web_bind_addr"
require_non_empty PI_WEB_PORT "$pi_web_port" require_non_empty PI_WEB_PORT "$pi_web_port"
@@ -380,6 +390,11 @@ require_non_empty HOSTEXEC_IMAGE "$hostexec_image"
require_non_empty PI_WEB_MAX_UPLOAD_BYTES "$pi_web_max_upload_bytes" require_non_empty PI_WEB_MAX_UPLOAD_BYTES "$pi_web_max_upload_bytes"
pi_web_extra_zypper_packages_env=$(dotenv_quote "$pi_web_extra_zypper_packages") pi_web_extra_zypper_packages_env=$(dotenv_quote "$pi_web_extra_zypper_packages")
pi_web_extra_host_paths_env=$(dotenv_quote "$pi_web_extra_host_paths")
compose_override_file=$install_dir/compose.override.yml
if ! pi_web_docker_host_write_compose_override "$compose_override_file" "$pi_web_host_profile" "$pi_web_extra_host_paths"; then
die "could not write host-specific Compose override"
fi
umask 077 umask 077
temp_env=$env_file.$$ temp_env=$env_file.$$
@@ -393,6 +408,11 @@ PI_WEB_UID=$pi_web_uid
PI_WEB_GID=$pi_web_gid PI_WEB_GID=$pi_web_gid
DOCKER_GID=$docker_gid DOCKER_GID=$docker_gid
# Detected Docker host profile and host capability toggles.
PI_WEB_DOCKER_HOST_PROFILE=$pi_web_host_profile
HOSTEXEC_MODE=$hostexec_mode
PI_WEB_DOCKER_EXTRA_HOST_PATHS=$pi_web_extra_host_paths_env
# Persistent data and localhost-only default exposure. # Persistent data and localhost-only default exposure.
PI_WEB_DOCKER_DATA_DIR=$data_dir PI_WEB_DOCKER_DATA_DIR=$data_dir
PI_WEB_BIND_ADDR=$pi_web_bind_addr PI_WEB_BIND_ADDR=$pi_web_bind_addr
@@ -417,6 +437,16 @@ mv "$temp_env" "$env_file"
log "Wrote Docker assets to $install_dir" log "Wrote Docker assets to $install_dir"
log "Wrote runtime environment to $env_file" log "Wrote runtime environment to $env_file"
log "Wrote host Compose override to $compose_override_file"
log "Selected PI WEB Docker host profile: $pi_web_host_profile"
case "$pi_web_host_profile" in
linux-native-docker)
log "Enabled Linux host mounts and hostexec namespace bridge."
;;
mac-docker-desktop)
log "Enabled Docker Desktop for Mac project mounts. hostexec is disabled because containers cannot enter native macOS namespaces."
;;
esac
log "Persistent PI WEB Docker data: $data_dir" log "Persistent PI WEB Docker data: $data_dir"
log "Custom image hooks: $custom_image_hooks_dir" log "Custom image hooks: $custom_image_hooks_dir"
@@ -443,13 +473,13 @@ log ""
log "Building $pi_web_image with --pull --no-cache (CACHE_BUST=$cache_bust) ..." log "Building $pi_web_image with --pull --no-cache (CACHE_BUST=$cache_bust) ..."
( (
cd "$install_dir" cd "$install_dir"
CACHE_BUST=$cache_bust compose_cmd -f compose.yml build --pull --no-cache CACHE_BUST=$cache_bust run_runtime_compose build --pull --no-cache
) )
log "Recreating split PI WEB Docker services ..." log "Recreating split PI WEB Docker services ..."
( (
cd "$install_dir" cd "$install_dir"
compose_cmd -f compose.yml up -d --force-recreate --remove-orphans run_runtime_compose up -d --force-recreate --remove-orphans
) )
log "" log ""
@@ -458,5 +488,5 @@ log "Install directory: $install_dir"
log "To update later, re-run this installer." log "To update later, re-run this installer."
( (
cd "$install_dir" cd "$install_dir"
compose_cmd -f compose.yml ps run_runtime_compose ps
) )
+355
View File
@@ -0,0 +1,355 @@
#!/usr/bin/env sh
# shellcheck disable=SC2034
pi_web_docker_host_yaml_quote() {
value=$1
escaped=$(printf '%s' "$value" | sed "s/'/''/g")
printf "'%s'" "$escaped"
}
pi_web_docker_host_socket_path_from_endpoint() {
endpoint=$1
case "$endpoint" in
unix://*) printf '%s\n' "${endpoint#unix://}" ;;
*) return 1 ;;
esac
}
pi_web_docker_host_mac_desktop_socket_path() {
[ -n "${HOME:-}" ] || return 1
printf '%s/.docker/run/docker.sock\n' "$HOME"
}
pi_web_docker_host_endpoint_is_linux_expected() {
endpoint=$1
[ "$endpoint" = unix:///var/run/docker.sock ]
}
pi_web_docker_host_endpoint_is_mac_expected() {
endpoint=$1
if ! socket_path=$(pi_web_docker_host_socket_path_from_endpoint "$endpoint" 2>/dev/null); then
return 1
fi
case "$socket_path" in
/var/run/docker.sock)
return 0
;;
esac
if mac_socket_path=$(pi_web_docker_host_mac_desktop_socket_path 2>/dev/null); then
[ "$socket_path" = "$mac_socket_path" ] && return 0
fi
return 1
}
pi_web_docker_host_socket_source_for_endpoint() {
endpoint=$1
pi_web_docker_host_socket_path_from_endpoint "$endpoint"
}
pi_web_docker_host_detect_docker_gid() {
case "${PI_WEB_DETECTED_DOCKER_HOST_PROFILE:-}" in
mac-docker-desktop)
printf '0\n'
return 0
;;
esac
socket_path=/var/run/docker.sock
if [ -n "${PI_WEB_DETECTED_DOCKER_ENDPOINT:-}" ]; then
if detected_socket_path=$(pi_web_docker_host_socket_path_from_endpoint "$PI_WEB_DETECTED_DOCKER_ENDPOINT" 2>/dev/null); then
socket_path=$detected_socket_path
fi
fi
if [ -S "$socket_path" ]; then
if gid=$(stat -c '%g' "$socket_path" 2>/dev/null); then
printf '%s\n' "$gid"
return 0
fi
if gid=$(stat -f '%g' "$socket_path" 2>/dev/null); then
printf '%s\n' "$gid"
return 0
fi
fi
if [ -S /var/run/docker.sock ]; then
if gid=$(stat -c '%g' /var/run/docker.sock 2>/dev/null); then
printf '%s\n' "$gid"
return 0
fi
if gid=$(stat -f '%g' /var/run/docker.sock 2>/dev/null); then
printf '%s\n' "$gid"
return 0
fi
fi
if command -v getent >/dev/null 2>&1; then
if gid=$(getent group docker | awk -F: 'NR == 1 { print $3 }'); then
if [ -n "$gid" ]; then
printf '%s\n' "$gid"
return 0
fi
fi
fi
printf '0\n'
}
pi_web_docker_host_detect_profile() {
PI_WEB_DETECTED_HOST_OS=$(uname -s 2>/dev/null || printf 'unknown')
PI_WEB_DETECTED_DOCKER_CONTEXT=
PI_WEB_DETECTED_DOCKER_ENDPOINT=
PI_WEB_DETECTED_DOCKER_HOST_ENV=${DOCKER_HOST:-}
PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT=
PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE=
PI_WEB_DETECTED_DOCKER_OS=
PI_WEB_DETECTED_DOCKER_HOST_PROFILE=
PI_WEB_DETECTED_HOSTEXEC_MODE=disabled
PI_WEB_DOCKER_HOST_PROFILE_ERROR=
if ! command -v docker >/dev/null 2>&1; then
PI_WEB_DOCKER_HOST_PROFILE_ERROR="docker CLI is required"
return 1
fi
PI_WEB_DETECTED_DOCKER_CONTEXT=$(docker context show 2>/dev/null || printf 'unknown')
if [ -n "$PI_WEB_DETECTED_DOCKER_CONTEXT" ] && [ "$PI_WEB_DETECTED_DOCKER_CONTEXT" != unknown ]; then
PI_WEB_DETECTED_DOCKER_ENDPOINT=$(docker context inspect "$PI_WEB_DETECTED_DOCKER_CONTEXT" --format '{{if .Endpoints.docker}}{{.Endpoints.docker.Host}}{{end}}' 2>/dev/null || printf '')
fi
case "$PI_WEB_DETECTED_HOST_OS" in
Linux)
if [ -n "$PI_WEB_DETECTED_DOCKER_HOST_ENV" ] && ! pi_web_docker_host_endpoint_is_linux_expected "$PI_WEB_DETECTED_DOCKER_HOST_ENV"; then
PI_WEB_DOCKER_HOST_PROFILE_ERROR="native Linux installs require DOCKER_HOST to be unset or exactly unix:///var/run/docker.sock, not $PI_WEB_DETECTED_DOCKER_HOST_ENV"
return 1
fi
if [ -n "$PI_WEB_DETECTED_DOCKER_ENDPOINT" ] && ! pi_web_docker_host_endpoint_is_linux_expected "$PI_WEB_DETECTED_DOCKER_ENDPOINT"; then
PI_WEB_DOCKER_HOST_PROFILE_ERROR="native Linux installs require the local /var/run/docker.sock Docker context, not $PI_WEB_DETECTED_DOCKER_ENDPOINT"
return 1
fi
PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT=${PI_WEB_DETECTED_DOCKER_HOST_ENV:-$PI_WEB_DETECTED_DOCKER_ENDPOINT}
PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE=/var/run/docker.sock
if [ ! -S "$PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE" ]; then
PI_WEB_DOCKER_HOST_PROFILE_ERROR="native Linux installs require a local Docker socket at /var/run/docker.sock"
return 1
fi
;;
Darwin)
if [ -n "$PI_WEB_DETECTED_DOCKER_ENDPOINT" ] && ! pi_web_docker_host_endpoint_is_mac_expected "$PI_WEB_DETECTED_DOCKER_ENDPOINT"; then
PI_WEB_DOCKER_HOST_PROFILE_ERROR="macOS installs require a Docker Desktop local Unix socket context, not $PI_WEB_DETECTED_DOCKER_ENDPOINT"
return 1
fi
if [ -n "$PI_WEB_DETECTED_DOCKER_HOST_ENV" ]; then
if ! pi_web_docker_host_endpoint_is_mac_expected "$PI_WEB_DETECTED_DOCKER_HOST_ENV"; then
PI_WEB_DOCKER_HOST_PROFILE_ERROR="macOS installs require DOCKER_HOST to be unset or a Docker Desktop local Unix socket, not $PI_WEB_DETECTED_DOCKER_HOST_ENV"
return 1
fi
PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT=$PI_WEB_DETECTED_DOCKER_HOST_ENV
else
PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT=$PI_WEB_DETECTED_DOCKER_ENDPOINT
fi
if [ -n "$PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT" ]; then
if ! pi_web_docker_host_endpoint_is_mac_expected "$PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT"; then
PI_WEB_DOCKER_HOST_PROFILE_ERROR="macOS installs require a Docker Desktop local Unix socket, not ${PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT:-unknown}"
return 1
fi
PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE=$(pi_web_docker_host_socket_source_for_endpoint "$PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT") || return 1
elif mac_socket_path=$(pi_web_docker_host_mac_desktop_socket_path 2>/dev/null) && [ -S "$mac_socket_path" ]; then
PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE=$mac_socket_path
else
PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE=/var/run/docker.sock
fi
if [ ! -S "$PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE" ]; then
PI_WEB_DOCKER_HOST_PROFILE_ERROR="Docker Desktop socket is not accessible at $PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE"
return 1
fi
;;
*)
PI_WEB_DOCKER_HOST_PROFILE_ERROR="unsupported host OS: $PI_WEB_DETECTED_HOST_OS"
return 1
;;
esac
if ! docker info >/dev/null 2>&1; then
PI_WEB_DOCKER_HOST_PROFILE_ERROR="docker daemon is not reachable by this user"
return 1
fi
PI_WEB_DETECTED_DOCKER_OS=$(docker info --format '{{.OperatingSystem}}' 2>/dev/null || printf '')
case "$PI_WEB_DETECTED_HOST_OS" in
Linux)
case "$PI_WEB_DETECTED_DOCKER_CONTEXT:$PI_WEB_DETECTED_DOCKER_OS" in
*desktop-linux*|*"Docker Desktop"*)
PI_WEB_DOCKER_HOST_PROFILE_ERROR="Docker Desktop on Linux is not supported by this installer because it runs containers inside a VM instead of the native Linux host"
return 1
;;
esac
PI_WEB_DETECTED_DOCKER_HOST_PROFILE=linux-native-docker
PI_WEB_DETECTED_HOSTEXEC_MODE=nsenter
;;
Darwin)
case "$PI_WEB_DETECTED_DOCKER_CONTEXT:$PI_WEB_DETECTED_DOCKER_OS:$PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT" in
*desktop-linux*|*"Docker Desktop"*|*"/.docker/run/docker.sock"*)
PI_WEB_DETECTED_DOCKER_HOST_PROFILE=mac-docker-desktop
PI_WEB_DETECTED_HOSTEXEC_MODE=disabled
;;
*)
PI_WEB_DOCKER_HOST_PROFILE_ERROR="macOS installs currently require Docker Desktop; detected context '$PI_WEB_DETECTED_DOCKER_CONTEXT' endpoint '${PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT:-unknown}'"
return 1
;;
esac
;;
esac
return 0
}
pi_web_docker_host_write_volume() {
source_path=$1
target_path=$2
read_only=${3:-false}
{
printf ' - type: bind\n'
printf ' source: %s\n' "$(pi_web_docker_host_yaml_quote "$source_path")"
printf ' target: %s\n' "$(pi_web_docker_host_yaml_quote "$target_path")"
if [ "$read_only" = true ]; then
printf ' read_only: true\n'
fi
} >>"$PI_WEB_DOCKER_HOST_OVERRIDE_TEMP"
}
pi_web_docker_host_write_existing_volume() {
source_path=$1
target_path=$2
read_only=${3:-false}
if [ -e "$source_path" ]; then
pi_web_docker_host_write_volume "$source_path" "$target_path" "$read_only"
fi
}
pi_web_docker_host_write_extra_volumes() {
extra_paths=$1
for extra_path in $extra_paths; do
case "$extra_path" in
/*) ;;
*)
printf '%s\n' "PI_WEB_DOCKER_EXTRA_HOST_PATHS entries must be absolute paths: $extra_path" >&2
return 1
;;
esac
if [ ! -e "$extra_path" ]; then
printf '%s\n' "PI_WEB_DOCKER_EXTRA_HOST_PATHS entry does not exist: $extra_path" >&2
return 1
fi
pi_web_docker_host_write_volume "$extra_path" "$extra_path" false
done
}
pi_web_docker_host_write_compose_override() {
target_file=$1
host_profile=$2
extra_paths=${3:-}
target_dir=$(dirname "$target_file")
mkdir -p "$target_dir" || return 1
PI_WEB_DOCKER_HOST_OVERRIDE_TEMP=$target_file.$$
case "$host_profile" in
linux-native-docker) hostexec_mode=nsenter ;;
mac-docker-desktop) hostexec_mode=disabled ;;
*)
printf '%s\n' "unsupported PI WEB Docker host profile: $host_profile" >&2
return 1
;;
esac
cat >"$PI_WEB_DOCKER_HOST_OVERRIDE_TEMP" <<EOF
# Generated by PI WEB Docker host profile detection. Do not edit by hand.
# Re-run the installer or docker/scripts/docker-compose-dev to refresh this file.
x-pi-web-host-volumes: &pi-web-host-volumes
EOF
socket_source=${PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE:-/var/run/docker.sock}
pi_web_docker_host_write_volume "$socket_source" /var/run/docker.sock false
case "$host_profile" in
linux-native-docker)
pi_web_docker_host_write_existing_volume /home /home false
pi_web_docker_host_write_existing_volume /srv /srv false
pi_web_docker_host_write_existing_volume /opt /opt false
pi_web_docker_host_write_volume / /host true
;;
mac-docker-desktop)
pi_web_docker_host_write_existing_volume /Users /Users false
pi_web_docker_host_write_existing_volume /Volumes /Volumes false
pi_web_docker_host_write_existing_volume /private /private false
;;
esac
if ! pi_web_docker_host_write_extra_volumes "$extra_paths"; then
rm -f "$PI_WEB_DOCKER_HOST_OVERRIDE_TEMP"
return 1
fi
cat >>"$PI_WEB_DOCKER_HOST_OVERRIDE_TEMP" <<EOF
services:
sessiond:
environment:
HOSTEXEC_MODE: $hostexec_mode
volumes: *pi-web-host-volumes
web:
environment:
HOSTEXEC_MODE: $hostexec_mode
volumes: *pi-web-host-volumes
EOF
mv "$PI_WEB_DOCKER_HOST_OVERRIDE_TEMP" "$target_file"
}
pi_web_docker_host_print_detection_failure() {
printf '%s\n' "PI WEB Docker setup could not determine a supported host profile." >&2
printf '%s\n' "" >&2
printf '%s\n' "Detected:" >&2
printf ' host OS: %s\n' "${PI_WEB_DETECTED_HOST_OS:-unknown}" >&2
printf ' docker context: %s\n' "${PI_WEB_DETECTED_DOCKER_CONTEXT:-unknown}" >&2
printf ' docker endpoint: %s\n' "${PI_WEB_DETECTED_DOCKER_ENDPOINT:-unknown}" >&2
printf ' DOCKER_HOST: %s\n' "${PI_WEB_DETECTED_DOCKER_HOST_ENV:-unset}" >&2
printf ' effective endpoint: %s\n' "${PI_WEB_DETECTED_DOCKER_EFFECTIVE_ENDPOINT:-unknown}" >&2
printf ' docker socket source: %s\n' "${PI_WEB_DETECTED_DOCKER_SOCKET_SOURCE:-unknown}" >&2
printf ' docker OS: %s\n' "${PI_WEB_DETECTED_DOCKER_OS:-unknown}" >&2
printf '%s\n' "" >&2
printf '%s\n' "Supported profiles:" >&2
printf '%s\n' " - native Linux Docker Engine using /var/run/docker.sock" >&2
printf '%s\n' " - Docker Desktop for Mac" >&2
if [ -n "${PI_WEB_DOCKER_HOST_PROFILE_ERROR:-}" ]; then
printf '%s\n' "" >&2
printf 'Reason: %s\n' "$PI_WEB_DOCKER_HOST_PROFILE_ERROR" >&2
fi
}
pi_web_docker_compose() {
if docker compose version >/dev/null 2>&1; then
docker compose "$@"
elif command -v docker-compose >/dev/null 2>&1; then
docker-compose "$@"
else
printf '%s\n' "Docker Compose is required (docker compose plugin or docker-compose)" >&2
return 1
fi
}
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env sh
set -eu
log() {
printf '%s\n' "$*" >&2
}
die() {
log "pi-web Docker dev compose: $*"
exit 1
}
script_dir=$(unset CDPATH; cd "$(dirname "$0")" && pwd -P)
repo_root=$(unset CDPATH; cd "$script_dir/../.." && pwd -P)
dev_config_file=$repo_root/.pi-web/docker-compose-dev.local.env
legacy_dev_env_file=$repo_root/.pi-web/docker-compose-dev.env
generated_env_file=$repo_root/.pi-web/docker-compose-dev.generated.env
# shellcheck source=../lib/host-profile.sh
# shellcheck disable=SC1091
. "$repo_root/docker/lib/host-profile.sh"
strip_wrapping_quotes() {
value=$1
case "$value" in
\"*\")
case "$value" in
*\") value=${value#\"}; value=${value%\"} ;;
esac
;;
\'*\')
case "$value" in
*\') value=${value#\'}; value=${value%\'} ;;
esac
;;
esac
printf '%s\n' "$value"
}
env_file_value() {
file=$1
key=$2
[ -f "$file" ] || return 1
raw=$(awk -v key="$key" '
function trim(value) {
sub(/^[ \t]+/, "", value)
sub(/[ \t\r]+$/, "", value)
return value
}
/^[ \t]*(#|$)/ { next }
{
line = $0
sub(/^[ \t]*export[ \t]+/, "", line)
name = line
sub(/=.*/, "", name)
name = trim(name)
if (name == key) {
sub(/^[^=]*=/, "", line)
print trim(line)
found = 1
exit
}
}
END { if (!found) exit 1 }
' "$file") || return 1
strip_wrapping_quotes "$raw"
}
dev_config_value() {
env_file_value "$dev_config_file" "$1"
}
runtime_env_value() {
env_file_value "$runtime_env_file" "$1"
}
write_initial_dev_config() {
[ ! -e "$dev_config_file" ] || return 0
temp_config=$dev_config_file.$$
previous_umask=$(umask)
umask 077
cat >"$temp_config" <<'EOF'
# PI WEB Docker dev settings. Safe to edit.
#
# docker/scripts/docker-compose-dev creates this file once and does not
# overwrite it. Put persistent dev Docker settings here.
#
# Precedence for values used by the wrapper:
# 1. current shell environment
# 2. this file
# 3. runtime installer env, usually ~/.local/share/pi-web-docker/.env
# 4. built-in defaults
#
# Generated effective values are written to:
# .pi-web/docker-compose-dev.generated.env
#
# Bind addresses:
# - 127.0.0.1 exposes only to this machine.
# - 0.0.0.0 exposes on all host interfaces. Use only on trusted networks.
#
# Uncomment or add values to persist them. PI_WEB_DEV_API_BIND_ADDR
# controls the web/API server; PI_WEB_DEV_BIND_ADDR controls the Vite UI.
# PI_WEB_DEV_API_BIND_ADDR=127.0.0.1
# PI_WEB_DEV_BIND_ADDR=127.0.0.1
# PI_WEB_DEV_API_PORT=8504
# PI_WEB_DEV_PORT=8505
#
# Shared Docker/runtime-style defaults may also be set here:
# PI_WEB_DOCKER_DATA_DIR=/absolute/path/to/pi-web-docker/data
# PI_WEB_DOCKER_EXTRA_HOST_PATHS="/absolute/path/one /absolute/path/two"
EOF
umask "$previous_umask"
if [ -f "$legacy_dev_env_file" ]; then
{
printf '\n%s\n' "# Values copied from the previous generated dev env file."
printf '%s\n' "# Keep, edit, or delete these lines as needed."
for key in PI_WEB_DEV_API_BIND_ADDR PI_WEB_DEV_BIND_ADDR PI_WEB_DEV_API_PORT PI_WEB_DEV_PORT; do
if value=$(env_file_value "$legacy_dev_env_file" "$key"); then
printf '%s=%s\n' "$key" "$value"
fi
done
} >>"$temp_config"
fi
mv "$temp_config" "$dev_config_file"
log "Created user-editable dev config: $dev_config_file"
}
value_from_env_or_config_or_runtime_or_default() {
key=$1
default_value=$2
eval "is_set=\${$key+x}"
if [ "${is_set:-}" = x ]; then
eval "printf '%s\n' \"\${$key}\""
elif existing=$(dev_config_value "$key"); then
printf '%s\n' "$existing"
elif existing=$(runtime_env_value "$key"); then
printf '%s\n' "$existing"
else
printf '%s\n' "$default_value"
fi
}
if ! pi_web_docker_host_detect_profile; then
pi_web_docker_host_print_detection_failure
die "refusing to run Docker Compose for an unsupported or unknown host setup"
fi
runtime_env_file=${PI_WEB_DOCKER_RUNTIME_ENV_FILE:-}
if [ -z "$runtime_env_file" ] && [ -n "${HOME:-}" ]; then
runtime_env_file=$HOME/.local/share/pi-web-docker/.env
fi
mkdir -p "$repo_root/.pi-web" || die "could not create .pi-web directory"
write_initial_dev_config
pi_web_uid=$(value_from_env_or_config_or_runtime_or_default PI_WEB_UID "$(id -u)")
pi_web_gid=$(value_from_env_or_config_or_runtime_or_default PI_WEB_GID "$(id -g)")
docker_gid=$(value_from_env_or_config_or_runtime_or_default DOCKER_GID "$(pi_web_docker_host_detect_docker_gid)")
default_data_dir=${HOME:-$repo_root/.pi-web}/.local/share/pi-web-docker/data
pi_web_data_dir=$(value_from_env_or_config_or_runtime_or_default PI_WEB_DOCKER_DATA_DIR "$default_data_dir")
pi_web_extra_host_paths=$(value_from_env_or_config_or_runtime_or_default PI_WEB_DOCKER_EXTRA_HOST_PATHS "")
pi_web_opensuse_image=$(value_from_env_or_config_or_runtime_or_default PI_WEB_OPENSUSE_IMAGE opensuse/tumbleweed)
pi_web_nodejs_major=$(value_from_env_or_config_or_runtime_or_default PI_WEB_NODEJS_MAJOR 22)
pi_web_nodejs_repo=$(value_from_env_or_config_or_runtime_or_default PI_WEB_NODEJS_REPO auto)
pi_web_extra_zypper_packages=$(value_from_env_or_config_or_runtime_or_default PI_WEB_EXTRA_ZYPPER_PACKAGES "")
pi_web_dev_image=$(value_from_env_or_config_or_runtime_or_default PI_WEB_DEV_IMAGE pi-web:dev)
hostexec_image=$(value_from_env_or_config_or_runtime_or_default HOSTEXEC_IMAGE alpine:3.22)
pi_web_max_upload_bytes=$(value_from_env_or_config_or_runtime_or_default PI_WEB_MAX_UPLOAD_BYTES 67108864)
default_dev_bind_addr=$(value_from_env_or_config_or_runtime_or_default PI_WEB_BIND_ADDR 127.0.0.1)
pi_web_dev_api_bind_addr=$(value_from_env_or_config_or_runtime_or_default PI_WEB_DEV_API_BIND_ADDR "$default_dev_bind_addr")
pi_web_dev_bind_addr=$(value_from_env_or_config_or_runtime_or_default PI_WEB_DEV_BIND_ADDR "$default_dev_bind_addr")
pi_web_dev_api_port=$(value_from_env_or_config_or_runtime_or_default PI_WEB_DEV_API_PORT 8504)
pi_web_dev_port=$(value_from_env_or_config_or_runtime_or_default PI_WEB_DEV_PORT 8505)
mkdir -p "$pi_web_data_dir" || die "could not create data directory: $pi_web_data_dir"
env_file=$generated_env_file
override_file=$repo_root/.pi-web/docker-compose-dev.host.generated.yml
if ! pi_web_docker_host_write_compose_override "$override_file" "$PI_WEB_DETECTED_DOCKER_HOST_PROFILE" "$pi_web_extra_host_paths"; then
die "could not write host-specific Compose override"
fi
umask 077
temp_env=$env_file.$$
cat >"$temp_env" <<EOF
# Generated by docker/scripts/docker-compose-dev. Do not edit by hand.
PI_WEB_UID=$pi_web_uid
PI_WEB_GID=$pi_web_gid
DOCKER_GID=$docker_gid
PI_WEB_DOCKER_DATA_DIR=$pi_web_data_dir
PI_WEB_DOCKER_HOST_PROFILE=$PI_WEB_DETECTED_DOCKER_HOST_PROFILE
HOSTEXEC_MODE=$PI_WEB_DETECTED_HOSTEXEC_MODE
PI_WEB_DOCKER_EXTRA_HOST_PATHS=$pi_web_extra_host_paths
PI_WEB_OPENSUSE_IMAGE=$pi_web_opensuse_image
PI_WEB_NODEJS_MAJOR=$pi_web_nodejs_major
PI_WEB_NODEJS_REPO=$pi_web_nodejs_repo
PI_WEB_EXTRA_ZYPPER_PACKAGES=$pi_web_extra_zypper_packages
PI_WEB_DEV_IMAGE=$pi_web_dev_image
HOSTEXEC_IMAGE=$hostexec_image
PI_WEB_MAX_UPLOAD_BYTES=$pi_web_max_upload_bytes
PI_WEB_DEV_API_BIND_ADDR=$pi_web_dev_api_bind_addr
PI_WEB_DEV_BIND_ADDR=$pi_web_dev_bind_addr
PI_WEB_DEV_API_PORT=$pi_web_dev_api_port
PI_WEB_DEV_PORT=$pi_web_dev_port
EOF
mv "$temp_env" "$env_file"
log "Selected PI WEB Docker host profile: $PI_WEB_DETECTED_DOCKER_HOST_PROFILE"
if [ -f "$runtime_env_file" ]; then
log "Reused runtime Docker environment defaults from: $runtime_env_file"
fi
case "$PI_WEB_DETECTED_DOCKER_HOST_PROFILE" in
linux-native-docker)
log "Enabled Linux host mounts and hostexec namespace bridge."
;;
mac-docker-desktop)
log "Enabled Docker Desktop for Mac project mounts. hostexec is disabled because containers cannot enter native macOS namespaces."
;;
esac
log "User-editable dev config: $dev_config_file"
log "Generated dev env: $env_file"
log "Generated dev Compose override: $override_file"
if [ "$#" -eq 0 ]; then
set -- up --build
fi
pi_web_docker_compose \
--env-file "$env_file" \
-f "$repo_root/docker/compose.dev.yml" \
-f "$override_file" \
"$@"