Install
$ agentstack add skill-robhowley-py-pit-skills-dockerize-service ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Skill: dockerize-service
Core position
This skill produces a local-development Docker Compose setup that reflects the project as-is. It does not introduce backing services the repo doesn't already use, and it does not create a second Docker pattern if one already exists.
Trigger
Use this skill when the user wants to:
- Containerize an existing Python project
- Add Docker Compose to a project
- Add a Dockerfile to a project
- "Dockerize" a service
Variables
{pkg_name}— snake_case package name (matches the project directory and Python package){PKG_NAME}_— env var prefix, only used if a pydantic-settingsenv_prefixis detected in the repo (e.g.,MY_SERVICE_); otherwise plain names are used
Step 1 — Inspect the repo
Inspect before asking. Work through each check in order and record findings.
1. Check for existing Docker artifacts
Look for Dockerfile, docker-compose.yml, docker-compose.yaml, .dockerignore. If any are present, extend them — do not create a competing setup. Note what was found.
2. Check for repo tooling
uv.lockpresent → uv-native project; use uv base image in Dockerfile- No
uv.lock→ pip-based project; usepython:3.12-slimwithpip install
3. Check for service signals
Inspect pyproject.toml dependencies and any settings/config files for:
sqlalchemy+ a non-sqlite database URL pattern, orpsycopg,psycopg2,asyncpgin deps → Postgresredisin deps → Redis- No signals → app-only (do not add Postgres as a default)
If Postgres is signaled, also note which driver is already present (psycopg2-binary, psycopg[binary], asyncpg, etc.) — this determines what to add in Step 6.
4. Check for an existing health endpoint
Grep routes for /health, /healthz, /ping, /api/v1/health, or similar. Record the exact path if found.
5. Detect app entrypoint
Look for the ASGI app object in {pkg_name}/main.py, {pkg_name}/app.py, or {pkg_name}/application.py. Record the module path (e.g., {pkg_name}.main:app). Also check [project.scripts] in pyproject.toml for any uvicorn/gunicorn invocations. This becomes the CMD in the Dockerfile — default to {pkg_name}.main:app only if no entrypoint is found elsewhere.
6. Detect env prefix
Grep for env_prefix in core/config.py, config.py, settings.py, or similar. If a pydantic-settings prefix is found (e.g., env_prefix="MY_SERVICE_"), use it in .env.compose. If no prefix is detected, use plain variable names (DATABASE_URL, REDIS_URL, DEBUG) without a package prefix.
7. Present inference summary
Tell the user what was found and what will be generated. Ask for confirmation only if something is genuinely ambiguous (e.g., multiple config files with conflicting signals, or an existing Dockerfile with unclear extension points).
Step 2 — Generate Dockerfile
Multi-stage build. Base image depends on repo tooling detected in Step 1.
If uv.lock is present:
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
COPY {pkg_name}/ ./{pkg_name}/
RUN uv sync --frozen --no-dev
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app/{pkg_name} /app/{pkg_name}
ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000
{HEALTHCHECK}
CMD ["/app/.venv/bin/uvicorn", "{detected_entrypoint|pkg_name.main:app}", "--host", "0.0.0.0", "--port", "8000"]
If no uv.lock:
FROM python:3.12-slim AS builder
WORKDIR /app
COPY pyproject.toml ./
COPY {pkg_name}/ ./{pkg_name}/
RUN pip install --no-cache-dir --prefix=/install .
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /install /usr/local
EXPOSE 8000
{HEALTHCHECK}
CMD ["uvicorn", "{detected_entrypoint|pkg_name.main:app}", "--host", "0.0.0.0", "--port", "8000"]
HEALTHCHECK selection:
- If a health endpoint was detected in Step 1:
``dockerfile HEALTHCHECK --interval=10s --timeout=3s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000{detected_path}')" ``
- If no health endpoint exists, use a TCP liveness check and note that a health endpoint should be added:
``dockerfile HEALTHCHECK --interval=10s --timeout=3s --retries=3 \ CMD python -c "import socket; s=socket.socket(); s.settimeout(2); s.connect(('localhost', 8000)); s.close()" ` > Note: No health route was found. This is a **liveness-only** check — it confirms the server is accepting TCP connections but does not verify app readiness. Add a /health` endpoint for a proper readiness check.
Step 3 — Generate docker-compose.yml
Always include the app service. Add db (Postgres) and/or cache (Redis) only based on signals found in Step 1.
# Local development only — not a production deployment target
services:
app:
build: .
ports:
- "8000:8000"
env_file: .env.compose
depends_on:
db: # only if postgres signaled
condition: service_healthy
cache: # only if redis signaled
condition: service_healthy
restart: unless-stopped
db: # only if postgres signaled
image: postgres:16-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: {pkg_name}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
cache: # only if redis signaled
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data: # only if postgres signaled
Step 4 — Generate .env.compose
Environment variables for the running containers. Use the env prefix detected in Step 1 if a pydantic-settings prefix was found; otherwise use plain variable names. Generate one .env.compose — not both variants.
If prefix detected (e.g. MY_SERVICE_):
# docker compose environment — do not commit real secrets
MY_SERVICE_DATABASE_URL=postgresql+psycopg2://postgres:postgres@db:5432/{pkg_name} # only if postgres signaled
MY_SERVICE_REDIS_URL=redis://cache:6379/0 # only if redis signaled
MY_SERVICE_DEBUG=false
If no prefix detected:
# docker compose environment — do not commit real secrets
DATABASE_URL=postgresql+psycopg2://postgres:postgres@db:5432/{pkg_name} # only if postgres signaled
REDIS_URL=redis://cache:6379/0 # only if redis signaled
DEBUG=false
Also create .env.compose.example with the same content — this file IS committed to version control as a template.
Add .env.compose to .gitignore:
echo ".env.compose" >> .gitignore
Step 5 — Generate .dockerignore
Skip if .dockerignore already exists and covers these patterns.
.venv/
__pycache__/
*.pyc
*.pyo
.pytest_cache/
.ruff_cache/
*.egg-info/
dist/
.env*
!.env.compose.example
Step 6 — Update dependencies if needed
Postgres driver — only if Postgres was signaled and no postgres driver is already present in pyproject.toml:
Inspect the existing stack first:
- Async stack (uses
asyncpgelsewhere, orAsyncSession) → addasyncpg - Sync stack with modern psycopg → add
psycopg[binary] - Sync stack with legacy psycopg2 → add
psycopg2-binary - Do not introduce a second driver if one already exists
Follow the repo's dependency management workflow:
uv.lockpresent →uv add- No
uv.lock→ add `to[project.dependencies]inpyproject.toml` and follow the project's dependency-management workflow
Redis — only if Redis was signaled and redis is not already in deps:
uv.lockpresent →uv add redis- No
uv.lock→ addredisto[project.dependencies]inpyproject.tomland follow the project's dependency-management workflow
Step 7 — Verify
docker compose up --build -d
docker compose ps # all services healthy
curl http://localhost:8000{detected_health_path_or_omit_if_none}
docker compose down
If no health endpoint exists, omit the curl line and note that one should be added before moving to production.
Hard constraints
- Always use multi-stage builds — keep the final image lean by excluding build tooling and intermediate artifacts
- Never embed real credentials in
docker-compose.yml— always useenv_file - Always include a
HEALTHCHECKin the Dockerfile andhealthcheck:on backing services - Use
depends_on: condition: service_healthy— not plaindepends_on - Add
.env.composeto.gitignore; provide.env.compose.exampleas a committed template - Never introduce a backing service (Postgres, Redis) unless the repo or user request shows it is needed — no signals → app-only
- If Docker artifacts already exist, extend them; do not create a parallel competing setup
- Match the project's existing dependency management workflow — do not assume uv if
uv.lockis absent - Do not invent or hardcode a health endpoint path — detect from existing routes or use the TCP liveness fallback
Completion checklist
- [ ]
Dockerfilepresent, multi-stage, appropriate base image for repo tooling - [ ]
docker-compose.ymlscoped to inferred services only; no uninferred backing services added - [ ]
HEALTHCHECKuses detected path or TCP liveness fallback — no invented path - [ ]
.env.composepresent,.gitignoreupdated,.env.compose.examplecommitted - [ ] No existing Docker artifacts replaced without user confirmation
- [ ] No new backing services introduced without repo evidence or explicit user request
- [ ] No hardcoded credentials in
docker-compose.yml - [ ]
docker compose up --buildsucceeds
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: robhowley
- Source: robhowley/py-pit-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.