innercontext/docs/DEPLOYMENT.md
Piotr Oleszczyk 91bc9e86d7 fix(deploy): install frontend prod deps on server after deploy
adapter-node bundles the SvelteKit framework but leaves package.json
`dependencies` (clsx, bits-ui, etc.) external — they must be present in
node_modules on the server at runtime.

- deploy.sh: rsync package.json + pnpm-lock.yaml, run pnpm install --prod
- DEPLOYMENT.md: add pnpm to server setup with explanation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 13:56:28 +01:00

9.4 KiB

Deployment guide — Proxmox LXC (home network)

Target architecture:

Reverse proxy (existing)             innercontext LXC (new, Debian 13)
┌──────────────────────┐             ┌────────────────────────────────────┐
│ reverse proxy        │────────────▶│ nginx :80                          │
│ innercontext.lan → * │             │   /api/*  → uvicorn :8000/*        │
└──────────────────────┘             │   /mcp/*  → uvicorn :8000/mcp/*    │
                                     │   /*      → SvelteKit Node :3000   │
                                     └────────────────────────────────────┘
                                             │                │
                                      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

  • Proxmox VE host with an existing PostgreSQL LXC and a reverse proxy
  • LAN hostname innercontext.lan resolvable on the network (via router DNS or /etc/hosts)
  • The PostgreSQL LXC must accept connections from the innercontext LXC IP

2. Create the LXC container

In the Proxmox UI (or via CLI):

# CLI example — adjust storage, bridge, IP to your environment
pct create 200 local:vztmpl/debian-13-standard_13.0-1_amd64.tar.zst \
  --hostname innercontext \
  --cores 2 \
  --memory 1024 \
  --swap 512 \
  --rootfs local-lvm:8 \
  --net0 name=eth0,bridge=vmbr0,ip=dhcp \
  --unprivileged 1 \
  --start 1

Note the container's IP address after it starts (pct exec 200 -- ip -4 a).


3. Container setup

pct enter 200   # or SSH into the container

System packages

apt update && apt upgrade -y
apt install -y git nginx curl ca-certificates gnupg lsb-release libpq5 rsync

Python 3.12+ + uv

apt install -y python3 python3-venv
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).

Node.js 24 LTS + pnpm

The server needs Node.js to run the pre-built frontend bundle, and pnpm to install production runtime dependencies (clsx, bits-ui, etc. — adapter-node bundles the SvelteKit framework but leaves these external). The frontend is never built on the server.

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
. "$HOME/.nvm/nvm.sh"
nvm install 24

Copy node to /usr/local/bin so it is accessible system-wide (required for sudo -u innercontext and for systemd). Use --remove-destination to replace any existing symlink with a real file:

cp --remove-destination "$(nvm which current)" /usr/local/bin/node

Install pnpm as a standalone binary — self-contained, no wrapper scripts, works system-wide:

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

useradd --system --create-home --shell /bin/bash innercontext

4. Create the database on the PostgreSQL LXC

Run on the PostgreSQL LXC:

psql -U postgres <<'SQL'
CREATE USER innercontext WITH PASSWORD 'change-me';
CREATE DATABASE innercontext OWNER innercontext;
SQL

Edit /etc/postgresql/18/main/pg_hba.conf and add (replace <lxc-ip> with the innercontext container IP):

host  innercontext  innercontext  <lxc-ip>/32  scram-sha-256

Then reload:

systemctl reload postgresql

5. Clone the repository

mkdir -p /opt/innercontext
git clone https://github.com/your-user/innercontext.git /opt/innercontext
chown -R innercontext:innercontext /opt/innercontext

6. Backend setup

cd /opt/innercontext/backend

Install dependencies

sudo -u innercontext uv sync

Create .env

cat > /opt/innercontext/backend/.env <<'EOF'
DATABASE_URL=postgresql+psycopg://innercontext:change-me@<pg-lxc-ip>/innercontext
GEMINI_API_KEY=your-gemini-api-key
# GEMINI_MODEL=gemini-flash-latest  # optional, this is the default
EOF
chmod 600 /opt/innercontext/backend/.env
chown innercontext:innercontext /opt/innercontext/backend/.env

Run database migrations

sudo -u innercontext bash -c '
  cd /opt/innercontext/backend
  uv run alembic upgrade head
'

This creates all tables on first run. On subsequent deploys it applies only the new migrations.

Existing database (tables already created by create_db_and_tables): Run uv run alembic stamp head instead to mark the current schema as migrated without re-running DDL.

Test

sudo -u innercontext bash -c '
  cd /opt/innercontext/backend
  uv run uvicorn main:app --host 127.0.0.1 --port 8000
'
# Ctrl-C after confirming it starts

Install systemd service

cp /opt/innercontext/systemd/innercontext.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now innercontext
systemctl status innercontext

7. Frontend setup

The frontend is built locally and uploaded via deploy.sh — never built on the server. This section only covers the one-time server-side configuration.

Create .env.production

cat > /opt/innercontext/frontend/.env.production <<'EOF'
PUBLIC_API_BASE=http://innercontext.lan/api
ORIGIN=http://innercontext.lan
EOF
chmod 600 /opt/innercontext/frontend/.env.production
chown innercontext:innercontext /opt/innercontext/frontend/.env.production

Grant innercontext passwordless sudo for service restarts

cat > /etc/sudoers.d/innercontext-deploy << 'EOF'
innercontext ALL=(root) NOPASSWD: \
    /usr/bin/systemctl restart innercontext, \
    /usr/bin/systemctl restart innercontext-node
EOF
chmod 440 /etc/sudoers.d/innercontext-deploy

Install systemd service

cp /opt/innercontext/systemd/innercontext-node.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable innercontext-node
# Do NOT start yet — build/ is empty until the first deploy.sh run

8. nginx setup

cp /opt/innercontext/nginx/innercontext.conf /etc/nginx/sites-available/innercontext
ln -s /etc/nginx/sites-available/innercontext /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t
systemctl reload nginx

9. Reverse proxy configuration

Point your existing reverse proxy at the innercontext LXC's nginx (<innercontext-lxc-ip>:80).

Example — Caddy:

innercontext.lan {
    reverse_proxy <innercontext-lxc-ip>:80
}

Example — nginx upstream:

server {
    listen 80;
    server_name innercontext.lan;
    location / {
        proxy_pass http://<innercontext-lxc-ip>:80;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Reload your reverse proxy after applying the change.


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

# 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

# From any machine on the LAN:
curl http://innercontext.lan/api/health-check    # {"status":"ok"}
curl http://innercontext.lan/api/products        # []
curl http://innercontext.lan/                    # SvelteKit HTML shell
curl -N http://innercontext.lan/mcp/mcp          # MCP StreamableHTTP endpoint

The web UI should be accessible at http://innercontext.lan.


12. Updating the application

# From the repo root on your local machine:
./deploy.sh            # full deploy (frontend + backend)
./deploy.sh frontend   # frontend only
./deploy.sh backend    # backend only

13. Troubleshooting

502 Bad Gateway on /api/*

systemctl status innercontext
journalctl -u innercontext -n 50
# Check .env DATABASE_URL is correct and PG LXC accepts connections

502 Bad Gateway on /

systemctl status innercontext-node
journalctl -u innercontext-node -n 50
# Verify /opt/innercontext/frontend/build/index.js exists (deploy.sh ran successfully)

MCP endpoint not responding

# MCP uses SSE — disable buffering is already in nginx config
# Verify the backend started successfully:
curl http://127.0.0.1:8000/health-check
# Check FastAPI logs:
journalctl -u innercontext -n 50

Database connection refused

# From innercontext LXC:
psql postgresql+psycopg://innercontext:change-me@<pg-lxc-ip>/innercontext -c "SELECT 1"
# If it fails, check pg_hba.conf on the PG LXC and verify the IP matches