Docker: a short guide to images, containers and local services
A practical introduction to Docker: the difference between images and containers, a small Dockerfile, everyday commands, Compose for local dependencies and the habits that make containers useful rather than mysterious.
An image is the package; a container is a running instance
A Docker image is a built, immutable package containing an application and what it needs to run. A container is a running instance of that image with its own process, filesystem and configuration. Many containers can be started from one image.
This is useful when a team needs a repeatable runtime across local development, CI and deployment. It reduces reliance on whatever language version, library or database happens to be installed on an individual laptop.
A container is not a tiny virtual machine. It is an isolated process, so it should run one clear responsibility and get configuration from its environment. Treating it like a long-lived server usually creates harder deployments and unclear state.
Start with a small Dockerfile
A Dockerfile is the recipe used to build an image. It usually chooses a base image, sets a working directory, copies dependency manifests, installs dependencies, copies application code and declares the command to run.
Copy dependency files before the rest of the source when it allows Docker to reuse the dependency-install layer. Add a .dockerignore file so local build output, dependencies, secrets and Git history are not accidentally sent to the builder.
Use a specific, maintained base-image version rather than an unexamined latest tag. Run the service as a non-root user where the runtime permits it, and keep the final image limited to the files it needs.
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]Build, run and inspect deliberately
Build an image with a meaningful name and tag, then run it with an explicit port mapping and any required configuration. The left side of a port mapping is the host port; the right side is the port the process listens on inside the container.
Use docker ps, docker logs and docker exec to understand what is actually running before changing the Dockerfile. A container that exits immediately is often correctly following its command; the logs are the first place to learn why that command ended.
Containers are disposable. Do not rely on a change made interactively inside one container. Put configuration and application changes in source-controlled files, then rebuild and run a new instance.
docker build -t example-api:local .
docker run --rm -p 3000:3000 --env-file .env example-api:local
docker ps
docker logs <container-name>
docker exec -it <container-name> sh
docker stop <container-name>Use Compose when local development has dependencies
Most services need more than one process locally: an application might depend on PostgreSQL, Redis or a message broker. Docker Compose describes these services, their environment, networks and volumes in one compose.yml file, which makes the development stack repeatable.
Give stateful services named volumes. Without one, database data disappears when the container is removed; with one, the data persists independently of the container lifecycle. Be deliberate about when local data should be reset.
Compose is a local coordination tool, not a substitute for understanding production deployment. Keep environment-specific secrets outside the committed file and make readiness, migrations and failure behaviour explicit in the deployment platform.
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:app@db:5432/app
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: app
volumes:
- postgres-data:/var/lib/postgresql/data
volumes:
postgres-data:Keep the boundary clear and observable
Pass configuration through environment variables or mounted files, not by baking credentials into an image. Expose only ports that another process actually needs, and give each container the least privilege practical for its job.
Log to standard output and error so the runtime can collect it. Add a health check only when it tests meaningful readiness, not merely whether a process exists. A process can be alive while its database connection, migration or required configuration is not ready.
Docker makes a runtime reproducible; it does not remove the need for tests, dependency updates, image scanning, monitoring or a rollback plan. Its value comes from making those responsibilities easier to repeat.
- Treat images as build artefacts and containers as disposable instances
- Use a small, versioned base image and a .dockerignore file
- Do not store secrets in Dockerfiles or images
- Inspect logs before guessing why a container stopped
- Use Compose to define local multi-service dependencies
- Use named volumes deliberately for local state
- Rebuild from source rather than editing a running container