diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 00000000..f2ec8efb --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,13 @@ +venv/ +__pycache__/ +*.pyc +*.pyo +.env +uploads/ +temp/ +tests/ +*.md +*.pdf +*.txt +*.png +*.sh diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..04fcc518 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Install system deps for bcrypt, pymongo, etc. +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies first (layer caching) +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Create upload/temp directories +RUN mkdir -p /app/uploads /app/temp + +EXPOSE 5137 + +CMD ["python", "run.py"] diff --git a/deploy.sh b/deploy.sh index 3703d31d..39c411e2 100755 --- a/deploy.sh +++ b/deploy.sh @@ -3,7 +3,7 @@ set -e # ───────────────────────────────────────────────────────────────────────────── # Semblance — deploy script -# Builds frontend locally, rsyncs dist/ + backend to server, manages services. +# Builds frontend locally, rsyncs dist/ to server, restarts Docker services. # # Server: optical-dev.oliver.solution (Ubuntu 24.04, Apache2, Docker) # SSH alias: optical-dev (see ~/.ssh/config) @@ -12,16 +12,14 @@ set -e # ssh optical-dev # sudo mkdir -p /opt/semblance && sudo chown vadym.samoilenko:vadym.samoilenko /opt/semblance # git clone git@bitbucket.org:zlalani/semblance-dev.git /opt/semblance -# cp /opt/semblance/backend/.env.example /opt/semblance/backend/.env && nano /opt/semblance/backend/.env -# sudo cp /opt/semblance/semblance.service /etc/systemd/system/ -# sudo systemctl daemon-reload && sudo systemctl enable semblance.service +# cp /opt/semblance/backend/.env.example /opt/semblance/backend/.env +# nano /opt/semblance/backend/.env # fill in real values +# cd /opt/semblance && docker compose up -d --build # ───────────────────────────────────────────────────────────────────────────── SSH_HOST="optical-dev" DEPLOY_DIR="/opt/semblance" FRONTEND_DEST="/var/www/html/semblance" -BACKEND_DIR="$DEPLOY_DIR/backend" -PYTHON_CMD="python3" echo "======================================" echo "Semblance — starting deployment" @@ -30,18 +28,18 @@ echo "======================================" # ── Pre-flight: backend/.env must exist on server ──────────────────────────── echo "" echo "[pre-flight] Checking backend/.env on server..." -if ! ssh "$SSH_HOST" "test -f $BACKEND_DIR/.env"; then +if ! ssh "$SSH_HOST" "test -f $DEPLOY_DIR/backend/.env"; then echo "" - echo "ERROR: $BACKEND_DIR/.env not found on server." + echo "ERROR: $DEPLOY_DIR/backend/.env not found on server." echo "Run on server:" - echo " cp $BACKEND_DIR/.env.example $BACKEND_DIR/.env" - echo " nano $BACKEND_DIR/.env" + echo " cp $DEPLOY_DIR/backend/.env.example $DEPLOY_DIR/backend/.env" + echo " nano $DEPLOY_DIR/backend/.env" exit 1 fi for VAR in SECRET_KEY JWT_SECRET_KEY OPENAI_API_KEY GEMINI_API_KEY; do - if ! ssh "$SSH_HOST" "grep -q '^${VAR}=.\+' $BACKEND_DIR/.env 2>/dev/null"; then - echo "ERROR: $VAR is not set in $BACKEND_DIR/.env on server" + if ! ssh "$SSH_HOST" "grep -q '^${VAR}=.\+' $DEPLOY_DIR/backend/.env 2>/dev/null"; then + echo "ERROR: $VAR is not set in backend/.env on server" exit 1 fi done @@ -49,12 +47,12 @@ echo "✓ backend/.env present and required vars set" # ── Step 1: Pull latest code on server ─────────────────────────────────────── echo "" -echo "[1/7] Pulling latest code on server..." +echo "[1/5] Pulling latest code on server..." ssh "$SSH_HOST" "cd $DEPLOY_DIR && git pull" # ── Step 2: Build frontend locally ─────────────────────────────────────────── echo "" -echo "[2/7] Building frontend locally..." +echo "[2/5] Building frontend locally..." cp .env.production .env npm install --silent npm run build @@ -62,48 +60,15 @@ echo "✓ Frontend built (dist/)" # ── Step 3: Deploy frontend to server ──────────────────────────────────────── echo "" -echo "[3/7] Deploying frontend to server..." -ssh "$SSH_HOST" "sudo mkdir -p $FRONTEND_DEST && sudo chown vadym.samoilenko:www-data $FRONTEND_DEST && sudo chmod 755 $FRONTEND_DEST" +echo "[3/5] Deploying frontend to server..." +ssh "$SSH_HOST" "sudo mkdir -p $FRONTEND_DEST" rsync -az --delete dist/ "$SSH_HOST:$FRONTEND_DEST/" ssh "$SSH_HOST" "sudo chown -R www-data:www-data $FRONTEND_DEST" echo "✓ Frontend deployed to $FRONTEND_DEST" -# ── Step 4: Set up Python virtual environment on server ────────────────────── +# ── Step 4: Ensure Apache proxy config for semblance ───────────────────────── echo "" -echo "[4/7] Setting up Python virtual environment..." -ssh "$SSH_HOST" " - cd $BACKEND_DIR - if [ ! -d venv ]; then - echo 'Creating new virtual environment...' - $PYTHON_CMD -m venv venv - fi - source venv/bin/activate - pip install -r requirements.txt --quiet - echo '✓ Python dependencies installed' -" - -# ── Step 5: Ensure MongoDB is running in Docker ─────────────────────────────── -echo "" -echo "[5/7] Ensuring MongoDB is running..." -ssh "$SSH_HOST" " - if ! docker ps --format '{{.Names}}' | grep -q '^semblance-mongo\$'; then - echo 'Starting MongoDB container...' - docker run -d \ - --name semblance-mongo \ - --restart unless-stopped \ - -p 127.0.0.1:27017:27017 \ - -v semblance-mongo-data:/data/db \ - mongo:7 - sleep 3 - echo '✓ MongoDB started' - else - echo '✓ MongoDB already running' - fi -" - -# ── Step 6: Ensure Apache proxy config for semblance ───────────────────────── -echo "" -echo "[6/7] Ensuring Apache proxy config..." +echo "[4/5] Ensuring Apache proxy config..." ssh "$SSH_HOST" " CONF='/etc/apache2/sites-enabled/optical-dev.oliver.solutions.conf' @@ -111,31 +76,22 @@ ssh "$SSH_HOST" " echo '✓ Apache semblance config already present' else echo 'Adding semblance blocks to Apache config...' - - # Insert before the closing tag sudo sed -i 's|| # ----------------------------------------------------------------\n # Semblance — Quart/Hypercorn backend at :5137\n # ----------------------------------------------------------------\n\n # WebSocket (Socket.IO)\n RewriteCond %{HTTP:Upgrade} websocket [NC]\n RewriteCond %{HTTP:Connection} upgrade [NC]\n RewriteRule ^/semblance_back/socket.io/(.*) ws://127.0.0.1:5137/socket.io/\$1 [P,L]\n\n # REST API\n ProxyPass /semblance_back/api/ http://127.0.0.1:5137/api/\n ProxyPassReverse /semblance_back/api/ http://127.0.0.1:5137/api/\n\n # Socket.IO HTTP polling fallback\n ProxyPass /semblance_back/socket.io/ http://127.0.0.1:5137/socket.io/\n ProxyPassReverse /semblance_back/socket.io/ http://127.0.0.1:5137/socket.io/\n\n # Semblance SPA — served at /semblance/\n Alias /semblance $FRONTEND_DEST\n \n Options -Indexes +FollowSymLinks\n AllowOverride None\n Require all granted\n RewriteEngine On\n RewriteBase /semblance/\n RewriteCond %{REQUEST_FILENAME} !-f\n RewriteCond %{REQUEST_FILENAME} !-d\n RewriteRule ^ index.html [L]\n \n\n|' \"\$CONF\" - sudo apache2ctl configtest && sudo systemctl reload apache2 echo '✓ Apache config updated and reloaded' fi " -# ── Step 7: Install / restart backend service ──────────────────────────────── +# ── Step 5: Rebuild and restart Docker services ─────────────────────────────── echo "" -echo "[7/7] Restarting backend service..." +echo "[5/5] Rebuilding and restarting Docker services..." ssh "$SSH_HOST" " - # Install or update service file - sudo cp $DEPLOY_DIR/semblance.service /etc/systemd/system/semblance.service - sudo systemctl daemon-reload - sudo systemctl enable semblance.service - - # Ensure upload/temp dirs exist with correct ownership - sudo mkdir -p $BACKEND_DIR/uploads $BACKEND_DIR/temp - sudo chown -R www-data:www-data $BACKEND_DIR/uploads $BACKEND_DIR/temp - - sudo systemctl restart semblance.service - sleep 2 - systemctl status semblance.service --no-pager || true + cd $DEPLOY_DIR + docker compose pull mongo 2>/dev/null || true + docker compose up -d --build + echo '' + echo 'Container status:' + docker compose ps " echo "" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..656f3bda --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +services: + mongo: + image: mongo:7 + restart: unless-stopped + ports: + - "127.0.0.1:27017:27017" + volumes: + - mongo-data:/data/db + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + + backend: + build: ./backend + restart: unless-stopped + ports: + - "127.0.0.1:5137:5137" + env_file: + - ./backend/.env + environment: + MONGO_URI: mongodb://mongo:27017/semblance_db + volumes: + - ./backend/uploads:/app/uploads + - ./backend/temp:/app/temp + depends_on: + mongo: + condition: service_healthy + +volumes: + mongo-data: diff --git a/semblance.service b/semblance.service deleted file mode 100755 index dd1249e2..00000000 --- a/semblance.service +++ /dev/null @@ -1,42 +0,0 @@ -[Unit] -Description=Semblance back end service -After=network.target - -[Service] -Type=exec -User=www-data -Group=www-data -WorkingDirectory=/opt/semblance/backend -EnvironmentFile=/opt/semblance/backend/.env -Environment=PATH=/opt/semblance/backend/venv/bin -ExecStart=/opt/semblance/backend/venv/bin/python /opt/semblance/backend/run.py -Restart=always -RestartSec=5 - -# Output to journal -StandardOutput=journal -StandardError=journal -SyslogIdentifier=semblance - -# Security settings (adjusted for file uploads) -NoNewPrivileges=yes -ProtectSystem=false -ProtectHome=yes - -# Allow access to temp directories -PrivateTmp=no - -# Writable directories for uploads and temp files -ReadWritePaths=/opt/semblance/backend/uploads -ReadWritePaths=/opt/semblance/backend/temp -ReadWritePaths=/tmp -ReadWritePaths=/var/tmp - -# Create necessary directories -ExecStartPre=/bin/mkdir -p /opt/semblance/backend/uploads -ExecStartPre=/bin/mkdir -p /opt/semblance/backend/temp -ExecStartPre=/bin/chown -R www-data:www-data /opt/semblance/backend/uploads -ExecStartPre=/bin/chown -R www-data:www-data /opt/semblance/backend/temp - -[Install] -WantedBy=multi-user.target