Congrats now that you have dockers installed and running on your computer, lets create our first image to show you how docker can help us to develop our programs. I suggest you make a new folder for this purpose an create a dockerfile inside
$ mkdir test
$ cd test
$ touch dockerfile
Write the following in your dockerfile with the instruction to create your first image to build a simple program in C. The image will install the necessary tools inside our future container, but won't touch your host machine
# Fetch a new image from archlinux
FROM archlinux:base
# install gcc and make
RUN pacman -Sy --noconfirm gcc make
#create and change directory to app
WORKDIR /app
# Copy the files from your local host directory to docker image
COPY main.c main.c
COPY makefile makefile
# build program
RUN make
# run program when container start
ENTRYPOINT ["./main"]
Also create the main.c file in your host machine on the docks
directory
#include <stdio.h>
int main( void )
{
printf("Hello, World! makefile\n\n");
return 0;
}
Create the makefile to build the program
all:
gcc -o main main.c
Builds your image from the dockerfile using docker build
, in our case we deiced to give the name of testimg, then type docker images
to verify your first image has been created
$ docker build -t testimg .
...
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
testimg latest e65b688dd6b8 5 minutes ago 901MB
Create and start a container from the image we build, then remove the container after it runs, all in one step. You’ll see that our source file was compiled, linked, and executed inside the container, without needing to install gcc and make or any other tool in our host machine. If you like you can add --name testcont
to provide the testcont name to our container
$ docker run --rm testimg
Hello, World! makefile
The command `docker run --rm testimg --name testcont
` is the combination of three docker commands
$ docker create --name testcont testimg
$ docker start testcont
$ docker rm testcont
But what should we do when we want to modify our program, such as adding a new line?
#include <stdio.h>
int main( void )
{
printf("Hello, World! makefile\n\n");
printf("Bye, World! makefile\n\n");
return 0;
}
Well, then you have to create the image from the dockerfile again, just use the same name to rewrite the previous one, and then run the container
$ docker build -t testimg .
$ docker run --rm testimg
Hello, World! makefile
Bye, World! makefile
And what happens with the output binary?, well the binary is lost every time you delete the container, the previous steps are fine for certain cases, but we are doing embedded and we need the output to later flash our microcontroller
We can create the container and not delete it for later use, like copying the output binary to our host machine
$ docker build -t testimg .
$ docker run --name testcont testimg
Hello, World! makefile
Bye, World! makefile
Check the container is there
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a72fe1495ca8 testimg "./main" 3 seconds ago Exited (0) 3 seconds ago testcont
Then copy the the output binary file main from the container to your local host directory using docker cp
$ docker cp testcont:/app/main .
Run the main binary in your local host machine and see the output, again we do not have make neither gcc installed in our machine
$ tree
.
├── dockerfile
├── main
├── main.c
└── makefile
$ ./main
Hello, World! makefile
Bye, World! makefile
If you are lazy like me, then you will use a makefile to automate the previous docker commands
all:
gcc -o main main.c
build:
docker build -t testimg .
docker run --name testcont testimg
docker cp testcont:/app/main .
docker rm testcont
A better approach
Okay, the last part demonstrates how to develop our app inside a container, but I think we can improve the overall process by using something called Docker volumes. This feature allows us to share a directory between our machine and the container, enabling us to interact with the installed tools within the container.
Modify the previous docker file to indicate the directory app in our container will be use as a shared volume
# Fetch a new image from archlinux
FROM archlinux:base
# install gcc and make
RUN pacman -Sy --noconfirm gcc make
#create and change directory to app
WORKDIR /app
# Share app directory between host and container
VOLUME /app
Re-build the image form the dockerfile
$ docker build -t testimg .
Create a container from previous image in interactive mode and indicating our local directory is connected to container app directory. this is done with -v "$(pwd)":/app
where pwd
indicate our current host machine directory
$ docker run -it --rm -v "$(pwd)":/app imgtest
[root@98dbd276cd10 app]#
Notice how your prompt changes indicating that now is running in our recently created container, and is in our container app directory where all our source files are located. Type command ls to corroborate what I’m saying
[root@98dbd276cd10 app]# ls
dockerfile main.c makefile
Inside the container you can compile with make and also run the program
[root@98dbd276cd10 app]# make
gcc -o main main.c
[root@98dbd276cd10 app]# ./main
Hello, World!
Adios, World!
Type exit to go back to our local host prompt, and type command ls to see the binary output is also in our working directory (docker container will be remove because we use the flag --rm
)
[root@98dbd276cd10 app]# exit
$ ls
dockerfile main.c makefile main
The program was build inside our container using tools that exist only within it, but the output is on our local host machine. How cool is that?. Now you get the idea how to use Docker for development
$ /main
Hello, World!
Adios, World!
You notice we use the flag --rm
when running our containers, the reason is pretty simple, this is the way of working with container, They are ephemeral, they only exist for the time we need to use it, do not collect them, always remove them from your system.
And extra option
But there’s more! Docker containers are incredibly fast, and we can leverage this to our advantage. For instance, we can simply run the container build and then remove the container with a single command. Additionally, we can create a second target to remove the binaries in our makefile.
# target to build our program
all:
gcc -o main main.c
# remove the genrated binary
clean:
rm main
Modify the dockerfile to run make as soon as the container starts using entry point and also add a parameter by default, in our case the default target all
# Fetch a new image from archlinux
FROM archlinux:base
# install gcc and make
RUN pacman -Sy --noconfirm gcc make
#create and change directory to app
WORKDIR /app
# Share app directory between host and container
VOLUME /app
# run make and all target from the makefile
ENTTRYPOINT[ "make" ]
# append the argument to run "make all"
CMD["all"]
Build again the image and then tun the container, but this time is no necessary the interact mode, just run and let it stop and be removed, you will see it builds our program
$ docker run --rm -v "$(pwd)":/app testimg
gcc -o main main.c
But that’s not all! We can override the parameter specified in the CMD
instruction by appending a new argument when we run the container. For instance, using the argument clean
will remove the main
binary.
$ docker run --rm -v "$(pwd)":/app testimg clean
rm main
That was pretty simple and yet effective for a simple program, you are free to decide how to organize your local directories and those in your containers, maybe will be wise to create a special directory like /home/application
or something like that, is up to you. Play around a little with what you learned so far.