Skip to main content

Docker

Trenissimo can execute and terminate Docker containers through Ant tasks. A use case could be executing an external dispatching algorithm that Trenissimo doesn't have in parallel with the simulation, or a reporting tool at the end of the simulation.

Note

The Trenissimo docker client is compatible with Docker Engine API v1.40. Make sure that you've installed a compatible Docker or Desktop Docker.

Note

This guide requires basic knowledge of Apache Ant and Docker.

Quick start

Launch a container that writes "Hello World" and exits before the simulation.

How it works

Tutorials

Container in execution during the simulation

The goal is to create and execute a container right before the simulation starts, wait till all the necessary services are in execution (before the simulation starts), and at the end of the simulation stop the container, and if necessary, execute, and delete it.

  1. Open the simulation's build.xml file, from Trenissimo's Files panel.
  2. Add the namespace declaration for com.trenolab.trilogy.docker module in the project root element, like so:
    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns:trilogy="antlib:com.trenolab.trilogy.ant"
    xmlns:if="ant:if"
    xmlns:unless="ant:unless"
    xmlns:docker="antlib:com.trenolab.trilogy.docker"
    basedir="."
    default="build"
    name="Simulation">
  3. Extend the target pre-pre-run, this way you can execute the necessary tasks for the container's execution before the simulation starts.
    <target extensionOf="pre-pre-run" name="pre-pre-run-myapp">
    ...
    </target>
  4. Inside the pre-pre-run-myapp target that we've just created, we should firstly initialize the Docker client.
    • The default configuration is compatible with Docker for Linux, and Docker Desktop for macOS:
      <target extensionOf="pre-pre-run" name="pre-pre-run-myapp">
      <docker:configureClient/>
      ...
      </target>
    • On Windows, the dockerHost parameter must be specified.
      <target extensionOf="pre-pre-run" name="pre-pre-run-myapp">
      <docker:configureClient dockerHost="npipe:////./pipe/docker_engine"/>
      ...
      </target>
    • If a cross-platform configuration is required, use an Ant condition like this:
      <target extensionOf="pre-pre-run" name="pre-pre-run-myapp">
      <condition property="docker.host"
      value="npipe:////./pipe/docker_engine"
      else="unix:///var/run/docker.sock" >
      <os family="windows" />
      </condition>
      <docker:configureClient dockerHost="${docker.host}"/>
      ...
      </target>
  5. After the client has been initialized, it is possible to execute the container. The following is a complete example (rather complex). We will later discuss in depth each part of the container's configuration.
    <target extensionOf="pre-pre-run" name="pre-pre-run-myapp">
    ...
    <docker:startContainer imageName="my-app:latest"
    containerIdProperty="docker.container.id"
    autoRemove="true">

    <authConfig username="user"
    password="P@ssw0rd"/>

    <bind source="${params.outputDir}/my-app/"
    destination="/var/my-app/logs"/>

    <portBinding exposedPort="80/tcp"
    publishedPortProperty="docker.port.tcp.80"/>

    <portBinding exposedPort="8088/tcp"
    hostPort="8088/tcp"/>

    <redirector alwayslog="false"
    output="${{params.outputDir}}/my-app-debug-log" />

    </docker:startContainer>
    ...
    </target>
    • It is essential to specify the name of the image to be used with the imageName parameter, using the repository[:tag] format. If the tag is omitted, the default latest will be used.
      <docker:startContainer imageName="my-app:latest"
      containerIdProperty="docker.container.id"
      autoRemove="true">
      ...
      </docker:startContainer>
    • The containerIdProperty parameter is the property name to be set, it contains the container's id (hash). It will be possible to reference this container using this property in other tasks.
    • The autoRemove parameter, when true, will delete the container at the end of the execution. It is equivalent to the --rm option in Docker client's CLI.
    • If the docker image has to be downloaded from a non-public registry, insert a authConfig element containing the username and password for the registry.
          <authConfig username="user"
      password="P@ssw0rd"/>
    • The bind element creates a bind between the host's directory and the container's directory. This way, for example, the logs of the application are written to the output directory of the simulation, to archive them together with the other results.
          <bind source="${params.outputDir}/my-app/" 
      destination="/var/my-app/logs"/>
      When possible, you should always bind the application's output directory inside the simulation's output directory, using the params.outputDir property. Trenissimo will automatically delete output directory during the clean phase, to avoid the project's size growth with time.
    • If the services in execution must be exposed to the host (the machine on which Trenissimo is running), add a port binding. The exposedPort parameter is mandatory, using the port[\proto] format. There are two ways of assigning the host port: either by specifying it in the hostPort parameter or by letting it be assigned automatically, in order to avoid possible conflicts. In the second case, the containerIdProperty parameter is the name of the property to be set, containing the port number automatically assigned.
      <portBinding exposedPort="80/tcp"
      publishedPortProperty="docker.port.tcp.80"/>
      <portBinding exposedPort="8088/tcp"
      hostPort="8088/tcp"/>
    • To redirect the container's standard output/error on a file, insert a redirector element. The alwayslog parameter enables the container's output on Trenissimo's standard output. It is recommended to set this parameter to false to avoid overlapping the simulation's log lines with the ones coming from the container. If possible, always write the output files inside the simulation's output directory, using the params.outputDir property. Trenissimo will automatically delete output directory during the clean phase, to avoid the project's size growth with time.
          <redirector alwayslog="false"
      output="${params.outputDir}/my-app-debug-log" />
      To learn more, check the Apache Ant manual
  6. Now that the container has been created and Docker has taken charge of its startup, it is not guaranteed that it will start correctly, or that it will have the running status immediately. Thus we need to wait for the container to start correctly:
     <target extensionOf="pre-pre-run" name="pre-pre-run-myapp">
    ...
    <docker:startContainer containerIdProperty="docker.container.id">
    ...
    </docker:startContainer>
    <waitfor maxwait="15" maxwaitunit="second"
    timeoutproperty="docker.container.timeout">
    <docker:status containerId="${docker.container.id}" status="running"/>
    </waitfor>
    <fail if:set="docker.container.timeout">Container not started</fail>
    ...
    Note the containerIdProperty's usage, which specifies the name of the property that needs to be set with the container's id, and the containerId that specifies the container's id. If the application executed in the container requires a startup time, it is possible to wait until it starts, for example searching for a specific message contained in the log. The following example is based on ActiveMQ, which is notorious for writing the line Connector ws started when the service is in execution and can accept connections:
     <waitfor maxwait="15" maxwaitunit="second" checkevery="500" 
    timeoutproperty="docker.activemq.container.timeout">
    <resourcecontains resource="${params.outputDir}/debug-activemq.log"
    substring="Connector ws started"/>
    </waitfor>
    <fail if:set="docker.container.timeout">ActiveMQ not started</fail>
    If the container does not start in the specified time interval, the simulation fails, through the fail task
  7. At this point, the container is being executed and can accept inbound network connections, or access the simulation files.
  8. If at the end of the simulation you want to stop the container and delete it, extend the target post-post-run and call the stopContainer task:
     <target extensionOf="post-post-run" name="post-post-run-myapp">
    <docker:stopContainer containerId="${docker.container.id}"/>
    </target>
  9. Execute the simulation

