Help us improve
Share bugs, ideas, or general feedback.
Set up Docker deployment for Flask applications with Gunicorn, automated versioning, and container registry publishing. Use when dockerizing a Flask app, containerizing for production, or setting up CI/CD with Docker.
npx claudepluginhub jmazzahacks/byteforge-claude-skills --plugin byteforge-skillsHow this skill is triggered — by the user, by Claude, or both
Slash command
/byteforge-claude-skills:flask-docker-deploymentThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill helps you containerize Flask applications using Docker with Gunicorn for production, automated version management, and seamless container registry publishing.
Guides technical evaluation of code review feedback: read fully, restate for understanding, verify against codebase, respond with reasoning or pushback before implementing.
Share bugs, ideas, or general feedback.
This skill helps you containerize Flask applications using Docker with Gunicorn for production, automated version management, and seamless container registry publishing.
Use this skill when:
Before using this skill, ensure:
requirements.txt exists with all dependenciesIMPORTANT: Before creating files, ask the user these questions:
"What is your Flask application entry point?"
{module_name}:{app_variable}flask_app:app or api_server:create_app()"What port does your Flask app use?"
"What is your container registry URL?"
ghcr.io/{org}/{project}docker.io/{user}/{project}{account}.dkr.ecr.{region}.amazonaws.com/{project}"Do you have private Git dependencies?" (yes/no)
"How many Gunicorn workers do you want?"
Create Dockerfile in the project root:
FROM python:3.13-slim
# Build-time token for cloning private GitHub deps. ARG ONLY — do NOT add an
# `ENV CR_PAT=${CR_PAT}` line. ARG makes the value available to the RUN steps
# below (which is all that's needed for the git config trick), while ENV would
# bake the live token into the final image's environment, where it is readable
# by anyone who runs `docker inspect`. See Step 6 for the verification check.
ARG CR_PAT
# Install curl (for health checks) and git (for private GitHub dependencies)
RUN apt-get update && apt-get install -y \
curl \
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
# Port is read from PORT env var so it can be overridden at runtime
CMD gunicorn --bind 0.0.0.0:$PORT --workers {workers} {module}:{app}
CRITICAL Replacements:
{port} → Default application port (e.g., 5678). This is the default value for the PORT env var — it can be overridden at runtime with -e PORT=XXXX{workers} → Number of workers (e.g., 4, or 1 for background jobs){module} → Python module name (e.g., flask_app){app} → App variable name (e.g., app or create_app())If NO private dependencies, remove these lines:
# Remove ARG CR_PAT, git installation, and git config commands
Simplified version without private deps:
FROM python:3.13-slim
# Install curl for health checks
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
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}
Create build-publish.sh in the project root:
#!/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/{org}/my-flask-app)If NO private dependencies, remove --build-arg CR_PAT=$CR_PAT:
docker build $NO_CACHE --platform linux/amd64 -t {registry_url}:$VERSION .
Make the script executable:
chmod +x build-publish.sh
example.envCreate or update example.env with required environment variables for running the containerized application:
# 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., "my_flask_app")Note: Remove CR_PAT if you don't have private dependencies.
Add VERSION file and .env to .gitignore:
# Environment variables
.env
# Version file (used by build system, not tracked)
VERSION
This prevents the VERSION file and environment secrets from being committed.
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
# Copy example environment file and configure
cp example.env .env
# Edit .env and fill in actual values
Load environment variables (if using .env):
# Export variables from .env for build process
set -a
source .env
set +a
Standard build (increments version, uses cache):
./build-publish.sh
Fresh build (no cache, pulls latest dependencies):
./build-publish.sh --no-cache
Using environment file:
docker run -p {port}:{port} \
--env-file .env \
{registry_url}:latest
Using explicit environment variables:
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
Test the container locally before publishing:
# 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
The ARG-not-ENV rule above only holds if it's actually followed. Prove the
token isn't baked into the image before publishing:
# Should print NOTHING. If it prints CR_PAT=..., the token leaked into the image —
# go back and remove any `ENV CR_PAT=...` line from the Dockerfile.
docker inspect {project}:test --format '{{range .Config.Env}}{{println .}}{{end}}' | grep -i CR_PAT
If a token was ever baked into a previously published image, fixing the Dockerfile only stops future images from carrying it. Images already pushed still contain the token, and it stays valid until rotated. Flag this to the user and recommend rotating the token; let them decide.
This pattern follows these principles:
--platform linux/amd64 ensures compatibilityENV PORT=6100
CMD gunicorn --bind 0.0.0.0:$PORT --workers 4 app:create_app()
create_app()ENV PORT=5678
CMD gunicorn --bind 0.0.0.0:$PORT --workers 1 daemon:app
ENV PORT=7200
CMD gunicorn --bind 0.0.0.0:$PORT --workers 8 --timeout 120 api:app
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]
For database-dependent apps:
# 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 ...
Login:
echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin
Registry URL format:
ghcr.io/{org}/{project}
Login:
docker login docker.io
Registry URL format:
docker.io/{username}/{project}
Login:
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}
chmod +x build-publish.sh
# Verify CR_PAT is set
echo $CR_PAT
# Test GitHub access
curl -H "Authorization: token $CR_PAT" https://api.github.com/user
# Check logs
docker logs {container_id}
# Run interactively to debug
docker run -it {registry_url}:latest /bin/bash
# If VERSION file gets corrupted, delete and rebuild
rm VERSION
./build-publish.sh
User: "Dockerize my Flask app"
Claude asks:
flask_app:app5678ghcr.io/{org}/my-flask-appyes (my-private-lib)1 (background job processor)Claude creates:
Dockerfile with gunicorn, 1 worker, port 5678build-publish.sh with GHCR registry URLVERSION to .gitignore.dockerignoreUser runs:
export CR_PAT=ghp_abc123
./build-publish.sh
Result:
ghcr.io/{org}/my-flask-app:1ghcr.io/{org}/my-flask-app:latest1Subsequent builds:
./build-publish.sh # Builds version 2
./build-publish.sh # Builds version 3
./build-publish.sh --no-cache # Builds version 4 (fresh)
latest tag for production/health endpoint for container orchestrationFor smaller images, use multi-stage builds:
# 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
# Install curl for health checks
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
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
ENV PORT=6100
CMD gunicorn --bind 0.0.0.0:$PORT --workers 4 app:app
This pattern: