b95989aa | Add guided installer for a bare Ubuntu/Debian server | Claude | 2026-05-15 | ↗ GitHub |
commit body `sudo bash install.sh` takes a clean Ubuntu Server 22.04+ or Debian 12+
host from nothing to a running PIP deployment.
Wizard (whiptail TUI):
- Hostname (any DNS name or LAN address) and TLS mode:
letsencrypt public Let's Encrypt; requires public DNS
internal Caddy local-CA self-signed; works anywhere
http HTTP only, port 80; first-run/LAN testing only
The choice writes the Caddyfile at ${DATA_ROOT}/caddy/Caddyfile
so re-running the installer can switch modes without touching
the git working tree.
- Bootstrap admin email and data root (any path; default /srv/pip).
- Postgres password (auto-generated by default; overridable).
- Email (Resend), External AI providers (disabled by default for
client matter), Microsoft Entra OIDC. All skipable.
- Ollama starter model: llama3.2:3b / llama3.1:8b / qwen2.5:14b /
skip.
Secrets handling (point 3 from the operator brief):
- AUTH_SECRET, USER_API_KEYS_ENCRYPTION_SECRET, DOWNLOAD_SIGNING_
SECRET, and STORAGE_ENCRYPTION_KEY are generated automatically
via openssl rand -hex 32.
- In addition to .env.compose (mode 600), a separate
${DATA_ROOT}/secrets-backup.txt is written (mode 400) with a
prominent "COPY THIS OFF THE SERVER" warning explaining what
each secret protects and the consequences of loss.
- The final report tells the operator where to find the backup
and what to do with it.
Web access before public TLS (point 4 from the operator brief):
- "internal" mode lets the operator reach https://<hostname>/
immediately with a browser TLS warning, even with no public DNS.
- "http" mode skips TLS entirely for first-run smoke testing.
- The Caddyfile is regenerated each run; switching from internal
to letsencrypt later is a re-run away.
Idempotency:
- Detects existing .env.compose and offers reuse-or-replace.
- Detects existing Docker install and skips the apt path.
- UFW (if active) opens 80/443; SSH untouched.
Companion scripts:
- update.sh: git pull + rebuild backend/frontend + recreate.
Backend entrypoint runs pending migrations on boot.
- bin/backup-now.sh: one-shot pg_dump into ${DATA_ROOT}/backups.
Compose change:
- caddy/Caddyfile renamed to caddy/Caddyfile.example (reference).
- Caddy now mounts ${DATA_ROOT}/caddy/Caddyfile (operator-owned).
README updated with the new install path and an explanation of
each wizard step.
Syntax-checked with bash -n; rendered all three Caddyfile variants
to confirm they are valid Caddy syntax.
|
76ef5f4f | Default to self-signed TLS in the installer | Claude | 2026-05-15 | ↗ GitHub |
commit body 'internal' is now the pre-selected option in the TLS mode wizard.
Caddy generates a local-CA cert on first run; the deployment works
with any hostname (including a LAN address or an IP) and the
operator can sign in immediately. Switching to Let's Encrypt is a
re-run of install.sh once public DNS is in place.
|
0db16e4c | Fix install.sh ownership of bind-mounted state dirs | Claude | 2026-05-15 | ↗ GitHub |
commit body The backend container runs as uid 10001 (Dockerfile creates pip user)
and the backup sidecar runs as the alpine postgres user (uid 70).
install.sh was leaving ${DATA_ROOT}/storage and ${DATA_ROOT}/backups
owned by root:root which made encrypted blob writes and nightly
pg_dump fail on a fresh deployment. chown them to the right uids
before bringing the stack up.
Also fixes a stale "mikeApi" reference in the session-token route
comment.
|
b3ad3540 | Support remote Ollama servers (or none) at install time | Claude | 2026-05-15 | ↗ GitHub |
commit body The bundled ollama service in docker-compose.yml moves behind a
new `local-ollama` profile, so docker compose only starts the
container when COMPOSE_PROFILES includes that profile.
install.sh now asks how PIP should reach Ollama:
local bundled in this compose stack (previous default)
remote point at an existing Ollama server URL
none no local AI, external providers only
The choice writes OLLAMA_MODE, LOCAL_LLM_BASE_URL, and
COMPOSE_PROFILES into .env.compose. The compose wrapper in
install.sh and update.sh sources the env file so subsequent
docker compose invocations honour the profile. The model-pull
wizard step and image pull are skipped when not in local mode.
The bundled image is also no longer in the unconditional pull
batch; remote/none deployments shouldn't have to download a
~1 GB image they'll never run.
Also fixes .gitignore so .env.compose.example is tracked (the
file is referenced by docker-compose.yml's header comment but
was being silently ignored by the broad .env.* rule).
|
a10c7ac5 | Fix backend docker build: include docker-entrypoint.sh | Claude | 2026-05-15 | ↗ GitHub |
The Dockerfile COPYs docker-entrypoint.sh into the runtime image
but backend/.dockerignore was excluding the file from the build
context, so the COPY failed with "/docker-entrypoint.sh: not found"
on a fresh install.
|
8bb78320 | Richer final report from install.sh | Claude | 2026-05-15 | ↗ GitHub |
commit body The post-install summary now prints:
- Configured URL plus a live HTTP probe through Caddy so operators
see immediately whether the stack is actually reachable.
- Every non-loopback IPv4 the host advertises (with interface
name) so they know which addresses work on the LAN before DNS is
set up.
- A colour-coded service status table built from docker compose ps
(NAME, SERVICE, STATE, HEALTH, PORTS).
- A complete configuration block: TLS mode + explanatory note,
Ollama mode and URL, external AI status with which provider keys
were supplied, Resend, Entra OIDC.
- The secrets backup path with the standing "copy this off the
server" warning.
- An operate / files cheatsheet so common follow-ups are at hand.
The probe hits the public root URL (not /api/health which doesn't
exist externally) and treats any 2xx/3xx as reachable.
|
2c726b5f | Caddy internal mode: cover host IPs and reload on rewrite | Claude | 2026-05-15 | ↗ GitHub |
commit body When TLS_MODE=internal the generated Caddyfile only listed the
configured PIP_DOMAIN in the site block, so hitting the box by IP
(common during initial bootstrap before DNS is wired up) tripped
ERR_SSL_PROTOCOL_ERROR in the browser because Caddy had no matching
site for that SNI.
write_caddyfile now expands the site block to include every
non-loopback IPv4 the host advertises plus localhost / 127.0.0.1.
Caddy issues internal-CA certs covering all of them, so the TLS
handshake succeeds for whichever address the operator hits. The
browser still warns on the self-signed cert (expected); the user
can click through and proceed.
bring_up_stack now restarts the Caddy container after `up -d` so
re-runs of install.sh against an existing deployment pick up the
regenerated Caddyfile (the bind-mount changing alone doesn't make
compose recreate the service).
|
56e15d04 | Internal TLS: pin to a pre-generated self-signed cert | Claude | 2026-05-15 | ↗ GitHub |
commit body The previous attempt at supporting raw IPs in the Caddy site block
relied on Caddy's auto-TLS issuing internal-CA certs for IPs, which
is finicky and was still producing ERR_SSL_PROTOCOL_ERROR in the
field.
Switch to a deterministic pattern: at install time, generate a
self-signed cert with SAN entries covering PIP_DOMAIN, every host
IPv4, localhost, and 127.0.0.1. The Caddyfile binds :443 to that
cert explicitly, so any address the operator hits gets a usable
TLS handshake. Browsers still warn on the self-signed cert (the
expected click-through) instead of refusing the connection at the
protocol layer.
Also add a :80 redirect block in internal mode so plain http://
URLs land on https:// automatically.
|
d22d1113 | Add bin/pip-status.sh health check + control panel | Claude | 2026-05-16 | ↗ GitHub |
commit body Single command for operators to see the deployment at a glance:
- Public-URL probe through Caddy
- Compose service status (NAME / SERVICE / STATE / HEALTH / PORTS)
- Container resource snapshot from docker stats (CPU%, mem usage,
mem%, network I/O, block I/O), filtered to compose-managed
containers for this project
- Host disk: DATA_ROOT filesystem free space plus per-subdir sizes
for postgres/, storage/, ollama/, caddy/, backups/
- Host load / memory / uptime
Modes:
bin/pip-status.sh status + interactive menu (TTY)
bin/pip-status.sh --status one-shot, no menu
bin/pip-status.sh --restart NAME status, then compose restart NAME
bin/pip-status.sh --restart-all status, then compose restart all
bin/pip-status.sh --watch refresh every 2s, ctrl-C to exit
Interactive menu numbers services so the operator picks by index
rather than typing the full container name. Restart-all prompts for
confirmation. Logs option tails the last 50 lines of any service.
|
afa7e87a | Add bin/pip-reset-admin.sh to reset the bootstrap admin password | Claude | 2026-05-16 | ↗ GitHub |
commit body Operators who lose the bootstrap password, or want to rotate it
after first sign-in, can now run:
sudo bash bin/pip-reset-admin.sh # interactive
sudo bash bin/pip-reset-admin.sh --generate
sudo bash bin/pip-reset-admin.sh --password '<value>'
The script:
- Hashes the new password via the running backend container's
bcryptjs (cost 12, identical to the application path).
- UPDATEs users.password_hash for BOOTSTRAP_ADMIN_EMAIL, clears any
pending password-reset token, and re-activates the row if it was
marked disabled.
- Errors out if exactly 1 row wasn't updated, so a typo in the
email or a missing user fails loudly.
- Writes a user.password.reset audit event.
- Mirrors the new value into ${DATA_ROOT}/secrets-backup.txt and
.env.compose so installer reruns stay consistent.
Also added a [p] entry to the interactive menu in pip-status.sh so
the same flow is one keypress away from the status screen.
|
510c379c | install.sh: don't silently change POSTGRES_PASSWORD on a re-run | Claude | 2026-05-16 | ↗ GitHub |
commit body The official postgres:alpine image only honours POSTGRES_PASSWORD
when it initialises an EMPTY data directory. If install.sh is
re-run (or the wizard re-runs and chooses "replace") while
${DATA_ROOT}/postgres already holds an initialised cluster,
regenerating POSTGRES_PASSWORD will deauth the backend against the
existing role -> migration crash loop with
"password authentication failed for user 'pip'".
Detect that case up front:
- If ${DATA_ROOT}/postgres is non-empty AND .env.compose already
had a POSTGRES_PASSWORD value, default to reusing it.
- If the operator deliberately wants a new password, surface a
warning with the ALTER USER one-liner needed to keep the existing
data, or the wipe instructions to start fresh.
|
0f2b965c | Bake PUBLIC_URL into the frontend bundle at build time | Claude | 2026-05-16 | ↗ GitHub |
commit body Next.js inlines NEXT_PUBLIC_* env vars into the client JS bundle at
`npm run build` time; they're not read at runtime. The frontend
Dockerfile wasn't accepting that value as a build arg, so the
client bundle was always built with the source-default fallback of
http://localhost:3001 and the browser tried to hit localhost for
every API call (visible in DevTools as ERR_CONNECTION_REFUSED on
/me, /me/onboarding-options, /user/profile, etc.).
Add an ARG NEXT_PUBLIC_API_BASE_URL to the frontend build stage and
wire docker-compose.yml to pass ${PUBLIC_URL} into it. The runtime
env block already mirrors the same value for server-side fetches.
After this lands, `update.sh` rebuilds the frontend image with the
right URL baked in.
|
b4a727b0 | Add bin/pip-uninstall.sh for clean test redeploys | Claude | 2026-05-16 | ↗ GitHub |
commit body Tears down the compose stack (containers, volumes, network), drops
the built backend/frontend images by default, removes ${DATA_ROOT}
(postgres, storage, ollama, caddy, backups, secrets-backup.txt) and
.env.compose, with confirmations before each destructive step.
Flags: --yes, --keep-data, --keep-images, --prune. README updated
alongside pip-status / pip-reset-admin so the new script is
discoverable.
Intended for test hosts and pre-go-live rebuilds. The header carries
a clear irreversible warning given the deployment holds documents
and the encryption key.
|
978ea483 | update.sh: regenerate Caddyfile so template changes land on hosts | Claude | 2026-05-16 | ↗ GitHub |
commit body Caddyfile generation lived inline in install.sh, which meant that
operators who pulled a Caddy-template change and ran update.sh got
the new backend / frontend code but kept their old Caddyfile. The
recent path+Accept-header routing fix did not reach a running
deployment until install.sh was re-run.
Extracted the generator into bin/pip-write-caddyfile.sh, a
standalone script that reads TLS_MODE / PIP_DOMAIN / DATA_ROOT from
.env.compose. install.sh now delegates to it (single source of
truth, no template drift) and update.sh calls it on every run so
operators do not need to know to re-run install.sh after a code
change to the proxy config.
The bootstrap self-signed cert generator moved into the same script
and gained an existing-cert check so update.sh runs do not
needlessly regenerate the cert on every invocation. install.sh's
now-dead detect_host_ips and generate_bootstrap_cert helpers
removed.
To apply on a running deployment that hit the recent admin /
projects / workflows JSON-401 issue:
cd /opt/pip
sudo git pull origin main
sudo bash bin/pip-write-caddyfile.sh
That writes the new Caddyfile and reloads caddy without touching
the rest of the stack.
|