Files
2025-11-30 08:27:45 +08:00

596 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: flask-docker-deployment
description: Set up Docker deployment for Flask applications with Gunicorn, automated versioning, and container registry publishing
---
# Flask Docker Deployment Pattern
This skill helps you containerize Flask applications using Docker with Gunicorn for production, automated version management, and seamless container registry publishing.
## When to Use This Skill
Use this skill when:
- You have a Flask application ready to deploy
- You want production-grade containerization with Gunicorn
- You need automated version management for builds
- You're publishing to a container registry (Docker Hub, GHCR, ECR, etc.)
- You want a repeatable, idempotent deployment pipeline
## What This Skill Creates
1. **Dockerfile** - Multi-stage production-ready container with security best practices
2. **build-publish.sh** - Automated build script with version management
3. **VERSION** file - Auto-incrementing version tracking (gitignored)
4. **.gitignore** - Entry for VERSION file
5. **Optional .dockerignore** - Exclude unnecessary files from build context
## Prerequisites
Before using this skill, ensure:
1. Flask application is working locally
2. `requirements.txt` exists with all dependencies
3. Docker is installed and running
4. You're authenticated to your container registry (if publishing)
## Step 1: Gather Project Information
**IMPORTANT**: Before creating files, ask the user these questions:
1. **"What is your Flask application entry point?"**
- Format: `{module_name}:{app_variable}`
- Example: `hyperopt_daemon:app` or `api_server:create_app()`
2. **"What port does your Flask app use?"**
- Default: 5000
- Example: 5678, 8080, 3000
3. **"What is your container registry URL?"**
- Examples:
- GitHub: `ghcr.io/{org}/{project}`
- Docker Hub: `docker.io/{user}/{project}`
- AWS ECR: `{account}.dkr.ecr.{region}.amazonaws.com/{project}`
4. **"Do you have private Git dependencies?"** (yes/no)
- If yes: Will need GitHub Personal Access Token (CR_PAT)
- If no: Can skip git installation step
5. **"How many Gunicorn workers do you want?"**
- Default: 4
- Recommendation: 2-4 × CPU cores
- Note: For background job workers, use 1
## Step 2: Create Dockerfile
Create `Dockerfile` in the project root:
```dockerfile
FROM python:3.13-slim
# Build argument for GitHub Personal Access Token (if needed for private deps)
ARG CR_PAT
ENV CR_PAT=${CR_PAT}
# Install git if you have private GitHub dependencies
RUN apt-get update && apt-get install -y \
git \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Copy requirements and install dependencies
COPY requirements.txt .
# Configure git to use PAT for GitHub access (if private deps)
RUN git config --global url."https://${CR_PAT}@github.com/".insteadOf "https://github.com/" \
&& pip install --no-cache-dir -r requirements.txt \
&& git config --global --unset url."https://${CR_PAT}@github.com/".insteadOf
# Copy application code
COPY . .
# Create non-root user for security
RUN useradd --create-home --shell /bin/bash appuser
RUN chown -R appuser:appuser /app
USER appuser
# Expose the application port
EXPOSE {port}
# Set environment variables
ENV PYTHONPATH=/app
ENV PORT={port}
# Run with gunicorn for production
CMD ["gunicorn", "--bind", "0.0.0.0:{port}", "--workers", "{workers}", "{module}:{app}"]
```
**CRITICAL Replacements:**
- `{port}` → Application port (e.g., 5678)
- `{workers}` → Number of workers (e.g., 4, or 1 for background jobs)
- `{module}` → Python module name (e.g., hyperopt_daemon)
- `{app}` → App variable name (e.g., app or create_app())
**If NO private dependencies**, remove these lines:
```dockerfile
# Remove ARG CR_PAT, ENV CR_PAT, git installation, and git config commands
```
Simplified version without private deps:
```dockerfile
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd --create-home --shell /bin/bash appuser
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE {port}
ENV PYTHONPATH=/app
ENV PORT={port}
CMD ["gunicorn", "--bind", "0.0.0.0:{port}", "--workers", "{workers}", "{module}:{app}"]
```
## Step 3: Create build-publish.sh Script
Create `build-publish.sh` in the project root:
```bash
#!/bin/sh
# VERSION file path
VERSION_FILE="VERSION"
# Parse command line arguments
NO_CACHE=""
if [ "$1" = "--no-cache" ]; then
NO_CACHE="--no-cache"
echo "Building with --no-cache flag"
fi
# Check if VERSION file exists, if not create it with version 1
if [ ! -f "$VERSION_FILE" ]; then
echo "1" > "$VERSION_FILE"
echo "Created VERSION file with initial version 1"
fi
# Read current version from file
CURRENT_VERSION=$(cat "$VERSION_FILE" 2>/dev/null)
# Validate that the version is a number
if ! echo "$CURRENT_VERSION" | grep -qE '^[0-9]+$'; then
echo "Error: Invalid version format in $VERSION_FILE. Expected a number, got: $CURRENT_VERSION"
exit 1
fi
# Increment version
VERSION=$((CURRENT_VERSION + 1))
echo "Building version $VERSION (incrementing from $CURRENT_VERSION)"
# Build the image with optional --no-cache flag
docker build $NO_CACHE --build-arg CR_PAT=$CR_PAT --platform linux/amd64 -t {registry_url}:$VERSION .
# Tag the same image as latest
docker tag {registry_url}:$VERSION {registry_url}:latest
# Push both tags
docker push {registry_url}:$VERSION
docker push {registry_url}:latest
# Update the VERSION file with the new version
echo "$VERSION" > "$VERSION_FILE"
echo "Updated $VERSION_FILE to version $VERSION"
```
**CRITICAL Replacements:**
- `{registry_url}` → Full container registry URL (e.g., `ghcr.io/mazza-vc/hyperopt-server`)
**If NO private dependencies**, remove `--build-arg CR_PAT=$CR_PAT`:
```bash
docker build $NO_CACHE --platform linux/amd64 -t {registry_url}:$VERSION .
```
Make the script executable:
```bash
chmod +x build-publish.sh
```
## Step 4: Create Environment Configuration
### File: `example.env`
Create or update `example.env` with required environment variables for running the containerized application:
```bash
# Server Configuration
PORT={port}
# Database Configuration (if applicable)
{PROJECT_NAME}_DB_HOST=localhost
{PROJECT_NAME}_DB_NAME={project_name}
{PROJECT_NAME}_DB_USER={project_name}
{PROJECT_NAME}_DB_PASSWORD=your_password_here
# Build Configuration (for private dependencies)
CR_PAT=your_github_personal_access_token
# Optional: Additional app-specific variables
DEBUG=False
LOG_LEVEL=INFO
```
**CRITICAL**: Replace:
- `{port}` → Application port (e.g., 5678)
- `{PROJECT_NAME}` → Uppercase project name (e.g., "HYPEROPT_SERVER")
- `{project_name}` → Snake case project name (e.g., "hyperopt_server")
**Note:** Remove CR_PAT if you don't have private dependencies.
### Update .gitignore
Add VERSION file and .env to `.gitignore`:
```gitignore
# Environment variables
.env
# Version file (used by build system, not tracked)
VERSION
```
This prevents the VERSION file and environment secrets from being committed.
## Step 5: Create .dockerignore (Optional but Recommended)
Create `.dockerignore` to exclude unnecessary files from Docker build context:
```
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
.venv/
ENV/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Environment files (secrets should not be in image)
.env
*.env
!example.env
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# Git
.git/
.gitignore
# CI/CD
.github/
# Documentation
*.md
docs/
# Build artifacts
VERSION
*.log
# OS
.DS_Store
Thumbs.db
```
## Step 6: Usage Instructions
### Setup
```bash
# Copy example environment file and configure
cp example.env .env
# Edit .env and fill in actual values
```
### Building and Publishing
**Load environment variables** (if using .env):
```bash
# Export variables from .env for build process
set -a
source .env
set +a
```
**Standard build** (increments version, uses cache):
```bash
./build-publish.sh
```
**Fresh build** (no cache, pulls latest dependencies):
```bash
./build-publish.sh --no-cache
```
### Running the Container
**Using environment file:**
```bash
docker run -p {port}:{port} \
--env-file .env \
{registry_url}:latest
```
**Using explicit environment variables:**
```bash
docker run -p {port}:{port} \
-e PORT={port} \
-e {PROJECT_NAME}_DB_PASSWORD=secret \
-e {PROJECT_NAME}_DB_HOST=db.example.com \
{registry_url}:latest
```
### Local Testing
Test the container locally before publishing:
```bash
# Build without pushing
docker build --platform linux/amd64 -t {project}:test .
# Run locally
docker run -p {port}:{port} {project}:test
# Test the endpoint
curl http://localhost:{port}/health
```
## Design Principles
This pattern follows these principles:
### Security:
1. **Non-root user** - Container runs as unprivileged user
2. **Minimal base image** - python:3.11-slim reduces attack surface
3. **Build-time secrets** - CR_PAT only available during build, not in final image
4. **Explicit permissions** - chown ensures correct file ownership
### Reliability:
1. **Gunicorn workers** - Production-grade WSGI server with process management
2. **Platform specification** - `--platform linux/amd64` ensures compatibility
3. **Version tracking** - Auto-incrementing versions for rollback capability
4. **Immutable builds** - Each version is reproducible
### Performance:
1. **Layer caching** - Dependencies cached separately from code
2. **No-cache option** - Force fresh builds when needed
3. **Slim base image** - Faster pulls and smaller storage
4. **Multi-worker** - Concurrent request handling
### DevOps:
1. **Automated versioning** - No manual version management
2. **Dual tagging** - Both version and latest tags for flexibility
3. **Idempotent builds** - Safe to run multiple times
4. **Simple CLI** - Single script handles build and publish
## Common Patterns
### Pattern 1: Standard Web API
```dockerfile
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:create_app()"]
```
- Multiple workers for concurrent requests
- Factory pattern with `create_app()`
### Pattern 2: Background Job Worker
```dockerfile
CMD ["gunicorn", "--bind", "0.0.0.0:5678", "--workers", "1", "daemon:app"]
```
- Single worker to avoid job conflicts
- Direct app instance
### Pattern 3: High-Traffic API
```dockerfile
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--workers", "8", "--timeout", "120", "api:app"]
```
- More workers for higher concurrency
- Increased timeout for long-running requests
## Integration with Other Skills
### flask-smorest-api Skill
Create the API first, then dockerize:
```
1. User: "Set up Flask API server"
2. [flask-smorest-api skill runs]
3. User: "Now dockerize it"
4. [flask-docker-deployment skill runs]
```
### postgres-setup Skill
For database-dependent apps:
```dockerfile
# Add psycopg2-binary to requirements.txt
# Set database env vars in docker run:
docker run -e DB_HOST=db.example.com -e DB_PASSWORD=secret ...
```
## Container Registry Setup
### GitHub Container Registry (GHCR)
**Login:**
```bash
echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin
```
**Registry URL format:**
```
ghcr.io/{org}/{project}
```
### Docker Hub
**Login:**
```bash
docker login docker.io
```
**Registry URL format:**
```
docker.io/{username}/{project}
```
### AWS ECR
**Login:**
```bash
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin \
{account}.dkr.ecr.us-east-1.amazonaws.com
```
**Registry URL format:**
```
{account}.dkr.ecr.{region}.amazonaws.com/{project}
```
## Troubleshooting
### Build fails with "permission denied"
```bash
chmod +x build-publish.sh
```
### Private dependency installation fails
```bash
# Verify CR_PAT is set
echo $CR_PAT
# Test GitHub access
curl -H "Authorization: token $CR_PAT" https://api.github.com/user
```
### Container won't start
```bash
# Check logs
docker logs {container_id}
# Run interactively to debug
docker run -it {registry_url}:latest /bin/bash
```
### Version file conflicts
```bash
# If VERSION file gets corrupted, delete and rebuild
rm VERSION
./build-publish.sh
```
## Example: Complete Workflow
**User:** "Dockerize my Flask hyperopt server"
**Claude asks:**
- Entry point? → `hyperopt_daemon:app`
- Port? → `5678`
- Registry? → `ghcr.io/mazza-vc/hyperopt-server`
- Private deps? → `yes` (arcana-core)
- Workers? → `1` (background job processor)
**Claude creates:**
1. `Dockerfile` with gunicorn, 1 worker, port 5678
2. `build-publish.sh` with GHCR registry URL
3. Adds `VERSION` to `.gitignore`
4. Creates `.dockerignore`
**User runs:**
```bash
export CR_PAT=ghp_abc123
./build-publish.sh
```
**Result:**
- ✅ Builds `ghcr.io/mazza-vc/hyperopt-server:1`
- ✅ Tags as `ghcr.io/mazza-vc/hyperopt-server:latest`
- ✅ Pushes both tags
- ✅ Updates VERSION to `1`
**Subsequent builds:**
```bash
./build-publish.sh # Builds version 2
./build-publish.sh # Builds version 3
./build-publish.sh --no-cache # Builds version 4 (fresh)
```
## Best Practices
1. **Use --no-cache strategically** - Only when dependencies updated or debugging
2. **Test locally first** - Build and run locally before pushing
3. **Keep VERSION in .gitignore** - Let build system manage it
4. **Use explicit versions** - Don't rely only on `latest` tag for production
5. **Document env vars** - List all required environment variables in README
6. **Health checks** - Add `/health` endpoint for container orchestration
7. **Logging** - Configure logging to stdout for container logs
8. **Resource limits** - Set memory/CPU limits in production deployment
## Advanced: Multi-Stage Builds
For smaller images, use multi-stage builds:
```dockerfile
# Build stage
FROM python:3.13-slim as builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Runtime stage
FROM python:3.13-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
RUN useradd --create-home appuser && chown -R appuser:appuser /app
USER appuser
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"]
```
This pattern:
- Installs dependencies in builder stage
- Copies only installed packages to runtime
- Results in smaller final image