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
In this repository14 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- aegis-nextjs-frontendSkill
- byteforge-loki-loggingSkill
- byteforge-prometheus-metricsSkill
- flask-smorest-apiSkill
- flask-telegram-botSkill
- gatekeeper-nginx-setupSkill
- mcp-async-upgradeSkill
- mcp-docker-deploymentSkill
- mcp-server-nginx-gatekeeperSkill
- postgres-setupSkill
- python-lib-setupSkill
- python-project-scaffoldSkill
- uv-supply-chain-hardeningSkill
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
- Distinctive: line 12
Use this skill when:
- Distinctive: line 25
.dockerignore: - Keep the project-root virtualenv, secrets and other unneeded files out of the image
- Distinctive: line 29
Before using this skill, ensure:
- Distinctive: line 37
IMPORTANT: Before creating files, ask the user these questions:
- Distinctive: line 45
Do NOT default to 5000
- Very specific: line 54
If yes: Will need GitHub Personal Access Token (CR_PAT)
- Distinctive: line 55
If no: Can skip git installation step
- Very specific: line 64
Create Dockerfile in the project root:
- Very specific: line 182
Create build-publish.sh in the project root.
- Very specific: line 295
Create or update example.env with required environment variables for running the containerized application:
- Very specific: line 327
Add VERSION file and .env to .gitignore:
- Distinctive: line 465
Test the container locally before publishing:
- Very specific: line 484
If PID 1 is sh, SIGTERM is being swallowed and every rotation severs in-flight requests.
- Distinctive: line 537
Automated versioning: - No manual version management
- Very specific: line 568
All patterns keep exec — Docker signals PID 1 only, and without exec the shell is PID 1 and swallows SIGTERM.
- Distinctive: line 573
Create the API first, then dockerize:
- Very specific: line 707
Use --no-cache strategically: - Only when dependencies updated or debugging
- Very specific: line 710
Use explicit versions: - Don't rely only on latest tag for production
- Very specific: line 712
Health checks: - Add /health endpoint for container orchestration
- Distinctive: line 763
If you find a bug or stale instruction while using this skill:
- Very specific: line 765
If HiveMake is connected and byteforge-skills-maintainer-agent is available, file a ticket with that agent.
- Distinctive: line 769
If you cannot submit a report, give the human the details and this link.
≈ “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.
Files
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
- Dockerfile - Multi-stage production-ready container with security best practices
- build-publish.sh - Automated build script with version management
- VERSION file - Auto-incrementing version tracking (gitignored)
- .gitignore - Entry for VERSION file
- .dockerignore - Keep the project-root virtualenv, secrets and other unneeded files out of the image
Prerequisites
Before using this skill, ensure:
- Flask application is working locally
requirements.txtexists with all dependencies- Docker is installed and running
- You're authenticated to your container registry (if publishing)
Step 1: Gather Project Information
IMPORTANT: Before creating files, ask the user these questions:
- "What is your Flask application entry point?"
- Format:
{module_name}:{app_variable} - Example:
flask_app:apporapi_server:create_app()
- Format:
- "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
- "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}
- GitHub:
- Examples:
- "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
- "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 thePORTenv 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.,apporcreate_app()). Keep the surrounding double quotes around"{module}:{app}"— without them, dash parses the trailing()ofcreate_app()as function-definition syntax and the container crash-loops withSyntax error: "(" unexpectedbefore 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
cdanchor silently becomes a no-op and--no-cachenever 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.shStep 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=INFOCRITICAL: 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)
VERSIONThis 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.dbStep 6: Usage Instructions
Setup
# Copy example environment file and configure
cp example.env .env
# Edit .env and fill in actual valuesBuilding 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.shFresh build (no cache, pulls latest dependencies):
./build-publish.sh --no-cacheRunning the Container
Using environment file:
docker run -p {port}:{port} \
--env-file .env \
{registry_url}:latestUsing 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}:latestLocal 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}/healthVerify 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}-pid1If 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:
- Non-root user - Container runs as unprivileged user
- Minimal base image - python:3.13-slim reduces attack surface
- 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 historyor any image layer. The build self-verifies with! grep -rq— a surviving token byte fails the build. - Explicit permissions - chown ensures correct file ownership
Reliability:
- Gunicorn workers - Production-grade WSGI server with process management
- Platform specification -
--platform linux/amd64ensures compatibility - Version tracking - Auto-incrementing versions for rollback capability
- Immutable builds - Each version is reproducible
Performance:
- Layer caching - Dependencies cached separately from code
- No-cache option - Force fresh builds when needed
- Slim base image - Faster pulls and smaller storage
- Multi-worker - Concurrent request handling
DevOps:
- Automated versioning - No manual version management
- Dual tagging - Both version and latest tags for flexibility
- Idempotent builds - Safe to run multiple times
- 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-stdinRegistry URL format:
ghcr.io/{org}/{project}Docker Hub
Login:
docker login docker.ioRegistry 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.comRegistry URL format:
{account}.dkr.ecr.{region}.amazonaws.com/{project}Troubleshooting
Build fails with "permission denied"
chmod +x build-publish.shPrivate dependency installation fails
# Verify CR_PAT is set
echo $CR_PAT
# Test GitHub access
curl -H "Authorization: token $CR_PAT" https://api.github.com/userContainer won't start
# Check logs
docker logs {container_id}
# Run interactively to debug
docker run -it {registry_url}:latest /bin/bashVersion 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.shbuild-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:
Dockerfilewith gunicorn, 1 worker, port 5678build-publish.shwith GHCR registry URL- Adds
VERSIONto.gitignore - Creates
.dockerignore
User runs:
export CR_PAT=ghp_abc123
./build-publish.shResult:
- ✅ 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
- Use --no-cache strategically - Only when dependencies updated or debugging
- Test locally first - Build and run locally before pushing
- Keep VERSION in .gitignore - Let build system manage it
- Use explicit versions - Don't rely only on
latesttag for production - Document env vars - List all required environment variables in README
- Health checks - Add
/healthendpoint for container orchestration - Logging - Configure logging to stdout for container logs
- 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:appThis pattern:
- Installs dependencies in builder stage
- Copies only installed packages to runtime, under
/usr/localso the non-rootappusercan read them. Do not usepip install --userhere:/rootis mode 700, soappusercan'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-agentis available, file a ticket with that agent. Usediscover_agentsto 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.
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
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.

