From dad8f7573aa08275e688690fc9466452d33b637b Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Mon, 23 Mar 2026 14:05:33 +0000 Subject: [PATCH] Add deploy script, .env.example, and Apache reverse proxy config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deploy.sh: idempotent Ubuntu deployment (git pull → docker build → extract frontend → copy to /var/www/html/ac-helper/ → restart container) - .env.example: production template with APP_PORT=8100 - docker-compose.yml: port now ${APP_PORT:-8100}:8000, updated proxy comment to Apache VirtualHost snippet - .gitignore: whitelist .env.example Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 36 +++++++++ .gitignore | 1 + deploy.sh | 197 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 37 ++++++--- 4 files changed, 258 insertions(+), 13 deletions(-) create mode 100644 .env.example create mode 100755 deploy.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c039f0f --- /dev/null +++ b/.env.example @@ -0,0 +1,36 @@ +# AC Tool — production environment configuration +# Copy to .env and fill in the required values: +# cp .env.example .env && nano .env + +# ── Application ─────────────────────────────────────────────────────────────── +# Host port Docker will bind to (container always listens on 8000 internally) +APP_PORT=8100 + +# ── Azure AD / MSAL (SPA PKCE — no client secret required) ─────────────────── +AZURE_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385 +AZURE_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef +AZURE_REDIRECT_URI=https://ai-sandbox.oliver.solutions/ac-helper/ + +# ── Admin bootstrap ─────────────────────────────────────────────────────────── +# First login with this email automatically receives the admin role +ADMIN_EMAIL= + +# ── AI providers ────────────────────────────────────────────────────────────── +# Required: Gemini is used for AI spreadsheet commands +GEMINI_API_KEY= +GEMINI_MODEL=gemini-2.0-flash-exp + +# Optional: only needed if you use the brief-extraction feature with these providers +OPENAI_API_KEY= +ANTHROPIC_API_KEY= +LLAMA_CLOUD_API_KEY= + +# ── Security ────────────────────────────────────────────────────────────────── +# Generate with: python3 -c "import secrets; print(secrets.token_hex(32))" +SESSION_SECRET= + +# ── CORS ────────────────────────────────────────────────────────────────────── +ALLOWED_ORIGINS=https://ai-sandbox.oliver.solutions + +# ── Dev mode (must be false in production) ──────────────────────────────────── +DEV_MODE=false diff --git a/.gitignore b/.gitignore index 9880168..419553c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ __pycache__/ *.so .env .env.* +!.env.example venv/ .venv/ *.egg-info/ diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..6c30a75 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +# deploy.sh — idempotent deployment script for ac-tool +# Usage: sudo bash /opt/ac-tool/deploy.sh +set -euo pipefail + +# ── Config ──────────────────────────────────────────────────────────────────── +APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WEB_DIR="/var/www/html/ac-helper" +WEB_USER="www-data" +ENV_FILE="$APP_DIR/.env" +ENV_EXAMPLE="$APP_DIR/.env.example" +CONTAINER_NAME="ac-tool" +FE_BUILD_TAG="ac-tool-fe-extract" + +# ── Colours ─────────────────────────────────────────────────────────────────── +log() { echo -e "\033[1;34m▶\033[0m $*"; } +ok() { echo -e "\033[1;32m✔\033[0m $*"; } +warn() { echo -e "\033[1;33m⚠\033[0m $*"; } +die() { echo -e "\033[1;31m✖\033[0m $*" >&2; exit 1; } +hr() { echo "────────────────────────────────────────────────────────────"; } + +hr +echo " AC Tool — deployment" +echo " $(date '+%Y-%m-%d %H:%M:%S') | dir: $APP_DIR" +hr + +# ── 1. Root check ───────────────────────────────────────────────────────────── +if [[ $EUID -ne 0 ]]; then + die "Run as root: sudo bash $0" +fi + +# ── 2. Prerequisites ────────────────────────────────────────────────────────── +log "Checking prerequisites..." +command -v docker >/dev/null 2>&1 || die "Docker not installed" +command -v git >/dev/null 2>&1 || die "Git not installed" +docker compose version >/dev/null 2>&1 || die "Docker Compose plugin not found (need 'docker compose', not 'docker-compose')" +ok "Prerequisites OK" + +# ── 3. .env setup ───────────────────────────────────────────────────────────── +log "Checking .env..." +if [[ ! -f "$ENV_FILE" ]]; then + [[ ! -f "$ENV_EXAMPLE" ]] && die ".env.example not found in $APP_DIR" + cp "$ENV_EXAMPLE" "$ENV_FILE" + warn ".env created from .env.example" + warn "Fill in the required values and re-run:" + warn " nano $ENV_FILE" + exit 0 +fi + +# Load .env into the current shell so we can read APP_PORT +set -o allexport +# shellcheck disable=SC1090 +source "$ENV_FILE" +set +o allexport + +APP_PORT="${APP_PORT:-8100}" + +# Warn about blank required keys +MISSING="" +for KEY in GEMINI_API_KEY ADMIN_EMAIL SESSION_SECRET; do + VAL="${!KEY:-}" + [[ -z "$VAL" ]] && MISSING="$MISSING $KEY" +done +[[ -n "$MISSING" ]] && warn "These required keys are empty in .env:$MISSING" + +ok ".env loaded (APP_PORT=$APP_PORT)" + +# ── 4. Port check ───────────────────────────────────────────────────────────── +log "Checking port $APP_PORT..." +if ss -tlnp | grep -q ":${APP_PORT} "; then + # Port is in use — check if it's our own container + if docker ps --format '{{.Names}} {{.Ports}}' | grep -q "${CONTAINER_NAME}.*${APP_PORT}"; then + warn "Port $APP_PORT already used by our container (will be restarted)" + else + die "Port $APP_PORT is occupied by another process. Change APP_PORT in .env or free the port." + fi +else + ok "Port $APP_PORT is free" +fi + +# ── 5. Git pull ─────────────────────────────────────────────────────────────── +log "Pulling latest code..." +cd "$APP_DIR" +git fetch origin +LOCAL=$(git rev-parse HEAD) +REMOTE=$(git rev-parse '@{u}') +if [[ "$LOCAL" == "$REMOTE" ]]; then + ok "Already up to date ($(git rev-parse --short HEAD))" +else + git pull --ff-only || die "git pull failed — local changes detected. Stash or reset them first." + ok "Updated to $(git rev-parse --short HEAD)" +fi + +# ── 6. Docker build (with cache, refresh base images) ───────────────────────── +log "Building Docker image..." +docker compose build --pull +ok "Docker image built" + +# ── 7. Extract frontend from Docker build stage ─────────────────────────────── +log "Building and extracting frontend..." +TMPDIR=$(mktemp -d) +trap 'rm -rf "$TMPDIR"; docker rmi "$FE_BUILD_TAG" >/dev/null 2>&1 || true' EXIT + +# Build only the Node.js frontend-builder stage (no Python/LibreOffice — fast) +docker build --target frontend-builder --tag "$FE_BUILD_TAG" "$APP_DIR" + +# Copy dist/ out of the image via a throwaway container +EXTRACT_CONTAINER=$(docker create "$FE_BUILD_TAG") +docker cp "$EXTRACT_CONTAINER:/app/frontend/dist/." "$TMPDIR/" +docker rm "$EXTRACT_CONTAINER" >/dev/null + +FILE_COUNT=$(find "$TMPDIR" -type f | wc -l) +ok "Frontend built: $FILE_COUNT files" + +# ── 8. Deploy frontend static files ─────────────────────────────────────────── +log "Deploying frontend to $WEB_DIR..." +mkdir -p "$WEB_DIR" +# Wipe old files (preserve directory itself so Apache doesn't break mid-deploy) +find "${WEB_DIR}" -mindepth 1 -delete +cp -r "$TMPDIR/." "$WEB_DIR/" +chown -R "$WEB_USER:$WEB_USER" "$WEB_DIR" +chmod -R 755 "$WEB_DIR" +ok "Frontend deployed to $WEB_DIR" + +# ── 9. Restart container ────────────────────────────────────────────────────── +log "Restarting application container..." +docker compose down --remove-orphans 2>/dev/null || true +docker compose up -d +ok "Container started" + +# ── 10. Health check ────────────────────────────────────────────────────────── +log "Waiting for application to be healthy..." +HEALTH_URL="http://localhost:${APP_PORT}/health" +for i in $(seq 1 30); do + if curl -sf "$HEALTH_URL" >/dev/null 2>&1; then + ok "Application is healthy" + break + fi + if [[ $i -eq 30 ]]; then + die "Health check failed after 60s. Check logs: docker logs $CONTAINER_NAME" + fi + sleep 2 +done + +# ── 11. First-run initialisation ────────────────────────────────────────────── +DATA_DIR="$APP_DIR/data" +if [[ ! -f "$DATA_DIR/users.json" ]]; then + log "First run — initialising data directory..." + mkdir -p "$DATA_DIR/uploads" "$DATA_DIR/outputs" "$DATA_DIR/sheets" + # Trigger the app's startup seeding (dropdowns.json + users.json are created + # automatically on the first API request if they don't exist) + curl -sf "http://localhost:${APP_PORT}/api/dropdowns/categories" >/dev/null 2>&1 || true + ok "Data directory initialised at $DATA_DIR" +fi + +# ── 12. Apache config reminder ──────────────────────────────────────────────── +if ! grep -rq "ac-helper" /etc/apache2/sites-enabled/ 2>/dev/null; then + echo "" + hr + echo " Apache config not detected — add this inside your VirtualHost block:" + hr + cat < + ProxyPass http://localhost:${APP_PORT}/api/ + ProxyPassReverse http://localhost:${APP_PORT}/api/ + + + # Proxy WebSocket + ProxyPass /ac-helper/ws ws://localhost:${APP_PORT}/ws + ProxyPassReverse /ac-helper/ws ws://localhost:${APP_PORT}/ws + + # Serve frontend static files + Alias /ac-helper/ /var/www/html/ac-helper/ + + Options -Indexes + AllowOverride None + Require all granted + FallbackResource /ac-helper/index.html + + +APACHE + hr +fi + +# ── Summary ─────────────────────────────────────────────────────────────────── +echo "" +ok "Deployment complete!" +echo " Container: docker logs -f $CONTAINER_NAME" +echo " Health: $HEALTH_URL" +echo " Frontend: $WEB_DIR ($FILE_COUNT files)" +echo " Data: $DATA_DIR" +echo " Commit: $(git -C "$APP_DIR" rev-parse --short HEAD)" +echo "" diff --git a/docker-compose.yml b/docker-compose.yml index 253bfc4..09395e9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,28 @@ -# Nginx reverse proxy config (add to your nginx site config): +# Apache reverse proxy config (add inside your VirtualHost block): # -# location /ac-helper/ { -# proxy_pass http://localhost:8000/; -# proxy_http_version 1.1; -# proxy_set_header Upgrade $http_upgrade; -# proxy_set_header Connection "upgrade"; -# proxy_set_header Host $host; -# proxy_set_header X-Real-IP $remote_addr; -# } +# # Required modules: a2enmod proxy proxy_http proxy_wstunnel # -# This strips /ac-helper/ prefix before forwarding to the container. -# The frontend uses /ac-helper/api and /ac-helper/ws which the proxy forwards -# as /api and /ws to the backend. +# # Proxy API requests to the Docker container +# +# ProxyPass http://localhost:8100/api/ +# ProxyPassReverse http://localhost:8100/api/ +# +# +# # Proxy WebSocket +# ProxyPass /ac-helper/ws ws://localhost:8100/ws +# ProxyPassReverse /ac-helper/ws ws://localhost:8100/ws +# +# # Serve frontend static files directly from disk +# Alias /ac-helper/ /var/www/html/ac-helper/ +# +# Options -Indexes +# AllowOverride None +# Require all granted +# FallbackResource /ac-helper/index.html +# +# +# Apache serves static files; Docker handles /api and /ws only. +# APP_PORT in .env controls the host port (default: 8100). version: '3.9' @@ -21,7 +32,7 @@ services: container_name: ac-tool restart: unless-stopped ports: - - "8000:8000" + - "${APP_PORT:-8100}:8000" volumes: - ./data:/app/data environment: