Docker

Docker

Introduction


Open Container Initiative

  1. Runtime Specification - How a image is converted to container and run
  2. Image Specification - What configuration in what format a image file should be written
  3. Distribution Specification - How the images should be distributed /shared. Like, container registry, docke hub etc..

Containers

  • Shared Kernel
  • No dependency conflicts
  • Isolated
  • Faster startup and shutdown
  • Faster provisioning and decommissioning
  • Lightweight enough to use in development!

Linux Kernel Internals

  1. Name spaces
  2. Cgroups ( Control memory and cpu allocation / priorities )
  3. overlayfs file system

Docker Desktop Creates a Linux VM to host docker containers in Windows and Mac OS, for Linux, since it's already a linux host new Linux VM is not created.

In the host system, docker container related files are in /var/lib/docker folder

Data within Containers

  • By default all data created or modified in containers is short lived
  • If some data should be present every time a container image is run (e.g dependency), it should be built into the image itself
  • if data is generated by the application that needs to be persisted, a volume should to store that outside of the ephemeral ( short lived ) container filesystem
  • bind mount should be used to persist configuration files, Not recommended for application data
  • Bind mount can also be used to support hot-reload during development

Building Container Images


👨‍🍳 Application Recipe:

  1. Start with an Operating System
  2. Install the language runtime
  3. Install any application dependencies
  4. Set up the execution environment
  5. Run the application

Docker Build Context

Common Build Commands

FROM: Specifies the base layer or operating system for the container image.
RUN: Executes a command during the build phase.
COPY: Copies files from the build context (e.g., your local system) to the container image.
CMD: Provides a command to be executed when the container starts.