In this guide we introduced the docker:configureClient, docker:startContainer, docker:stopContainer tasks, the docker:status condition and the data types bind, portBinding and authConfig. docker:startContainer, docker:stopContainer. You can learn more in this Reference

Networking between containers

The goal is to execute multiple containers and to allow communication between them and the host over the network. A use case is if you want to execute an ActiveMQ broker or other middleware in order to allow the simulator to communicate with a dispatching algorithm external to Trenissimo.

This guide requires that one of the two containers is correctly configured following the Tutorial

Note

The integration between Trenissimo and Docker takes into account that Trenissimo may execute multiple simulations at the same time. It is often impossible, and also very discouraged to assign manually ip addresses or names to the containers, networks, volumes, etc. In order to reference these objects in the scripts, the docker client takes charge of assigning unique names where needed and makes them available to the scripts through Ant properties.

  1. To allow communication between containers, it is necessary to firstly create a user-defined bridge. The networkIdProperty parameter should be set to the created network's id (hash). The autoRemove parameter makes it so that this bridge is deleted when the simulation is over, avoiding the consumption of all the Docker's host available networks.
     <target extensionOf="pre-pre-run" name="pre-pre-run-myapp">
    <docker:configureClient/>
    <docker:createNetwork networkIdProperty="docker.network.id"
    autoRemove="true"/>
    ...
    </target>
  2. Call a startContainer task for each needed container, specifying the networkId parameter. Make sure you use different names for containerIdProperty in each container.
  3. Chiamare un task startContainer per ciascun container necessario, specificando il parametro networkId. Fare attenzione a usare un nome diverso per containerIdProperty in ciascun container!
    <docker:startContainer imageName="my-app:latest"
    containerIdProperty="docker.activemq.container.id"
    networkId="${docker.network.id}">
    ...
  4. User-defined bridges provide automatic DNS resolution between containers, by means of the container alias. Define a unique alias for each container used by the simulation.
     <docker:startContainer imageName="activemq:latest"
    containerIdProperty="docker.activemq.container.id"
    networkId="${docker.network.id}">
    <alias value="activemq"/>

    </docker:startContainer>
    <docker:startContainer imageName="my-app:latest"
    containerIdProperty="docker.myapp.container.id"
    networkId="${docker.network.id}">
    <alias value="myapp"/>

    </docker:startContainer>
  5. If necessary, you can pass the alias of the first container to the second container through a command line argument or an environment variable:
    <docker:startContainer>
    <arg value="activemq"/>
    </docker:startContainer>
    <docker:startContainer>
    <env key="ACTIVEMQ_HOST" value="activemq"/>
    </docker:startContainer>
  6. At the end of the simulation, stop both containers, following the reverse creation order.
     <target extensionOf="post-post-run" name="post-post-run-myapp">
    <docker:stopContainer containerId="${docker.myapp.container.id}"/>
    <docker:stopContainer containerId="${docker.activemq.container.id}"/>
    </target>
  7. Execute the simulation

Execute a container at the end of the simulation

TODO

Post-processing of the deterministic simulation results

Post-processing of the stochastic simulation results

Examples

In depth guides