guides: update python & add files component (#25206)

<!--Delete sections as needed -->

## Description

Updated Python guide

- Removed DOI in favor of DHI only. DHI Community is now free, so
there's no reason to keep the DOI fallback path.
- Removed the git clone sample-app pattern. Maintaining external sample
repos is a burden, and split source of truth between the docs and the
sample.
- New file browser / scaffolding component. Lets users copy individual
files or scaffold the whole project with one command. Replaces the role
the cloned sample repo used to play.
- New "Secure your supply chain" topic highlighting what DHI gives you
and how to attach matching attestations to your own image in CI.
- A bunch of smaller improvements: clearer intros for each topic,
progressively updating the same app in all topics, ran and fixed issues,
etc.

https://deploy-preview-25206--docsdocker.netlify.app/guides/python/

## Related issues or tickets

ENGDOCS-3308

## Reviews

<!-- Notes for reviewers here -->
<!-- List applicable reviews (optionally @tag reviewers) -->

- [ ] Technical review
- [ ] Editorial review

---------

Signed-off-by: Craig Osterhout <craig.osterhout@docker.com>
This commit is contained in:
Craig Osterhout
2026-06-08 12:07:31 -07:00
committed by GitHub
parent fbcb7d48c6
commit 5862e80e5b
10 changed files with 2348 additions and 516 deletions
+1 -1
View File
@@ -33,4 +33,4 @@ Vale.Terms = NO
[*.md]
BasedOnStyles = Docker, Vale
TokenIgnores = ({{[^}]+}}\S*)
BlockIgnores = (?m)^[ \t]*({{[^}]+}})[ \t]*$, (\[[^\]]*{{[^}]+}}[^\]]*\]\([^)]*\))
BlockIgnores = (?m)^[ \t]*({{[^}]+}})[ \t]*$, (\[[^\]]*{{[^}]+}}[^\]]*\]\([^)]*\)), ({{<\s*file\s[^>]*>}}[\s\S]*?{{<\s*/\s*file\s*>}})
@@ -27,12 +27,14 @@ If you didn't create a [GitHub repository](https://github.com/new) for your proj
## Overview
GitHub Actions is a CI/CD (Continuous Integration and Continuous Deployment) automation tool built into GitHub. It allows you to define custom workflows for building, testing, and deploying your code when specific events occur (e.g., pushing code, creating a pull request, etc.). A workflow is a YAML-based automation script that defines a sequence of steps to be executed when triggered. Workflows are stored in the `.github/workflows/` directory of a repository.
GitHub Actions is a CI/CD automation tool built into GitHub. A workflow is a
YAML file that tells GitHub which jobs to run when something happens in your
repository, like a push to a branch or a pull request opening. Workflows live
in the `.github/workflows/` directory of your repository.
In this section, you'll learn how to set up and use GitHub Actions to build your Docker image as well as push it to Docker Hub. You will complete the following steps:
1. Define the GitHub Actions workflow.
2. Run the workflow.
In this section, you'll add a workflow that runs your linting, formatting, and
type checks on every push to the main branch, then builds your Docker image
and pushes it to Docker Hub.
## 1. Define the GitHub Actions workflow
@@ -45,13 +47,20 @@ If you prefer to use the GitHub web interface, follow these steps:
2. Select **set up a workflow yourself**.
This takes you to a page for creating a new GitHub Actions workflow file in
your repository. By default, the file is created under `.github/workflows/main.yml`, let's change it name to `build.yml`.
your repository. By default, the file is created under `.github/workflows/main.yml`. Change the file name to `build.yml`.
If you prefer to use your text editor, create a new file named `build.yml` in the `.github/workflows/` directory of your repository.
Add the following content to the file:
{{< files name="python-docker-example" >}}
{{< file path=".github/workflows/build.yml" status="new" >}}
```yaml
# GitHub Actions workflow that runs on every push to main.
# - lint-test: runs pre-commit hooks (Ruff) and Pyright type checks.
# - build_and_push: signs in to Docker Hub and the DHI registry, then
# builds and pushes the image (with SBOM and provenance attestations).
name: Build and push Docker image
on:
@@ -64,16 +73,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@{{% param "checkout_action_version" %}}
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.14'
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pre-commit pyright
- name: Run pre-commit hooks
run: pre-commit run --all-files
@@ -84,12 +94,21 @@ jobs:
build_and_push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@{{% param "checkout_action_version" %}}
- name: Login to Docker Hub
uses: docker/login-action@{{% param "login_action_version" %}}
with:
username: ${{ vars.DOCKER_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Docker Hardened Images
uses: docker/login-action@{{% param "login_action_version" %}}
with:
registry: dhi.io
username: ${{ vars.DOCKER_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@{{% param "setup_buildx_action_version" %}}
@@ -99,6 +118,9 @@ jobs:
push: true
tags: ${{ vars.DOCKER_USERNAME }}/${{ github.event.repository.name }}:latest
```
{{< /file >}}
{{< /files >}}
Each GitHub Actions workflow includes one or several jobs. Each job consists of steps. Each step can either run a set of commands or use already [existing actions](https://github.com/marketplace?type=actions). The action above has three steps:
@@ -128,9 +150,12 @@ Related information:
- [Introduction to GitHub Actions](/guides/gha.md)
- [Docker Build GitHub Actions](/manuals/build/ci/github-actions/_index.md)
- [Workflow syntax for GitHub Actions](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions)
- [docker/login-action](https://github.com/docker/login-action)
- [docker/build-push-action](https://github.com/docker/build-push-action)
- [Create a Docker Hub access token](/manuals/security/access-tokens.md#create-an-access-token)
## Next steps
In the next section, you'll learn how you can develop locally using kubernetes.
In the next section, you'll learn how to inspect and generate supply chain
attestations for your image. See [Secure your supply chain](secure-supply-chain.md).
+152 -206
View File
@@ -14,159 +14,66 @@ aliases:
## Prerequisites
- You have installed the latest version of [Docker Desktop](/get-started/get-docker.md).
- You have a [Git client](https://git-scm.com/downloads). The examples in this section use a command-line based Git client, but you can use any client.
## Overview
This section walks you through containerizing and running a Python application.
Containerizing your application means packaging it together with its
dependencies, configuration, and runtime into a single portable unit called a
container image. Running that image creates a container, an isolated process
that behaves the same on any machine, whether it's your laptop, a CI runner, or
a production server.
## Get the sample application
In this section, you'll containerize a simple
[FastAPI](https://fastapi.tiangolo.com) web application. You'll write a
`Dockerfile` that describes how to build the image, add a `compose.yaml` file
that defines how Docker runs your container, and then build and start the
application with one command.
The sample application uses the popular [FastAPI](https://fastapi.tiangolo.com) framework.
You'll use [Docker Hardened Images](/dhi/) as the base. These are minimal,
secure Python images maintained by Docker.
Clone the sample application to use with this guide. Open a terminal, change directory to a directory that you want to work in, and run the following command to clone the repository:
## Create the application
```console
$ git clone https://github.com/estebanx64/python-docker-example && cd python-docker-example
The sample application is a minimal FastAPI service with a single endpoint
that returns a JSON greeting. Create the following files in a new
`python-docker-example` directory. To create all the files at once, switch to
the **Scaffold script** tab in the file browser and copy the shell command.
{{< files name="python-docker-example" >}}
{{< file path="app.py" status="new" >}}
```python
# A minimal FastAPI application.
# The root endpoint (GET /) returns a JSON "Hello World" response.
# See https://fastapi.tiangolo.com/ for the framework reference.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
```
{{< /file >}}
## Create Docker assets
{{< file path="requirements.txt" status="new" >}}
```text
# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
Now that you have an application, you can create the necessary Docker assets to
containerize your application.
> [!TIP]
>
> [Gordon](/ai/gordon/), Docker's AI assistant, can generate Docker assets for your project. Ask Gordon to create a Dockerfile, Compose file, and `.dockerignore` tailored to your application.
Before creating your Dockerfile, you need to choose a base image. You can use the [Python Docker Official Image](https://hub.docker.com/_/python),
or a [Docker Hardened Image (DHI)](https://hub.docker.com/hardened-images/catalog/dhi/python).
Docker Hardened Images (DHIs) are minimal, secure, and production-ready base images maintained by Docker.
They help reduce vulnerabilities and simplify compliance. For more details, see [Docker Hardened Images](/dhi/).
{{< tabs >}}
{{< tab name="Using the official Docker image" >}}
Create the following files in your project directory.
Create a file named `Dockerfile` with the following contents.
```dockerfile {collapse=true,title=Dockerfile}
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/go/dockerfile-reference/
# This Dockerfile uses Python Docker Official Image
ARG PYTHON_VERSION=3.12
FROM python:${PYTHON_VERSION}-slim
# Prevents Python from writing pyc files.
ENV PYTHONDONTWRITEBYTECODE=1
# Keeps Python from buffering stdout and stderr to avoid situations where
# the application crashes without emitting any logs due to buffering.
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Create a non-privileged user that the app will run under.
# See https://docs.docker.com/go/dockerfile-user-best-practices/
ARG UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
appuser
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them into
# into this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
python -m pip install -r requirements.txt
# Switch to the non-privileged user to run the application.
USER appuser
# Copy the source code into the container.
COPY . .
# Expose the port that the application listens on.
EXPOSE 8000
# Run the application.
CMD ["python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
fastapi==0.115.12
uvicorn==0.34.3
```
{{< /file >}}
Create a file named `compose.yaml` with the following contents.
{{< file path=".gitignore" status="new" >}}
```text
# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
```yaml {collapse=true,title=compose.yaml}
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Docker Compose reference guide at
# https://docs.docker.com/go/compose-spec-reference/
# Here the instructions define your application as a service called "server".
# This service is built from the Dockerfile in the current directory.
# You can add other services your application may depend on here, such as a
# database or a cache. For examples, see the Awesome Compose repository:
# https://github.com/docker/awesome-compose
services:
server:
build:
context: .
ports:
- 8000:8000
```
Create a file named `.dockerignore` with the following contents.
```text {collapse=true,title=".dockerignore"}
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
# For more help, visit the .dockerignore file reference guide at
# https://docs.docker.com/go/build-context-dockerignore/
**/.DS_Store
**/__pycache__
**/.venv
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/bin
**/charts
**/docker-compose*
**/compose.y*ml
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
```
Create a file named `.gitignore` with the following contents.
```text {collapse=true,title=".gitignore"}
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -221,28 +128,85 @@ venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txt
```
{{< /file >}}
{{< /files >}}
If you already have Python installed and want to verify the app works before
containerizing it, you can run it locally:
```console
$ python3 -m venv .venv
$ source .venv/bin/activate
$ pip install -r requirements.txt
$ uvicorn app:app --reload
```
{{< /tab >}}
{{< tab name="Using Docker Hardened Image" >}}
> [!NOTE]
>
> On Windows, activate the virtual environment with `.venv\Scripts\activate`
> instead of `source .venv/bin/activate`.
Docker Hardened Images (DHIs) are available for Python in the [Docker Hardened Images catalog](https://hub.docker.com/hardened-images/catalog/dhi/python). Docker Hardened Images are freely available to everyone with no subscription required. You can pull and use them like any other Docker image after signing in to the DHI registry. For more information, see the [DHI quickstart](/dhi/get-started/) guide.
If you don't have Python installed, skip ahead to the next section. The
remaining steps run the application in a container, with no local Python
required.
1. Sign in to the DHI registry:
## Create the Docker assets
```console
$ docker login dhi.io
```
Sign in to the DHI registry so Docker can pull the Python base images during
the build. The available Python images are listed in the
[catalog](https://hub.docker.com/hardened-images/catalog/dhi/python).
2. Pull the Python DHI (check the catalog for available versions):
```console
$ docker login dhi.io
```
```console
$ docker pull dhi.io/python:3.12.12-debian13-fips-dev
```
Add the following three files to your `python-docker-example` directory. The
`Dockerfile` describes how to build the image, `compose.yaml` defines how
Docker runs the container, and `.dockerignore` keeps unwanted files out of the
build context.
Create a file named `Dockerfile` with the following contents.
> [!TIP]
>
> [Gordon](/ai/gordon/), Docker's AI assistant, can generate Docker assets for
> your project. Ask Gordon to create a Dockerfile, Compose file, and
> `.dockerignore` tailored to your application.
```dockerfile {collapse=true,title=Dockerfile}
{{< files name="python-docker-example" >}}
{{< file path="app.py" >}}
```python
# A minimal FastAPI application.
# The root endpoint (GET /) returns a JSON "Hello World" response.
# See https://fastapi.tiangolo.com/ for the framework reference.
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
```
{{< /file >}}
{{< file path="requirements.txt" >}}
```text
# Python package dependencies for the application, pinned for reproducible builds.
# See https://pip.pypa.io/en/stable/reference/requirements-file-format/
fastapi==0.115.12
uvicorn==0.34.3
```
{{< /file >}}
{{< file path="Dockerfile" status="new" >}}
```dockerfile
# syntax=docker/dockerfile:1
# Comments are provided throughout this file to help you get started.
@@ -251,43 +215,30 @@ Create a file named `Dockerfile` with the following contents.
# This Dockerfile uses Docker Hardened Images (DHI) for enhanced security.
# For more information, see https://docs.docker.com/dhi/
ARG PYTHON_VERSION=3.12.12-debian13-fips-dev
FROM dhi.io/python:${PYTHON_VERSION}
# Prevents Python from writing pyc files.
ENV PYTHONDONTWRITEBYTECODE=1
# Keeps Python from buffering stdout and stderr to avoid situations where
# the application crashes without emitting any logs due to buffering.
ENV PYTHONUNBUFFERED=1
#Add dependencies for adduser
RUN apt update -y && apt install adduser -y
# Use the dev image to build and install dependencies.
FROM dhi.io/python:3.12-dev AS builder
WORKDIR /app
# Create a non-privileged user that the app will run under.
# See https://docs.docker.com/go/dockerfile-user-best-practices/
ARG UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
appuser
RUN python3 -m venv /venv
ENV PATH="/venv/bin:$PATH"
# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.cache/pip to speed up subsequent builds.
# Leverage a bind mount to requirements.txt to avoid having to copy them into
# into this layer.
# this layer.
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=bind,source=requirements.txt,target=requirements.txt \
python -m pip install -r requirements.txt
pip install -r requirements.txt
# Switch to the non-privileged user to run the application.
USER appuser
# Use the minimal runtime image. It runs as nonroot by default.
FROM dhi.io/python:3.12
WORKDIR /app
COPY --from=builder /venv /venv
ENV PATH="/venv/bin:$PATH"
# Copy the source code into the container.
COPY . .
@@ -296,12 +247,12 @@ COPY . .
EXPOSE 8000
# Run the application.
CMD ["python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
CMD ["/venv/bin/python3", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]
```
{{< /file >}}
Create a file named `compose.yaml` with the following contents.
```yaml {collapse=true,title=compose.yaml}
{{< file path="compose.yaml" status="new" >}}
```yaml
# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Docker Compose reference guide at
# https://docs.docker.com/go/compose-spec-reference/
@@ -318,10 +269,10 @@ services:
ports:
- 8000:8000
```
{{< /file >}}
Create a file named `.dockerignore` with the following contents.
```text {collapse=true,title=".dockerignore"}
{{< file path=".dockerignore" status="new" >}}
```text
# Include any files or directories that you don't want to be copied to your
# container here (e.g., local build artifacts, temporary files, etc.).
#
@@ -357,10 +308,14 @@ Create a file named `.dockerignore` with the following contents.
LICENSE
README.md
```
{{< /file >}}
Create a file named `.gitignore` with the following contents.
{{< file path=".gitignore" >}}
```text
# Files and directories that Git should ignore. This is the standard Python
# template covering bytecode, build artifacts, virtual environments, and IDE
# settings. See https://git-scm.com/docs/gitignore for syntax reference.
```text {collapse=true,title=".gitignore"}
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -415,30 +370,18 @@ venv/
ENV/
env.bak/
venv.bak/
# Secrets
db/password.txt
```
{{< /file >}}
{{< /tab >}}
{{< /tabs >}}
{{< /files >}}
You should now have the following contents in your `python-docker-example`
directory.
```text
├── python-docker-example/
│ ├── app.py
│ ├── requirements.txt
│ ├── .dockerignore
│ ├── .gitignore
│ ├── compose.yaml
│ ├── Dockerfile
│ └── README.md
```
To learn more about the files, see the following:
To learn more about each file, see the following:
- [Dockerfile](/reference/dockerfile.md)
- [.dockerignore](/reference/dockerfile.md#dockerignore-file)
- [.gitignore](https://git-scm.com/docs/gitignore)
- [compose.yaml](/reference/compose-file/_index.md)
## Run the application
@@ -486,6 +429,9 @@ application using Docker.
Related information:
- [Docker Hardened Images](/dhi/)
- [Dockerfile reference](/reference/dockerfile.md)
- [Multi-stage builds](/manuals/build/building/multi-stage.md)
- [Docker Compose overview](/manuals/compose/_index.md)
## Next steps
+107 -26
View File
@@ -16,14 +16,47 @@ aliases:
## Overview
In this section, you'll learn how to use Docker Desktop to deploy your application to a fully-featured Kubernetes environment on your development machine. This allows you to test and debug your workloads on Kubernetes locally before deploying.
[Kubernetes](https://kubernetes.io/) is an open source platform that runs and
orchestrates container workloads across one or more machines. You describe
what you want to run, like which container images, how many replicas, and
which network ports to expose, in YAML manifest files. Kubernetes reads the
manifests and makes the cluster match that description.
In this section, you'll use the Kubernetes environment built into Docker
Desktop to deploy your application locally. You'll write two manifest files,
one for the PostgreSQL database and one for the FastAPI application, apply
them with `kubectl`, and verify the deployment by hitting your application
from a terminal.
## Registry authentication
The Docker Hardened Images used in this guide are hosted on `dhi.io`. Docker
Desktop's Kubernetes shares credentials with Docker Desktop, so the `docker login dhi.io`
you completed earlier is all that's needed. No additional image pull secret is required.
> [!NOTE]
>
> If you're deploying to a Kubernetes cluster outside of Docker Desktop, you'll
> need to create an image pull secret and reference it in your pod specs. See
> [Use a Docker Hardened Image](/dhi/how-to/use/#use-with-kubernetes) for instructions.
## Create a Kubernetes YAML file
In your `python-docker-dev-example` directory, create a file named `docker-postgres-kubernetes.yaml`. Open the file in an IDE or text editor and add
the following contents.
Create the following two Kubernetes manifest files in your
`python-docker-example` directory. Before applying
`docker-python-kubernetes.yaml`, replace `DOCKER_USERNAME/REPO_NAME` with your
Docker username and the repository name that you created in [Configure CI/CD for
your Python application](./configure-github-actions.md).
{{< files name="python-docker-example" >}}
{{< file path="docker-postgres-kubernetes.yaml" status="new" >}}
```yaml
# Kubernetes manifests for the PostgreSQL database used by the FastAPI app.
# Contains a Deployment, Service, PersistentVolumeClaim, and Secret.
# Deployment: runs one PostgreSQL pod. The image, port, env vars, and the
# persistent volume mount are all defined here.
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -41,7 +74,7 @@ spec:
spec:
containers:
- name: postgres
image: postgres:18
image: dhi.io/postgres:18
ports:
- containerPort: 5432
env:
@@ -62,6 +95,8 @@ spec:
persistentVolumeClaim:
claimName: postgres-pvc
---
# Service: exposes PostgreSQL inside the cluster on port 5432 so the
# application pod can reach it by the DNS name `postgres`.
apiVersion: v1
kind: Service
metadata:
@@ -73,6 +108,7 @@ spec:
selector:
app: postgres
---
# PersistentVolumeClaim: storage that survives pod restarts.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
@@ -85,6 +121,8 @@ spec:
requests:
storage: 1Gi
---
# Secret: holds the database password (base64-encoded). Referenced by both
# the postgres Deployment and the application Deployment.
apiVersion: v1
kind: Secret
metadata:
@@ -94,13 +132,16 @@ type: Opaque
data:
POSTGRES_PASSWORD: cG9zdGdyZXNfcGFzc3dvcmQ= # Base64 encoded password (e.g., 'postgres_password')
```
{{< /file >}}
In your `python-docker-dev-example` directory, create a file named
`docker-python-kubernetes.yaml`. Replace `DOCKER_USERNAME/REPO_NAME` with your
Docker username and the repository name that you created in [Configure CI/CD for
your Python application](./configure-github-actions.md).
{{< file path="docker-python-kubernetes.yaml" status="new" >}}
```yaml
# Kubernetes manifests for the FastAPI application.
# Contains a Deployment and a NodePort Service.
# Deployment: runs the FastAPI app. Connection details to the postgres
# service are passed in via environment variables, and the database
# password comes from the shared postgres-secret.
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -135,8 +176,10 @@ spec:
- name: POSTGRES_PORT
value: "5432"
ports:
- containerPort: 8001
- containerPort: 8000
---
# Service: exposes the FastAPI app on port 30001 of the cluster node so
# you can reach it from your host with `curl http://localhost:30001/`.
apiVersion: v1
kind: Service
metadata:
@@ -147,12 +190,15 @@ spec:
selector:
service: fastapi
ports:
- port: 8001
targetPort: 8001
- port: 8000
targetPort: 8000
nodePort: 30001
```
{{< /file >}}
In these Kubernetes YAML file, there are various objects, separated by the `---`:
{{< /files >}}
In these Kubernetes YAML files, there are various objects, separated by the `---`:
- A Deployment, describing a scalable group of identical pods. In this case,
you'll get just one replica, or copy of your pod. That pod, which is
@@ -161,20 +207,20 @@ In these Kubernetes YAML file, there are various objects, separated by the `---`
your Python application](configure-github-actions.md).
- A Service, which will define how the ports are mapped in the containers.
- A PersistentVolumeClaim, to define a storage that will be persistent through restarts for the database.
- A Secret, Keeping the database password as an example using secret kubernetes resource.
- A Secret, which stores the database password as a Kubernetes Secret resource.
- A NodePort service, which will route traffic from port 30001 on your host to
port 8001 inside the pods it routes to, allowing you to reach your app
port 8000 inside the pods it routes to, so you can reach your app
from the network.
To learn more about Kubernetes objects, see the [Kubernetes documentation](https://kubernetes.io/docs/home/).
> [!NOTE]
>
> - The `NodePort` service is good for development/testing purposes. For production you should implement an [ingress-controller](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/).
> The `NodePort` service is good for development and testing. For production, implement an [ingress controller](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/) instead.
## Deploy and check your application
1. In a terminal, navigate to `python-docker-dev-example` and deploy your database to
1. In a terminal, navigate to `python-docker-example` and deploy your database to
Kubernetes.
```console
@@ -190,10 +236,10 @@ To learn more about Kubernetes objects, see the [Kubernetes documentation](https
secret/postgres-secret created
```
Now, deploy your python application.
Now, deploy your Python application.
```console
kubectl apply -f docker-python-kubernetes.yaml
$ kubectl apply -f docker-python-kubernetes.yaml
```
You should see output that looks like the following, indicating your Kubernetes objects were created successfully.
@@ -229,20 +275,55 @@ To learn more about Kubernetes objects, see the [Kubernetes documentation](https
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 13h
postgres ClusterIP 10.43.209.25 <none> 5432/TCP 3m10s
service-entrypoint NodePort 10.43.67.120 <none> 8001:30001/TCP 79s
service-entrypoint NodePort 10.43.67.120 <none> 8000:30001/TCP 79s
```
In addition to the default `kubernetes` service, you can see your `service-entrypoint` service, accepting traffic on port 30001/TCP and the internal `ClusterIP` `postgres` with the port `5432` open to accept connections from you python app.
In addition to the default `kubernetes` service, you can see your `service-entrypoint` service, accepting traffic on port 30001/TCP and the internal `ClusterIP` `postgres` with the port `5432` open to accept connections from your Python app.
3. In a terminal, curl the service. Note that a database was not deployed in
this example.
3. In a terminal, curl the root endpoint to verify the application is running.
```console
$ curl http://localhost:30001/
Hello, Docker!!!
Hello, Docker!
```
4. Run the following commands to tear down your application.
4. Exercise the database by creating a hero with a POST request:
```console
$ curl -X 'POST' \
'http://localhost:30001/heroes/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"id": 1,
"name": "my hero",
"secret_name": "austing",
"age": 12
}'
```
You should receive the following response:
```json
{
"age": 12,
"id": 1,
"name": "my hero",
"secret_name": "austing"
}
```
Then read it back with a GET request:
```console
$ curl http://localhost:30001/heroes/
```
You should receive an array containing the hero you just created. This
confirms the application can read from and write to the PostgreSQL database
running in the cluster.
5. Run the following commands to tear down your application.
```console
$ kubectl delete -f docker-python-kubernetes.yaml
@@ -257,4 +338,4 @@ Related information:
- [Kubernetes documentation](https://kubernetes.io/docs/home/)
- [Deploy on Kubernetes with Docker Desktop](/manuals/desktop/use-desktop/kubernetes.md)
- [Swarm mode overview](/manuals/engine/swarm/_index.md)
- [Use a Docker Hardened Image with Kubernetes](/dhi/how-to/use/#use-with-kubernetes)
File diff suppressed because it is too large Load Diff
+99 -33
View File
@@ -10,31 +10,38 @@ aliases:
## Prerequisites
Complete [Develop your app](develop.md).
Complete [Develop your app](develop.md). This topic requires a local Python
installation because the tools and Git hooks introduced here run on your
host. If you don't want to install Python locally, skip this topic. The same
checks run in CI in the [next topic](configure-github-actions.md).
## Overview
In this section, you'll learn how to set up code quality tools for your Python application. This includes:
Linting, formatting, and type checking are automated ways to catch bugs,
enforce style, and spot type errors before code runs. Running them on every
commit, in CI, and in your editor catches problems early when they're cheap
to fix.
- Linting and formatting with Ruff
- Static type checking with Pyright
- Automating checks with pre-commit hooks
In this section, you'll configure three tools for your Python application.
Ruff handles linting and formatting in a single fast pass. Pyright statically
checks your code for type errors. Pre-commit hooks run both of these
automatically before each Git commit so problems are caught locally before
they're committed.
## Linting and formatting with Ruff
Ruff is an extremely fast Python linter and formatter written in Rust. It replaces multiple tools like flake8, isort, and black with a single unified tool.
Before using Ruff, install it in your Python environment:
Create a `pyproject.toml` file in your `python-docker-example` directory:
```bash
pip install ruff
```
If you're using a virtual environment, make sure it is activated so the `ruff` command is available when you run the commands below.
Create a `pyproject.toml` file:
{{< files name="python-docker-example" >}}
{{< file path="pyproject.toml" status="new" >}}
```toml
# Configuration for code-quality tools.
# - [tool.ruff]: linting and formatting (https://docs.astral.sh/ruff/)
# - [tool.pyright]: static type checking (https://microsoft.github.io/pyright/)
[tool.ruff]
target-version = "py312"
@@ -56,61 +63,114 @@ ignore = [
"B904", # Allow raising exceptions without from e, for HTTPException
]
```
{{< /file >}}
### Using Ruff
{{< /files >}}
Install Ruff:
```console
$ pip install ruff
```
If you're using a virtual environment, make sure it is activated so the `ruff`
command is available.
Run these commands to check and format your code:
```bash
```console
# Check for errors
ruff check .
$ ruff check .
# Automatically fix fixable errors
ruff check --fix .
$ ruff check --fix .
# Format code
ruff format .
$ ruff format .
```
## Type checking with Pyright
Pyright is a fast static type checker for Python that works well with modern Python features.
Add `Pyright` configuration in `pyproject.toml`:
Update `pyproject.toml` to add the Pyright configuration at the bottom.
{{< files name="python-docker-example" >}}
{{< file path="pyproject.toml" status="modified" hl_lines="25-29" >}}
```toml
# Configuration for code-quality tools.
# - [tool.ruff]: linting and formatting (https://docs.astral.sh/ruff/)
# - [tool.pyright]: static type checking (https://microsoft.github.io/pyright/)
[tool.ruff]
target-version = "py312"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG001", # unused arguments in functions
]
ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"W191", # indentation contains tabs
"B904", # Allow raising exceptions without from e, for HTTPException
]
[tool.pyright]
typeCheckingMode = "strict"
pythonVersion = "3.12"
exclude = [".venv"]
```
{{< /file >}}
### Running Pyright
{{< /files >}}
To check your code for type errors:
Install Pyright and run it:
```bash
pyright
```console
$ pip install pyright
$ pyright
```
## Setting up pre-commit hooks
Pre-commit hooks automatically run checks before each commit. The following `.pre-commit-config.yaml` snippet sets up Ruff:
Pre-commit hooks run checks automatically before each commit on your local
machine. Create a `.pre-commit-config.yaml` file in your `python-docker-example`
directory to set up Ruff hooks:
{{< files name="python-docker-example" >}}
{{< file path=".pre-commit-config.yaml" status="new" >}}
```yaml
https: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.2.2
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
# Pre-commit hook configuration. Runs Ruff (lint + format) on every
# `git commit`. See https://pre-commit.com/
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.15
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
```
{{< /file >}}
{{< /files >}}
To install and use:
```bash
pre-commit install
git commit -m "Test commit" # Automatically runs checks
```console
$ pip install pre-commit
$ pre-commit install
$ git commit -m "Test commit" # Automatically runs checks
```
## Summary
@@ -123,6 +183,12 @@ In this section, you learned how to:
These tools help maintain code quality and catch errors early in development.
Related information:
- [Ruff documentation](https://docs.astral.sh/ruff/)
- [Pyright documentation](https://microsoft.github.io/pyright/)
- [pre-commit framework](https://pre-commit.com/)
## Next steps
- [Configure GitHub Actions](configure-github-actions.md) to run these checks automatically
@@ -0,0 +1,144 @@
---
title: Secure your Python image supply chain
linkTitle: Secure your supply chain
weight: 45
keywords: python, sbom, provenance, attestations, docker scout, supply chain, security
description: Learn how to inspect, generate, and verify supply chain attestations for your Python container image.
---
## Prerequisites
Complete [Configure CI/CD for your Python application](configure-github-actions.md).
## Overview
When you ship a container image, what's inside it and where it came from
matters. Supply chain attestations are signed records that answer questions
like which packages are in the image, what vulnerabilities affect them, how
the image was built, and what security checks it passed.
In this section, you'll inspect the attestations that ship with your Docker
Hardened Image base, generate your own SBOM and provenance attestations
during CI, and pin the base image by digest so your builds are reproducible.
The inspection commands in this topic are shown manually so you can see what
each one returns. In a real workflow you'd automate these checks with
[Docker Scout](/scout/), which runs the same scans on every push,
enforces policies in CI, and surfaces results in your registry and pull
requests.
## Inspect the base image attestations
Docker Hardened Images are built to SLSA Build Level 3 and ship with a set of
signed attestations covering bill-of-materials, vulnerabilities, build
provenance, and security scans. See
[DHI attestations](/manuals/dhi/core-concepts/attestations.md) for the full
list of types and how to verify their signatures with Cosign.
List all the attestations available on the Python DHI:
```console
$ docker scout attest list registry://dhi.io/python:3.12
```
View the SBOM:
```console
$ docker scout sbom registry://dhi.io/python:3.12
```
Check known vulnerabilities:
```console
$ docker scout cves registry://dhi.io/python:3.12
```
> [!NOTE]
>
> The `registry://` prefix forces `docker scout` to fetch the image and its
> attestations from the registry instead of reading a locally pulled copy. If
> you've already pulled or built against the base image, the local copy
> doesn't have the attached attestations, so the prefix is required to see
> them.
When you base your own image on a DHI image, these attestations stay attached to the base layer in the registry. Tools that inspect your image can follow the chain back to the DHI source.
## Generate attestations for your image
Update your GitHub Actions workflow to attach SBOM and provenance attestations to the image you push.
Edit `.github/workflows/build.yml` and update the build-and-push step:
```yaml {hl_lines="6-7"}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
sbom: true
provenance: mode=max
tags: ${{ steps.meta.outputs.tags }}
```
- `sbom: true` tells BuildKit to scan the built image and attach an SBOM attestation.
- `provenance: mode=max` records detailed build provenance, including the source repository, commit, and build parameters.
The next time your workflow runs, the pushed image will carry these attestations alongside the image manifest in the registry.
## Inspect your pushed image's attestations
After your workflow pushes the image, inspect it the same way you inspected the base image:
```console
$ docker scout attest list registry://DOCKER_USERNAME/REPO_NAME:latest
$ docker scout sbom registry://DOCKER_USERNAME/REPO_NAME:latest
```
The SBOM includes packages from every layer, including those inherited from `dhi.io/python:3.12`. The provenance record references the DHI base image by digest, so consumers of your image can trace the build chain back to the DHI source.
## Pin the base image by digest
Image tags like `dhi.io/python:3.12` move over time as new patches land. For reproducible builds, pin to an immutable digest.
The Dockerfile uses two tags, `dhi.io/python:3.12-dev` in the builder stage
and `dhi.io/python:3.12` in the runtime stage. Each tag has its own digest,
so look up both:
```console
$ docker buildx imagetools inspect dhi.io/python:3.12-dev --format "{{ .Manifest.Digest }}"
sha256:4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945
$ docker buildx imagetools inspect dhi.io/python:3.12 --format "{{ .Manifest.Digest }}"
sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```
Each digest is a 64-character hex string. Update your `Dockerfile` to reference
each digest on the matching `FROM` line:
```dockerfile
FROM dhi.io/python:3.12-dev@sha256:4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945 AS builder
# ...
FROM dhi.io/python:3.12@sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
```
> [!TIP]
>
> Pinning by digest also pins you to that image's vulnerabilities. Use [Dependabot](https://docs.github.com/en/code-security/dependabot) or [Renovate](https://docs.renovatebot.com/) to automate digest updates so you get a PR when a new patched image is available, with a changelog to review before merging.
## Summary
In this section, you learned how to:
- Inspect the supply chain attestations that ship with the DHI base image, including SBOMs, CVE reports, VEX statements, and scan results
- Generate SBOM and provenance attestations for your own image in CI
- Pin base images by digest for reproducible builds
Related information:
- [DHI attestations](/manuals/dhi/core-concepts/attestations.md)
- [Verify a Docker Hardened Image](/manuals/dhi/how-to/verify.md)
- [Docker Scout](/scout/)
- [Build attestations](/manuals/build/metadata/attestations/_index.md)
## Next steps
In the next section, you'll deploy your application to Kubernetes.
+78
View File
@@ -0,0 +1,78 @@
{{/*
file shortcode — child of the `files` shortcode.
The body must be a fenced code block. The fence's info string is used as
the syntax-highlighting language. Wrapping the body in a fence keeps
markdownlint and Vale happy (they treat fence content as code and skip
lint rules that would otherwise fire on `#` comment lines).
Usage:
{{< file path="app.py" >}}
```python
from fastapi import FastAPI
```
{{< /file >}}
Attributes:
path required. File path within the project, can include folders (e.g. "db/password.txt").
lang optional. Overrides the fence info string for syntax highlighting.
status optional. One of "modified" or "new". Shows a colored badge in the file tree.
hl_lines optional. Line numbers/ranges to highlight (e.g. "8" or "6,8-11"). Passed to chroma.
*/}}
{{ if ne .Parent.Name "files" }}
{{- errorf "file shortcode missing its 'files' parent: %s" .Position -}}
{{ end }}
{{ $path := trim (.Get "path") " " }}
{{ if not $path }}
{{- errorf "file shortcode requires a path attribute: %s" .Position -}}
{{ end }}
{{ if not (.Parent.Store.Get "files") }}
{{ .Parent.Store.Set "files" slice }}
{{ end }}
{{/* Normalize line endings so CRLF source files behave identically to LF,
then trim leading/trailing whitespace so the fence on the first line
can be detected. */}}
{{ $content := .Inner }}
{{ $content = replace $content "\r\n" "\n" }}
{{ $content = replace $content "\r" "\n" }}
{{ $content = strings.TrimLeft "\n\t " $content }}
{{ $content = strings.TrimRight "\n\t " $content }}
{{/* Body must be wrapped in a fenced code block. Strip the fence and adopt
the info string as the default language. */}}
{{ $lines := split $content "\n" }}
{{ $lineCount := len $lines }}
{{ $firstLine := index $lines 0 }}
{{ $lastLine := index $lines (sub $lineCount 1) }}
{{ if or (lt $lineCount 2) (not (hasPrefix $firstLine "```")) (ne (trim $lastLine " \t") "```") }}
{{- errorf "file %q: body must be a fenced code block (```language ... ```): %s" $path .Position -}}
{{ end }}
{{ $fenceLang := trim (strings.TrimPrefix "```" $firstLine) " \t" }}
{{ $content = delimit (after 1 (first (sub $lineCount 1) $lines)) "\n" }}
{{/* Language: explicit lang= attribute wins; otherwise use the fence info string. */}}
{{ $lang := .Get "lang" | default $fenceLang }}
{{/* Normalize optional status attribute. */}}
{{ $status := lower (.Get "status" | default "") }}
{{ if and $status (not (in (slice "" "modified" "new") $status)) }}
{{- errorf "file shortcode 'status' must be \"modified\", \"new\", or empty: %s" .Position -}}
{{ end }}
{{/* hl_lines: accept either "8" or "8-10" or "6,8-10,12"; chroma expects spaces. */}}
{{ $hlLines := .Get "hl_lines" | default "" }}
{{ if $hlLines }}
{{ $hlLines = replace $hlLines "," " " }}
{{ end }}
{{ $.Parent.Store.Add "files" (dict
"path" $path
"lang" $lang
"content" $content
"status" $status
"hl_lines" $hlLines
) }}
+391
View File
@@ -0,0 +1,391 @@
{{/*
files shortcode — renders a VS Code-style file browser with scaffolding commands.
Usage:
{{< files name="my-project" >}}
{{< file path="app.py" >}}
...contents...
{{< /file >}}
{{< file path="db/password.txt" >}}
secretpassword
{{< /file >}}
{{< /files >}}
Attributes:
name required. Project name. Used as the window title and as the
top-level directory in the scaffolding commands.
*/}}
{{ with .Inner }}{{/* trigger child rendering */}}{{ end }}
{{ $name := trim (.Get "name") " " }}
{{ if not $name }}
{{- errorf "files shortcode requires a name attribute: %s" .Position -}}
{{ end }}
{{ $files := .Store.Get "files" }}
{{ if not $files }}
{{- errorf "files shortcode is empty: %s" .Position -}}
{{ end }}
{{/* Collect unique parent directories (relative to project root) for mkdir. */}}
{{ $dirs := slice }}
{{ range $files }}
{{ $d := path.Dir .path }}
{{ if and (ne $d ".") (not (in $dirs $d)) }}
{{ $dirs = $dirs | append $d }}
{{ end }}
{{ end }}
{{/* Build the bash scaffolding command. */}}
{{ $bashLines := slice }}
{{ if $dirs }}
{{ $mkdirPaths := slice }}
{{ range $d := $dirs }}
{{ $mkdirPaths = $mkdirPaths | append (printf "%s/%s" $name $d) }}
{{ end }}
{{ $bashLines = $bashLines | append (printf "mkdir -p %s && cd %s" (delimit $mkdirPaths " ") $name) }}
{{ else }}
{{ $bashLines = $bashLines | append (printf "mkdir %s && cd %s" $name $name) }}
{{ end }}
{{/* Use an unlikely heredoc delimiter so a file whose content contains a
bare `EOF` line (for example, a Dockerfile that itself has a `RUN <<EOF`
block) doesn't close the scaffold's heredoc early and leak the rest of
the file as shell commands. */}}
{{ range $files }}
{{ $bashLines = $bashLines | append (printf "cat > %s <<'__DOCKER_DOCS_SCAFFOLD_EOF__'" .path) }}
{{ $bashLines = $bashLines | append .content }}
{{ $bashLines = $bashLines | append "__DOCKER_DOCS_SCAFFOLD_EOF__" }}
{{ end }}
{{ $bashScript := delimit $bashLines "\n" }}
{{/* Build the PowerShell scaffolding command. */}}
{{/* Write files via [System.IO.File]::WriteAllText with an explicit
UTF8Encoding($false), so output is UTF-8 without BOM in both Windows
PowerShell 5.1 and PowerShell 7+. The built-in `Set-Content -Encoding utf8`
emits a BOM on 5.1, which can break Dockerfile `# syntax=` directives and
some YAML parsers. */}}
{{ $psLines := slice }}
{{ $psLines = $psLines | append "# Write files as UTF-8 without BOM. Works on Windows PowerShell 5.1 and PowerShell 7+." }}
{{ $psLines = $psLines | append "function WriteFile([string]$Path, [string]$Content) {" }}
{{ $psLines = $psLines | append " $full = Join-Path -Path (Get-Location).ProviderPath -ChildPath $Path" }}
{{ $psLines = $psLines | append " [System.IO.File]::WriteAllText($full, $Content, [System.Text.UTF8Encoding]::new($false))" }}
{{ $psLines = $psLines | append "}" }}
{{ $psLines = $psLines | append "" }}
{{ $psLines = $psLines | append (printf "New-Item -ItemType Directory -Force -Path %s | Out-Null" $name) }}
{{ range $d := $dirs }}
{{ $psLines = $psLines | append (printf "New-Item -ItemType Directory -Force -Path %s/%s | Out-Null" $name $d) }}
{{ end }}
{{ $psLines = $psLines | append (printf "Set-Location %s" $name) }}
{{ range $files }}
{{ $psLines = $psLines | append (printf "WriteFile '%s' @'" .path) }}
{{ $psLines = $psLines | append .content }}
{{ $psLines = $psLines | append "'@" }}
{{ end }}
{{ $psScript := delimit $psLines "\n" }}
{{/*
Build a tree-friendly view of the file list.
- $rootFiles: files at project root (dict with "index" and "name")
- $folderPaths: ordered, deduped list of folder paths (parents auto-included)
- $folderFiles: map of folder path -> slice of files in that folder
We then sort $folderPaths lexicographically so parents naturally come
before their children when iterating.
*/}}
{{ $rootFiles := slice }}
{{ $folderPaths := slice }}
{{ $folderFiles := dict }}
{{ range $i, $f := $files }}
{{ $dir := path.Dir $f.path }}
{{ $entry := dict "index" $i "name" (path.Base $f.path) "path" $f.path "status" ($f.status | default "") }}
{{ if eq $dir "." }}
{{ $rootFiles = $rootFiles | append $entry }}
{{ else }}
{{/* Make sure every parent folder along the path is registered. */}}
{{ $parts := split $dir "/" }}
{{ $prefix := "" }}
{{ range $part := $parts }}
{{ if $prefix }}
{{ $prefix = printf "%s/%s" $prefix $part }}
{{ else }}
{{ $prefix = $part }}
{{ end }}
{{ if not (in $folderPaths $prefix) }}
{{ $folderPaths = $folderPaths | append $prefix }}
{{ end }}
{{ end }}
{{ $existing := index $folderFiles $dir | default slice }}
{{ $existing = $existing | append $entry }}
{{ $folderFiles = merge $folderFiles (dict $dir $existing) }}
{{ end }}
{{ end }}
{{ $folderPaths = sort $folderPaths }}
<div
data-pagefind-ignore
class="not-prose my-6"
x-data="{
view: 'files',
shell: 'bash',
selected: 0,
scaffoldCopied: false,
fileCopied: -1,
bashScript: atob('{{ encoding.Base64Encode $bashScript }}'),
psScript: atob('{{ encoding.Base64Encode $psScript }}'),
fileContents: [
{{- range $i, $f := $files -}}
{{- if $i }},{{ end -}}
atob('{{ encoding.Base64Encode $f.content }}')
{{- end -}}
],
copyScaffold() {
const script = this.shell === 'bash' ? this.bashScript : this.psScript;
window.navigator.clipboard.writeText(script);
this.scaffoldCopied = true;
setTimeout(() => this.scaffoldCopied = false, 2000);
},
copyFile(i) {
window.navigator.clipboard.writeText(this.fileContents[i]);
this.fileCopied = i;
setTimeout(() => { if (this.fileCopied === i) this.fileCopied = -1; }, 2000);
}
}"
>
<div
class="overflow-x-auto [&::-webkit-scrollbar]:h-3 [&::-webkit-scrollbar-thumb]:rounded [&::-webkit-scrollbar-thumb]:bg-gray-400 [&::-webkit-scrollbar-track]:bg-gray-100 dark:[&::-webkit-scrollbar-thumb]:bg-gray-600 dark:[&::-webkit-scrollbar-track]:bg-gray-800"
style="touch-action: pan-x pan-y;"
>
<div
class="min-w-[40rem] overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900"
>
{{/* Window title bar */}}
<div
class="flex flex-wrap items-center gap-3 border-b border-gray-200 bg-gray-100 px-3 py-2 dark:border-gray-800 dark:bg-gray-800"
>
<div class="flex items-center gap-2">
<div class="flex gap-1.5">
<span class="h-3 w-3 rounded-full bg-red-400"></span>
<span class="h-3 w-3 rounded-full bg-yellow-400"></span>
<span class="h-3 w-3 rounded-full bg-green-400"></span>
</div>
<span
class="ml-2 font-mono text-sm text-gray-700 dark:text-gray-200"
>{{ $name }}</span
>
</div>
{{/* Primary view toggle: Files | Scaffold script */}}
<div
class="ml-auto flex gap-0.5 rounded border border-gray-300 bg-white p-0.5 text-xs dark:border-gray-700 dark:bg-gray-900"
role="tablist"
>
<button
type="button"
role="tab"
class="rounded px-2 py-1 font-medium"
:class="view === 'files' ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'"
@click="view = 'files'"
>
Files
</button>
<button
type="button"
role="tab"
class="rounded px-2 py-1 font-medium"
:class="view === 'scaffold' ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'"
@click="view = 'scaffold'; scaffoldCopied = false"
>
Scaffold script
</button>
</div>
</div>
{{/* Files view: sidebar + content (fixed height with internal scroll) */}}
<div class="flex h-[28rem]" x-show="view === 'files'">
{{/* Sidebar */}}
<aside
class="w-56 shrink-0 overflow-y-auto border-r border-gray-200 bg-gray-50 py-2 dark:border-gray-800 dark:bg-gray-950"
>
<ul class="text-sm" role="tablist">
{{/* Project root folder header — always at the top. */}}
<li>
<div
class="flex items-center gap-2 py-1 pr-3 font-mono font-medium text-gray-700 dark:text-gray-200"
style="padding-left: 12px"
>
<span class="icon-svg icon-sm shrink-0 text-gray-500 dark:text-gray-400"
>{{ partialCached "icon" "folder-open" "folder-open" }}</span
>
<span class="truncate">{{ $name }}</span>
</div>
</li>
{{/* Folders first (sorted so parents precede children), then root files. All shifted one level deeper because the project root is depth 0. */}}
{{ range $folderPath := $folderPaths }}
{{ $parts := split $folderPath "/" }}
{{ $depth := len $parts }}
{{ $basename := index $parts (sub (len $parts) 1) }}
<li>
<div
class="flex items-center gap-2 py-1 pr-3 font-mono text-gray-500 dark:text-gray-400"
style="padding-left: {{ add 12 (mul $depth 16) }}px"
>
<span class="icon-svg icon-sm shrink-0 text-gray-400"
>{{ partialCached "icon" "folder" "folder" }}</span
>
<span class="truncate">{{ $basename }}</span>
</div>
</li>
{{ $filesInFolder := index $folderFiles $folderPath | default slice }}
{{ range $entry := $filesInFolder }}
<li>
<button
type="button"
role="tab"
class="flex w-full items-center gap-2 py-1 pr-3 text-left font-mono"
:class="selected === {{ $entry.index }} ? 'bg-gray-200 text-blue-700 dark:bg-gray-800 dark:text-blue-400' : 'text-gray-700 hover:bg-gray-100 hover:text-blue-700 dark:text-gray-200 dark:hover:bg-gray-900 dark:hover:text-blue-400'"
@click="selected = {{ $entry.index }}"
title="{{ $entry.path }}"
style="padding-left: {{ add 12 (mul (add $depth 1) 16) }}px"
>
<span class="icon-svg icon-sm shrink-0 text-gray-400"
>{{ partialCached "icon" "document-text" "document-text" }}</span
>
<span class="truncate">{{ $entry.name }}</span>
{{ if eq $entry.status "modified" }}
<span class="ml-auto shrink-0 text-xs font-bold text-amber-600 dark:text-amber-400" title="Modified">M</span>
{{ else if eq $entry.status "new" }}
<span class="ml-auto shrink-0 text-xs font-bold text-green-600 dark:text-green-400" title="Added">A</span>
{{ end }}
</button>
</li>
{{ end }}
{{ end }}
{{ range $entry := $rootFiles }}
<li>
<button
type="button"
role="tab"
class="flex w-full items-center gap-2 py-1 pr-3 text-left font-mono"
:class="selected === {{ $entry.index }} ? 'bg-gray-200 text-blue-700 dark:bg-gray-800 dark:text-blue-400' : 'text-gray-700 hover:bg-gray-100 hover:text-blue-700 dark:text-gray-200 dark:hover:bg-gray-900 dark:hover:text-blue-400'"
@click="selected = {{ $entry.index }}"
title="{{ $entry.path }}"
style="padding-left: 28px"
>
<span class="icon-svg icon-sm shrink-0 text-gray-400"
>{{ partialCached "icon" "document-text" "document-text" }}</span
>
<span class="truncate">{{ $entry.name }}</span>
{{ if eq $entry.status "modified" }}
<span class="ml-auto shrink-0 text-xs font-bold text-amber-600 dark:text-amber-400" title="Modified">M</span>
{{ else if eq $entry.status "new" }}
<span class="ml-auto shrink-0 text-xs font-bold text-green-600 dark:text-green-400" title="Added">A</span>
{{ end }}
</button>
</li>
{{ end }}
</ul>
</aside>
{{/* Content pane. The outer `main` is the positioning context for the
copy button and stays fixed; the inner div is the scroll container
so content scrolls independently of the button. The chroma
`.highlight` wrapper's own overflow-x is disabled so the scroll
container owns both axes and scrollbars sit at the window edges. */}}
<main class="group relative min-w-0 flex-1 overflow-hidden">
{{/* Per-file copy button. Matches the codeblock copy style: icon-only,
hover-revealed, swaps to a check icon for two seconds on copy. */}}
<button
type="button"
class="absolute top-2 right-5 z-10 text-gray-300 dark:text-gray-500"
title="Copy file contents"
@click="copyFile(selected)"
>
<span :class="{ 'group-hover:block' : fileCopied !== selected }" class="icon-svg hidden"
>{{ partialCached "icon" "document-duplicate" "document-duplicate" }}</span
>
<span :class="{ 'group-hover:block' : fileCopied === selected }" class="icon-svg hidden"
>{{ partialCached "icon" "check-circle" "check-circle" }}</span
>
</button>
<div class="h-full overflow-auto [&_.highlight]:overflow-visible">
{{ range $i, $f := $files }}
{{ $opts := "" }}
{{ with $f.hl_lines }}
{{ $opts = printf "hl_lines=%s" . }}
{{ end }}
<div
role="tabpanel"
x-show="selected === {{ $i }}"
class="syntax-light dark:syntax-dark"
>
{{ (transform.Highlight $f.content $f.lang $opts) | safeHTML }}
</div>
{{ end }}
</div>
</main>
</div>
{{/* Scaffold script view, with Bash | PowerShell secondary toggle (fixed height with internal scroll) */}}
<div class="flex h-[28rem] flex-col" x-show="view === 'scaffold'" x-cloak>
{{/* Yellow callout: warning + shell selector */}}
<div class="flex flex-wrap items-center gap-3 border-b border-yellow-200 bg-yellow-50 px-3 py-2 text-xs text-yellow-900 dark:border-yellow-900/40 dark:bg-yellow-950/40 dark:text-yellow-200">
<div class="flex items-center gap-2">
<span class="icon-svg icon-sm shrink-0"
>{{ partialCached "icon" "exclamation-triangle" "exclamation-triangle" }}</span
>
<span>Overwrites existing files with the same names. Run from the parent of your project directory.</span>
</div>
<div
class="ml-auto flex gap-0.5 rounded border border-yellow-300 bg-white p-0.5 dark:border-yellow-900/60 dark:bg-gray-900"
role="tablist"
>
<button
type="button"
role="tab"
class="rounded px-2 py-0.5 font-medium"
:class="shell === 'bash' ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'"
@click="shell = 'bash'; scaffoldCopied = false"
>
Bash
</button>
<button
type="button"
role="tab"
class="rounded px-2 py-0.5 font-medium"
:class="shell === 'powershell' ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'"
@click="shell = 'powershell'; scaffoldCopied = false"
>
PowerShell
</button>
</div>
</div>
{{/* Code area. Outer div is the positioning context for the copy
button and stays fixed; inner div is the scroll container. */}}
<div class="group relative flex-1 overflow-hidden">
<button
type="button"
class="absolute top-2 right-5 z-10 text-gray-300 dark:text-gray-500"
title="Copy scaffold script"
@click="copyScaffold()"
>
<span :class="{ 'group-hover:block' : !scaffoldCopied }" class="icon-svg hidden"
>{{ partialCached "icon" "document-duplicate" "document-duplicate" }}</span
>
<span :class="{ 'group-hover:block' : scaffoldCopied }" class="icon-svg hidden"
>{{ partialCached "icon" "check-circle" "check-circle" }}</span
>
</button>
<div class="h-full overflow-auto [&_.highlight]:overflow-visible">
<div x-show="shell === 'bash'" class="syntax-light dark:syntax-dark">
{{ (transform.Highlight $bashScript "bash" "") | safeHTML }}
</div>
<div x-show="shell === 'powershell'" x-cloak class="syntax-light dark:syntax-dark">
{{ (transform.Highlight $psScript "powershell" "") | safeHTML }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
+20
View File
@@ -0,0 +1,20 @@
{{- /*
Markdown output of the files shortcode (used for the *.md
alternative output format that LLMs consume).
Renders the project name and each file as a fenced code block with
its path as a label. The file.html child populates the same .Store
this template reads from.
*/ -}}
{{- with .Inner }}{{/* trigger child shortcodes */}}{{ end -}}
{{- $name := trim (.Get "name") " " -}}
{{- $files := .Store.Get "files" -}}
**`{{ $name }}/`**
{{ range $files }}
`{{ .path }}`{{ with .status }} ({{ . }}){{ end }}:
```{{ .lang }}
{{ .content }}
```
{{ end }}