This cheatsheet grew out of a lab where I built and deployed an full stack application using Docker Compose.
#docker #container #cloud
Along the way I realized what’s actually happening inside container images. Not just running commands, but learning why certain patterns, flags, and tools matter.
This page collects the commands, best practices, and little insights I kept referring back to while building and debugging my setup.
#list all iamges
docker images
#Inspect build layer
docker history <image_name>
#Show running container stats
docker stats
#Get the image digest for image-pinning
docker buildx imagetools inspect traefik:3.6.2
#Name: docker.io/library/traefik:3.6.2
#MediaType: application/vnd.oci.image.index.v1+json
#Digest: sha256:aaf0f6185419a50c746...2ddb7b8749eed505d55bf8b8ea
# Scan image for vulnerabilities
grype <image>
Below is a well-formed RUN instruction that demonstrates all the apt-get recommendations.
# --no-install-recommends ensures your Dockerfile installs the latest package versions with no further coding or manual intervention. This technique is known as cache busting.
RUN apt-get update && apt-get install -y --no-install-recommends \
aufs-tools \
automake \
build-essential \
curl \
dpkg-sig \
libcap-dev \
libsqlite3-dev \
mercurial \
reprepro \
ruby1.9.1 \
ruby1.9.1-dev \
s3cmd=1.1.* \ # Cache busting by version pinning
&& rm -rf /var/lib/apt/lists/*
# In addition, when you clean up the apt cache by removing `/var/lib/apt/lists` it reduces the image size, since the apt cache isn't stored in a layer. Since the `RUN` statement starts with `apt-get update`, the package cache is always refreshed prior to `apt-get install`.
Docker executes these commands using the /bin/sh -c interpreter. In this example the build succeeds as long as the wc -l commands succeeds, even if the wget command fails.
RUN wget -O - https://some.site | wc -l > /number
If you want the command to fail due to an error at any stage in the pipe, prepend set -o pipefail &&
RUN set -o pipefail && wget -O - https://some.site | wc -l > /number
[!NOTE]
Not all shells support the
-o pipefailoption.In cases such as the
dashshell on Debian-based images, consider using the exec form ofRUNto explicitly choose a shell that does support thepipefailoption. For example:
RUN ["/bin/bash", "-c", "set -o pipefail && wget -O - https://some.site | wc -l > /number"]
If a service can run without privileges, use USER to change to a non-root user. Start by creating the user and group in the Dockerfile with something like the following:
RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres
[!NOTE] Most images come with a nonroot user
to temporarily add a requirements.txt file for a RUN pip install instruction:
RUN --mount=type=bind,source=requirements.txt,target=/tmp/requirements.txt \
pip install --requirement /tmp/requirements.txt
Bind mounts are more efficient than COPY for including files from the build
context in the container. Note that bind-mounted files are only added
temporarily for a single RUN instruction, and don’t persist in the final
image. If you need to include files from the build context in the final image,
use COPY.
I’ve succesfully deployed the given application. All processes inside the containers run rootless.

#ARG GO_VERSION=1.25
# Build
# I choose the base version, because golang stated themselves that the -alpine version is not fully supported
# Also if I have the golang base image already in my cache the building time is the same
# And I think in an real-world example you would choose the base image for compatability reason
# And since we are multi-staging it doesn't affect the size of the final image
#FROM golang:${GO_VERSION} AS build
FROM golang@sha256:698183780de28062f4ef46f82a79ec0ae69d2d22f7b160cf69f71ea8d98bf25d AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY *.go ./
RUN CGO_ENABLED=0 go build -a -o ./bin/api
FROM gcr.io/distroless/static-debian12@sha256:87bce11be0af225e4ca761c40babb06d6d559f5767fbf7dc3c47f0f1a466b92c AS production
COPY --from=build --chown=nonroot:nonroot /app/bin/api ./
USER nonroot:nonroot
EXPOSE 8000
CMD ["./api"]
# This would be the standard command if no argument is given.
# Since we just call the api and give the argument als env-variables I had to uncomment this for it to work
# I'm leaving this for my personal reference
#CMD ["help"]
#ENTRYPOINT ["./api"]
This Dockerfile is intentionally overengineered (lol) for this application.
#ARG NODE_VERSION=22.21.1
# Base image
#FROM node:${NODE_VERSION}-alpine AS base
FROM node@sha256:b2358485e3e33bc3a33114d2b1bdb18cdbe4df01bd2b257198eb51beb1f026c5 AS base
WORKDIR /app
# Dependencies
# Runs only when package.json changes
FROM base AS deps
COPY package*.json ./
# info: npm ci deletes the node_modules folder and does install packages from package-lock.json
# in that way it guarantees to install the exact applications that also succeeded in testing
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
npm ci --omit=dev && \
npm cache clean --force
# Copy source files (runs only when source files change or layer deps gets rebuilded)
# This step is not really necessary since we dont have a testing stage
FROM base AS build
COPY src ./
# Production
FROM gcr.io/distroless/nodejs22-debian12@sha256:4c4b23e6694fa5a5081f79f94ad1c272fb7ff5c4a9609edf228e5e39492543b5 AS prod
COPY --from=deps --chown=nonroot:nonroot /app ./
COPY --from=build --chown=nonroot:nonroot /app ./
USER nonroot:nonroot
EXPOSE 8080
# Best practice ist to avoid npm scripts to CMD, because they dont pass OS Signals to the code.
# This prevents problems with child-process, signal handling, graceful shutdown and having processes.
CMD ["./bin/www"]
# I cancelled this part because our program is way to small to implement testing in it
########################
# Test the programm
# FROM build AS test
#RUN npm run test (run some tests)
# Test if the build breaks with explicit exit 1 call
#RUN exit 1
#RUN echo fake-test-pass
#########################
# For Debian/Ubuntu based base images because useradd and groupadd is the underlaying function when you call addgroup or adduser so this would be the nicer way to do it
###
#RUN groupadd -g 1001 app && \
# useradd -u 1001 -g app app \
# chown -R app:app /app
###
services:
traefik:
#image: traefik:3.6.2
image: traefik@sha256:aaf0f6185419a50c74651448c1a5bf4606bd2d2ddb7b8749eed505d55bf8b8ea
user: "999:988"
container_name: traefik
security_opt:
- no-new-privileges:true
restart: unless-stopped
networks:
- traefik_net
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./certs:/certs:ro
command:
# EntryPoints
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--entrypoints.web.http.redirections.entrypoint.permanent=true"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.websecure.http.tls=true"
# Providers
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
labels:
# Enable self‑routing
- "traefik.enable=true"
deploy:
resources:
limits:
cpus: "0.50"
memory: "256M"
web:
build: ./web
container_name: tta-web
restart: always
networks:
- traefik_net
- web_api_net
environment:
MODULE_NAME: "web"
GROUP_NAME: "g14"
API_HOST: "api"
API_PORT: "8000"
PORT: "8080"
labels:
- "traefik.http.routers.web.rule=Host(`cldinf-docker-g14.network.garden`)"
- "traefik.http.routers.whoami.entrypoints=websecure"
- "traefik.http.routers.web.tls=true"
- "traefik.enable=true"
deploy:
resources:
limits:
cpus: "0.50"
memory: "256M"
api:
build: ./api
container_name: tta-api
depends_on:
- db
restart: always
environment:
PG_USER_FILE: /run/secrets/db_user
PG_PASSWORD_FILE: /run/secrets/db_password
PG_DATABASE: "dbtta"
PG_HOST: "db"
PG_PORT: "5432"
networks:
- web_api_net
- api_db_net
secrets:
- db_password
- db_user
deploy:
resources:
limits:
cpus: "0.50"
memory: "256M"
db:
#image: postgres:18.0-alpine
image: postgres@sha256:48c8ad3a7284b82be4482a52076d47d879fd6fb084a1cbfccbd551f9331b0e40
user: "70:70"
restart: always
# shm_size: 256mb
environment:
POSTGRES_USER_FILE: /run/secrets/db_user
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
POSTGRES_DB: "dbtta"
volumes:
- db_data:/var/lib/postgresql/data
networks:
- api_db_net
secrets:
- db_password
- db_user
deploy:
resources:
limits:
cpus: "0.50"
memory: "256M"
secrets:
db_password:
file: ./secrets/db_password.txt
db_user:
file: ./secrets/db_user.txt
networks:
traefik_net:
name: traefik_net
web_api_net:
name: web_api_net
api_db_net:
name: api_db_net
volumes:
db_data: