agentleFS
jmazzahacksjmazzahacks/byteforge-claude-skills/skills/flask-docker-deployment/SKILL.md

flask-docker-deployment

Skill1 starsChanged 7 days agoMITView on GitHub

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.

  • Reads credentials
  • Deletes or force-pushes
  • Installs packages

The file

Show its rules and text

We split this skill into the rules it gives an agent

22 rules. About 0 are common, 11 are distinctive, and 11 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈11

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 11

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use this skill when:

    line 12
  2. Distinctive:

    .dockerignore: - Keep the project-root virtualenv, secrets and other unneeded files out of the image

    line 25
  3. Distinctive:

    Before using this skill, ensure:

    line 29
  4. Distinctive:

    IMPORTANT: Before creating files, ask the user these questions:

    line 37
  5. Distinctive:

    Do NOT default to 5000

    line 45
  6. Very specific:

    If yes: Will need GitHub Personal Access Token (CR_PAT)

    line 54
  7. Distinctive:

    If no: Can skip git installation step

    line 55
  8. Very specific:

    Create Dockerfile in the project root:

    line 64
  9. Very specific:

    Create build-publish.sh in the project root.

    line 182
  10. Very specific:

    Create or update example.env with required environment variables for running the containerized application:

    line 295
  11. Very specific:

    Add VERSION file and .env to .gitignore:

    line 327
  12. Distinctive:

    Test the container locally before publishing:

    line 465
  13. Very specific:

    If PID 1 is sh, SIGTERM is being swallowed and every rotation severs in-flight requests.

    line 484
  14. Distinctive:

    Automated versioning: - No manual version management

    line 537
  15. Very specific:

    All patterns keep exec — Docker signals PID 1 only, and without exec the shell is PID 1 and swallows SIGTERM.

    line 568
  16. Distinctive:

    Create the API first, then dockerize:

    line 573
  17. Very specific:

    Use --no-cache strategically: - Only when dependencies updated or debugging

    line 707
  18. Very specific:

    Use explicit versions: - Don't rely only on latest tag for production

    line 710
  19. Very specific:

    Health checks: - Add /health endpoint for container orchestration

    line 712
  20. Distinctive:

    If you find a bug or stale instruction while using this skill:

    line 763
  21. Very specific:

    If HiveMake is connected and byteforge-skills-maintainer-agent is available, file a ticket with that agent.

    line 765
  22. Distinctive:

    If you cannot submit a report, give the human the details and this link.

    line 769

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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. .dockerignore - Keep the project-root virtualenv, secrets and other unneeded files out of the image

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: flask_app:app or api_server:create_app()
  2. "What port does your Flask app use?"
    • Pick a random port above 5000 (e.g., 5678, 6100, 7200) — avoid well-known ports
    • Do NOT default to 5000
  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:

FROM python:3.13-slim

# 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 .

# Install deps via a BuildKit secret mount so CR_PAT is NEVER recorded in
# `docker history` and never lands in any image layer. The final RUN steps
# scrub the token out of pip's install metadata (both direct_url.json
# records and hatchling-baked METADATA lines) and self-verify — the build
# FAILS if any token byte survives in site-packages. Requires BuildKit,
# which is the default in Docker 23+; for older Docker,
# `export DOCKER_BUILDKIT=1`.
#
# Do NOT re-introduce `ARG CR_PAT` + `--build-arg`: any layer that consumes
# the ARG records the value verbatim in `docker history`, readable by
# anyone who can pull the image.
RUN --mount=type=secret,id=cr_pat \
    export CR_PAT="$(cat /run/secrets/cr_pat)" \
    && pip install --no-cache-dir -r requirements.txt \
    && find /usr/local/lib/python3.13/site-packages -name direct_url.json -delete \
    && grep -rl "${CR_PAT}" /usr/local/lib/python3.13/site-packages | xargs -r sed -i "s|${CR_PAT}|REDACTED|g" \
    && ! grep -rq "${CR_PAT}" /usr/local/lib/python3.13/site-packages

# Copy application code
COPY . .

# Create non-root user for security
RUN useradd --uid 1000 --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}

# Container-level liveness signal for orchestrators. Assumes the app
# exposes GET /health returning 2xx (convention for this skill's
# Flask services — see "Best Practices" below).
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
    CMD curl -f http://localhost:$PORT/health || exit 1

