Docker
Updated: March 29, 2026
Docker creates containers that run processes. They are lightweight. Best Practices is a must read!!
Docker is not a VM. It utilizes features of the kernel to separate/isolate processes from each other, a sort of sandbox.
Linux is best for running docker! MacOS and Windows need to run a small linux VM to run docker. So not fully native!
VM install and run a second OS on top rather than like docker with shares the OS kernel with the host.
Table of Contents
- Install
- Setup
- Arguments
- Images
- Instructions
- Containers
- Tags
- Networking
- Port Mapping
- Volume Mapping
- Environment Variables
- Namespaces
- Docker Compose
- Docker Swarm
- SSH
Install
docs.docker shows many installations so just follow instructions there.
I like to use NixOS or Darwin with Home-Manager for my systems (linux and mac). I no longer us Windows at all.
environment.systemPackages = with pkgs; [ docker docker-compose ];
If docker is installed through any non-nix means it is likely that you will have setup the daemon for it’s services to run.
sudo systemctl start docker # This will start the docker service
sudo systemctl enable docker # This adds service to launch on bootup
If systemctl is not used in the distro, try service instead:
sudo service docker start
sudo service docker enable
Setup
Just like after you install git, you should likewise setup user and group information.
Docker is owned by userrootand will always requiresudofor all it’s commands.
We can create a group calleddockerso we do not have to sudo everytime.
sudo groupadd docker # creates the docker group (may already exist)
sudo usermod -aG docker $USER # adds your user to the docker group
# -a = append; -G = supplement group - same restriction of its main group -g
newgrp docker # activate the changes (reboot anyway?)
docker network ls # test that it works without sudo
For nix simply add to group for each user that needs access:
users.users."${username}".groups = [ "docker" ];
Arguments
run # start a container
ps # list containers; use -a to see all; -s to see size
stop # stop container; use container ID or name
attach # attach to running container; use container ID or name
inspect # get specific container info; use container ID or name (json)
rm # remove a container
images # lists all images available
rmi # remove images; no containers can be running off this image (stop && rm them first)
pull # download an image
logs # view container logs, even if in detached mode
exec # execute a command; docker exec cont_name command
network # use various network (such as ls to view networks in docker)
swarm # used for node/container orchestration
stack #
-d # detached mode, runs docker in background
-i # get input for stdin (without t, not attached to container terminal)
-t # adds pseudo-terminal; commonly used -it for interactive terminal
-v # attach a volume
Images
An image is a package template (blueprint) used to create one or more containers.
Class (image) > Object (container)
Images are immutable (cannot be changed) but can be rebuilt kinda like nix :)
Always do fixes or permanent changes by rebuilding images.
When layering place things that change often at the top, and most unchanged at the bottom.
- application code
- dependencies
- runtime
- system libraries
- baseOS
The images is created and modified by Instructions
Instructions
Words that are all caps are instructions.
While the stuff after them are arguments.
Only the instructionsADD, COPY, RUNcreate layers.
Other instructions create temporary intermediate images, and do not increase the size of the build.
FROM # The base image for building a new image. This command must be on top of the docker file.
MAINTAINER # Optional, it contains the name of the maintainer of the image.
LABEL # Adds meta data to an image.
ADD # Copy a file from the host machine to the new docker image. Docker can use a URL to download files.
COPY # Copies new files or directories from host to fs of the image.
RUN # Used to execute a command during the build process of the docker image.
ARG # Defines a variable that users can use with `docker build` using `--build-arg <var>=<value>` flag.
ENV # Define an environment variable.
CMD # Allows for arguments, more changeable, get overridden at runtime. ["worker.js"]
ENTRYPOINT # The executable to run when the container starts. Not as flexible as CMD. ["node"]
ONBUILD # Add image instruction to trigger at later time as a base for another build.
HEALTHCHECK # Check container health by running a command inside the container.
WORKDIR # This is directive for CMD command to be executed.
USER # Set the user or UID for the container created with the image.
VOLUME # Enable access/linked directory between the container and the host machine.
EXPOSE # Informs the container to listen on specific ports (does not make it accessible to the host - see port mapping)
WORKDIR # Sets the working directory for any `RUN, CMD, ENTRYPOINT, COPY, and ADD` instructions that follow it.
STOPSIGNAL # Set the system call signal sent to the container to exit.
SHELL # Allows other shells to be used (zsh, tcsh, ksh) instead of default (bash)
Order of instruction matters a lot because of caching.
Here is an example dockerfile:
FROM node:20-alpine # base os image of the container - they all must start with a FROM instruction
WORKDIR /app # sets working directory for everything that follows
COPY package.json . # create new layer containing just this file
RUN apk add --no-cache curl # install curl on the alpine image
RUN npm install # creates new layer with all installed node modules
COPY . . # creates a layer with rest of app code
CMD ["node", "server.js"]
Containers
Containers are running instances of images that have their own isolated processes split by the kernel into two sections:
cgroups: limits resources (CPU, memory, IO, etc)
namespace: isolated view (mount, network, pid, user, etc)
Tags
Tags are often used for using a different versions of a program. Supported tags for an image are listed on dockerHub
docker run redis:3.0 # runs redis version 3 instead of latest (version 5 currently)
Networking
Docker has 6 network types: bridge, host, ipVLAN, macVLAN, none, user-defined
Bridge Network (Default)
A bridge network is a Link Layer device which forwards traffic between network segments through hardware or software vis mac addresses or arp.
This is not ideal for networking being L2.
Docker uses a software bridge. Containers are connected to the default bridge. It is recommended to use User-Defined Bridge instead.
- Docker0 is the virtual bridge interface and acts as the network usually named Bridge which is also it’s network type = driver.
- can reach other containers only by ip address on default or host. (172.17.x.x)
- containers get their own mac address | ip address | veth on docker0 network.
- has access to the internet but by default cannot serve the internet.
- can’t isolate containers from each other
- can’t have name resolution
- can’t run services without opening up ports
Default Bridge User-Defined Bridge (Custom Network) Access by IP only Also by Name or Alias Isolated to Docker Isolated to Specific Network All containers have same config Each bridge given own set of rules Linked containers can share env variables No link but uses other ways to share env variables
Use the
--network=to assign a container to a network. If it is not used it will automatically join the default bridge network. Containers can also have no network using--network=none. Containers may also have no isolation and be placed on the host network using--network=host. This would not need the-pflag to publish the port to expose from docker. You cannot spawn multiple of the same container on the host network since the port is common to all containers.
# To see the list of networks run
docker network ls
User-Defined Bridge (Connect containers to a vhost network)
- Isolates from docker0 and host networks
- Has DNS resolution (resolves containers by name)
- Used 90% of the time
- Good for services that are not needed across all devices
docker network create <custom-name>
Host Network (Connect containers directly to the host)
- Create different ip address but remain on the host
- perfect for pi-hole or wireguard
- does dns or dhcp so you need to setup either a macVLAN or ipVLAN
- Don’t need to expose any ports
- has no isolation from the host
MacVLAN Bridge (Connect containers directly to the network)
- essentially a bridge network attatched switch.
- as if eth interfaces are connected directly to the switch port.
- creates different mac | ip addresses for every container on the network.
- some switch ports will not handle the multiple mac addresses on one port.
- requires promiscuous mode
- no DHCP
Some applications expect to be directly connected to the physical network. A macvlan network driver can be used to assign a MAC address to each container’s virtual network interface. This gives the appearance of being connected to a physical network. So what is needed??
- Designate a physical interface on the docker host
- Create a subnet and gateway of the macvlan
- Network needs to be in promiscuous mode as well as on the host `sudo ip link set enp0s3 promisc on'
Be careful not to assign too many unique MAC addreses in the network (VLAN spread).
Generally it is better to use a bridge or overlay network in the long run. MacVLAN is really only needed for legacy apps.
docker network create -d macvlan --subnet 192.168.0.1/24 --gateway 192.168.0.1 --ip-range 192.168.0.253/32 -o parent=enp0s3 <customname>
docker run -itd -rm --network allspark --ip 192.168.1.92 --name styges busybox # creating a container needs --ip and --name
MacVLAN 802.1q
- can specify a eth interface like eth.20 eth.30 etc
- then creates sub interface networks eth0.20 eth0.30 creating a trunk
docker create network -d macvlan --subnet 192.168.20.0/24 --gateway 192.168.20.1 -o parent=enp0s3.20 macvlan20
Have to have trunking setup for this to work.
ipVLAN L2 (Default)
- like a MacVLAN but doesn’t need promiscuous mode.
- hosts shares mac address for all the containers.
docker create network -d ipvlan --subnet 192.168.20.0/24 --gateway 192.168.20.1 -o parent=enp0s3.20 macvlan20
docker run -itd -rm --network allspark --ip 192.168.1.92 --name styges busybox # creating a container needs --ip and --name
ipVLAN L3 (No more switching or arp)
- connect containers to the host as if the host was a router.
- eliminates broadcast traffic (best practice)
- do not specify gateway as hosts gateway is assumed
- all subnets must be created at time of L3 network creation
- can only be isolated by separate network interfaces. (all subnets communicate with each other)
docker network create -d ipvlan --subnet 192.168.44.0/24 -o parent=enp0s3 -o ipvlan_mode=l3 --subnet 192.168.54.0/24 allspark
docker run -itd -rm --network allspark --ip 192.168.44.4 --name styges busybox # creating container needs --ip and --name
add static route for each subnet to the network (unifi or whatever you use) Name: Allspark Destination Network: 192.168.44.0/24 Next Hop: IP of the host
Overlay Network (Connects multiple containers on multiple hosts - k8s | Docker Swarm)
- pretty complicated
None Network
- already created
- container will have no networking
Port Mapping
When dealing with port you do not need to focus on the ip of the container or host.
The only thing we need to control is the ports. Docker will know the ip addresses.
For dockerfiles the EXPOSE for port is just for documentation, so you know which port the container needs.
# We need to map with -p for dockerfiles otherwise there is no service leaving the container!
docker run -p host:container someApp
The container has a single port allows for spawning multiple instances of the same container as we give them different host ports.
docker run -p 9305:3306 mysql
docker run -p 9306:3306 mysql
docker run -p 9307:3306 mysql
For docker-compose we list port mappings and have no need for the -p flag.
This is the point of docker-compose, to alleviate the use to so many flags! More on that later!
Volumes
There are two ways to handle persistent storage:
Named Volumes which are managed by docker and best used for databases & states.
Named Volumes copy volumePATH data into volumeName (happens only on initial empty volume)
docker run -v volumeName:volumePATH postgres
docker run -v /opt/datadir:/var/lib/mysql mysql
Bind Mounts which maps a host directory and best used for source code & dev workflowes.
Bind Mounts override like rsync so do not bind to folders in container that already have content!
# bind mount (local dir is mapped directly into container)
docker run -v ./srcPATH:/app/src devProject
Environment Variables
Convert static variables into an environment variable (according to the language used) will allow to set different values while using docker run -e <envvar>=<value> <image>. Environment variables can be found on containers that already running using docker inspect <container> and will be listed in the JSON file under Env.
Namespaces
Docker Compose
Docker-Compose is great for setting up containers as they handle all the flags and arguments in a clean declarative file.
Both Docker Swarm and Docker-Compose have the following similarities:
- They both take YAML formatted definitions of your application stack.
- They are both meant to deal with multi-container applications (microservices)
- They both have a scale parameter that allows you to run multiple containers of the same image allowing your microservice to scale horizontally.
# start up the containers
docker-compose up
depends_on: only control startup order. It does not alone ensure service is up prior!
Never fear, we have a way to do this in docker-compose:
# Compose will wait for healthcheck to pass before starting the API ^ . ^
depends_on:
db:
condition: service_healthy
Example docker-compose:
services:
api:
build: ./api
ports:
- "4096:3000"
environment:
- DATABASE_URL=postgres://postgres://...@db:5432/mydb
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
web-api:
condition: service_healthy
postgres:
image: postgres:16
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=mydb
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"] # specific for postgres
interval: 30s
timeout: 10s
retries: 5
redis:
image: redis:latest
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
web-api:
image: my-web-api:latest
healthcheck:
test: ["CMD", "curl -fsL http://localhost/ || exit 1"]
interval: 15s
timeout: 5s
retries: 5
start_period: 20s
volumes:
db-data:
Docker Swarm
docker swarm init # creates and makes current node a manager of the swarm cluster
docker node ls # list all the nodes in a swarm
docker service tasks <task> # see how many instances (replicas) of a particular task are running
USE SSH TO ACCESS PRIVATE DATA IN BUILDS
This is a Dockerfile for using SSH in the container.
From alpine
# Install ssh and git
RUN apk add -no-cache openssh-client git
# Download public key for gitlab
RUN mkdir -p -m 0600 ~/.ssh && ssh-keyscan gitlab.com >> ~/.ssh/known_hosts
# RUN --mount=type=ssh git clone git@github.com:myorg/myproject.git myproject