596 lines
14 KiB
Markdown
596 lines
14 KiB
Markdown
---
|
||
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
|