Compare commits
2 Commits
eb627047e5
...
3dd38dc6fa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dd38dc6fa | ||
|
|
7df1b9f718 |
53
.dockerignore
Normal file
53
.dockerignore
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
*.so
|
||||||
|
*.egg
|
||||||
|
*.egg-info
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.pytest_cache
|
||||||
|
.coverage
|
||||||
|
htmlcov
|
||||||
|
.mypy_cache
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv
|
||||||
|
env
|
||||||
|
ENV
|
||||||
|
.venv
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# Lock files (not needed for pip install)
|
||||||
|
uv.lock
|
||||||
|
|
||||||
|
# Vercel
|
||||||
|
vercel.json
|
||||||
|
|
||||||
|
# Distribution files
|
||||||
|
*.tar.gz
|
||||||
|
*.zip
|
||||||
215
DOCKER.md
Normal file
215
DOCKER.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# Docker Setup Guide
|
||||||
|
|
||||||
|
This document explains how to build and run the Akern-Genai application using Docker.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Docker configuration includes:
|
||||||
|
|
||||||
|
- **Multi-stage build** for smaller image size
|
||||||
|
- **Entrypoint script** with proper signal handling and exit status management
|
||||||
|
- **Configurable port** via environment variables
|
||||||
|
- **Health checks** for monitoring
|
||||||
|
- **Non-root user** for security
|
||||||
|
- **Optimized build** with `.dockerignore`
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- [`Dockerfile`](Dockerfile) - Multi-stage Docker build configuration
|
||||||
|
- [`entrypoint.sh`](entrypoint.sh) - Entrypoint script with signal handling
|
||||||
|
- [`docker-compose.yml`](docker-compose.yml) - Docker Compose configuration
|
||||||
|
- [`.dockerignore`](.dockerignore) - Files excluded from Docker build context
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The following environment variables can be configured:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
| ----------- | --------- | ----------------------------------------------------- |
|
||||||
|
| `PORT` | `8000` | Port on which the application listens |
|
||||||
|
| `HOST` | `0.0.0.0` | Host address to bind to |
|
||||||
|
| `WORKERS` | `1` | Number of uvicorn worker processes |
|
||||||
|
| `LOG_LEVEL` | `info` | Logging level (debug, info, warning, error, critical) |
|
||||||
|
|
||||||
|
## Building the Image
|
||||||
|
|
||||||
|
### Using Docker directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t akern-genai:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Docker Compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Container
|
||||||
|
|
||||||
|
### Using Docker directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run with default configuration (port 8000)
|
||||||
|
docker run -p 8000:8000 akern-genai:latest
|
||||||
|
|
||||||
|
# Run with custom port
|
||||||
|
docker run -p 3000:3000 -e PORT=3000 akern-genai:latest
|
||||||
|
|
||||||
|
# Run with multiple workers
|
||||||
|
docker run -p 8000:8000 -e WORKERS=4 akern-genai:latest
|
||||||
|
|
||||||
|
# Run with custom log level
|
||||||
|
docker run -p 8000:8000 -e LOG_LEVEL=debug akern-genai:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Docker Compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run with default configuration
|
||||||
|
docker-compose up
|
||||||
|
|
||||||
|
# Run with custom port
|
||||||
|
PORT=3000 docker-compose up
|
||||||
|
|
||||||
|
# Run in detached mode
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# Stop the container
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using environment file:
|
||||||
|
|
||||||
|
Create a `.env` file in the project root:
|
||||||
|
|
||||||
|
```env
|
||||||
|
PORT=8000
|
||||||
|
HOST=0.0.0.0
|
||||||
|
WORKERS=1
|
||||||
|
LOG_LEVEL=info
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
## Signal Handling
|
||||||
|
|
||||||
|
The entrypoint script properly handles termination signals:
|
||||||
|
|
||||||
|
- **SIGTERM** - Graceful shutdown (sent by `docker stop`)
|
||||||
|
- **SIGINT** - Interrupt signal (Ctrl+C)
|
||||||
|
|
||||||
|
The script will:
|
||||||
|
|
||||||
|
1. Forward the signal to uvicorn
|
||||||
|
2. Wait up to 30 seconds for graceful shutdown
|
||||||
|
3. Force kill if shutdown doesn't complete in time
|
||||||
|
4. Exit with the same status code as uvicorn
|
||||||
|
|
||||||
|
## Health Check
|
||||||
|
|
||||||
|
The container includes a health check that monitors the application:
|
||||||
|
|
||||||
|
- **Interval**: 30 seconds
|
||||||
|
- **Timeout**: 10 seconds
|
||||||
|
- **Start period**: 5 seconds
|
||||||
|
- **Retries**: 3
|
||||||
|
|
||||||
|
Check container health:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker ps
|
||||||
|
docker inspect <container_id> | grep -A 10 Health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Stage Build
|
||||||
|
|
||||||
|
The Dockerfile uses a two-stage build:
|
||||||
|
|
||||||
|
### Stage 1: Builder
|
||||||
|
|
||||||
|
- Installs build dependencies (gcc, musl-dev, etc.)
|
||||||
|
- Creates a virtual environment
|
||||||
|
- Installs Python dependencies
|
||||||
|
|
||||||
|
### Stage 2: Runtime
|
||||||
|
|
||||||
|
- Uses minimal Alpine Linux
|
||||||
|
- Only includes runtime dependencies
|
||||||
|
- Copies the virtual environment from builder
|
||||||
|
- Runs as non-root user
|
||||||
|
|
||||||
|
This results in a significantly smaller final image.
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
- **Non-root user**: Application runs as `appuser` (UID 1000)
|
||||||
|
- **Minimal base image**: Alpine Linux with only necessary packages
|
||||||
|
- **No build tools in runtime**: Compiler and build tools are not included in final image
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Container exits immediately
|
||||||
|
|
||||||
|
Check logs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs <container_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port already in use
|
||||||
|
|
||||||
|
Change the port:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -p 3000:3000 -e PORT=3000 akern-genai:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Health check failing
|
||||||
|
|
||||||
|
Verify the application is running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -it <container_id> wget -O- http://localhost:8000/
|
||||||
|
```
|
||||||
|
|
||||||
|
### View real-time logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs -f <container_id>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Image Size Comparison
|
||||||
|
|
||||||
|
- **Before**: ~200-300 MB (single stage with build tools)
|
||||||
|
- **After**: ~100-150 MB (multi-stage, minimal runtime)
|
||||||
|
|
||||||
|
## Production Considerations
|
||||||
|
|
||||||
|
For production deployment:
|
||||||
|
|
||||||
|
1. **Use specific image tags** instead of `latest`
|
||||||
|
2. **Set appropriate worker count** based on CPU cores
|
||||||
|
3. **Configure resource limits** in docker-compose.yml or Kubernetes
|
||||||
|
4. **Use a reverse proxy** (nginx, traefik) for SSL termination
|
||||||
|
5. **Set up logging aggregation** (ELK, Loki, etc.)
|
||||||
|
6. **Configure monitoring** (Prometheus, Grafana)
|
||||||
|
|
||||||
|
Example docker-compose.yml with resource limits:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: "2"
|
||||||
|
memory: 1G
|
||||||
|
reservations:
|
||||||
|
cpus: "0.5"
|
||||||
|
memory: 512M
|
||||||
|
```
|
||||||
85
Dockerfile
Normal file
85
Dockerfile
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# ============================================
|
||||||
|
# Stage 1: Builder
|
||||||
|
# ============================================
|
||||||
|
FROM python:3.13-alpine AS builder
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
gcc \
|
||||||
|
musl-dev \
|
||||||
|
libffi-dev \
|
||||||
|
openssl-dev \
|
||||||
|
cargo
|
||||||
|
|
||||||
|
# Create virtual environment
|
||||||
|
RUN python -m venv /opt/venv
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Copy requirements first for better caching
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN pip install --no-cache-dir --upgrade pip && \
|
||||||
|
pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Stage 2: Runtime
|
||||||
|
# ============================================
|
||||||
|
FROM python:3.13-alpine AS runtime
|
||||||
|
|
||||||
|
# Install runtime dependencies only
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
libstdc++ \
|
||||||
|
ca-certificates
|
||||||
|
|
||||||
|
# Create non-root user for security
|
||||||
|
RUN addgroup -g 1000 appuser && \
|
||||||
|
adduser -D -u 1000 -G appuser appuser
|
||||||
|
|
||||||
|
# Copy credentials file if it exists
|
||||||
|
COPY --chown=appuser:appuser credentials.json .
|
||||||
|
|
||||||
|
# Copy virtual environment from builder
|
||||||
|
COPY --from=builder /opt/venv /opt/venv
|
||||||
|
ENV PATH="/opt/venv/bin:$PATH"
|
||||||
|
|
||||||
|
# Create application directory
|
||||||
|
RUN mkdir /app && \
|
||||||
|
chown -R appuser:appuser /app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY --chown=appuser:appuser app.py .
|
||||||
|
COPY --chown=appuser:appuser main.py .
|
||||||
|
COPY --chown=appuser:appuser static ./static
|
||||||
|
|
||||||
|
# Copy and setup entrypoint script
|
||||||
|
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
|
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Expose default port (can be overridden via PORT env var)
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD wget --no-verbose --tries=1 --spider http://localhost:${PORT:-8000}/ || exit 1
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PORT=8000 \
|
||||||
|
HOST=0.0.0.0 \
|
||||||
|
WORKERS=1 \
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# Use entrypoint script
|
||||||
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
||||||
|
|
||||||
|
|
||||||
32
docker-compose.yml
Normal file
32
docker-compose.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: akern-genai-app
|
||||||
|
ports:
|
||||||
|
- "${PORT:-8000}:8000"
|
||||||
|
environment:
|
||||||
|
# Application configuration
|
||||||
|
- PORT=${PORT:-8000}
|
||||||
|
- HOST=${HOST:-0.0.0.0}
|
||||||
|
- WORKERS=${WORKERS:-1}
|
||||||
|
- LOG_LEVEL=${LOG_LEVEL:-info}
|
||||||
|
|
||||||
|
# Google GenAI configuration (load from .env file)
|
||||||
|
- GOOGLE_APPLICATION_CREDENTIALS=${GOOGLE_APPLICATION_CREDENTIALS}
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"wget",
|
||||||
|
"--no-verbose",
|
||||||
|
"--tries=1",
|
||||||
|
"--spider",
|
||||||
|
"http://${HOST:-127.0.0.1}:${PORT:-8000}/",
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 5s
|
||||||
55
entrypoint.sh
Normal file
55
entrypoint.sh
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Default values
|
||||||
|
PORT="${PORT:-8000}"
|
||||||
|
HOST="${HOST:-0.0.0.0}"
|
||||||
|
WORKERS="${WORKERS:-1}"
|
||||||
|
LOG_LEVEL="${LOG_LEVEL:-info}"
|
||||||
|
|
||||||
|
# Display configuration
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Starting FastAPI application"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Host: ${HOST}"
|
||||||
|
echo "Port: ${PORT}"
|
||||||
|
echo "Workers: ${WORKERS}"
|
||||||
|
echo "Log Level: ${LOG_LEVEL}"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# Trap signals for graceful shutdown
|
||||||
|
cleanup() {
|
||||||
|
echo "Received termination signal, shutting down gracefully..."
|
||||||
|
# Send SIGTERM to uvicorn process group
|
||||||
|
if [ -n "$UVICORN_PID" ]; then
|
||||||
|
kill -TERM "$UVICORN_PID" 2>/dev/null || true
|
||||||
|
# Wait for uvicorn to exit, but with a timeout
|
||||||
|
timeout 30 sh -c "while kill -0 $UVICORN_PID 2>/dev/null; do sleep 1; done" || {
|
||||||
|
echo "Uvicorn did not shut down gracefully, forcing exit..."
|
||||||
|
kill -KILL "$UVICORN_PID" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Register signal handlers
|
||||||
|
trap cleanup SIGTERM SIGINT
|
||||||
|
|
||||||
|
# Start uvicorn in the background to capture PID
|
||||||
|
uvicorn app:app \
|
||||||
|
--host "${HOST}" \
|
||||||
|
--port "${PORT}" \
|
||||||
|
--workers "${WORKERS}" \
|
||||||
|
--log-level "${LOG_LEVEL}" \
|
||||||
|
--access-log \
|
||||||
|
&
|
||||||
|
|
||||||
|
UVICORN_PID=$!
|
||||||
|
|
||||||
|
# Wait for uvicorn process
|
||||||
|
wait $UVICORN_PID
|
||||||
|
EXIT_STATUS=$?
|
||||||
|
|
||||||
|
# Exit with the same status as uvicorn
|
||||||
|
echo "Uvicorn exited with status: ${EXIT_STATUS}"
|
||||||
|
exit ${EXIT_STATUS}
|
||||||
@@ -1,34 +1,35 @@
|
|||||||
(($) => {
|
(($) => {
|
||||||
var ws = new WebSocket("ws://localhost:8000/ws");
|
var ws = new WebSocket(
|
||||||
const input = $("#message");
|
`ws://${location.protocol + "//" + location.host}${location.port ? ":" + location.port : ""}/ws`,
|
||||||
const messages = $('#messages');
|
);
|
||||||
var lastMessage;
|
const input = $("#message");
|
||||||
|
const messages = $("#messages");
|
||||||
|
var lastMessage;
|
||||||
|
|
||||||
$('#button').on('click', () => {
|
$("#button").on("click", () => {
|
||||||
const message = input.val();
|
const message = input.val();
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
ws.send(message);
|
ws.send(message);
|
||||||
lastMessage = $('<div class="message received"><p>Loading...</p></div>')
|
lastMessage = $('<div class="message received"><p>Loading...</p></div>');
|
||||||
messages.append(`<div class="message sent"><p>${message}</p></div>`)
|
messages.append(`<div class="message sent"><p>${message}</p></div>`);
|
||||||
messages.append(lastMessage);
|
messages.append(lastMessage);
|
||||||
|
|
||||||
input.val("");
|
input.val("");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
ws.onmessage = (event) => {
|
||||||
if (lastMessage.text() === "Loading...") {
|
if (lastMessage.text() === "Loading...") {
|
||||||
lastMessage.empty();
|
lastMessage.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
var content = document.createTextNode(event.data);
|
var content = document.createTextNode(event.data);
|
||||||
|
|
||||||
if (content.textContent === "<<END>>") {
|
if (content.textContent === "<<END>>") {
|
||||||
lastMessage.html(marked.parse(lastMessage.text()));
|
lastMessage.html(marked.parse(lastMessage.text()));
|
||||||
}
|
} else {
|
||||||
else {
|
lastMessage.append(content);
|
||||||
lastMessage.append(content);
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
})(jQuery);
|
})(jQuery);
|
||||||
|
|||||||
Reference in New Issue
Block a user