- Install Docker for your platform.
A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image.
Dockerfile sytax help:
- The
# syntaxdirective defines the location of the Dockerfile syntax that is used to build theDockerfile. ARGsets up a variable to be passed at build-time to the builder. This can be later be used like${CODE_VERSION}in theFROMline.FROMthis is the basic container that your container is going to be based on. Kali Linux is used here as a base image just as an example.LABELkey-value pairs of metadata for your container. This can later be inspected withdocker image inspect --format='' myimage.SHELLdefines the default shell for the container. In Linux by default it isSHELL ["/bin/sh", "-c"](included in the exampleDockerfileabove for educational purposes).RUNwill execute commands in a new layer on top of the current image and commit the results. The result will be used for any next step in the Dockerfile.WORKDIRsets the working directory fory anyRUN,CMD,ENTRYPOINT,COPYandADDinstructions that follow it.
For a complete reference of Dockerfiles see here: https://docs.docker.com/engine/reference/builder/
To create a docker image you need to build your Dockerfile. This is done by the Docker daemon, not by the CLI. The CLI passes the build context to the daemon with the following command.
docker build -f ./Dockerfile -t thekyria/thekali:latest .WARNING: In the above, ./Dockerfile asusmes Linux path naming.
Syntax help for docker build:
-fspecifies the Dockerfile location. If omitted, the default location is./Dockerfile.-tspecifies a repository (thekyria/thekali) and a version (latest) to tag the image with..is the context of thedocker buildcommand.
If you want to not consider cached layers when building you can include the flag --no-cache.
If you want to always attempt to pull a latest version of the underlying (i.e. FROM) image when building you can include the flag --pull.
If everything is successfull, you should be able to see your new image.
PS > docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
thekyria/thekali latest 8b77b2f4032d 13 minutes ago 265MBIt is a good practice to include a .dockerignore file in the directory of your Dockerfile. Patterns matched in .dockerignore (in a similar fashion as for .gitignore) will be excluded from the build context (i.e. . in the above example).
You can start the container with:
docker run -i -t --name kali1 thekyria/thekali:latest bashExplanation of the docker run command:
-iflag directs STDIN of the host into the container.-tflag gives you TTY to the docker container as if you were inside the shell of the container. The combination ofiandtgives access the the prompt of the container.--name kali1gives the namekali1to the container instance. This can be seen indocker ps. If the--nameis not specified, a random name is generated upondocker runexecution, e.g.lucid_dewdney.thekyria/thekali:latestspecify the image to runbashspecifies the command to run in the container
For a complete reference on the run command see here: https://docs.docker.com/engine/reference/run/
This will bring you in the bash of the container.
┌──(root㉿b254622c25d6)-[/home/kali]
└─#From another cmd on the host, you can verify that the container is running with:
PS > docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b254622c25d6 thekyria/thekali:latest "/bin/bash" 3 seconds ago Up 4 seconds xenodochial_newtonA container can be started in detached mode.
docker run -it -d thekyria/thekali:latest bashThe above will start the container, launch the bash command in the container, and return to the prompt in the host. You can verify that an image is running with the docker ps command.
The -it flag is a combination of -i, -t. It is still needed
We can attach to (the process running in the) container with:
docker attach ff506a507bbdWhere ff506a507bbd is the CONTAINER ID as shown by the docker ps command.
The CONTAINER_ID can be used to start and stop containers.
docker run -i -t -d thekyria/thekali:latest bash
docker run -i -t -d thekyria/thekali:latest bash
docker psThe above will list two containers, based off the same image, in running state.
We can stop them:
docker stop 3f8447557f1b
docker stop f6872f405f0bNow the output list of docker ps will be empty. We should look into docker ps -a for the stopped containers.
We can start them again with:
docker start 3f8447557f1b
docker start f6872f405f0bOnce they have been restarted, we can attach host prompts to the containers with
docker attach 3f8447557f1bAnd from another bash:
docker attach f6872f405f0bStart two containers.
docker run -i -t -d --name kali1 --network="bridge" --rm thekyria/thekali:latest bash
docker run -i -t -d --name kali2 --network="bridge" --rm thekyria/thekali:latest bashThe --network="bridge" flag creates a network stack on the default Docker bridge (this is the default behavior). Alternative options (e.g. "none", "host", etc.) are available here.
The docker pseudonetworks can be examined on the host with:
docker network lsMore information on the bridge network can be seen with:
docker network inspect bridgeAttach to the two containers from different prompts on the host. Check the IP addresses with ip a. On one of the two start tcpdump and on the other ping the first with ping <ip-address>.
For the above to work, make sure ping and tcpdump are installed on the image(s) that you use.
A similar walkthrough can be found under: https://docs.docker.com/network/network-tutorial-standalone/
In Windows there is not a docker0 bridge interface and therefore linux containers cannot be pinged. See also:
https://docs.docker.com/desktop/windows/networking/
When you start a container without the --rm flag it persists after exiting/stopping. List all containers in the system with:
docker ps -aYou can prune all stopped containers with:
docker container prunePrune unused images with:
docker image pruneThis will clean up all dangling images, i.e. not tagged and not reference by any container.
A deeper cleanup is possible with:
docker image prune -aProtocol buffers are Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.
All pages under this one are worth it. https://developers.google.com/protocol-buffers/docs/overview Especially interesting is the Encoding page.
From here: https://developers.google.com/protocol-buffers/docs/cpptutorial
Create a .proto file.
syntax = "proto2";
package tutorial;
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
Run the protoc compiler to your .proto file.
protoc.exe --proto_path=. --cpp_out=. .\addressbook.protoYou should now have a .pb.h and a .pb.c in your output directory.
An application can copied over to a container through Dockerfile COPY commands and can be run from there via a combination of ENTRYPOINT and CMD commands.
The sourcefiles of this example can be found in this repo. Two python sibling applications (server-client) have been created that can exchange UDP messages. The payload of the exchanged message is serialized using Protocol Buffers.
A basic protocol description has been created in simple_message.proto.
syntax = "proto2";
package thepb;
message simple_message {
optional int32 opcode = 1;
optional string payload = 2;
optional int32 crc32 = 3;
}
The corresponding .py file can be created by parsing the .proto definition with protoc (as seen in Dockerfile:32):
protoc --proto_path=. --python_out=. simple_message/simple_message.protoThe output simple_message_pb2.py is imported and used where needed. Example excerpt from udp_client.py.
...
from simple_message import simple_message_pb2
...
message_tx = simple_message_pb2.simple_message()
message_tx.opcode = 11
message_tx.payload = payload
message_tx.crc32 = crc32.calculate_crc(payload)
...
rx_tx_socket.sendto(message_tx.SerializeToString(), (_target_ip, _target_port))
...Two new Dockerfiles are generated:
Dockerfile.servercorresponding to the "server" application of the example, andDockerfile.clientcorresponding to the client.
# syntax=docker/dockerfile:1
ARG CODE_VERSION=latest
FROM thekyria/thekali:${CODE_VERSION}
ENTRYPOINT ["/home/kali/udp_client.py"]
CMD ["-a", "172.17.0.2", "-p", "20212"]Syntax help:
FROMbasically denoting that this image is extending the one fromthekyria/thekalibeforeENTRYPOINT(exec form) is the command that is automatically executed when the corresponding image is run with adocker run.CMD(exec form) is the command that will execute right after theENTRYPOINT. In total the command that will be executed upon running the container isENTRYPOINT + CMD.
We can build the two images with:
docker build -f ./Dockerfile -t thekyria/thekali:latest .
docker build -f ./Dockerfile.server -t thekyria/udp_server:latest .
docker build -f ./Dockerfile.client -t thekyria/udp_client:latest .WARNING: In the above, ./Dockerfile asusmes Linux path naming.
Notice that before we build Dockerfile.server or Dockerfile.client we need to make sure that thekyria/thekali is up to date (first build command).
We can run containers from the two images (on separate prompts).
docker run -i -t --name udp_server1 --network="bridge" --rm thekyria/udp_server:latest 20211docker run -i -t --name udp_client1 --network="bridge" --rm thekyria/udp_client:latest -a 172.17.0.2 -p 20211Let's examine the first command:
- A container named
udp_server1is started from the imagethekyria/udp_client:latest - The container is attached to docker network
bridge. - An interactive shell is started to the container (
-i -t). - The daemon is notified to delete the container after it exits (
-rm). - Since the container is
run, it starts by executing itsENTRYPOINT, i.e./home/kali/udp_server.py -p, as seen inDockerfile.server - The last part of the command
20211, is the part where theCMDdefault of["20212"](ofDockerfile.server) is overriden. The full command that will be executed in the container when it is run will be:ENTRYPOINT + CMD=/home/kali/udp_server.py -p 20211.
Analogous explanations hold for the second command. 172.17.0.2 has been chosen to match the Docker bridge network IP address of container udp_server1. This is to initialize the sample UDP client application to target a valid IP (through the -a argparse argument of udp_client.py).
We can see that the two containers can communicate to each other.
udp_client1
udp_client[1024] :20212 -> 172.17.0.2:20212
input message payload: asdf
tx (172.17.0.2:20212): | 11 | asdf | 1361703869 |
rx (172.17.0.2:20212): ACK | 11 | asdf | 1361703869 |udp_server1
udp_server[1024] 0.0.0.0:20212
rx (('172.17.0.3', 20212)): | 11 | asdf | 1361703869 | (raw: b'\x08\x0b\x12\x04asdf\x18\xbd\xe7\xa7\x89\x05')
tx (('172.17.0.3', 20212)): ACK | 11 | asdf | 1361703869 |The entrypoint can be overriden with docker run --entrypoint XXX. For example, we can always start a bash in our container with:
docker run -i -t --name udp_client1 --network="bridge" --rm --entrypoint bash thekyria/udp_client:latestA more thorough description of ENTRYPOINT and CMD can be found here.
Compose is a tool for defining and running multi-container Docker applications, quite similar to what we want to do in this case.
The way services are defined is through a YAML file, the docker-compose.yaml.
Syntax help:
versionis the version of thedocker-composeyamlsyntax.networksdefines networks for our deployment.driver: bridgerefers to the type of the underlying Docker network stack as explained in a previous section. We will useudpexampleto connect our services to.servicesis the section where our containers are specified. In this example we have three:base,udp_serverandudp_client. The reasonbase(dummy service) is needed is that the images of the latter two services (i.e.thekyria/udp_server,thekyria/udp_client) depend on imagethekyria/thekali. This service-image dependency is declared withdepends_on:.- For each service:
imagespecifies the image needed for the service.buildgives the instructions on how to build the image. When bothimageandbuildare specified, then compose names the built image according to theimagevalue. Substatementscontextanddockerfileare the equivalents ofdocker buildcommand flags (e.g.-f ./Dockerfile).networkssection points to theudpexamplenetwork defined before and assignes an ip for the container from the subnet defined in the network; this is analogous to thedocker runcommand flag--network="bridge".network_modecomplements how the container will be connected to the network.ttyandstdin_openare the compose equivalents ofdocker runflags-i -t.entrypointandcommandare related to theDockerfilekeywordsENTRYPOINTandCMD(or equivalently with thedocker run --entrypoint ENTRYPOINT myimage COMMAND)
Putting everything together we see that indeed the command executed under the hood for the udp_server is something like:
docker run -it --network=udpexample --entrypoint "/home/kali/udp_server.py -p" thekyria/udp_server:latest 20212A complete reference on compose files can be here.
The .env file is used to set environment variables
CLIENT_IP=10.1.0.3which are then available to docker-compose.yaml,e.g.
ipv4_address: ${CLIENT_IP}The default filename that docker compose will search for environment variables is .env but this can be overriden:
docker compose --env-file ./path/to/.env.file up You can test that substitution of the environment variables happens correctly with:
docker compose configWhile in the same folder as docker-compose.yaml, execute:
docker compose up -dThe up command builds, (re)creates, starts, and attaches to containers for a service. Unless they are already running, this command also starts any linked services.
You can only build (and not run them) the images defined in the compose file with docker compose build instead..
The -d flag tells compose to start in a detached mode, again similar to the docker run -d flag.
You can verify that two containers started with either:
docker ps$ docker compose ps
NAME COMMAND SERVICE STATUS
PORTS
pb_tutorial_py-base-1 "bash" base exited (0)
udp_client1 "/home/kali/udp_clie…" udp_client running
udp_server1 "/home/kali/udp_serv…" udp_server runningWe can see that base service was only used to build the image, and the corresponding pb_tutorial_py-base-1 container is not running. We can attach to any of the other two containers and verify their behavior.
$ docker attach udp_client1
tx (10.1.0.2:20212): | 11 | | 0 |
rx (10.1.0.2:20212): ACK | 11 | | 0 |
input message payload: asdf
tx (10.1.0.2:20212): | 11 | asdf | 1361703869 |
rx (10.1.0.2:20212): ACK | 11 | asdf | 1361703869 |
input message payload:The compose containers can be stopped with:
docker compose downCleanup corresponding images, containers and volumes can be done:
docker compose rm -fsvThis is the equivalent of the -rm flag in the docker run command.
docker-composeis the V1 version of the commanddocker composeis the V2 version of the command (starting Docker Desktop version 3.4.0 - check your version withdocker version).
In all commands in this example, either can be used, i.e. by using the dash "-" or not.