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>
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-nodeesbuild step is CPU/RAM-intensive and will hang on a small LXC. Build locally, deploy thebuild/artifact viadeploy.sh.
1. Prerequisites
- Proxmox VE host with an existing PostgreSQL LXC and a reverse proxy
- LAN hostname
innercontext.lanresolvable 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): Runuv run alembic stamp headinstead 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:
- Build the frontend locally (
pnpm run build) - Upload
frontend/build/to the server via rsync - Restart
innercontext-node - Upload
backend/source to the server - Run
uv sync --frozenon the server - 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