chore(deploy): wire OIDC runtime configuration

This commit is contained in:
Piotr Oleszczyk 2026-03-12 15:55:32 +01:00
parent ffa3b71309
commit 4bfa4ea02d
7 changed files with 115 additions and 100 deletions

View file

@ -346,7 +346,8 @@ check_backend_health() {
check_frontend_health() {
local i
for ((i = 1; i <= 30; i++)); do
if remote "curl -sf http://127.0.0.1:3000/ >/dev/null"; then
# Allow 200 OK or 302/303/307 Redirect (to login)
if remote "curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/ | grep -qE '^(200|302|303|307)$'"; then
log "Frontend health check passed"
return 0
fi

View file

@ -94,117 +94,82 @@ chown -R innercontext:innercontext /opt/innercontext
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
chmod 600 /opt/innercontext/shared/backend/.env
chmod 600 /opt/innercontext/shared/frontend/.env.production
chown innercontext:innercontext /opt/innercontext/shared/backend/.env
chown innercontext:innercontext /opt/innercontext/shared/frontend/.env.production
```
### 4) Grant deploy sudo permissions
## OIDC Setup (Authelia)
```bash
cat > /etc/sudoers.d/innercontext-deploy << 'EOF'
innercontext ALL=(root) NOPASSWD: \
/usr/bin/systemctl restart innercontext, \
/usr/bin/systemctl restart innercontext-node, \
/usr/bin/systemctl restart innercontext-pricing-worker, \
/usr/bin/systemctl is-active innercontext, \
/usr/bin/systemctl is-active innercontext-node, \
/usr/bin/systemctl is-active innercontext-pricing-worker
EOF
This project uses OIDC for authentication. You need an OIDC provider like Authelia.
chmod 440 /etc/sudoers.d/innercontext-deploy
visudo -c -f /etc/sudoers.d/innercontext-deploy
### Authelia Client Configuration
# Must work without password or TTY prompt:
sudo -u innercontext sudo -n -l
Add the following to your Authelia `configuration.yml`:
```yaml
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
```
If `sudo -n -l` fails, deployments will fail during restart/rollback with:
`sudo: a terminal is required` or `sudo: a password is required`.
### Bootstrap Admin
### 5) Install systemd and nginx configs
After first deploy (or after copying repo content to `/opt/innercontext/current`), install configs:
```bash
cp /opt/innercontext/current/systemd/innercontext.service /etc/systemd/system/
cp /opt/innercontext/current/systemd/innercontext-node.service /etc/systemd/system/
cp /opt/innercontext/current/systemd/innercontext-pricing-worker.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable innercontext
systemctl enable innercontext-node
systemctl enable innercontext-pricing-worker
cp /opt/innercontext/current/nginx/innercontext.conf /etc/nginx/sites-available/innercontext
ln -sf /etc/nginx/sites-available/innercontext /etc/nginx/sites-enabled/innercontext
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl reload nginx
```
## Local Machine Setup
`~/.ssh/config`:
```
Host innercontext
HostName <lxc-ip>
User innercontext
```
Ensure your public key is in `/home/innercontext/.ssh/authorized_keys`.
## Deploy Commands
From repository root on external machine:
```bash
./deploy.sh # full deploy (default = all)
./deploy.sh all
./deploy.sh backend
./deploy.sh frontend
./deploy.sh list
./deploy.sh rollback
```
Optional overrides:
```bash
DEPLOY_SERVER=innercontext ./deploy.sh all
DEPLOY_ROOT=/opt/innercontext ./deploy.sh backend
DEPLOY_ALLOW_DIRTY=1 ./deploy.sh frontend
```
## What `deploy.sh` Does
For `backend` / `frontend` / `all`:
1. Local checks (strict, fail-fast)
2. Acquire `/opt/innercontext/.deploy.lock`
3. Create `<timestamp>` release directory
4. Upload selected component(s)
5. Link shared env files in the release directory
6. `uv sync` + `alembic upgrade head` (backend scope)
7. Upload `scripts/`, `systemd/`, `nginx/`
8. Switch `current` to the prepared release
9. Restart affected services
10. Run health checks
11. Remove old releases (keep last 5)
12. Write deploy entry to `/opt/innercontext/deploy.log`
If anything fails after promotion, script auto-rolls back to previous release.
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`
- Frontend: `http://127.0.0.1:3000/`
- 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:

View file

@ -10,6 +10,8 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
# SvelteKit Node server
@ -19,5 +21,7 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
}
}

View file

