Agile module packaging
AGILE modules are packaged and distributed as Docker images. This approach provides several advantages:
- Typically, Docker images are built as incremental additions on top of a base image. There are numerous base images available, any of which could be used. What is important for AGILE is that the image should be an ARM architecture image, suitable for the Raspberry Pi 2/3. You only need one working way of installing your system;
- Docker builds are described in a simple script like file, the Dockerfile. This can easily be derived from installation instructions and included in your module's repository.
- Dockerfiles are portable, i.e. any docker installation can build any Dockerfile independent of the underlying host OS (processor architecture does matter: you can't build for another architecture except if there is emulation). The result of this build is a docker image.
- Docker images are also portable, i.e. they can run on any host having a docker installation (given that the architecture of the docker image is supported).
- ARM based docker images can also be run through emulation on x86_64 hosts. Even if this emulation is not perfect, it simplifies testing and fast delivery of a first release.
- ARM based docker images can also be built through emulation on x86_64 hosts. This is a direct consequence of the point above, since image building is done by executing instructions inside an existing image.
- Docker images are opaque in the sense that only their service is exposed, not their filesystem. This allows for preparing a quick and dirty first release, and optimizing image size later on.
Preparing an AGILE module will involve the following steps:
- Preparing a list of installation instructions for your modules, including prerequisites (apt-get install ...) and installation command lines.
- Selecting an example Dockerfile and base image, and modifying this Dockerfile with the installation instructions of your module.
- Troubleshooting the Docker container build process and gradually correcting errors.
- Publishing the image on DockerHub
- Setting up an automated build process
- Integration in the AGILE distribution
- Optimization of the Dockerfile
- 1 Preparing the install instructions
- 2 Dockerfile template and base image
- 3 Troubleshooting the build process
- 4 Publishing on DockerHub
- 5 Setting up an automated build process
- 6 Integration in the AGILE distribution
- 7 Dockerfile optimization
- 8 Recommended base images
- 9 FAQ
Preparing the install instructions
We start from the installation instructions of your module. Ideally, you should already have a set of instructions in your repository's README with which your module could be installed on a freshly installed Linux system. For most, the simplest way is to provide instructions for Debian or Ubuntu. However, almost any other Linux based instructions can be used when writing a Dockerfile: it is enough to select a base image that is based on a distribution compatible with the commands used during the build. E.g., if the build instructions contain commands specific to a version of Alpine Linux, one can use a base image based on that version of Alpine.
Dockerfile template and base image
The easiest way to start is to look at Dockerfiles of one of the existing AGILE modules.
A simple example is the way we build our Node-RED module in https://github.com/Agile-IoT/agile-node-red-nodes/blob/master/Dockerfile A more complex example can be found at https://github.com/Agile-IoT/agile-core/blob/master/Dockerfile
Important parts of the Dockerfile are:
- the FROM line defining the base image (use one of the recommended base images; see below)
- Line(s) installing dependencies using RUN: if the base image is Debian/Ubuntu based, use this template to minimize image space
RUN apt-get update && apt-get install --no-install-recommends -y \ <package1> \ <package2> \ && apt-get clean && rm -rf /var/lib/apt/lists/*
Use as many similar RUN lines as you want. You can consolidate these later during Dockerfile optimization.
- COPY lines copying your files inside the image.
Note that Docker invalidates the build cache for and after this line of the copied file/folder is changed.
- CMD line with the default start command of your module.
NEW: since Docker version 17.05.0-ce, multi-stage build is supported, similar to what previously was possible with Rockerfile or with external scripting. This means that a Dockerfile can contain multiple FROM lines, creating multiple images, while files can be copied between these images. This allows us first create a development image with all the build environment, then derive a smaller deployment image with only the runtime and application binaries installed.
Troubleshooting the build process
First of all, try to build the image:
docker build .
Docker caches the build process line-by-line (by lines of the Dockerfile), so in case there is an error, correction can go faster. Just correct errors one-by-one, and issue docker build . again. Some useful hints:
- you will find many missing dependencies. Since you find them one-by-one, don't merge them yet in previous lines, but add then as new lines right before the failing line. This speeds up development, and you can consolidate these later on.
- If the error is more complex, you can start the last cached image manually and experiment in the command line:
docker run -it <nameofimage> /bin/bash
You can see the name of the image in the build logs. Once you've found the solution, exit the shell (ctrl-d), update the Dockerfile and build again.
If you get an error similar to this one:
standard_init_linux.go:195: exec user process caused "no such file or directory" ERROR: Service 'agile-osjs' failed to build: The command '/bin/sh -c npm install -g grunt-cli supervisor' returned a non-zero code: 1
It is very likely that you need an emulation package. In Debian-based systems you can install it like this:
apt-get install qemu binfmt-support qemu-user-static
Publishing on DockerHub
We use DockerHub repositories for distributing agile module images. Please follow this naming convention:
Where arch is
- armv7l : our default image intended for the AGILE gateway
- x86_64: optionally, you can also build an x86_64 version
Tag your image with
docker build -t agileiot/agile-<modulename>-<arch> .
Create a DockerHub account at https://hub.docker.com/
Send your DockerHub username to Csaba (email@example.com) to add you to the agileiot organization and create your repo.
Push your image using
docker push agileiot/agile-<modulename>-<arch>
Setting up an automated build process
Note: this will NOT work for ARM Java images!
Currently, we support preparing Docker images in two ways:
- manually on your device, as described above;
- (preferred) automatically after each push to the respective GitHub repository, using Travis CI.
To enable the latter you need the following steps:
- ask Csaba to enable Travis integration for you GitHub repo;
- ask Csaba to configure credentials on Travis for pushing to DockerHub
- add a .travis.yml file in the root folder of your repo with content based on this template: https://github.com/Agile-IoT/agile-osjs/blob/master/.travis.yml (do not commit yet)
- update the COMPONENT env variable in the template (do not commit yet)
- update the BASEIMAGE env variable in the x86_64 sub-build part to match your runtime (java, node, etc.)
- commit the changes
Integration in the AGILE distribution
Service startup and bindings between services are governed by agile-scripts (https://github.com/Agile-IoT/agile-scripts). More specifically, we rely on three layers of indirection and automation on top of Docker:
- a docker-compose configuration file: https://github.com/Agile-IoT/agile-scripts/blob/master/compose/docker-compose.yml
- a script to add some macro capabilities on top of docker-compose (missing from docker-compose language): https://github.com/Agile-IoT/agile-scripts/blob/master/compose/agile-compose
- a simplified startup script: https://github.com/Agile-IoT/agile-scripts/blob/master/agile
These levels of indirection allow some flexibility in the architecture. AGILE micro-services are based on Docker images, but there doesn't need to be a one-to-one correspondence. For example, we start up three core services (ProtocolManager, DeviceManager, and the HTTP API) from a single image (called agile-core).
In order to add your service to the distribution follow these steps:
Create your own branch
git checkout -b <modulename>
Add your image to compose/docker-compose.yml . Follow examples of similar modules to get it right.
Test, upload on git, and send a pull request.
Docker images can be optimized in several dimensions.
- Image size optimization
- Small base image: Use base images based on Alpine linux. These are around 100MB smaller than Debian/Ubuntu based base images. This of course means that apt based package management is not available, and you have to install packages using Alpine commands. Eventually, if you have a static binary, or you have all dependencies, you can use a special empty base image called "scratch".
- Avoid build tools in the image:
- Download time optimization
- Stable base
- Tagged base image: Use a date tagged base image (such as FROM resin/raspberry-pi3-node:7.8.0-20170426)
- Build cache
- Stable base
- Build time optimization
- Runtime overhead optimization
- Cross-image optimization
- Common base image across uServices: use one of our recommended base images (see below). These share many of the underlying layers, leading to faster downloads and smaller disk utilization
Recommended base images
- FROM resin/raspberry-pi3-openjdk:openjdk-8-jdk-20170426 and for the x86_64 build" BASEIMAGE=resin\\/intel-nuc-openjdk:openjdk-8-jdk-20170425
- FROM resin/raspberry-pi3-node:7.8.0-20170426 and for the x86_64 build: BASEIMAGE=resin\\/intel-nuc-node:7.8.0-20170506
Q: How to create a docker microservices directly from .jar files?
Important: this way of building a docker image is NOT preferred, since it is not based on an automated process but on generating a jar external to the dockerized build environment. Use it as a start, but then please migrate to a process where the generation of the jar is in the Dockerfile itself!
- export your jar file from your IDE: myapp.jar
- create a dockerfile as below, name the file as "Dockerfile":
FROM resin/raspberrypi3-openjdk:openjdk-8-jdk-20170217 VOLUME /tmp ADD myapp-0.0.1-SNAPSHOT.jar app.jar RUN sh -c 'touch /app.jar' ENV JAVA_OPTS="" ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ]
- copy myapp.jar and Dockerfile to raspberry pi (I copied under /home/pi/Desktop/myapp)
- open a terminal on your raspberry pi (via ssh or connect to a screen and plug keyboard&mouse) then run cd:
- build docker image:
docker build -t agileiot/myapp
output will be : Successfully built 28ad96fc73cb
- tag docker image:
docker tag 28ad96fc73cb agileiot/myapp
- login to docker:
(your username should be added to agileiot group to push an image there)
- push your service to dockerhub:
docker push agileiot/myapp
- run your service under a specific port
docker run -it -p 9000:9000 agileiot/myapp