Add deploy script, .env.example, and Apache reverse proxy config
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
38a550bd5f
commit
dad8f7573a
4 changed files with 258 additions and 13 deletions
36
.env.example
Normal file
36
.env.example
Normal file
|
|
@ -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
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,6 +4,7 @@ __pycache__/
|
|||
*.so
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
venv/
|
||||
.venv/
|
||||
*.egg-info/
|
||||
|
|
|
|||
197
deploy.sh
Executable file
197
deploy.sh
Executable file
|
|
@ -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 <<APACHE
|
||||
|
||||
# Required: a2enmod proxy proxy_http proxy_wstunnel
|
||||
|
||||
# Proxy API to Docker
|
||||
<Location /ac-helper/api/>
|
||||
ProxyPass http://localhost:${APP_PORT}/api/
|
||||
ProxyPassReverse http://localhost:${APP_PORT}/api/
|
||||
</Location>
|
||||
|
||||
# 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/
|
||||
<Directory /var/www/html/ac-helper>
|
||||
Options -Indexes
|
||||
AllowOverride None
|
||||
Require all granted
|
||||
FallbackResource /ac-helper/index.html
|
||||
</Directory>
|
||||
|
||||
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 ""
|
||||
|
|
@ -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
|
||||
# <Location /ac-helper/api/>
|
||||
# ProxyPass http://localhost:8100/api/
|
||||
# ProxyPassReverse http://localhost:8100/api/
|
||||
# </Location>
|
||||
#
|
||||
# # 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/
|
||||
# <Directory /var/www/html/ac-helper>
|
||||
# Options -Indexes
|
||||
# AllowOverride None
|
||||
# Require all granted
|
||||
# FallbackResource /ac-helper/index.html
|
||||
# </Directory>
|
||||
#
|
||||
# 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:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue