Compare commits

..

2 Commits

Author SHA1 Message Date
Matteo Rosati
3dd38dc6fa copy creds in container 2026-01-20 11:03:29 +01:00
Matteo Rosati
7df1b9f718 dockerize app 2026-01-20 10:46:14 +01:00
6 changed files with 467 additions and 26 deletions

53
.dockerignore Normal file
View 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
View 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
View 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
View 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
View 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}

View File

@@ -1,16 +1,18 @@
(($) => {
var ws = new WebSocket("ws://localhost:8000/ws");
var ws = new WebSocket(
`ws://${location.protocol + "//" + location.host}${location.port ? ":" + location.port : ""}/ws`,
);
const input = $("#message");
const messages = $('#messages');
const messages = $("#messages");
var lastMessage;
$('#button').on('click', () => {
$("#button").on("click", () => {
const message = input.val();
if (message) {
ws.send(message);
lastMessage = $('<div class="message received"><p>Loading...</p></div>')
messages.append(`<div class="message sent"><p>${message}</p></div>`)
lastMessage = $('<div class="message received"><p>Loading...</p></div>');
messages.append(`<div class="message sent"><p>${message}</p></div>`);
messages.append(lastMessage);
input.val("");
@@ -26,8 +28,7 @@
if (content.textContent === "<<END>>") {
lastMessage.html(marked.parse(lastMessage.text()));
}
else {
} else {
lastMessage.append(content);
}
};