Files
praxis/scripts/install-service.sh
T
Praxis CI d32e4d487e feat(P01): SLICE-05+06+07 — firstboot hook, install-service, deploy orchestrator
SLICE-05 (devops-engineer): firstboot-hook.sh — installs Docker inside CT
  (apt: docker.io, docker-compose-v2, git, curl), clones praxis repo from
  Gitea (GITEA_TOKEN baked into snippet per G-101 fix), runs install-service.sh
SLICE-06 (devops-engineer): install-service.sh — creates praxis user, writes
  /etc/praxis/server.env from lxc.environment vars (G-103: all 16 env vars),
  installs praxis.service systemd unit (Type=simple, ExecStartPre=docker compose
  build, ExecStart=docker compose up, TimeoutStartSec=600 per RESEARCH Q8)
SLICE-07 (devops-engineer): lxc-deploy.sh orchestrator — stage snippet →
  clone → config → start → health-check, idempotent (--recreate/--reconfigure),
  rollback on failure, auto VMID allocation (D-027)

REQ-DEPLOY-06, 09, 10, 11 covered.

---ci---
project: praxis
phase: 1
milestone: v0.2
status: execute
slice: 05-07
wave: 3
---/ci---
2026-08-01 14:19:05 +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=root
Group=root
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."