d32e4d487e
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---
122 lines
4.2 KiB
Bash
Executable File
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." |