# Run with gunicorn for production
# Port is read from PORT env var so it can be overridden at runtime
# The gunicorn target is quoted so factory-pattern forms like
# `create_app()` don't collide with dash's function-def syntax.
#
# `exec` is REQUIRED. Shell-form CMD runs under `/bin/sh -c`; without
# `exec`, sh stays PID 1 and gunicorn is its child. `docker stop` signals
# PID 1 only, dash does not forward SIGTERM, so gunicorn never drains
# in-flight requests — they are severed when SIGKILL lands at the 10s
# timeout. `exec` replaces sh with gunicorn and keeps $PORT expansion.
CMD exec 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()). Keep the surrounding double quotes around "{module}:{app}" — without them, dash parses the trailing () of create_app() as function-definition syntax and the container crash-loops with Syntax error: "(" unexpected before gunicorn ever starts.

Transitive private dependencies: the secret mount makes CR_PAT available, but an inline ${CR_PAT} URL in requirements.txt only authenticates the top-level clones. If a private lib declares another private lib token-free in its own pyproject.toml (which it must — see the global hatchling-metadata rule), pip clones that transitive dep anonymously and fails with git exit 128. Configure git url.<token-url>.insteadOf inside the same RUN so every clone in the graph is authenticated — the exact pattern is in the uv-supply-chain-hardening skill, Step 4.

If NO private dependencies, remove these lines:

# Remove `git` from apt-get, and replace the secret-mount RUN block
# with a plain `RUN pip install --no-cache-dir -r requirements.txt`

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 --uid 1000 --create-home --shell /bin/bash appuser
RUN chown -R appuser:appuser /app
USER appuser

EXPOSE {port}
ENV PYTHONPATH=/app
ENV PORT={port}

# Container-level liveness signal. Assumes GET /health returns 2xx.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
    CMD curl -f http://localhost:$PORT/health || exit 1

# Quote the gunicorn target so factory forms like `create_app()`
# don't collide with dash's function-def syntax. `exec` makes gunicorn
# PID 1 so it receives SIGTERM and drains gracefully (see full template).
CMD exec 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.

Transcribe this template exactly — do not "simplify" the directory anchor or the argument loop into shell positional parameters (a dollar sign followed by a digit). Claude Code substitutes dollar-digit tokens in a SKILL.md with the skill's invocation arguments before the text reaches you, so a template written that way arrives corrupted: the cd anchor silently becomes a no-op and --no-cache never matches. ${BASH_SOURCE[0]} and "$@" survive substitution intact, which is why the script uses bash.

#!/usr/bin/env bash

# Anchor to the script's directory so VERSION, Dockerfile, and `.` all
# resolve to the project root — not the caller's cwd. Without this,
# running as `./myapp/build-publish.sh` from a parent directory writes
# a stray VERSION file in the parent, uses the parent as build context
# (which in a multi-repo workspace can sweep sibling projects' venvs —
# and their token-bearing pip metadata — into the image), and corrupts
# this project's version tracking.
cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1

# VERSION file path
VERSION_FILE="VERSION"

# Parse command line arguments
NO_CACHE=""
for arg in "$@"; do
    if [ "$arg" = "--no-cache" ]; then
        NO_CACHE="--no-cache"
        echo "Building with --no-cache flag"
    fi
done

# Seed at 0, not 1: the version published is CURRENT+1, so seeding at
# 1 makes the very first image :2 and leaves :1 permanently missing
# from the registry.
if [ ! -f "$VERSION_FILE" ]; then
    echo "0" > "$VERSION_FILE"
    echo "Created VERSION file with initial version 0 (first publish will be :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))
IMAGE_TAG="{registry_url}:$VERSION"

# VERSION is local and gitignored, so a stale copy can point at a tag that is
# already published. Pushing would silently replace that image. Only a clear
# "not found" from the registry counts as unused: auth, network and unrecognized
# errors stop the script too.
check_tag_unused() {
    local output
    if output=$(docker manifest inspect "$IMAGE_TAG" 2>&1); then
        echo "Error: $IMAGE_TAG already exists in the registry, so $VERSION_FILE is stale."
        echo "Set $VERSION_FILE to the highest published numeric tag and re-run."
        exit 1
    fi
    if ! grep -qiE 'manifest unknown|no such manifest|name unknown' <<< "$output"; then
        echo "Error: Could not confirm $IMAGE_TAG is unused:"
        echo "$output"
        exit 1
    fi
}

check_tag_unused

echo "Building version $VERSION (incrementing from $CURRENT_VERSION)"

# Build the image with optional --no-cache flag.
# --secret exposes CR_PAT to the Dockerfile's secret mount ONLY during
# the RUN that consumes it — the value is never recorded in `docker history`.
#
# `|| exit 1` on every docker call is essential: without it, a failed
# build followed by a successful push of a locally-cached `:latest`
# silently ships stale code to the registry AND burns a version number,
# with exit code 0 — invisible in CI.
docker build $NO_CACHE --secret id=cr_pat,env=CR_PAT --platform linux/amd64 -t {registry_url}:$VERSION . || exit 1