A hash (#) is used for comments. Instructions are written in all caps, followed by arguments.

Dockerfile

A image spec and configuration file

A Naive Implementation example

FROM node
COPY . .
RUN npm install
CMD [ "node", "index.js" ]

Improvements

  1. Pin the base image to improve security and build time
  2. Set a working directory for clarity
  3. Copy dependencies files before source code and install dependencies. This will cache dependency installation layer while performing subsequent builds. While copying it with source code and getting dependencies afterwards, changes in source code will cause dependency fetch everytime the image is built. But, source code change doesnot always means the dependencies are also changed
  4. Use a non root user for security
  5. Configure for production environment ( No debugger, no dev dependencies )
  6. Add useful metadata
  7. Use a cache mount to speedup dependency installation ( Buildkit feature )
  8. Use multi-stage build

A good implementation example

#-------------------------------------------
# Name the first stage "base" to reference later
FROM node:19.6-bullseye-slim AS base
#-------------------------------------------
LABEL org.opencontainers.image.authors="sid@devopsdirective.com"
WORKDIR /usr/src/app
COPY package*.json ./
#-------------------------------------------
# Use the base stage to create dev image
FROM base AS dev
#-------------------------------------------
RUN --mount=type=cache,target=/usr/src/app/.npm \
  npm set cache /usr/src/app/.npm && \
  npm install
COPY . .
CMD ["npm", "run", "dev"]
#-------------------------------------------
# Use the base stage to create separate production image
FROM base AS production
#-------------------------------------------
ENV NODE_ENV production
RUN --mount=type=cache,target=/usr/src/app/.npm \
  npm set cache /usr/src/app/.npm && \
  npm ci --only=production
USER node
COPY --chown=node:node ./src/ .
EXPOSE 3000
CMD [ "node", "index.js" ]

Running Docker Container


There are two ways to run a docker container

  1. Create a docker-compose.yml file and do docker compose up
  2. Use docker run command directly

We can run multiple containers with docker compose at the same time also it manages container life cycle nicely than the docker run option.

Commands Examples

  1. docker run --env POSTGRES_PASSWORD=foorbar --publish 5432:5432 postgres:15.1-alpine
  2. docker run docker/whalesay cowsay "Hello, World"
  3. docker run --interactive --tty --rm ubuntu:22.04
  4. docker start <container-name>
  5. docker attach <container-name>
    • Attach to the shell of running container
  6. docker network ls list all network
  7. docker network create my-network creates a my-network docker network
  8. docker run --network my-network ubuntu sleep 99 Run container in the custom created network

Most Used Options

  1. -d detach and run in background
  2. --entrypoint override entrypoint defined in dockerfile
  3. --env, -e, --env-file Set environment variables in runtime
  4. --init run docker-init process as main process. If not passed the first command/entrypoint will be process id 1 or main process
  5. --interactive --tty or -it We get running shell within that container
  6. --rm removes the container automatically after we stop container process
  7. --name give container a name, can't have two container with same name in the system
  8. --network <name>, --net Assign network to the container
  9. --publish 8080:80, -p is used for mapping port from docker container to host. 8080 is the host port and 80 is container port
  10. --platform <linux/arm64/v8> Run in different CPU architecture, uses QEMU under the hood
  11. --restart unlesss-stopped Restart container if crashed
  12. --cpu-shares No of cpu cycles assigned to the container processes
  13. --memory, -m Amount of memory assigned to the container processes
  14. --pid, --pids-limit No of sub processes the container can create and manage
  15. --privileged Ignore all security config and give privileged level permision to container process
  16. --read-only Make container process read only
  17. --link=db To use db, which points to the db container internal network ip

Docker Compose

version:3.9
services:
    service1:
        image: 
        build:
            context:
            dockerfile:
        init: true
        volumes:
            - host_path/volume:container_path # Bind mount
            - type: bind # Another way
                source: path
                target: path
        networks:
            - network1
        ports:
            - 5173:5173
    
    
    service2:
        image:
        build:
            context:
            dockerfile:
            target: stagename  # Build a specific stage only when using multistage build
        init: true # Good for security
        depends_on:
            - service1 # Starts service1 Before starting this
        environment:
            - Key=Value
        networks:
            - network1
            - network2
        ports: 
            - 3000:3000
        restart: unless-stopped
        
    service3:
        image:
        volumes:
            - volume1:container path # Volumes
        environment:
            - Key=Value
        networks:
            - network2
        ports:
            - 5432:5432
            

# Create Volumes and Network that are being used

volumes:
    volume1:
    
networks:
    network1:
    network2:

By default bridge network is created.

In the above example,

  • service2 can see both service1 and service3

  • service 1 and service 3 are are not visible to each other

  • docker compose up Start all containers and attach to them all

  • docker compose up -d Start all container as daemon or detach

  • docker compose down Stop all containers

NOTE: We can use multiple docker-compose.yml file with docker compose up command. In this case, the later file will override the configuration on previous file. So, only the configuration that need to be override can be put into the new docker-compose.yml file.

Container Security


Image Security

  • Use minimal base image ( chainguard.dev )
  • Scan images docker scout cves image-tag
  • Use users with minimal permissions
  • Keep sensitive info out of images
  • Sigh and verify images
  • Use fixed image tags ( pin major.minor versions )

Runtime Security

  • Start docker daemon with --userns-remap
  • Use --read-only if no write access is required
  • --cap-drop=all, then --cap-add anything you need
  • Limit CPU's and memory
  • Use --security-opt

Deploying to Production


COOL CONCEPT: You can specify the host where docker engine is running to all docker commands applies to that host. It is possible using ssh.

Just do,
export DOCKER_HOST=ssh://user@ip
Remember: You should setup your SSH key in the server to make this work

Things to keep in mind

  1. Security
  2. Ergonomics / Developer Experience
  3. Scalability
  4. Downtime
  5. Observability
  6. Persistent storage configuration
  7. Cost

Why not use Docker Compose for Deployment

  1. No way to deploy without downtime
  2. No way to handle credentials
  3. Can only be used on a single host

Docker Swarm

  • docker swarm init Enable swarm mode in the host
  • docker stack deploy -c docker-swarm.yml stack_name Deploy containers to docker swarm, docker-swarm.yml and docker-compose.yml are nearly identical
  • docker stack rm stack_name To remove stack
  • docker service ls To list all container services and information about replicas
  • docker secret create To create secret in the docker host machine

In the yml file, add following lines

secrets:
    database-url:
        external: true
    postgres-passwd:
        external: true # Means we dont havve to pass secret here in compose file, its in docker secret alreadys

The created secrets will be available to the application through a file. So, application should read the file to get the secret value.

environment:
    - DATABASE_URL=/run/secrets/database-url
secrets:
    - database-url # Tell application that we'll consume it

We can also add deployment configuration to specify no of replicas, and how the updates should be handled

service:
    deploy:
        mode: replicated
        replicas: 2
        update_config:
            order: start-first # start new container before stopping old one

Adding healthcheck will tell the docker engine that the running container is healthy or not.

While performing updates, the new container won't accept traffic and old containers wont get deleted until the new container status is healthy.

service:
    healthcheck:
        test: ["CMD", "Put a test command here"]
        interval: 30s
        timeout: 5s
        retries: 3 # 3 retries before declaring the status unhealthy
        start_period: 10s # Performs first health check after 10 seconds container starts

Docker Network

  • bridge: The default network driver.
  • host: Remove network isolation between the container and the Docker host.
  • none Completely isolate a container from the host and other containers.
  • overlay Overlay networks connect multiple Docker daemons together.
  • ipvlan IPvlan networks provide full control over both IPv4 and IPv6 addressing.
  • macvlan Assign a MAC address to a container.

Docker Volumes

Volumes are stored in a part of the host filesystem which is managed by Docker (/var/lib/docker/volumes/ on Linux). Non-Docker processes should not modify this part of the filesystem. Volumes are the best way to persist data in Docker.

Bind mounts may be stored anywhere on the host system. They may even be important system files or directories. Non-Docker processes on the Docker host or a Docker container can modify them at any time.

tmpfs mounts are stored in the host system's memory only, and are never written to the host system's filesystem.

References