6.2 KiB
Deployment Guide (LXC + systemd + nginx)
This project deploys from an external machine (developer laptop or CI runner) to a Debian LXC host over SSH.
Deployments are push-based, release-based, and atomic:
- Build and validate locally
- Upload to
/opt/innercontext/releases/<timestamp> - Run backend dependency sync and migrations in that release directory
- Promote once by switching
/opt/innercontext/current - Restart services and run health checks
- Auto-rollback on failure
Environment files have exactly two persistent locations on the server:
/opt/innercontext/shared/backend/.env/opt/innercontext/shared/frontend/.env.production
Each release links to those files from:
/opt/innercontext/current/backend/.env->../../../shared/backend/.env/opt/innercontext/current/frontend/.env.production->../../../shared/frontend/.env.production
Architecture
external machine (manual now, CI later)
|
| ssh + rsync
v
LXC host
/opt/innercontext/
current -> releases/<timestamp>
releases/<timestamp>
shared/backend/.env
shared/frontend/.env.production
scripts/
Services:
innercontext(FastAPI, localhost:8000)innercontext-node(SvelteKit Node, localhost:3000)innercontext-pricing-worker(background worker)
nginx routes:
/api/*->http://127.0.0.1:8000/*/*->http://127.0.0.1:3000/*
Run Model
- Manual deploy: run
./deploy.sh ...from repo root on your laptop. - Optional CI deploy: run the same script from a manual workflow (
workflow_dispatch). - The server never builds frontend assets.
One-Time Server Setup
Run on the LXC host as root.
1) Install runtime dependencies
apt update && apt upgrade -y
apt install -y git nginx curl ca-certificates libpq5 rsync python3 python3-venv
curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=/usr/local/bin sh
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
. "$HOME/.nvm/nvm.sh"
nvm install 24
cp --remove-destination "$(nvm which current)" /usr/local/bin/node
curl -fsSL "https://github.com/pnpm/pnpm/releases/latest/download/pnpm-linux-x64" \
-o /usr/local/bin/pnpm
chmod 755 /usr/local/bin/pnpm
2) Create app user and directories
useradd --system --create-home --shell /bin/bash innercontext
mkdir -p /opt/innercontext/releases
mkdir -p /opt/innercontext/shared/backend
mkdir -p /opt/innercontext/shared/frontend
mkdir -p /opt/innercontext/scripts
chown -R innercontext:innercontext /opt/innercontext
3) Create shared env files
cat > /opt/innercontext/shared/backend/.env <<'EOF'
DATABASE_URL=postgresql+psycopg://innercontext:change-me@<pg-ip>/innercontext
GEMINI_API_KEY=your-key
# OIDC Configuration
OIDC_ISSUER=https://auth.example.com
OIDC_CLIENT_ID=innercontext-backend
OIDC_DISCOVERY_URL=https://auth.example.com/.well-known/openid-configuration
OIDC_ADMIN_GROUPS=admins
OIDC_MEMBER_GROUPS=members
# Bootstrap Admin (Optional, used for initial setup)
# BOOTSTRAP_ADMIN_OIDC_ISSUER=https://auth.example.com
# BOOTSTRAP_ADMIN_OIDC_SUB=user-sub-from-authelia
# BOOTSTRAP_ADMIN_EMAIL=admin@example.com
# BOOTSTRAP_ADMIN_NAME="Admin User"
# BOOTSTRAP_HOUSEHOLD_NAME="My Household"
EOF
cat > /opt/innercontext/shared/frontend/.env.production <<'EOF'
PUBLIC_API_BASE=http://127.0.0.1:8000
ORIGIN=http://innercontext.lan
# Session and OIDC
SESSION_SECRET=generate-a-long-random-string
OIDC_ISSUER=https://auth.example.com
OIDC_CLIENT_ID=innercontext-frontend
OIDC_DISCOVERY_URL=https://auth.example.com/.well-known/openid-configuration
EOF
OIDC Setup (Authelia)
This project uses OIDC for authentication. You need an OIDC provider like Authelia.
Authelia Client Configuration
Add the following to your Authelia configuration.yml:
identity_providers:
oidc:
clients:
- id: innercontext-frontend
description: InnerContext Frontend
secret: '$pbkdf2-sha512$...' # Not used for public client, but Authelia may require it
public: true
authorization_policy: one_factor
redirect_uris:
- http://innercontext.lan/auth/callback
scopes:
- openid
- profile
- email
- groups
userinfo_signed_response_alg: none
- id: innercontext-backend
description: InnerContext Backend
secret: '$pbkdf2-sha512$...'
public: false
authorization_policy: one_factor
redirect_uris: []
scopes:
- openid
- profile
- email
- groups
userinfo_signed_response_alg: none
Bootstrap Admin
To create the first user and household, set the BOOTSTRAP_ADMIN_* environment variables in the backend .env file and restart the backend. The backend will automatically create the user and household on startup if they don't exist. After the first successful login, you can remove these variables.
Health Checks
- Backend:
http://127.0.0.1:8000/health-check(returns 200) - Frontend:
http://127.0.0.1:3000/(returns 200 or 302 redirect to login) - Worker:
systemctl is-active innercontext-pricing-worker
Manual checks:
curl -sf http://127.0.0.1:8000/health-check
curl -sf http://127.0.0.1:3000/
systemctl is-active innercontext
systemctl is-active innercontext-node
systemctl is-active innercontext-pricing-worker
Troubleshooting
Lock exists
cat /opt/innercontext/.deploy.lock
rm -f /opt/innercontext/.deploy.lock
Only remove the lock if no deployment is running.
Sudo password prompt during deploy
Re-check /etc/sudoers.d/innercontext-deploy and run:
visudo -c -f /etc/sudoers.d/innercontext-deploy
sudo -u innercontext sudo systemctl is-active innercontext
Backend migration failure
Validate env file and DB connectivity:
ls -la /opt/innercontext/shared/backend/.env
grep '^DATABASE_URL=' /opt/innercontext/shared/backend/.env
Service fails after deploy
journalctl -u innercontext -n 100
journalctl -u innercontext-node -n 100
journalctl -u innercontext-pricing-worker -n 100
Manual CI Deploy (Optional)
Use the manual Forgejo workflow (workflow_dispatch) to run the same ./deploy.sh all path from CI once server secrets and SSH trust are configured.