Docker / Images and container fundamentals / Lesson 3
Dockerfiles, build context, and COPY
Make the files available to a build explicit and separate build-time instructions from runtime behavior.
On this page
Lesson goals
- Trace COPY paths from the context root and apply .dockerignore.
- Read a complete synthetic application and Dockerfile without executing a build.
The context is a declared input boundary
Place app.py and Dockerfile in a directory named parcel-lab. A build context supplies files available to COPY and ADD. The final positional argument to docker build chooses that context; the -f option chooses the Dockerfile, which can live elsewhere. A source path is relative to the context root, not automatically the Dockerfile's directory or the shell's remembered project location. Files outside the context are not arbitrary host inputs.
Define the entire teaching service
The fixture below uses only Python's standard library and listens on all container interfaces at port 8080. Every GET returns the same synthetic text; it does not implement authentication, database checks, or production readiness. Keeping the program complete makes its process and networking behavior understandable without relying on hidden application files. Save it as app.py only inside the optional isolated lab; this lesson displays code rather than executing it.
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b"parcel-api\n")
HTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
parcel-api
HTTPServer remains in the foreground until stopped. A successful GET body is parcel-api followed by a newline. Header dates and request-log timestamps vary and are deliberately omitted. Binding 0.0.0.0 inside the container permits container-interface traffic; it does not publish any host port by itself.
Read the image recipe in order
FROM chooses a base, WORKDIR sets the later working directory, COPY adds the declared source, USER selects runtime identity, and CMD specifies the default process. EXPOSE documents the intended container port without opening a host listener. The tag is readable teaching input rather than an immutable release pin. There is no package installer or network download step hidden in this small application recipe.
# syntax=docker/dockerfile:1
FROM python:3.13-slim
WORKDIR /app
COPY app.py ./app.py
USER 10001:10001
EXPOSE 8080
CMD ["python", "-u", "app.py"]
The JSON-array CMD directly launches Python instead of placing a shell between the runtime and application. The numeric user avoids requiring a named account lookup for this example. app.py is readable by that user; a later lesson treats writable directories separately. No image id, build duration, or successful engine run is asserted.
Exclude irrelevant and sensitive inputs
A .dockerignore at the context root filters files before ordinary context use. Excluding .git, local caches, generated environments, and secret files reduces accidental inclusion and unnecessary invalidation. Patterns must retain required build inputs; an overbroad *.py rule would remove app.py and make COPY fail. Ignore rules are a convenience boundary, not a substitute for checking the final image and using proper secret delivery.
Review path resolution before changing paths
For docker build -f docker/Dockerfile ., COPY app.py still reads app.py from the dot context root. For docker build -f Dockerfile src, it reads from src instead. Moving the recipe does not automatically move the boundary. Draw the directory tree, identify the context root, apply ignore rules, and only then calculate each COPY source. This procedure often solves missing-file errors without broadening the build to an entire home directory.
Common errors and fixes
COPY source was not found.
- Why it happens
- The chosen context lacks app.py, or .dockerignore excludes it before the build receives it.
- How to fix it
- Align the context and relative COPY source, then retain only necessary inputs.
- Prevent it next time
- Document the build command beside the repository layout and review ignore patterns.
A secret file is included by COPY . .
- Why it happens
- A broad copy imported local files that were never intended as image content.
- How to fix it
- Rotate any exposed secret and use specific sources plus reviewed exclusions.
- Prevent it next time
- Separate secret material from build context and review image history.
The service is unreachable despite EXPOSE 8080.
- Why it happens
- EXPOSE was mistaken for host port publication or for starting a listening process.
- How to fix it
- Check the application listener and separately plan an approved host mapping.
- Prevent it next time
- Keep image metadata, process binding, and network publication distinct.
Check your understanding
1. The Dockerfile is under docker/ but app.py is at repository root. Which COPY source fits a root context?
Show worked solution
With a root context, COPY app.py ./app.py is correct even when -f docker/Dockerfile selects the recipe. COPY ../app.py is not a request to read arbitrary parent files. Choose the boundary first; directory placement of the Dockerfile does not redefine it.
2. Why does the fixture use exec-form CMD and foreground serving?
Show worked solution
The runtime directly supervises the Python process, which remains alive while serving requests. A command that backgrounds the server and exits would end the main process and container. Direct execution also avoids accidental shell expansion and simplifies signal delivery, though the application must still handle shutdown correctly.
Key takeaways
- COPY sources follow the context root and ignore rules.
- A complete Dockerfile describes an image; it does not prove a build or deployment occurred.