requirements.txt and installing dependencies before copying source code means code edits do not reinstall dependencies..dockerignore file. Exclude .git, node_modules, build artifacts, and secrets from the build context — smaller context, faster builds, fewer accidental leaks.-slim, -alpine, or distroless variants instead of full Debian/Ubuntu images.python:3.12-slim, not python:latest.RUN commands and clean up in the same layer: a later rm does not shrink earlier layers.# .dockerignore
.git
node_modules
__pycache__
*.log
.env
Dockerfile
.dockerignore
Both define what runs when the container starts, but they behave differently when you pass arguments to docker run:
| CMD | ENTRYPOINT | |
|---|---|---|
| Purpose | Default command — easily overridden | Fixed executable — the container's purpose |
docker run image foo | foo replaces CMD | foo is appended as an argument |
| Typical use | Defaults and arguments | CLIs and executables (nginx, java -jar ...) |
They combine naturally: ENTRYPOINT is the executable, CMD is its default arguments:
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8000"]
# docker run myapi -> python app.py --port 8000
# docker run myapi --port 9000 -> python app.py --port 9000
# docker run --entrypoint sh myapi -> override for debugging
Always use the exec (JSON array) form — CMD ["python","app.py"]. The shell form (CMD python app.py) runs under /bin/sh -c, which does not forward signals like SIGTERM, so docker stop ends up force-killing your app after the grace period.
COPY does one thing: copies local files into the image. ADD does that plus two extras: auto-extracting local .tar archives and fetching remote URLs. The community convention is to use COPY unless you specifically need tar extraction — it is explicit and predictable. Remote-URL downloads are better done with RUN curl (cacheable, inspectable).
| Mount type | Stored where | Managed by | Best for |
|---|---|---|---|
Named volume -v mydata:/data | Docker storage area on the host | Docker CLI/API | Persistent production data (databases), portability |
Bind mount -v /host/code:/app | Any path on the host | You (the host OS) | Development: live-edit host code in the container |
tmpfs mount --mount type=tmpfs,dst=/tmp | Host memory only | Never persisted | Sensitive temporary data, scratch files |
# Development pattern: bind mount the source, keep dependencies inside
docker run -d -p 3000:3000 \
-v "$(pwd)":/app \
-v /app/node_modules \
node:20 npm run dev
The second -v /app/node_modules is an anonymous volume that "shields" the container's installed dependencies from being hidden by the host bind mount — a very common trick.
Docker ships with several network drivers:
-p needed).docker network create mynet
docker network ls
docker network inspect mynet
docker run -d --name db --network mynet postgres:16
docker network connect mynet web # attach a running container
On any user-defined network (custom bridge or Compose's default network), Docker runs an embedded DNS server: containers resolve each other by container name (and network aliases). This is the foundation of multi-container apps:
docker network create appnet
docker run -d --name db --network appnet postgres:16
docker run -d --name api --network appnet -e DB_HOST=db my-api:1.0
# the api container connects using host "db" — DNS resolves it automatically
Best practice: put only the containers that need to talk on the same user-defined network, and keep databases off the public bridge.
| ARG | ENV | |
|---|---|---|
| Available during | docker build only | Build and container runtime |
| Override | --build-arg NAME=value | docker run -e NAME=value |
| Persisted in image | No (unless promoted to ENV) | Yes — visible via docker inspect |
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-slim
ENV APP_ENV=production \
PORT=8000
Use ARG for build-time knobs (versions, build profiles). Use ENV for sensible defaults that can still be overridden per container. Never pass secrets as build args or ENV — both leak into image metadata and docker history (see Advanced: build secrets).
The twelve-factor principle: config belongs in the environment, not the image. One image, many configurations:
docker run -d \
-e DB_HOST=prod-db.internal \
-e DB_PASSWORD_FILE=/run/secrets/db_pw \
--env-file ./config/app.env \
my-api:1.0
Options, from simple to robust: -e single variables, --env-file for batches, mounted config files for structured config, and orchestrator secrets for credentials.
A HEALTHCHECK tells Docker how to test the application is actually working — not just that the process is alive:
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -fsS http://localhost:8000/health || exit 1
interval.docker ps (Up (healthy)).start-period gives slow-starting apps time before failures count.depends_on: condition: service_healthy and orchestrators use it for ordering and self-healing.Without limits, one container can starve the whole host (the "noisy neighbor" problem):
docker run -d \
--memory=512m --memory-swap=512m \
--cpus=1.5 \
--restart unless-stopped \
my-api:1.0
Restart policies: no (default), on-failure[:max-retries], always, unless-stopped (like always, but stays stopped if you stopped it manually — the usual production choice).
Compose defines a multi-container application (app + database + cache + queue) in a single docker-compose.yml and manages the whole stack with one command: docker compose up. It handles the network creation, name-based DNS, startup order, and volumes for you. It is the default tool for local development and test environments. A full walkthrough lives in the dedicated Docker Compose page.
docker ps -a — is it running, restarting, or exited? Check the status column.docker logs --tail 200 <name> — the application's own output is the first clue.docker inspect --format '{{.State.ExitCode}} {{.State.Error}}' <name> for exit codes (137 = OOM/killed, 126/127 = command problems).docker exec -it <name> sh — poke around inside: env vars present? Can it reach the database? curl the dependency.docker inspect — verify mounts, ports, networks, and healthcheck results (.State.Health).docker run --rm -it --network <net> nicolaka/netshoot.Most incidents boil down to: wrong environment variable, port already in use, unreachable dependency, or a volume masking application files.
← Back to IT Knowledge Base