Initial commit
This commit is contained in:
595
skills/flask-docker-deployment/SKILL.md
Normal file
595
skills/flask-docker-deployment/SKILL.md
Normal file
@@ -0,0 +1,595 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user