fix(infra): run services as padelnomics_service user instead of root
- setup_server.sh now requires root, creates padelnomics_service user, adds to docker group, generates deploy key in service user's home, owns /opt/padelnomics and /data/padelnomics to service user - supervisor service: User=padelnomics_service, updated PATH - landing-backup service: User=padelnomics_service Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=oneshot
|
||||||
|
User=padelnomics_service
|
||||||
EnvironmentFile=/opt/padelnomics/.env
|
EnvironmentFile=/opt/padelnomics/.env
|
||||||
Environment=LANDING_DIR=/data/padelnomics/landing
|
Environment=LANDING_DIR=/data/padelnomics/landing
|
||||||
ExecStart=/usr/bin/rclone sync ${LANDING_DIR} :s3:${LITESTREAM_R2_BUCKET}/padelnomics/landing \
|
ExecStart=/usr/bin/rclone sync ${LANDING_DIR} :s3:${LITESTREAM_R2_BUCKET}/padelnomics/landing \
|
||||||
|
|||||||
@@ -1,36 +1,58 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# One-time server setup: create app directory and GitLab deploy key.
|
# One-time server setup. Run as root on a fresh server.
|
||||||
# Run as root on a fresh server before deploying.
|
# Creates padelnomics_service user, installs system dependencies,
|
||||||
|
# and registers systemd services that run as that user.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# bash infra/setup_server.sh
|
# sudo bash infra/setup_server.sh
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
APP_DIR="/opt/padelnomics"
|
APP_DIR="/opt/padelnomics"
|
||||||
KEY_PATH="$HOME/.ssh/padelnomics_deploy"
|
SERVICE_USER="padelnomics_service"
|
||||||
|
SERVICE_HOME="/home/${SERVICE_USER}"
|
||||||
|
KEY_PATH="${SERVICE_HOME}/.ssh/padelnomics_deploy"
|
||||||
|
|
||||||
# Create app directory
|
# Ensure running as root
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo "Error: must run as root (use sudo)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create service user if not present
|
||||||
|
if ! id "$SERVICE_USER" &>/dev/null; then
|
||||||
|
useradd --system --create-home --shell /usr/sbin/nologin "$SERVICE_USER"
|
||||||
|
echo "Created user $SERVICE_USER"
|
||||||
|
else
|
||||||
|
echo "User $SERVICE_USER already exists, skipping"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add service user to docker group (needed for deploy.sh)
|
||||||
|
usermod -aG docker "$SERVICE_USER"
|
||||||
|
echo "Added $SERVICE_USER to docker group"
|
||||||
|
|
||||||
|
# Create app directory owned by service user
|
||||||
mkdir -p "$APP_DIR"
|
mkdir -p "$APP_DIR"
|
||||||
|
chown "$SERVICE_USER:$SERVICE_USER" "$APP_DIR"
|
||||||
echo "Created $APP_DIR"
|
echo "Created $APP_DIR"
|
||||||
|
|
||||||
# Generate deploy key if not already present
|
# Generate deploy key as service user if not present
|
||||||
if [ ! -f "$KEY_PATH" ]; then
|
if [ ! -f "$KEY_PATH" ]; then
|
||||||
mkdir -p "$HOME/.ssh"
|
sudo -u "$SERVICE_USER" mkdir -p "${SERVICE_HOME}/.ssh"
|
||||||
ssh-keygen -t ed25519 -f "$KEY_PATH" -N "" -C "padelnomics-server"
|
sudo -u "$SERVICE_USER" ssh-keygen -t ed25519 -f "$KEY_PATH" -N "" -C "padelnomics-server"
|
||||||
chmod 700 "$HOME/.ssh"
|
chmod 700 "${SERVICE_HOME}/.ssh"
|
||||||
chmod 600 "$KEY_PATH"
|
chmod 600 "$KEY_PATH"
|
||||||
chmod 644 "$KEY_PATH.pub"
|
chmod 644 "${KEY_PATH}.pub"
|
||||||
|
|
||||||
# Configure SSH to use this key for gitlab.com
|
# Configure SSH to use this key for gitlab.com
|
||||||
if ! grep -q "# padelnomics" "$HOME/.ssh/config" 2>/dev/null; then
|
if ! grep -q "# padelnomics" "${SERVICE_HOME}/.ssh/config" 2>/dev/null; then
|
||||||
cat >> "$HOME/.ssh/config" <<EOF
|
sudo -u "$SERVICE_USER" tee -a "${SERVICE_HOME}/.ssh/config" > /dev/null <<EOF
|
||||||
|
|
||||||
# padelnomics
|
# padelnomics
|
||||||
Host gitlab.com
|
Host gitlab.com
|
||||||
IdentityFile $KEY_PATH
|
IdentityFile $KEY_PATH
|
||||||
EOF
|
EOF
|
||||||
chmod 600 "$HOME/.ssh/config"
|
chmod 600 "${SERVICE_HOME}/.ssh/config"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Generated deploy key: $KEY_PATH"
|
echo "Generated deploy key: $KEY_PATH"
|
||||||
@@ -38,32 +60,27 @@ else
|
|||||||
echo "Deploy key already exists, skipping"
|
echo "Deploy key already exists, skipping"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# NOTE: sops + age installation and keypair generation is handled by deploy.sh
|
# Install rclone system-wide (we are root, no sudo needed)
|
||||||
# (self-provisioning on first run). No need to install here.
|
|
||||||
|
|
||||||
# Install rclone (landing zone backup to R2)
|
|
||||||
if ! command -v rclone &>/dev/null; then
|
if ! command -v rclone &>/dev/null; then
|
||||||
echo "Installing rclone..."
|
echo "Installing rclone..."
|
||||||
curl -fsSL https://rclone.org/install.sh | sudo bash
|
curl -fsSL https://rclone.org/install.sh | bash
|
||||||
echo "Installed rclone $(rclone version --check | head -1)"
|
echo "Installed rclone $(rclone --version | head -1)"
|
||||||
else
|
else
|
||||||
echo "rclone already installed, skipping"
|
echo "rclone already installed, skipping"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create landing data directory
|
# Create data directories owned by service user
|
||||||
mkdir -p /data/padelnomics/landing
|
mkdir -p /data/padelnomics/landing
|
||||||
|
chown -R "$SERVICE_USER:$SERVICE_USER" /data/padelnomics
|
||||||
echo "Created /data/padelnomics/landing"
|
echo "Created /data/padelnomics/landing"
|
||||||
|
|
||||||
# Install and enable landing backup timer
|
# Install and enable systemd services
|
||||||
cp "$APP_DIR/infra/landing-backup/padelnomics-landing-backup.service" /etc/systemd/system/
|
cp "$APP_DIR/infra/landing-backup/padelnomics-landing-backup.service" /etc/systemd/system/
|
||||||
cp "$APP_DIR/infra/landing-backup/padelnomics-landing-backup.timer" /etc/systemd/system/
|
cp "$APP_DIR/infra/landing-backup/padelnomics-landing-backup.timer" /etc/systemd/system/
|
||||||
|
cp "$APP_DIR/infra/supervisor/padelnomics-supervisor.service" /etc/systemd/system/
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable --now padelnomics-landing-backup.timer
|
systemctl enable --now padelnomics-landing-backup.timer
|
||||||
echo "Enabled landing backup timer (every 30 min)"
|
echo "Enabled landing backup timer (every 30 min)"
|
||||||
|
|
||||||
# Install and enable supervisor service
|
|
||||||
cp "$APP_DIR/infra/supervisor/padelnomics-supervisor.service" /etc/systemd/system/
|
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl enable --now padelnomics-supervisor.service
|
systemctl enable --now padelnomics-supervisor.service
|
||||||
echo "Enabled supervisor service"
|
echo "Enabled supervisor service"
|
||||||
|
|
||||||
@@ -71,10 +88,10 @@ echo ""
|
|||||||
echo "=== Next steps ==="
|
echo "=== Next steps ==="
|
||||||
echo "1. Add this deploy key to GitLab (Settings → Repository → Deploy Keys, read-only):"
|
echo "1. Add this deploy key to GitLab (Settings → Repository → Deploy Keys, read-only):"
|
||||||
echo ""
|
echo ""
|
||||||
cat "$KEY_PATH.pub"
|
cat "${KEY_PATH}.pub"
|
||||||
echo ""
|
echo ""
|
||||||
echo "2. Clone the repo:"
|
echo "2. Clone the repo as $SERVICE_USER:"
|
||||||
echo " git clone git@gitlab.com:YOUR_USER/padelnomics.git $APP_DIR"
|
echo " sudo -u $SERVICE_USER git clone git@gitlab.com:deemanone/padelnomics.git $APP_DIR"
|
||||||
echo ""
|
echo ""
|
||||||
echo "3. Deploy (first run installs sops+age and generates server keypair):"
|
echo "3. Deploy (first run generates server age keypair — follow the printed instructions):"
|
||||||
echo " cd $APP_DIR && bash deploy.sh"
|
echo " sudo -u $SERVICE_USER bash $APP_DIR/deploy.sh"
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=root
|
User=padelnomics_service
|
||||||
WorkingDirectory=/opt/padelnomics
|
WorkingDirectory=/opt/padelnomics
|
||||||
ExecStart=/bin/sh -c 'exec uv run python src/padelnomics/supervisor.py'
|
ExecStart=/bin/sh -c 'exec uv run python src/padelnomics/supervisor.py'
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=10
|
RestartSec=10
|
||||||
EnvironmentFile=/opt/padelnomics/.env
|
EnvironmentFile=/opt/padelnomics/.env
|
||||||
Environment=PATH=/root/.local/bin:/usr/local/bin:/usr/bin:/bin
|
Environment=PATH=/home/padelnomics_service/.local/bin:/usr/local/bin:/usr/bin:/bin
|
||||||
Environment=LANDING_DIR=/data/padelnomics/landing
|
Environment=LANDING_DIR=/data/padelnomics/landing
|
||||||
Environment=DUCKDB_PATH=/data/padelnomics/lakehouse.duckdb
|
Environment=DUCKDB_PATH=/data/padelnomics/lakehouse.duckdb
|
||||||
Environment=SERVING_DUCKDB_PATH=/data/padelnomics/analytics.duckdb
|
Environment=SERVING_DUCKDB_PATH=/data/padelnomics/analytics.duckdb
|
||||||
|
|||||||
Reference in New Issue
Block a user