From 1f92f4a9b933c57e133314b2bd2dd671343f94a5 Mon Sep 17 00:00:00 2001 From: nogueiraguh Date: Thu, 2 Apr 2026 15:07:45 -0300 Subject: [PATCH] feat: single container image (nginx + backend), fix fresh DB init - deploy/Dockerfile: unified image with supervisord (nginx + uvicorn) - distribution/docker-compose.single.yml: single container option - distribution/README.md: 3 deployment options (single, compose, docker run) - Fix: add status column to chat_messages CREATE TABLE (was only in migration, broke fresh DB) - push-images.sh: builds 3 images (unified, backend, frontend) --- backend/app.py | 1 + deploy/Dockerfile | 51 ++++++++++++++++++++++++++ deploy/scripts/push-images.sh | 27 ++++++++++---- distribution/README.md | 32 +++++++++++++++- distribution/docker-compose.single.yml | 31 ++++++++++++++++ 5 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 deploy/Dockerfile create mode 100644 distribution/docker-compose.single.yml diff --git a/backend/app.py b/backend/app.py index 2f6706c..9bde731 100644 --- a/backend/app.py +++ b/backend/app.py @@ -310,6 +310,7 @@ def init_db(): user_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, model_id TEXT, + status TEXT DEFAULT 'done', created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS chat_summaries ( diff --git a/deploy/Dockerfile b/deploy/Dockerfile new file mode 100644 index 0000000..26a78b5 --- /dev/null +++ b/deploy/Dockerfile @@ -0,0 +1,51 @@ +FROM python:3.12-slim + +WORKDIR /app + +# ── System deps + Chromium + Terraform + nginx + supervisor ───────────────── +ARG TERRAFORM_VERSION=1.14.7 +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl gcc libffi-dev unzip \ + chromium fonts-liberation fonts-dejavu-core fontconfig \ + nginx supervisor && \ + ARCH=$(dpkg --print-architecture) && \ + curl -fsSL "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_${ARCH}.zip" -o /tmp/tf.zip && \ + unzip /tmp/tf.zip -d /usr/local/bin/ && rm /tmp/tf.zip && \ + rm -rf /var/lib/apt/lists/* + +# ── Python deps + OCI CLI ─────────────────────────────────────────────────── +COPY backend/requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt && \ + pip install --no-cache-dir oci-cli + +# ── Backend app ───────────────────────────────────────────────────────────── +COPY backend/app.py . +COPY backend/cis_reports.py . +COPY backend/mcp_cis_server.py . +COPY backend/gen_tf_reference.py . + +# ── Frontend (pre-built React SPA) ───────────────────────────────────────── +RUN rm -rf /usr/share/nginx/html/* +COPY frontend-react/dist /usr/share/nginx/html/app + +# ── Nginx config (proxy to localhost instead of backend hostname) ─────────── +RUN printf 'map $uri $empty {\n default "";\n}\n\nserver {\n listen 8080;\n server_name _;\n client_max_body_size 50M;\n absolute_redirect off;\n\n location / {\n root /usr/share/nginx/html/app;\n try_files $uri $uri/ /index.html;\n add_header Cache-Control "no-cache, no-store, must-revalidate" always;\n add_header X-Frame-Options "DENY" always;\n add_header X-Content-Type-Options "nosniff" always;\n add_header X-XSS-Protection "1; mode=block" always;\n add_header Referrer-Policy "strict-origin-when-cross-origin" always;\n add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;\n }\n\n location /api/ {\n proxy_pass http://127.0.0.1:8000/api/;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_read_timeout 900s;\n proxy_send_timeout 900s;\n proxy_connect_timeout 60s;\n add_header X-Frame-Options "SAMEORIGIN" always;\n add_header X-Content-Type-Options "nosniff" always;\n }\n}\n' > /etc/nginx/sites-available/default + +# ── Supervisord config ────────────────────────────────────────────────────── +RUN printf '[supervisord]\nnodaemon=true\nuser=root\nlogfile=/var/log/supervisor/supervisord.log\nlogfile_maxbytes=10MB\n\n[program:backend]\ncommand=runuser -u agent -- uvicorn app:app --host 127.0.0.1 --port 8000 --workers 8\ndirectory=/app\nautostart=true\nautorestart=true\nstdout_logfile=/dev/fd/1\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/fd/2\nstderr_logfile_maxbytes=0\n\n[program:nginx]\ncommand=nginx -g "daemon off;"\nautostart=true\nautorestart=true\nstdout_logfile=/dev/fd/1\nstdout_logfile_maxbytes=0\nstderr_logfile=/dev/fd/2\nstderr_logfile_maxbytes=0\n' > /etc/supervisor/conf.d/app.conf + +# ── Non-root user + data dir ──────────────────────────────────────────────── +RUN groupadd -r agent && useradd -r -g agent -d /app -s /bin/bash agent && \ + mkdir -p /data /var/log/supervisor && chown -R agent:agent /app /data + +# ── Generate Terraform resource reference ─────────────────────────────────── +RUN python gen_tf_reference.py || echo "WARN: tf reference generation skipped" + +# ── Entrypoint ────────────────────────────────────────────────────────────── +RUN printf '#!/bin/bash\nchown -R agent:agent /data 2>/dev/null || true\nexec supervisord -c /etc/supervisor/conf.d/app.conf\n' > /entrypoint.sh && \ + chmod +x /entrypoint.sh + +VOLUME /data +EXPOSE 8080 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/deploy/scripts/push-images.sh b/deploy/scripts/push-images.sh index 0f2c8a6..b6ef5d5 100755 --- a/deploy/scripts/push-images.sh +++ b/deploy/scripts/push-images.sh @@ -9,12 +9,14 @@ OCIR_NAMESPACE="${OCIR_NAMESPACE:?Set OCIR_NAMESPACE}" OCIR_REPO_PREFIX="${OCIR_REPO_PREFIX:-oci-cis-agent}" IMAGE_TAG="${IMAGE_TAG:-latest}" +UNIFIED="${OCIR_REGION}.ocir.io/${OCIR_NAMESPACE}/${OCIR_REPO_PREFIX}:${IMAGE_TAG}" BACKEND="${OCIR_REGION}.ocir.io/${OCIR_NAMESPACE}/${OCIR_REPO_PREFIX}-backend:${IMAGE_TAG}" FRONTEND="${OCIR_REGION}.ocir.io/${OCIR_NAMESPACE}/${OCIR_REPO_PREFIX}-frontend:${IMAGE_TAG}" echo "========================================" echo "OCI CIS Agent — Build & Push to OCIR" echo "========================================" +echo "Unified: $UNIFIED" echo "Backend: $BACKEND" echo "Frontend: $FRONTEND" echo "" @@ -29,33 +31,44 @@ echo "Frontend build OK." # 2. Ensure buildx builder exists docker buildx inspect multiarch >/dev/null 2>&1 || \ docker buildx create --name multiarch --use - docker buildx use multiarch # 3. Login to OCIR echo ">>> Logging in to OCIR..." docker login "${OCIR_REGION}.ocir.io" -# 4. Build + push backend (multi-arch) -echo ">>> Building & pushing backend (amd64 + arm64)..." +# 4. Build + push UNIFIED image (single container — nginx + backend) +echo ">>> Building & pushing unified image (amd64 + arm64)..." cd "$PROJECT_ROOT" +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t "$UNIFIED" \ + --push \ + -f deploy/Dockerfile . +echo "Unified image pushed." + +# 5. Build + push backend (multi-arch) — for docker-compose deployments +echo ">>> Building & pushing backend (amd64 + arm64)..." docker buildx build \ --platform linux/amd64,linux/arm64 \ -t "$BACKEND" \ --push \ -f backend/Dockerfile backend/ +echo "Backend image pushed." -# 5. Build + push frontend (multi-arch) +# 6. Build + push frontend (multi-arch) — for docker-compose deployments echo ">>> Building & pushing frontend (amd64 + arm64)..." docker buildx build \ --platform linux/amd64,linux/arm64 \ -t "$FRONTEND" \ --push \ -f deploy/Dockerfile.frontend . +echo "Frontend image pushed." echo "" echo "========================================" -echo "Done! Both images pushed." -echo "Backend: $BACKEND" -echo "Frontend: $FRONTEND" +echo "All 3 images pushed!" +echo "Unified: $UNIFIED (single container)" +echo "Backend: $BACKEND (docker-compose)" +echo "Frontend: $FRONTEND (docker-compose)" echo "========================================" diff --git a/distribution/README.md b/distribution/README.md index 43b26a8..bca17f8 100644 --- a/distribution/README.md +++ b/distribution/README.md @@ -117,7 +117,7 @@ APP_SECRET= ### 2. Login to OCIR ```bash -docker login ${OCIR_REGION}.ocir.io +docker login .ocir.io ``` - **Username:** `/` or `/oracleidentitycloudservice/` @@ -125,17 +125,45 @@ docker login ${OCIR_REGION}.ocir.io ### 3. Run +**Option A — Single Container** (recommended for simplicity): + +```bash +docker compose -f docker-compose.single.yml up -d +``` + +One container runs everything (nginx + backend). Port `8080`. + +**Option B — Two Containers** (recommended for production): + ```bash docker compose up -d ``` +Separate backend and frontend containers with internal networking. + +**Option C — Docker Run** (no compose needed): + +```bash +docker run -d \ + --name oci-cis-agent \ + -p 8080:8080 \ + -v agent-data:/data \ + -e APP_SECRET=$(openssl rand -hex 64) \ + -e TZ=America/Sao_Paulo \ + .ocir.io//oci-cis-agent:latest +``` + ### 4. Access Open `http://localhost:8080` -The initial admin password is generated automatically and displayed in the backend logs: +The initial admin password is generated automatically and displayed in the container logs: ```bash +# Single container / docker run +docker logs oci-cis-agent | grep "password" + +# Two containers docker compose logs backend | grep "password" ``` diff --git a/distribution/docker-compose.single.yml b/distribution/docker-compose.single.yml new file mode 100644 index 0000000..05fa316 --- /dev/null +++ b/distribution/docker-compose.single.yml @@ -0,0 +1,31 @@ +services: + agent: + image: ${OCIR_REGION}.ocir.io/${OCIR_NAMESPACE}/oci-cis-agent:${IMAGE_TAG:-latest} + container_name: oci-cis-agent + restart: unless-stopped + environment: + - APP_SECRET=${APP_SECRET:?Set APP_SECRET in .env (openssl rand -hex 64)} + - JWT_EXPIRY_HOURS=${JWT_EXPIRY_HOURS:-12} + - DATA_DIR=/data + - CORS_ORIGINS=${CORS_ORIGINS:-} + - TZ=${TZ:-America/Sao_Paulo} + - OCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING=True + ports: + - "${PORT:-8080}:8080" + volumes: + - agent-data:/data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] + interval: 30s + timeout: 10s + retries: 3 + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + mem_limit: 4g + +volumes: + agent-data: + driver: local