@ -25,13 +25,27 @@ log() {
check_service() {
local service_name=$1
local url=$2
local allow_redirect=${3:-false}
if systemctl is-active --quiet "$service_name"; then
if curl -sf --max-time "$TIMEOUT" "$url" > /dev/null 2>&1; then
local curl_opts="-s --max-time $TIMEOUT"
if [ "$allow_redirect" = false ]; then
curl_opts="$curl_opts -f"
fi
if curl $curl_opts "$url" > /dev/null 2>&1; then
log "${GREEN}${NC} $service_name is healthy"
return 0
else
log "${YELLOW}${NC} $service_name is running but not responding at $url"
# If allow_redirect is true, we check if it's a 302
if [ "$allow_redirect" = true ]; then
local status=$(curl -s -o /dev/null -w "%{http_code}" --max-time "$TIMEOUT" "$url")
if [ "$status" = "302" ] || [ "$status" = "303" ] || [ "$status" = "307" ] || [ "$status" = "200" ]; then
log "${GREEN}${NC} $service_name is healthy (status $status)"
return 0
fi
fi
log "${YELLOW}${NC} $service_name is running but not responding correctly at $url"
return 1
fi
else
@ -45,8 +59,10 @@ backend_ok=0
frontend_ok=0
worker_ok=0
# Backend health-check is public and should return 200
check_service "innercontext" "$BACKEND_URL" || backend_ok=1
check_service "innercontext-node" "$FRONTEND_URL" || frontend_ok=1
# Frontend root may redirect to login (302)
check_service "innercontext-node" "$FRONTEND_URL" true || frontend_ok=1
# Worker doesn't have HTTP endpoint, just check if it's running
if systemctl is-active --quiet "innercontext-pricing-worker"; then

View file

@ -25,7 +25,7 @@ warnings=0
log_error() {
echo -e "${RED}${NC} $1"
((errors++))
errors=$((errors + 1))
}
log_success() {
@ -34,7 +34,7 @@ log_success() {
log_warning() {
echo -e "${YELLOW}${NC} $1"
((warnings++))
warnings=$((warnings + 1))
}
check_symlink() {
@ -131,6 +131,21 @@ if [ -f "$SHARED_BACKEND_ENV" ]; then
check_var "$SHARED_BACKEND_ENV" "GEMINI_API_KEY"
check_var "$SHARED_BACKEND_ENV" "LOG_LEVEL" true
check_var "$SHARED_BACKEND_ENV" "CORS_ORIGINS" true
# OIDC Configuration
check_var "$SHARED_BACKEND_ENV" "OIDC_ISSUER"
check_var "$SHARED_BACKEND_ENV" "OIDC_CLIENT_ID"
check_var "$SHARED_BACKEND_ENV" "OIDC_DISCOVERY_URL"
check_var "$SHARED_BACKEND_ENV" "OIDC_ADMIN_GROUPS"
check_var "$SHARED_BACKEND_ENV" "OIDC_MEMBER_GROUPS"
check_var "$SHARED_BACKEND_ENV" "OIDC_JWKS_CACHE_TTL_SECONDS" true
# Bootstrap Admin (Optional, used for initial setup)
check_var "$SHARED_BACKEND_ENV" "BOOTSTRAP_ADMIN_OIDC_ISSUER" true
check_var "$SHARED_BACKEND_ENV" "BOOTSTRAP_ADMIN_OIDC_SUB" true
check_var "$SHARED_BACKEND_ENV" "BOOTSTRAP_ADMIN_EMAIL" true
check_var "$SHARED_BACKEND_ENV" "BOOTSTRAP_ADMIN_NAME" true
check_var "$SHARED_BACKEND_ENV" "BOOTSTRAP_HOUSEHOLD_NAME" true
fi
echo ""
@ -138,6 +153,12 @@ echo "=== Validating Frontend Environment Variables ==="
if [ -f "$SHARED_FRONTEND_ENV" ]; then
check_var "$SHARED_FRONTEND_ENV" "PUBLIC_API_BASE"
check_var "$SHARED_FRONTEND_ENV" "ORIGIN"
# Session and OIDC
check_var "$SHARED_FRONTEND_ENV" "SESSION_SECRET"
check_var "$SHARED_FRONTEND_ENV" "OIDC_ISSUER"
check_var "$SHARED_FRONTEND_ENV" "OIDC_CLIENT_ID"
check_var "$SHARED_FRONTEND_ENV" "OIDC_DISCOVERY_URL"
fi
echo ""

View file

@ -1,5 +1,8 @@
[Unit]
Description=innercontext SvelteKit Node frontend
# Required env vars in .env.production:
# PUBLIC_API_BASE, ORIGIN, SESSION_SECRET
# OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_DISCOVERY_URL
After=network.target
[Service]

View file

@ -1,5 +1,10 @@
[Unit]
Description=innercontext FastAPI backend
# Required env vars in .env:
# DATABASE_URL, GEMINI_API_KEY
# OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_DISCOVERY_URL
# OIDC_ADMIN_GROUPS, OIDC_MEMBER_GROUPS
# (Optional) BOOTSTRAP_ADMIN_OIDC_ISSUER, BOOTSTRAP_ADMIN_OIDC_SUB, etc.
After=network.target
[Service]