Docker packages an application with everything it needs to run: the language runtime, the libraries and the start command. The same package runs the same way on your laptop, a colleague's laptop and a server. That removes a whole class of "it works on my machine" problems.
This guide covers the parts you use every week: images and containers, a Dockerfile for a Python app and one for a front-end build, Docker Compose, volumes, a daily workflow and the security basics.
Images and containers
An image is a read-only template. It is built in layers, one per instruction in a Dockerfile, and Docker reuses a layer when nothing in it has changed. A container is a running instance of an image, with a thin writable layer on top. You can start ten containers from one image, and removing a container leaves the image untouched.
A useful way to think of it: the image is the class, the container is the object.
A Dockerfile for a Python app
This file builds an image for a Django or Flask app served by Gunicorn. The order of the steps matters. Dependencies change less often than code, so copying requirements.txt and installing it first lets Docker reuse that layer on most rebuilds.
# Dockerfile for a Python web app
FROM python:3.14-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
# Dependencies first, so this layer is cached
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Then the code
COPY . .
# Run as an ordinary user, not root
RUN useradd --create-home appuser
USER appuser
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "mysite.wsgi"]
The python:3.14-slim base is a small Debian image with Python 3.14. Python 3.15 was released on 9 October 2026, so check that your libraries support it before you move. The Python versions page lists every branch and its end of life.
Add a .dockerignore next to the Dockerfile. It keeps secrets, local environments and build output out of the image, and makes builds faster:
# .dockerignore
.git
.venv
__pycache__
node_modules
dist
.env
Build and run it:
docker build -t myapp:dev .
docker run --rm -p 8000:8000 myapp:dev
-p 8000:8000 maps a port on your machine to the port inside the container, and --rm removes the container when it stops.
A Dockerfile for a front-end build
A React, Vue or Svelte app needs Node.js to build, but the result is just static files. A multi-stage build uses Node in the first stage and copies only the finished files into a small web server image. Node, your packages and your source code never reach the final image.
# Dockerfile for a front-end build (two stages)
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginxinc/nginx-unprivileged:stable-alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 8080
Node 24 and Node 26 are both long-term support lines according to the Node.js release schedule. The nginx-unprivileged image runs nginx as an ordinary user and listens on port 8080. If your app uses client-side routing, add an nginx rule that sends unknown paths to index.html, as described in Deploying a React or Django app.
Docker Compose
Most apps need more than one container: the app, a database, maybe a cache. Compose describes them in one file, compose.yaml, and starts them with one command. Compose v2 is part of the Docker CLI, so the command is docker compose with a space. The old docker-compose tool and the top-level version: key are no longer needed; Docker's reference calls that key obsolete.
# compose.yaml
services:
web:
build: .
ports:
- "8000:8000"
env_file: .env
depends_on:
- db
develop:
watch:
- action: sync
path: .
target: /app
ignore:
- .venv/
- action: rebuild
path: requirements.txt
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:
Services on the same Compose project reach each other by service name, so the app connects to the database at host db. Keep the real password in a .env file that is listed in .gitignore.
Volumes
Anything written inside a container's own layer disappears when the container is removed. For data that must survive, use a volume.
In the Compose file above, dbdata is a named volume, so the database keeps its rows across docker compose down and up. To delete it on purpose, run docker compose down -v. Bind mounts map a folder on your machine into the container. They are handy in development and best avoided in production, where the image should contain the code it runs.
A daily workflow
The develop.watch section in the Compose file tells Compose what to do when files change: sync copies edited code into the running container, and rebuild builds a new image when the dependency list changes. With a framework that reloads on its own, such as Django's development server, you see changes without restarting anything.
docker compose up --watch # start everything, sync code changes
docker compose logs -f web # follow one service's output
docker compose exec web sh # open a shell inside it
docker compose down # stop and remove the containers
A few habits help:
- Run tests inside the container, with
docker compose exec web pytest, so they use the same versions as production. - Pin base images to a version (
python:3.14-slim,postgres:17) and update them on purpose.latestchanges under you. - Clear old images now and then with
docker image prune.
Security basics
- Run as a normal user. Add a
USERline, as in the Python Dockerfile, or use an image that already does it, such asnginx-unprivileged. If someone breaks into the app, they do not get root inside the container. - Use small base images. Slim and Alpine images contain fewer packages, so there is less to patch and less to attack.
- Keep secrets out of the image. Pass them at run time with environment variables or Compose secrets. Anything copied in with
COPYcan be read by anyone who has the image. - Rebuild regularly. Base images get security fixes. A rebuild picks them up; an old image keeps its old holes.
- Scan your images.
docker scout cves myapp:devlists known vulnerabilities in the packages inside an image. - Publish only the ports you need. A database rarely needs a port open to the outside world.
The Docker build best practices page goes deeper on each of these.
Common questions
- Is a container a virtual machine?
- No. A container shares the host's kernel and isolates processes, files and network. It starts in about a second and uses far less memory than a virtual machine.
- Should I use Alpine for Python?
- Usually the slim Debian image is the easier choice. Many Python packages ship ready-built wheels for Debian-based systems, while on Alpine some have to compile from source.
- Do I need Docker in production?
- Not always. A small app runs fine on a plain server. Docker pays off when you deploy often, run several services or want the same setup everywhere.
- Why does my rebuild take so long?
- A step near the top of the Dockerfile changed, so every layer after it was rebuilt. Copy files that change often as late as possible.
If you build and ship containers for clients, Getting paid on Internetivo explains how professionals find paid tasks.