# Tag the same image as latest
docker tag {registry_url}:$VERSION {registry_url}:latest || exit 1

# Check again: the tag may have been published while this image was building.
check_tag_unused

# Push both tags
docker push {registry_url}:$VERSION || exit 1
docker push {registry_url}:latest || exit 1

# Update the VERSION file with the new version (only reached on push success)
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 --secret id=cr_pat,env=CR_PAT:

docker build $NO_CACHE --platform linux/amd64 -t {registry_url}:$VERSION .

Make the script executable:

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:

# 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

# 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., "MY_APP")
  • {project_name} → Snake case project name (e.g., "my_flask_app")

Do NOT put CR_PAT in example.env / .env. It's a build-time secret, not a runtime variable — the container doesn't need it after the image is built. docker run --env-file .env (below) injects every variable in the file into the running container's environment, where any process in the container can read it via os.environ and it shows up in docker inspect <container_id>. Keep CR_PAT in the shell env only (see "Building and Publishing" below) so it never reaches docker run.

Update .gitignore

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.

Step 5: Create .dockerignore

Create .dockerignore to exclude unnecessary files from the Docker build context. This matters most when the virtualenv is at the project root, the ByteForge convention: without these entries, COPY . . puts its bin/ (with host-path shebangs), libraries and pip metadata into the image. Docker matches these patterns from the context root only, so a nested directory such as src/{project_name}/bin/ is still copied. If the project keeps its own code in a top-level bin/, lib/, include/ or share/, remove that entry.

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
.venv/
ENV/
bin/
include/
share/
pyvenv.cfg
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

# Copy example environment file and configure
cp example.env .env
# Edit .env and fill in actual values

Building and Publishing

Export the build-time token (private dependencies only):

# CR_PAT must be in the shell environment — build-publish.sh reads $CR_PAT
# directly. Do NOT put it in .env; see Step 4 for why.
export CR_PAT=ghp_your_github_personal_access_token

# Persist for future shells by adding the same line to ~/.zshrc (or ~/.bashrc).

Skip this if the project has no private GitHub dependencies (no --secret id=cr_pat,env=CR_PAT in build-publish.sh).

Standard build (increments version, uses cache):

./build-publish.sh

Fresh build (no cache, pulls latest dependencies):

./build-publish.sh --no-cache

Running the Container

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

Local Testing

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

Verify gunicorn is PID 1 (catches a missing exec in the CMD):

docker run -d --name {project}-pid1 {project}:test
docker exec {project}-pid1 cat /proc/1/comm   # want gunicorn/python, NOT "sh"
docker stop {project}-pid1                    # should return in ~1-2s, not 10s
docker rm {project}-pid1

If PID 1 is sh, SIGTERM is being swallowed and every rotation severs in-flight requests.

Verify the Token Did Not Leak (private deps only)

The secret-mount + scrub RUN in the Dockerfile self-verifies at build time — if any token byte survives in site-packages, the build fails. Still, prove all three post-build surfaces are clean before publishing:

# 1. .Config.Env — should print NOTHING. If it prints CR_PAT=..., an
#    `ENV CR_PAT=...` line snuck back into the Dockerfile.
docker inspect {project}:test --format '{{range .Config.Env}}{{println .}}{{end}}' | grep -i CR_PAT

# 2. docker history — should print 0. If it prints a non-zero count, the
#    Dockerfile is back on `ARG CR_PAT` + `--build-arg` (which records the
#    value in every layer that consumes it, readable by anyone who can pull).
docker history --no-trunc {project}:test | grep -c "$CR_PAT"

# 3. site-packages metadata — should print NOTHING. If it prints paths, the
#    Dockerfile is missing the direct_url.json / METADATA scrub, and pip's
#    install records still contain the resolved tokenized URL.
docker run --rm --entrypoint sh {project}:test -c \
    "grep -rl '$CR_PAT' /usr/local/lib/python3.13/site-packages 2>/dev/null"

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.

Design Principles

This pattern follows these principles:

Security:

  1. Non-root user - Container runs as unprivileged user
  2. Minimal base image - python:3.13-slim reduces attack surface
  3. Build-time secrets - CR_PAT enters the build via a BuildKit secret mount, is scrubbed from pip's install metadata inside the same RUN, and never appears in docker history or any image layer. The build self-verifies with ! grep -rq — a surviving token byte fails the build.
  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

