1. Basic CMD
These basic cmd are same structure for both docker and podman.
# build image
$ docker build -t <NAME> -f /PATH/TO/Dockerfile <BUILD_CONTEXT>
$ docker image ls
# run docker container -d detached
$ docker run --name <NAME> -d <BASE_IMAGE>
# check running contianers
$ docker ps
# check process in container
$ docker exec <CONTAINER_NAME> ps
$ docker rm <CONTAINER_NAME> --force
<BUILD_CONTEXT>: the directory/path that Docker sends to the daemon, containing:
files referenced by
COPYfiles referenced by
ADDanything needed during build
In short, BUILD_CONTEXT is the dir/path when the Dockerfile try to do the “COPY” or “ADD” operations it will searched dir/path.
1.1 Tags
if you build the same image using docker build -t <SAME_NAME> -f /PATH/TO/Dockerfile <BUILD_CONTEXT>, it will build a new image, the old one’s tag will be removed as <none>.
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
base-image latest 2487f22f50cb 5 seconds ago 5.58MB
<none> <none> e9a4699a5cbb 17 minutes ago 5.58MB
<none> <none> 8a8330c3730a 23 minutes ago 5.58MB
1.2 Layers (Caching)
More layers → more caching → faster builds.
Docker creates a new layer for:
FROMRUNCOPYADD
BUT NOT for:
CMDENTRYPOINTENVEXPOSEUSERWORKDIR
Notice:
CMDandENTRYPOINTDO NOT create layers. They are metadata, not filesystem layers.
Example:
RUN apk update
RUN apk add curl
RUN apk add git
Each of these caches independently.
If merging them together:
RUN apk update && apk add curl git
Then any small change breaks the entire cache.
So sometimes more layers = faster builds.
Notice: I don’t mean more layers are always good, more layers will create more temporal artifacts. So reduce layers where possible, but caching and maintainability matter more than minimizing layers.
1.3 share PID
docker run --name sidecar --pid=container:app -d busybox sleep infinity
apiVersion: v1
kind: Pod
metadata:
name: example
spec:
shareProcessNamespace: true
containers:
- name: app
image: nginx
- name: sidecar
image: busybox
command: ["sleep", "3600"]
2. Dockerfile Best Practice - Multi-stage build
# syntax=docker/dockerfile:1
FROM golang:1.24
WORKDIR /src
COPY <<EOF ./main.go
package main
import "fmt"
func main() {
fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.go
FROM scratch
COPY --from=0 /bin/hello /bin/hello
CMD ["/bin/hello"]
the second FROM scratch will build on first FROM golang:1.24, as “The COPY --from=0 line copies just the built artifact from the previous stage into this new stage.”
more details check here.
3. Dockerfile Best Practice - Run as non-root user
RUN addgroup -S appgroup && \
adduser -S appuser -G appgroup -h /home/appuser
# or for Alphine
RUN adduser -D -g '' appuser
Both are secure as long as you do USER appuser.
The explicit version gives more control, but the simpler version is secure enough for typical apps.
-D: create user with no password, no shell, minimal config-g '': empty GECOS field
4. Dockerfile Best Practice - Never echo secrets in Dockerfile
example
Layer 1: RUN echo $TOKEN > /tmp/token
Layer 2: RUN sh -c 'register.sh /tmp/token'
Layer 3: RUN rm /tmp/token
each RUN will create a new layer, so if we use cmd to save “TOKEN”, which means it will be saved inside that layer, even then using RUN rm command. Therefore, DO NOT save secrets in Dockerfile.
# syntax=docker/dockerfile:1.2
RUN --mount=type=secret,id=token \
sh -c 'cat /run/secrets/token | register.sh'
# or
RUN register.sh $TOKEN
For better practice, secret can be passed into the container during runtime as env variable TOKEN
5. Docker related Security best practice
basic configuration files:
/usr/lib/systemd/system/docker.socket/usr/lib/systemd/system/docker.service
if you forgot the path, just check their status, eg. systemctl status docker.socket.
After changing the setting, remember to restart corresponding service.
systemctl daemon-reload
systemctl restart docker.socket
systemctl restart docker.service
5.1 Do NOT expose Docker over TCP
Otherwise it will expose ROOT ACCESS without authentication.
Edit /usr/lib/systemd/system/docker.service, remove -H tcp://0.0.0.0:2375 from ExecStart.
# remove -H tcp://0.0.0.0:2375
[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutSec=0
RestartSec=2
Restart=always
check the TCP
ss -tlnp | grep dockerd
5.2 Enforce TLS if you must expose TCP
If remote Docker API is absolutely required:
enable TLS
require certificates
never use TCP without TLS
Example:
-H tcp://127.0.0.1:2376
--tlsverify
--tlscacert=ca.pem
--tlscert=server-cert.pem
--tlskey=server-key.pem
5.3 Run dockerd as root (default), but avoid giving docker group to users
docker group = root equivalent
in /usr/lib/systemd/system/docker.socket set the
[Unit]
Description=Docker Socket for the API
[Socket]
ListenStream=/run/docker.sock
SocketMode=0660
SocketUser=root
SocketGroup=docker
# either set SocketGroup=root or docker
[Install]
WantedBy=sockets.target
Never add regular users to docker:
# check user belonging group
group <USER_NAME> # eg. group docker
# show all groups
getent group
vi /etc/group
# print real and effective user and group IDs
id <USER>
vi /etc/passwd
# $ id ubuntu
# uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),105(lxd)
# delete teh user from group
gpasswd -d <USER> <FROM_GROUP>
gpasswd -d <USER> docker
groupdel docker
# then socket file created with group = root
5.4 Use AppArmor or SELinux
Hardens container isolation.
Ubuntu:
# --security-opt apparmor=default
docker run --security-opt apparmor=myprofile ...
RHEL-based:
# --security-opt label=type:container_t
docker run --security-opt label:type:svirt_lxc_net_t ...
which is similar as Kubernetes-level AppArmor
In Kubernetes, we do NOT call docker run. We must annotate the Pod:
metadata:
annotations:
container.apparmor.security.beta.kubernetes.io/mycontainer: localhost/myprofile
5.5 Restrict container capabilities
Capabilities = parts of root’s power.
Remove everything, then add only needed:
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE ...
which is similar with Kubernetes-level capabilities
In Pods:
securityContext:
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
Kubernetes simply passes these to the container runtime (Docker, containerd, CRI-O).
5.6 Make /var/lib/docker read-only
Prevents tampering.
--read-only
5.7 Enable logging limits
Avoid log spamming or disk filling.
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
Top comments (0)