docs: add deploy.sh and rewrite DEPLOYMENT.md for local-build workflow

- Add deploy.sh: builds frontend locally, rsyncs build/ to server,
  restarts services via passwordless sudo
- DEPLOYMENT.md: remove pnpm build from server setup (frontend is never
  built on the LXC — esbuild hangs on low-resource containers), add rsync
  to apt packages, document deploy.sh setup (SSH config, sudoers), replace
  manual update steps with ./deploy.sh invocation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Piotr Oleszczyk 2026-03-01 13:51:51 +01:00
parent 99584521a1
commit 3428885aaa
2 changed files with 121 additions and 47 deletions

59
deploy.sh Executable file
View file

@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Usage: ./deploy.sh [frontend|backend|all]
# default: all
#
# SSH config (~/.ssh/config) — recommended:
# Host innercontext
# HostName <IP_LXC>
# User innercontext
#
# The innercontext user needs passwordless sudo for systemctl only:
# /etc/sudoers.d/innercontext-deploy:
# innercontext ALL=(root) NOPASSWD: /usr/bin/systemctl restart innercontext, /usr/bin/systemctl restart innercontext-node, /usr/bin/systemctl is-active innercontext, /usr/bin/systemctl is-active innercontext-node
set -euo pipefail
SERVER="${DEPLOY_SERVER:-innercontext}" # ssh host alias or user@host
REMOTE="/opt/innercontext"
SCOPE="${1:-all}"
# ── Frontend ───────────────────────────────────────────────────────────────
deploy_frontend() {
echo "==> [frontend] Building locally..."
(cd frontend && pnpm run build)
echo "==> [frontend] Uploading build/..."
rsync -az --delete frontend/build/ "$SERVER:$REMOTE/frontend/build/"
echo "==> [frontend] Restarting service..."
ssh "$SERVER" "sudo systemctl restart innercontext-node && echo OK"
}
# ── Backend ────────────────────────────────────────────────────────────────
deploy_backend() {
echo "==> [backend] Uploading source..."
rsync -az --delete \
--exclude='.venv/' \
--exclude='__pycache__/' \
--exclude='*.pyc' \
--exclude='.env' \
backend/ "$SERVER:$REMOTE/backend/"
echo "==> [backend] Syncing dependencies..."
ssh "$SERVER" "cd $REMOTE/backend && uv sync --frozen"
echo "==> [backend] Restarting service (alembic runs on start)..."
ssh "$SERVER" "sudo systemctl restart innercontext && echo OK"
}
# ── Dispatch ───────────────────────────────────────────────────────────────
case "$SCOPE" in
frontend) deploy_frontend ;;
backend) deploy_backend ;;
all) deploy_frontend; deploy_backend ;;
*)
echo "Usage: $0 [frontend|backend|all]"
exit 1
;;
esac
echo "==> Done."

View file

@ -14,6 +14,10 @@ Reverse proxy (existing) innercontext LXC (new, Debian 13)
FastAPI + MCP SvelteKit Node FastAPI + MCP SvelteKit Node
``` ```
> **Frontend is never built on the server.** The `vite build` + `adapter-node`
> esbuild step is CPU/RAM-intensive and will hang on a small LXC. Build locally,
> deploy the `build/` artifact via `deploy.sh`.
## 1. Prerequisites ## 1. Prerequisites
- Proxmox VE host with an existing PostgreSQL LXC and a reverse proxy - Proxmox VE host with an existing PostgreSQL LXC and a reverse proxy
@ -53,7 +57,7 @@ pct enter 200 # or SSH into the container
```bash ```bash
apt update && apt upgrade -y apt update && apt upgrade -y
apt install -y git nginx curl ca-certificates gnupg lsb-release libpq5 apt install -y git nginx curl ca-certificates gnupg lsb-release libpq5 rsync
``` ```
### Python 3.12+ + uv ### Python 3.12+ + uv
@ -65,7 +69,10 @@ curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=/usr/local/bin sh
Installing to `/usr/local/bin` makes `uv` available system-wide (required for `sudo -u innercontext uv sync`). Installing to `/usr/local/bin` makes `uv` available system-wide (required for `sudo -u innercontext uv sync`).
### Node.js 24 LTS + pnpm ### Node.js 24 LTS
The server only needs Node.js to **run** the pre-built frontend bundle.
`pnpm` is **not** needed on the server — the frontend is always built locally.
```bash ```bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
@ -75,23 +82,12 @@ nvm install 24
Copy `node` to `/usr/local/bin` so it is accessible system-wide Copy `node` to `/usr/local/bin` so it is accessible system-wide
(required for `sudo -u innercontext` and for systemd). (required for `sudo -u innercontext` and for systemd).
Symlinking into `/root/.nvm/` won't work — other users can't traverse `/root/`.
Use `--remove-destination` to replace any existing symlink with a real file: Use `--remove-destination` to replace any existing symlink with a real file:
```bash ```bash
cp --remove-destination "$(nvm which current)" /usr/local/bin/node cp --remove-destination "$(nvm which current)" /usr/local/bin/node
``` ```
Install pnpm as a standalone binary from GitHub releases — self-contained,
no wrapper scripts, works system-wide. Do **not** use `corepack enable pnpm`
(the shim requires its nvm directory structure and breaks when copied/linked):
```bash
curl -fsSL "https://github.com/pnpm/pnpm/releases/latest/download/pnpm-linux-x64" \
-o /usr/local/bin/pnpm
chmod 755 /usr/local/bin/pnpm
```
### Application user ### Application user
```bash ```bash
@ -194,11 +190,10 @@ systemctl status innercontext
--- ---
## 7. Frontend build and setup ## 7. Frontend setup
```bash The frontend is **built locally and uploaded** via `deploy.sh` — never built on the server.
cd /opt/innercontext/frontend This section only covers the one-time server-side configuration.
```
### Create `.env.production` ### Create `.env.production`
@ -211,25 +206,24 @@ chmod 600 /opt/innercontext/frontend/.env.production
chown innercontext:innercontext /opt/innercontext/frontend/.env.production chown innercontext:innercontext /opt/innercontext/frontend/.env.production
``` ```
### Install dependencies and build ### Grant `innercontext` passwordless sudo for service restarts
```bash ```bash
sudo -u innercontext bash -c ' cat > /etc/sudoers.d/innercontext-deploy << 'EOF'
cd /opt/innercontext/frontend innercontext ALL=(root) NOPASSWD: \
pnpm install /usr/bin/systemctl restart innercontext, \
PUBLIC_API_BASE=http://innercontext.lan/api pnpm build /usr/bin/systemctl restart innercontext-node
' EOF
chmod 440 /etc/sudoers.d/innercontext-deploy
``` ```
The production build lands in `/opt/innercontext/frontend/build/`.
### Install systemd service ### Install systemd service
```bash ```bash
cp /opt/innercontext/systemd/innercontext-node.service /etc/systemd/system/ cp /opt/innercontext/systemd/innercontext-node.service /etc/systemd/system/
systemctl daemon-reload systemctl daemon-reload
systemctl enable --now innercontext-node systemctl enable innercontext-node
systemctl status innercontext-node # Do NOT start yet — build/ is empty until the first deploy.sh run
``` ```
--- ---
@ -276,7 +270,40 @@ Reload your reverse proxy after applying the change.
--- ---
## 10. Verification ## 10. First deploy from local machine
All subsequent deploys (including the first one) use `deploy.sh` from your local machine.
### SSH config
Add to `~/.ssh/config` on your local machine:
```
Host innercontext
HostName <innercontext-lxc-ip>
User innercontext
```
Make sure your SSH public key is in `/home/innercontext/.ssh/authorized_keys` on the server.
### Run the first deploy
```bash
# From the repo root on your local machine:
./deploy.sh
```
This will:
1. Build the frontend locally (`pnpm run build`)
2. Upload `frontend/build/` to the server via rsync
3. Restart `innercontext-node`
4. Upload `backend/` source to the server
5. Run `uv sync --frozen` on the server
6. Restart `innercontext` (runs alembic migrations on start)
---
## 11. Verification
```bash ```bash
# From any machine on the LAN: # From any machine on the LAN:
@ -290,30 +317,18 @@ The web UI should be accessible at `http://innercontext.lan`.
--- ---
## 11. Updating the application ## 12. Updating the application
```bash ```bash
cd /opt/innercontext # From the repo root on your local machine:
git pull ./deploy.sh # full deploy (frontend + backend)
./deploy.sh frontend # frontend only
# Sync backend dependencies if pyproject.toml changed: ./deploy.sh backend # backend only
cd backend && sudo -u innercontext uv sync && cd ..
# Apply any new DB migrations (runs automatically via ExecStartPre, but safe to run manually first):
sudo -u innercontext bash -c 'cd /opt/innercontext/backend && uv run alembic upgrade head'
# Rebuild frontend:
cd frontend && sudo -u innercontext bash -c '
pnpm install
PUBLIC_API_BASE=http://innercontext.lan/api pnpm build
'
systemctl restart innercontext innercontext-node
``` ```
--- ---
## 12. Troubleshooting ## 13. Troubleshooting
### 502 Bad Gateway on `/api/*` ### 502 Bad Gateway on `/api/*`
@ -328,7 +343,7 @@ journalctl -u innercontext -n 50
```bash ```bash
systemctl status innercontext-node systemctl status innercontext-node
journalctl -u innercontext-node -n 50 journalctl -u innercontext-node -n 50
# Verify /opt/innercontext/frontend/build/index.js exists (pnpm build ran successfully) # Verify /opt/innercontext/frontend/build/index.js exists (deploy.sh ran successfully)
``` ```
### MCP endpoint not responding ### MCP endpoint not responding