ENV PORT=6100
CMD exec gunicorn --bind 0.0.0.0:$PORT --workers 4 "app:create_app()"
  • Multiple workers for concurrent requests
  • Factory pattern with create_app() — the target must be quoted; unquoted, dash reads the trailing () as function-definition syntax and the container crash-loops before gunicorn starts.

Pattern 2: Background Job Worker

ENV PORT=5678
CMD exec gunicorn --bind 0.0.0.0:$PORT --workers 1 daemon:app
  • Single worker to avoid job conflicts
  • Direct app instance

Pattern 3: High-Traffic API

ENV PORT=7200
CMD exec gunicorn --bind 0.0.0.0:$PORT --workers 8 --timeout 120 api:app
  • More workers for higher concurrency
  • Increased timeout for long-running requests

All patterns keep exec — Docker signals PID 1 only, and without exec the shell is PID 1 and swallows SIGTERM. (The byteforge-prometheus-metrics entrypoint uses exec gunicorn for the same reason.)

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:

# 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:

echo $CR_PAT | docker login ghcr.io -u USERNAME --password-stdin

Registry URL format:

ghcr.io/{org}/{project}

Docker Hub

Login:

docker login docker.io

Registry URL format:

docker.io/{username}/{project}

AWS ECR

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}

Troubleshooting

Build fails with "permission denied"

chmod +x build-publish.sh

Private dependency installation fails

# 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

# Check logs
docker logs {container_id}

# Run interactively to debug
docker run -it {registry_url}:latest /bin/bash

Version file conflicts

# If VERSION is corrupted or behind the registry, set it to the highest numeric
# tag published in the registry (24 is an example). Do not just delete it: it
# would restart at :1, and the script stops if that tag already exists.
echo "24" > VERSION
./build-publish.sh

build-publish.sh says a tag already exists

The local VERSION is behind the registry, which is common on a new machine or fresh clone because VERSION is gitignored. Update it as shown above. Do not delete the existing tag to get past the check. The check and push are not atomic, so two machines publishing at the same moment can still collide; keep to one publisher at a time.

build-publish.sh says it could not confirm a tag is unused

The registry did not give a clear "not found", so the script stops rather than risk overwriting an image. Read the error it prints: usually you need to run docker login for that registry. Docker Hub also answers denied for a repository that does not exist yet, so create the repository before the first publish there.

Example: Complete Workflow

User: "Dockerize my Flask app"

Claude asks:

  • Entry point? → flask_app:app
  • Port? → 5678
  • Registry? → ghcr.io/{org}/my-flask-app
  • Private deps? → yes (my-private-lib)
  • 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:

export CR_PAT=ghp_abc123
./build-publish.sh

Result:

  • ✅ Builds ghcr.io/{org}/my-flask-app:1
  • ✅ Tags as ghcr.io/{org}/my-flask-app:latest
  • ✅ Pushes both tags
  • ✅ Updates VERSION to 1

Subsequent builds:

./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:

# Build stage
FROM python:3.13-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --prefix=/install --no-cache-dir -r requirements.txt

# For private GitHub deps, install `git` in this stage and replace the
# pip install line with the same secret-mount + scrub pattern from Step 2 —
# but scrub `/install/lib/python3.13/site-packages` (that's where
# `pip install --prefix=/install` writes). The scrubbed tree is what gets
# COPY'd into the runtime stage, so a leak here follows the image
# downstream just as it would in the single-stage build.

# 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 /install /usr/local
COPY . .
RUN useradd --uid 1000 --create-home appuser && chown -R appuser:appuser /app
USER appuser
ENV PORT=6100
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
    CMD curl -f http://localhost:$PORT/health || exit 1
CMD exec gunicorn --bind 0.0.0.0:$PORT --workers 4 app:app

This pattern:

  • Installs dependencies in builder stage
  • Copies only installed packages to runtime, under /usr/local so the non-root appuser can read them. Do not use pip install --user here: /root is mode 700, so appuser can't reach /root/.local
  • Results in smaller final image
  • Includes curl for health checks

Reporting problems with this skill

If you find a bug or stale instruction while using this skill:

  • If HiveMake is connected and byteforge-skills-maintainer-agent is available, file a ticket with that agent. Use discover_agents to find its project ID.
  • Otherwise, open an issue at GitHub Issues. If you cannot submit a report, give the human the details and this link.

Include the skill name, plugin version if known, the instruction followed, expected and actual results, relevant errors/runtime versions, and any fix or workaround that worked. Omit credentials and private data.

Address the user's immediate problem first; report the skill defect afterward.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.