Jhonatan Pinheiro
Loading page...
Jhonatan Pinheiro
Loading page...
Jhonatan Pinheiro
Loading page...
Containers, images, multi-stage Dockerfiles, volumes, networks, Compose and what changes in production: 150 commands with copy-ready examples.
Docker solves an old problem: "it works on my machine". It packages the application with everything it needs — runtime, libraries, configuration — into an image that runs the same anywhere.
These 150 commands go from the first docker run to diagnosing a container that dies in production. The examples use the modern CLI (docker container ...), mentioning the classic shortcut when that is the one people actually use.
A container is not a virtual machine. It shares the host's kernel and isolates only what it needs (processes, network, filesystem). That is why it starts in milliseconds and weighs megabytes instead of gigabytes.
Check the installation, start the first container and understand what happened.
Confirms that the client is talking to the daemon.
docker version
docker infoDownloads the image, creates the container, runs it and exits. Docker's "hello world".
docker run hello-worldCreates and starts a container from an image.
docker run nginx-d (detached) gives you the terminal back and leaves the container running.
docker run -d nginx-p host:container exposes the service on your machine.
docker run -d -p 8080:80 nginx
# go to http://localhost:8080Without --name, Docker invents a random name and you waste time looking for it.
docker run -d --name meu-nginx -p 8080:80 nginx-it connects your terminal to the container — indispensable for a shell.
docker run -it ubuntu bash--rm deletes the container as soon as it stops. Ideal for a quick test.
docker run --rm -it alpine shThe standard way of configuring an application in a container.
docker run -d -e POSTGRES_PASSWORD=senha -e POSTGRES_DB=loja postgres:16Avoids exposing a secret in the shell history.
docker run --env-file .env -d minha-appThe command you will type the most.
docker ps-a includes the stopped ones — where whatever failed usually is.
docker ps -aKeeps the list readable in a narrow terminal.
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"stop sends SIGTERM and waits; start brings the same container back up.
docker stop meu-nginx
docker start meu-nginxA shortcut for stop + start.
docker restart meu-nginxAn immediate SIGKILL, with no graceful shutdown. A last resort.
docker kill meu-nginxDeletes the stopped container; -f forces it even while running.
docker rm meu-nginx
docker rm -f meu-nginxThe first thing to do when something does not work.
docker logs meu-nginx-f follows the output; --tail limits the initial history.
docker logs -f --tail 100 meu-nginxOpens a shell inside the running container.
docker exec -it meu-nginx bash
docker exec -it meu-alpine sh # Alpine images have no bashThe image is the mould; the container is the running instance.
Shows what has been downloaded and how much space it takes.
docker images
docker image lsBrings it from the registry without running it.
docker pull postgres:16Never use latest in production: it changes without warning and breaks what was working.
docker pull node:20-alpine
docker pull node:20.11.1-alpine3.19 # even more specificSearches Docker Hub from the command line.
docker search postgresReads the Dockerfile in the directory and produces the image with the given tag.
docker build -t minha-app:1.0 .When there is more than one (production, development, testing).
docker build -f Dockerfile.prod -t minha-app:prod .Forces every layer to be redone — useful when the cache is masking a problem.
docker build --no-cache -t minha-app:1.0 .ARG parameterises the build (not to be confused with ENV, which applies at runtime).
docker build --build-arg NODE_VERSION=20 -t minha-app .The same image can have several names.
docker tag minha-app:1.0 usuario/minha-app:latestPublishes to Docker Hub or to a private registry.
docker login
docker push usuario/minha-app:1.0The image name carries the registry address.
docker tag minha-app registry.empresa.com/time/minha-app:1.0
docker push registry.empresa.com/time/minha-app:1.0It only goes if no container depends on it.
docker rmi minha-app:1.0
docker image rm -f minha-app:1.0Shows each layer and its size — this is how you find out what made the image fat.
docker history minha-app:1.0Complete metadata: variables, entrypoint, ports, layers.
docker inspect minha-app:1.0Moves an image between machines without a registry, as a file.
docker save -o minha-app.tar minha-app:1.0
docker load -i minha-app.tarOrphan layers from old builds, which do nothing but take up disk.
docker images -f "dangling=true"
docker image prune-a removes every image with no associated container. It frees a lot of space.
docker image prune -aAlpine is about 5 MB; distroless does not even have a shell. Less surface, fewer vulnerabilities.
# 1.1 GB
FROM node:20
# 130 MB
FROM node:20-alpine
# ~50 MB, no shell and no package manager
FROM gcr.io/distroless/nodejs20-debian12Checks for known CVEs in the image's dependencies.
docker scout cves minha-app:1.0
trivy image minha-app:1.0Compares image sizes to find the fat one.
docker images --format "{{.Repository}}:{{.Tag}}\t{{.Size}}" | sort -k2 -hCreates a temporary container just to copy something out of it.
id=$(docker create minha-app:1.0)
docker cp $id:/app/dist ./dist
docker rm $idProduces the same image for amd64 and arm64 — necessary for Apple Silicon and Graviton.
docker buildx build --platform linux/amd64,linux/arm64 -t usuario/app:1.0 --push .Inspecting, copying, limiting and understanding what is going on in there.
Complete JSON: network, volumes, variables, state, exit code.
docker inspect meu-nginxWith a Go template, you take only what you need.
docker inspect -f '{{.State.Status}}' meu-nginx
docker inspect -f '{{.NetworkSettings.IPAddress}}' meu-nginxCPU, memory, network and I/O of each container — Docker's top.
docker stats
docker stats --no-streamWhat is running in there, seen from the host.
docker top meu-nginxMoves files between host and container in both directions.
docker cp ./config.json meu-nginx:/etc/app/config.json
docker cp meu-nginx:/var/log/nginx/error.log ./error.logLists files created (A), changed (C) and removed (D) since the image.
docker diff meu-nginxWith no limit, one container can bring down the entire host.
docker run -d --memory 512m --memory-swap 512m minha-app--cpus sets how many cores the container may use.
docker run -d --cpus 1.5 minha-appBrings the container back on its own after a failure or a reboot.
docker run -d --restart unless-stopped minha-app
# options: no | on-failure | on-failure:3 | always | unless-stoppedDocker gets to know whether the container is healthy, not just up.
docker run -d --health-cmd "curl -f http://localhost/health || exit 1" \
--health-interval 30s --health-retries 3 minha-appShows the result of the last checks.
docker inspect -f '{{.State.Health.Status}}' minha-appTells you why the container died — 137 is OOM/kill, 1 is an application error.
docker inspect -f '{{.State.ExitCode}}' minha-app
docker ps -a --filter "exited=137"Enters the container with a specific user (useful for debugging permissions).
docker exec -u root -it minha-app shA container running as root is an unnecessary risk.
docker run -d --user 1000:1000 minha-appWithout recreating anything.
docker rename nome-antigo nome-novoFreezes the processes without terminating the container.
docker pause minha-app
docker unpause minha-appBlocks until it exits and returns the code — useful in a script.
docker wait minha-appA live stream of everything happening: creation, death, health, network.
docker events --filter container=minha-appFinds out where the container is mapped to.
docker port meu-nginxRoutine cleanup.
docker container pruneAn emergency, or the end of the working day.
docker stop $(docker ps -q)Freezes the current state as an image. It is for debugging — never as a way of building an image.
docker commit minha-app minha-app:debugThe recipe for the image. Each instruction becomes a layer — and the order matters a great deal.
The base image. The first instruction of every Dockerfile.
FROM node:20-alpineSets the working directory and creates it if it does not exist. Better than RUN cd.
WORKDIR /appCopies from the build context into the image.
COPY package.json package-lock.json ./
COPY src ./srcLike COPY, but it also downloads a URL and unpacks a tar. Prefer COPY: fewer surprises.
ADD https://exemplo.com/arquivo.tar.gz /tmp/Runs a command during the build and freezes the result into a layer.
RUN npm ci --omit=devEach RUN is a layer. Group related commands and clean up on the same line.
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*The trick that speeds up the build the most: copy the manifest and install before copying the code.
COPY package*.json ./
RUN npm ci # only redone when the dependencies change
COPY . . # changes with every commit, but it is the last layerAn environment variable available at build time and at runtime.
ENV NODE_ENV=production PORT=3000A build-only variable. It does not stay in the final image — but it does appear in the history, so no secrets.
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpineDocuments the port in use. It does not publish anything on its own — -p is what publishes.
EXPOSE 3000The container's default command, replaceable on the command line.
CMD ["node", "server.js"]A fixed command. Combined with CMD, it becomes "program + default arguments".
ENTRYPOINT ["node"]
CMD ["server.js"]Always use the exec form (JSON): in the shell form the process becomes a child of /bin/sh and never receives the signals.
CMD ["node", "server.js"] # exec — receives SIGTERM
CMD node server.js # shell — does notSwitches the user. Everything after it runs without root.
RUN addgroup -S app && adduser -S app -G app
USER appMarks a directory as a mount point for persistent data.
VOLUME ["/var/lib/postgresql/data"]Teaches Docker how to check whether the application is really responding.
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1Image metadata: version, author, source of the code.
LABEL org.opencontainers.image.source="https://github.com/usuario/projeto"Shrinks the build context. Without it, you send node_modules and .git to the daemon on every build.
node_modules
.git
.env
dist
*.logCompiles in one stage and copies only the result into the final image. It cuts hundreds of MB.
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER node
CMD ["node", "dist/server.js"]--from accepts any image, not only a previous stage.
COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/nginx.confKeeps the package manager's cache between builds, without bloating the image.
RUN --mount=type=cache,target=/root/.npm npm ciUses the credential during the build without writing it into any layer.
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
# docker build --secret id=npmrc,src=$HOME/.npmrc .A container is disposable; data cannot be. That is what volumes exist for.
A named volume, managed by Docker.
docker volume create dados-postgresShows where the volume lives on the host.
docker volume ls
docker volume inspect dados-postgresThe data survives the removal of the container.
docker run -d -v dados-postgres:/var/lib/postgresql/data postgres:16Mounts a folder from the host. It is what gives you hot reload in development.
docker run -d -v $(pwd)/src:/app/src minha-appMore explicit than -v and recommended in scripts.
docker run -d --mount type=bind,source=$(pwd)/src,target=/app/src minha-appA container that should not write, does not write.
docker run -d -v $(pwd)/config:/app/config:ro minha-appA volume for production data (Docker manages it); a bind mount for code in development.
# production
docker run -v dados:/var/lib/mysql mysql:8
# development
docker run -v $(pwd):/app node:20 npm run devAn in-memory filesystem: it vanishes when the container stops. Good for sensitive data.
docker run -d --tmpfs /tmp:rw,size=64m minha-appA temporary container that packs up the volume's contents.
docker run --rm -v dados-postgres:/dados -v $(pwd):/backup alpine \
tar czf /backup/dados-postgres.tar.gz -C /dados .The backup path in reverse.
docker run --rm -v dados-postgres:/dados -v $(pwd):/backup alpine \
tar xzf /backup/dados-postgres.tar.gz -C /dadosA volume does not go away with the container — it has to be deleted on purpose.
docker volume rm dados-postgresCareful: this deletes real data. Check the list first.
docker volume ls -f dangling=true
docker volume prune-v deletes that container's anonymous volumes.
docker rm -v minha-appThe classic Linux mistake: the UID inside is not the same as the one outside.
docker run -d --user $(id -u):$(id -g) -v $(pwd):/app minha-appHow containers talk to each other and to the world.
Every installation already comes with bridge, host and none.
docker network lsThe project's own network: the containers can see each other by name.
docker network create minha-redeAt creation time or later, with the container already running.
docker run -d --network minha-rede --name api minha-app
docker network connect minha-rede outro-containerInside the network, the container name is the host. Do not use the IP.
# from inside the "api" container:
curl http://banco:5432
# instead of http://172.18.0.3:5432Shows the IP range and who is connected.
docker network inspect minha-redeA network in use cannot be removed.
docker network disconnect minha-rede api
docker network rm minha-redeNo network isolation: the container uses the host's stack. Fast, but with no isolation (Linux).
docker run -d --network host nginxTotal isolation. Good for processing untrusted data.
docker run --rm --network none alpine ip addrExposes it only on localhost, not on the whole network.
docker run -d -p 127.0.0.1:8080:80 nginxYou can also expose ranges and another protocol.
docker run -d -p 8000-8010:8000-8010 minha-app
docker run -d -p 53:53/udp meu-dnshost.docker.internal resolves to the host machine.
curl http://host.docker.internal:5432For when you really do need it.
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' apiA disposable container purely to diagnose the network.
docker run --rm --network minha-rede alpine ping -c 3 banco
docker run --rm --network minha-rede nicolaka/netshoot nslookup apiRoutine cleanup.
docker network pruneSeveral containers described in one file and brought up with a single command.
Describes services, ports, variables and dependencies.
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://app:senha@banco:5432/loja
depends_on:
banco:
condition: service_healthy
banco:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: senha
POSTGRES_DB: loja
volumes:
- dados:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
retries: 5
volumes:
dados:-d in the background; without it, the logs stay in the terminal.
docker compose up -dForces the images to be rebuilt before starting.
docker compose up -d --builddown removes containers and networks; with -v, it deletes the volumes too.
docker compose down
docker compose down -v # deletes the data!The status of each service in the stack.
docker compose psAll the services together, or one specific one.
docker compose logs -f
docker compose logs -f api --tail 100exec on the running container; run creates a new one.
docker compose exec api sh
docker compose run --rm api npm run migrateWithout touching the rest of the stack.
docker compose restart apiStarts several instances of the same service.
docker compose up -d --scale worker=3Shows the final configuration, with the variables already resolved.
docker compose configOne base and one per environment — the second overrides the first.
docker compose -f compose.yaml -f compose.prod.yaml up -dCompose reads the .env in the directory automatically.
# .env
POSTGRES_PASSWORD=senha-forte
# compose.yaml
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}Services that only come up when you ask for them — such as development tools.
services:
adminer:
image: adminer
profiles: ["dev"]
# docker compose --profile dev up -dWithout condition, Compose only waits for the container to start, not to be ready.
depends_on:
banco:
condition: service_healthyStops one service from eating the whole machine.
deploy:
resources:
limits:
cpus: "1.5"
memory: 512MKeeps the containers so you can bring them back later.
docker compose stop
docker compose startUpdates the images before starting.
docker compose pullstats filtered by the project's containers.
docker compose top
docker stats $(docker compose ps -q)What separates a container that runs on your machine from one that holds up in production.
Shows how much space images, containers, volumes and cache are taking.
docker system df
docker system df -v-a also removes images with no container. Check first — --volumes deletes data.
docker system prune
docker system prune -a
docker system prune -a --volumesContainer logs grow without limit and fill the server's disk. This is mistake number 1 in production.
# /etc/docker/daemon.json
{
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" }
}A root container + a host volume = a real risk of compromising the machine.
USER node # in the Dockerfile
docker run --user 1000:1000 minha-appIf the application does not need to write, do not let it.
docker run -d --read-only --tmpfs /tmp minha-appRemoves kernel privileges the application does not use.
docker run -d --cap-drop ALL --cap-add NET_BIND_SERVICE nginxBlocks a setuid binary from gaining privilege inside the container.
docker run -d --security-opt no-new-privileges minha-appIt amounts to giving the container root on the host. Only in very specific and deliberate cases.
# avoid
docker run --privileged minha-appMounting /var/run/docker.sock gives whoever is in the container full control of the host.
# dangerous
docker run -v /var/run/docker.sock:/var/run/docker.sock minha-appDocker sends SIGTERM and waits 10s before the SIGKILL. Your application has to handle the signal.
docker stop -t 30 minha-app--init inserts a minimal init that reaps orphan processes.
docker run -d --init minha-appLook at the log of the previous run to find the cause.
docker ps -a
docker logs --tail 200 minha-app
docker inspect -f '{{.State.ExitCode}} {{.State.Error}}' minha-appExit 137 is almost always a lack of memory.
docker inspect -f '{{.State.OOMKilled}}' minha-app
docker stats --no-stream minha-appDistroless has no shell: use an auxiliary container in the same namespace.
docker run --rm -it --pid container:minha-app --network container:minha-app \
nicolaka/netshoot shRun it like production before publishing: no bind mount, with the real variables.
docker run --rm -p 3000:3000 --env-file .env.production minha-app:1.0Publish with a version tag and deploy by digest — a guarantee that it is exactly that image.
docker push usuario/app:1.4.2
docker inspect --format='{{index .RepoDigests 0}}' usuario/app:1.4.2Build and push inside the pipeline, with a cache between runs.
docker buildx build --push \
--cache-from type=registry,ref=usuario/app:cache \
--cache-to type=registry,ref=usuario/app:cache,mode=max \
-t usuario/app:$GIT_SHA .Compose handles one server. Several nodes, automatic scaling and rolling updates call for an orchestrator.
# Compose: 1 host, a simple stack, a small team
# Kubernetes: multiple nodes, autoscaling, deploys with no downtime