Update deploy script and .env.example for PostgreSQL

- deploy.sh: wait for postgres healthcheck before app healthcheck
- deploy.sh: add one-time JSON→PostgreSQL migration prompt (step 13)
  with migration marker file to avoid re-prompting on future deploys
- deploy.sh: update summary to show both app and DB container names
- deploy.sh: check POSTGRES_PASSWORD in required keys
- .env.example: add POSTGRES_PASSWORD, ADMIN_EMAILS, emergency access vars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-03-23 19:53:27 +00:00
parent 8da149b84e
commit 1a1bc97bfc
2 changed files with 78 additions and 28 deletions

View file

@ -12,8 +12,17 @@ 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
# First login with these emails automatically receives the admin role (comma-separated)
ADMIN_EMAIL=
ADMIN_EMAILS=
# ── Emergency access (bypass SSO) ────────────────────────────────────────────
# Set EMERGENCY_TOKEN to a long random string to allow token-based login when
# Azure AD / 2FA is unavailable. Leave blank to disable entirely.
# Generate with: python3 -c "import secrets; print(secrets.token_hex(32))"
EMERGENCY_TOKEN=
EMERGENCY_USER_EMAIL=
EMERGENCY_USER_NAME=Emergency Access
# ── OpenAI ────────────────────────────────────────────────────────────────────
OPENAI_API_KEY=
@ -51,6 +60,11 @@ ENABLE_COST_ESTIMATION=true
MAX_PROCESSING_COST_USD=10.00
MAX_CONCURRENT_JOBS=5
# ── PostgreSQL ────────────────────────────────────────────────────────────────
# Password for the ac-tool DB user. Change before deploying.
# Generate with: python3 -c "import secrets; print(secrets.token_hex(24))"
POSTGRES_PASSWORD=
# ── Security ──────────────────────────────────────────────────────────────────
# Generate with: python3 -c "import secrets; print(secrets.token_hex(32))"
SESSION_SECRET=

View file

@ -10,6 +10,7 @@ WEB_USER="www-data"
ENV_FILE="$APP_DIR/.env"
ENV_EXAMPLE="$APP_DIR/.env.example"
CONTAINER_NAME="ac-tool"
DB_CONTAINER_NAME="ac-tool-db"
FE_BUILD_TAG="ac-tool-fe-extract"
# ── Colours ───────────────────────────────────────────────────────────────────
@ -47,28 +48,28 @@ if [[ ! -f "$ENV_FILE" ]]; then
exit 0
fi
# Load .env into the current shell so we can read APP_PORT
# Load .env into the current shell so we can read APP_PORT etc.
set -o allexport
# shellcheck disable=SC1090
source "$ENV_FILE"
set +o allexport
APP_PORT="${APP_PORT:-8100}"
POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-}"
# Warn about blank required keys
MISSING=""
for KEY in GEMINI_API_KEY ADMIN_EMAIL SESSION_SECRET; do
for KEY in GEMINI_API_KEY ADMIN_EMAIL SESSION_SECRET POSTGRES_PASSWORD; do
VAL="${!KEY:-}"
[[ -z "$VAL" ]] && MISSING="$MISSING $KEY"
done
[[ -n "$MISSING" ]] && warn "These required keys are empty in .env:$MISSING"
[[ -n "$MISSING" ]] && warn "These 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
@ -93,7 +94,7 @@ else
ok "Updated to $(git rev-parse --short HEAD)"
fi
# ── 6. Docker build (with cache, refresh base images) ─────────────────────────
# ── 6. Docker build ───────────────────────────────────────────────────────────
log "Building Docker image..."
docker compose build --pull
ok "Docker image built"
@ -103,10 +104,7 @@ 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
@ -117,20 +115,32 @@ 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..."
# ── 9. Restart containers ─────────────────────────────────────────────────────
log "Restarting containers (app + postgres)..."
docker compose down --remove-orphans 2>/dev/null || true
docker compose up -d
ok "Container started"
ok "Containers started"
# ── 10. Health check ──────────────────────────────────────────────────────────
# ── 10. Wait for PostgreSQL ────────────────────────────────────────────────────
log "Waiting for PostgreSQL to be ready..."
for i in $(seq 1 30); do
if docker exec "$DB_CONTAINER_NAME" pg_isready -U achelper -d achelper >/dev/null 2>&1; then
ok "PostgreSQL is ready"
break
fi
if [[ $i -eq 30 ]]; then
die "PostgreSQL did not become ready after 60s. Check logs: docker logs $DB_CONTAINER_NAME"
fi
sleep 2
done
# ── 11. Health check ──────────────────────────────────────────────────────────
log "Waiting for application to be healthy..."
HEALTH_URL="http://localhost:${APP_PORT}/health"
for i in $(seq 1 30); do
@ -144,18 +154,43 @@ for i in $(seq 1 30); do
sleep 2
done
# ── 11. First-run initialisation ──────────────────────────────────────────────
# ── 12. Data directory ────────────────────────────────────────────────────────
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"
mkdir -p "$DATA_DIR/uploads" "$DATA_DIR/outputs"
ok "Data directories ready at $DATA_DIR"
# ── 13. JSON → PostgreSQL migration (first deploy after adding Postgres) ───────
#
# If old JSON data files exist, offer to run the one-time migration script
# that imports users, clients, dropdowns, sheets, and export templates into DB.
#
JSON_MIGRATION_MARKER="$DATA_DIR/.pg_migrated"
if [[ ! -f "$JSON_MIGRATION_MARKER" ]]; then
HAS_JSON=false
for f in "$DATA_DIR/users.json" "$DATA_DIR/sheets_metadata.json" "$DATA_DIR/clients.json"; do
[[ -f "$f" ]] && HAS_JSON=true && break
done
if [[ "$HAS_JSON" == "true" ]]; then
echo ""
warn "Old JSON data files detected. Run the one-time migration to import them into PostgreSQL?"
warn " yes — migrate now (recommended)"
warn " no — skip (data already in DB or you'll migrate manually)"
read -r -p " Migrate now? [yes/no]: " MIGRATE_ANSWER </dev/tty
if [[ "${MIGRATE_ANSWER,,}" == "yes" ]]; then
log "Running JSON → PostgreSQL migration..."
docker exec "$CONTAINER_NAME" python -m server.db.migrate_json \
&& touch "$JSON_MIGRATION_MARKER" \
&& ok "Migration complete. Marker written to $JSON_MIGRATION_MARKER" \
|| warn "Migration reported errors — check logs above. Re-run manually with: docker exec $CONTAINER_NAME python -m server.db.migrate_json"
else
warn "Skipped. To migrate manually: docker exec $CONTAINER_NAME python -m server.db.migrate_json"
warn "To suppress this prompt in future deployments, create: $JSON_MIGRATION_MARKER"
fi
fi
fi
# ── 12. Apache config reminder ────────────────────────────────────────────────
# ── 14. Apache config reminder ────────────────────────────────────────────────
if ! grep -rq "ac-helper" /etc/apache2/sites-enabled/ 2>/dev/null; then
echo ""
hr
@ -191,9 +226,10 @@ 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 " App container: docker logs -f $CONTAINER_NAME"
echo " DB container: docker logs -f $DB_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 ""