A minimal container image built to demonstrate the Dockerfile conventions that GitHub Copilot will not apply unless you ask for them.
The application is a fifty-line HTTP server. It is trivial on purpose — the subject here is the Dockerfile.
Ask Copilot for "a Dockerfile for a Python service" in a repository with no
instruction file and you will typically get a single-stage image, running as
root, with CMD in shell form. Every one of those is defensible as a first
draft and every one is wrong in production:
| Default | Consequence |
|---|---|
| Single stage | Build tooling and source history ship to production |
| Runs as root | A container escape starts as uid 0 |
CMD python app.py (shell form) |
/bin/sh does not forward SIGTERM; the container takes the full kill timeout and drops in-flight requests |
FROM python:latest |
Rebuilds are silent upgrades |
No .dockerignore |
COPY . . ships .git and any local .env into a readable layer |
.github/copilot-instructions.md states each rule and the reason. The Dockerfile
in this repository follows all of them.
- Docker with BuildKit (Docker 23 or later)
copilot-docker-starter/
├── Dockerfile Multi-stage, non-root, exec-form entrypoint
├── .dockerignore A security control before it is a performance one
├── app/main.py The trivial service
└── .github/
├── copilot-instructions.md
└── instructions/docker.instructions.md Scoped to **/Dockerfile*
docker build -t copilot-docker-starter .
docker run --rm -p 8080:8080 copilot-docker-starter
curl -s localhost:8080/health$ curl -s localhost:8080/health
{"status": "ok"}
$ docker inspect --format '{{.Config.User}}' copilot-docker-starter
10001:10001docker stop returns in 830ms (measured), because the entrypoint is exec
form and the process handles SIGTERM correctly. Two things have to be right
for that, and the second one is easy to get wrong:
- Exec-form
ENTRYPOINT, so the process is PID 1 and receives the signal at all rather than sitting behind a/bin/shthat does not forward it. - The handler must not call
server.shutdown()directly. That call blocks untilserve_forever()returns, and a handler interruptingserve_forever()in the same thread deadlocks waiting for itself.
The first draft of this project had (1) and not (2), and stopped in 10,499ms
— indistinguishable from a container that ignores SIGTERM entirely. See
app/main.py for the fix and the reasoning.
docker build -t copilot-docker-starter .
docker run --rm --read-only copilot-docker-starter python -c "print('ok')"The image runs with a read-only root filesystem because the application is byte-compiled in the build stage and writes nothing at runtime.
Validation status: docker build is executed as part of
npm run assets:check in The Copilot Stack repository. hadolint is not
installed there and is not run.
- No secrets in any layer, build argument or environment variable.
- Runs as uid 10001; application code is owned by root and not writable by the running process.
.dockerignoreexcludes.git,.env*,*.pemand*.key.- Base image is pinned to a minor version, not
latest.
- Docker — Dockerfile reference
- Docker — Building best practices
- GitHub Docs — Adding repository custom instructions for GitHub Copilot
MIT. See LICENSE.