Files
praxis/scripts/install-service.sh
T
Praxis CI 6cf63cb064 docs(P01): verify — APPROVE_WITH_NOTES, 4 P0 fixed, 18/20 REQ covered
Verification layers:
  Structural: PASS (all scripts executable, syntax clean, Dockerfile valid)
  Behavioral: PASS (121 bats, 77 pytest, docker build succeeds, compose valid)
  Security: PASS (no secrets committed, .dockerignore excludes .env*, env_file pattern)
  Quality: PASS (coreci patterns followed, no coreci refs, G-104/G-105/G-106 verified)

P0 issues found and auto-fixed:
  1. docker-compose.yml: removed invalid restart_policy key, fixed env_file syntax
  2. pyproject.toml: added fastapi + uvicorn deps (v0.1 gap exposed by Dockerfile)
  3. timing.sh: renamed coreci_deploy_timing → praxis_deploy_timing (TASK-03-07)
  4. firstboot-hook.sh: fixed idempotency check (/opt/praxis/.git not /usr/local/bin/praxis-deploy)

P1+ issues: 8 (1 fixed: lxc-config.sh default alignment, 7 noted for post-hoc review)
REQ coverage: 18/20 covered, 2 deferred (live first-boot timing + live E2E require cluster)
Must-haves: 25/28 pass, 2 partial (comment-only diffs, no Makefile), 1 deferred

---ci---
project: praxis
phase: 1
milestone: v0.2
status: verify
---/ci---
2026-08-03 18:37:45 +00:00

122 lines
4.2 KiB
Bash
Executable File

#!/bin/sh
# Praxis — Install the systemd service for Docker-based deployment.
#
# Adapted from coreci/scripts/install-service.sh.
# Coreci installs a Go binary + systemd unit; praxis creates the env
# file from lxc.environment vars, installs the systemd unit that runs
# `docker compose up` (foreground, Type=simple per RESEARCH.md Q8),
# and starts it. The Docker image is built by ExecStartPre.
#
# This script runs INSIDE the CT (called by firstboot-hook.sh via pct exec).
# It must run as root.
set -e
USER_NAME="praxis"
GROUP_NAME="praxis"
DATA_DIR="/var/lib/praxis/data"
LOG_DIR="/var/log/praxis"
ENV_FILE="/etc/praxis/server.env"
SERVICE_FILE="/etc/systemd/system/praxis.service"
APP_DIR="/opt/praxis"
if [ "$(id -u)" -ne 0 ]; then
echo "install-service.sh: must run as root" >&2
exit 1
fi
# Create the praxis user if it does not exist.
if ! id "$USER_NAME" >/dev/null 2>&1; then
echo "Creating user $USER_NAME"
useradd --system --home "$DATA_DIR" --shell /usr/sbin/nologin "$USER_NAME"
fi
# Create data, log, and config directories.
mkdir -p "$DATA_DIR" "$LOG_DIR" /etc/praxis "$APP_DIR"
chown -R "$USER_NAME:$GROUP_NAME" "$DATA_DIR" "$LOG_DIR"
chown "root:$GROUP_NAME" /etc/praxis
chmod 0750 "$DATA_DIR" "$LOG_DIR" /etc/praxis
# Write the env file from the current environment (lxc.environment vars
# are available inside the CT's environment). This file is read by
# docker-compose.yml via env_file (G-101/G-102 secret injection chain).
# G-103 FIX: include ALL env vars the server reads.
cat > "$ENV_FILE" <<EOF
# Praxis service environment. Sourced by docker-compose.yml env_file.
# Do NOT commit — contains secrets injected via lxc.environment.
PRAXIS_HOST=${PRAXIS_HOST:-0.0.0.0}
PRAXIS_PORT=${PRAXIS_PORT:-8789}
PRAXIS_DB_PATH=${PRAXIS_DB_PATH:-/app/data/praxis.db}
PRAXIS_SCENARIOS_DIR=${PRAXIS_SCENARIOS_DIR:-/app/scenarios}
PRAXIS_TTS=${PRAXIS_TTS:-cartesia}
PRAXIS_SCENARIO=${PRAXIS_SCENARIO:-customer_service_refund_ca_v01}
DEEPGRAM_API_KEY=${DEEPGRAM_API_KEY:-}
CARTESIA_API_KEY=${CARTESIA_API_KEY:-}
OLLAMA_API_KEY=${OLLAMA_API_KEY:-}
OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-https://ollama.com/v1}
OLLAMA_CHAT_URL=${OLLAMA_CHAT_URL:-https://ollama.com/api/chat}
OLLAMA_ROLEPLAY_MODEL=${OLLAMA_ROLEPLAY_MODEL:-gemma4:cloud}
OLLAMA_DEBRIEF_MODEL=${OLLAMA_DEBRIEF_MODEL:-deepseek-v4-flash:cloud}
DEEPGRAM_MODEL=${DEEPGRAM_MODEL:-nova-3}
DEEPGRAM_LANGUAGE=${DEEPGRAM_LANGUAGE:-en}
DEEPGRAM_REGION=${DEEPGRAM_REGION:-na}
CARTESIA_VOICE_ID=${CARTESIA_VOICE_ID:-a3536a36-1d18-4efb-a95a-7c44b7b5e384}
EOF
chown "root:${GROUP_NAME}" "$ENV_FILE"
chmod 0640 "$ENV_FILE"
# Ensure curl is present for health checks (stock LXC templates may lack it).
if ! command -v curl >/dev/null 2>&1; then
apt-get update -qq && apt-get install -y -qq curl
fi
# Install the systemd unit.
cat > "$SERVICE_FILE" <<'UNIT'
[Unit]
Description=Praxis — voice-first AI apprenticeship platform
Documentation=https://git.cloudinit.dev/coreci/praxis
After=network-online.target docker.service
Wants=network-online.target
Requires=docker.service
[Service]
Type=simple
User=praxis
Group=praxis
WorkingDirectory=/opt/praxis
EnvironmentFile=-/etc/praxis/server.env
# Build the image first (ExecStartPre), then run in foreground.
# Type=simple + foreground `docker compose up` (no -d) so systemd
# tracks the process. TimeoutStartSec=600 covers the build (RESEARCH Q8).
ExecStartPre=/usr/bin/docker compose build
ExecStart=/usr/bin/docker compose up
ExecStop=/usr/bin/docker compose down
Restart=on-failure
RestartSec=5
TimeoutStartSec=600
TimeoutStopSec=60
# NOTE: Do NOT use coreci's hardening directives (ProtectSystem, PrivateDevices,
# etc.) — they break Docker's need to access /var/run/docker.sock, cgroups,
# and namespaces. Docker-in-LXC requires relaxed sandboxing (RESEARCH Q8).
StandardOutput=journal
StandardError=journal
SyslogIdentifier=praxis
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable praxis.service
# Start the service (this triggers ExecStartPre=docker compose build,
# which may take 3-5 min on first boot).
echo "Starting praxis service (Docker build may take 3-5 min)..."
systemctl start praxis.service || {
echo "Failed to start praxis; check 'journalctl -u praxis -n 50'" >&2
exit 1
}
echo "Praxis service installed and started."