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)
This commit is contained in:
nogueiraguh
2026-04-02 15:07:45 -03:00
parent db983935b1
commit 1f92f4a9b9
5 changed files with 133 additions and 9 deletions

View File

@@ -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 (

51
deploy/Dockerfile Normal file
View File

@@ -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"]

View File

@@ -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 "========================================"

View File

@@ -117,7 +117,7 @@ APP_SECRET=<generate with: openssl rand -hex 64>
### 2. Login to OCIR
```bash
docker login ${OCIR_REGION}.ocir.io
docker login <OCIR_REGION>.ocir.io
```
- **Username:** `<namespace>/<username>` or `<namespace>/oracleidentitycloudservice/<email>`
@@ -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_REGION>.ocir.io/<OCIR_NAMESPACE>/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"
```

View File

@@ -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