Ideas, Solutions, Work in progress

and other things

Octopress in a Docker Container

I use Octopress for my, albeit infrequent, blogging. I like the idea of statically generating the blog site with the posts and keeping it all in Git. I also enjoy the ability to quickly type up a post using markdown and then publish it. No need to log in to a site to write the post.

My biggest problem with Octopress is that it requires me to install and manage a Ruby environment. Either by outright installing Ruby on my machine or through rbenv. This usually means, for me at least, a struggle to get all the gems successfully installed. Adding plugins often result in errors while installing gems either due to dependencies between gem versions, or some system level dependency that is not available.

These system level dependencies really bugs me so when I recently replaced my laptop I decided to keep my OS Ruby free by managing the Octopress Ruby environment and the system level dependencies in a Docker container rather than using rbenv

TLDR; The impatient can find the Dockerfile at: https://github.com/dirkvanrensburg/octopress2-dockerfile

Tested environment

Thing Version
OS Ubuntu 16.04
Docker 17.03.1-ce
Octopress 2.0


Assumptions

Github Pages to publish and host my blog and this post assumes that you are too. However, this post should be useful even if you don’t want to use Github Pages.

Octopress

Now that you have a working container, it is time to do the Octopress installation. You can either start with a fresh copy of Octopress, see the documentation or with an existing Octopress blog. If you have an exising blog then you may have to tweak the Dockerfile a bit to add any system level dependencies your blog my have.

New Octopress blog

For a new blog you have a bit of a chicken and egg problem. You need to get the Octopress stuff, but you can’t run any Ruby dependent commands because Ruby is in the Docker container.

Clone Octopress and take note of the Gemfile in the root of the repository:

1
2
git clone git://github.com/imathis/octopress.git octopress
cd octopress

You’ll need the Gemfile in the root of the blog repository for building the Docker image later on.

Existing octopress blog

The following steps should get the blog repository in the correct state for generating and publishing.

  • If you don’t have a current copy of your blog then clone the repository from Github
  • Once you have cloned the repository change into the repository directory and change the working branch from master to source
  • Create the _deploy folder and change into that directory
  • Now link the _deploy directory with the master branch of your blog repository
    • git init
    • git remote add origin <githubrepourl>
    • git pull origin master

You’ll need the Gemfile in the root of the blog repository for building the Docker image later on.

Docker

Docker gives you the ability to create a lightweight container for processes and their dependencies. You create a container from some published base image and then install the necessary packages as you would on a normal machine. You can then run the container, which will start the required process and run it without poluting the host operating system with unecessary dependencies.

Dockerfile

A Dockerfile is a file which tells the Docker daemon what you want in your container. How to write a Dockerfile and what you can do with it is extensively documented here.

You basically start with a FROM statement telling Docker where you want to start and then telling it which package to install. In this case you have a number of dependencies as seen here:

1
2
3
4
5
6
7
8
9
10
FROM ubuntu:16.04

RUN apt-get update -y && apt-get -y install \
  sudo \
  gcc make \
  git \
  vim less \
  curl \
  ruby ruby-dev \
  python2.7 python-pip python-dev

These packages should be enough to get going with Octopress. The next step is to set up a user so that you don’t have to run the rake commands as root.

1
2
3
4
5
# Add blogger user
  RUN adduser --disabled-password --gecos "" blogger && \
  echo "blogger ALL=(root) NOPASSWD:ALL" > /etc/sudoers

  USER blogger

Next create the working folder octopress and grant permissions to blogger to by changing ownership of the folders where you need to make changes in the future

1
2
3
4
5
6
7
8
# Directory for the blog files
RUN sudo mkdir /octopress
WORKDIR /octopress

# Set permissions so blogger can install gems
  RUN sudo chown -Rv blogger:blogger /octopress
  RUN sudo chown -Rv blogger:blogger /var/lib/gems
  RUN sudo chown -Rv blogger:blogger /usr/local/bin

Running rake preview in your octopress blog folder will generate the blog and serve it on port 4000. In order to access the blog from outside the container you need to tell Docker to expose port 4000 for connections.

1
2
# Expose port 4000 so we can preview the blog
EXPOSE 4000

Next it adds the Gemfile. The contents of this file will be custom to your blog so copy it from the blog repository as mentioned earlier. Easiest is to copy your Gemfile to be in the same folder as the Dockerfile since the docker ADD command is relative to the directory you build from.

The next section will add the Gemfile to the Docker image and install the bundles.

1
2
3
4
  # Add the Gemfile and install the gems
  ADD Gemfile /octopress/Gemfile
  RUN gem install bundler
  RUN bundle install

Then it adds your gitconfig to the image. This is necessary to provide the same git experience in or outside of the container. As with the Gemfile you’ll have to copy your .gitconfig file to the same folder as the Dockerfile

1
ADD .gitconfig /home/blogger/.gitconfig

And that is it. See the repository mentioned above for the complete Dockerfile.

Build the docker image

Everything is ready so you can now build the Docker image.

  • Get the Dockerfile from Github git clone git@github.com:dirkvanrensburg/octopress2-dockerfile.git octopress-dockerfile
  • Then copy the Gemfile from the blog repository into the same folder.
  • Then copy your .gitconfig file from your home folder into the same folder
  • Then in the folder containing you Dockerfile run the following:
1
docker build . -t blog/octopress
Flag Description
-t Tag the built image with that name. Later you can use the tag to start a container.


This command instructs Docker to create a container image using the instructions in the Dockerfile. If all goes well you should see a message saying something like this: Successfully built b847ccd963fa

Test the container

To test the Docker image, start the container using the following command:

1
docker run --rm -ti blog/octopress /bin/bash
Flag Description
–rm Clean up by removing the contaier and its filesystem when it exits.
-ti Tells docker to create a pseudo TTY for the container and to keep the standard in open so you can send keystrokes to the container.


The container will start up and your terminal should be attached and in the octopress folder. You should see something like:

1
blogger@eba0e6a691ef:/octopress$

The exit command will exit the container and clean up.

Rakefile

In order to preview the blog while working on a post, you need to change the Rakefile in the root of the blog repository to bind the preview server to the IP wildcard 0.0.0.0. This makes it possible to access the blog preview in your browser at http://localhost:4000

Change

1
  rackupPid = Process.spawn("rackup --port #{server_port}")

To

1
  rackupPid = Process.spawn("rackup -o 0.0.0.0 --port #{server_port}")

Launch the container

The next step is to launch the container. Execute this command from anywhere, replacing the paths to your blog and .ssh keys:

1
docker run -p 4000:4000 --rm --volume <absolute-path-to-blog-repository>:/octopress --volume <absolute-path-to-user-home>/.ssh:/home/blogger/.ssh -ti blog/octopress /bin/bash
Flag Description
docker run Instructs docker to run a previously built image. blog/octopress in this case
-p 4000:4000 Instructs docker to expose the internal (to the container) port to the host interfaces so that the host can send data to a process listening on that port in the container
–rm Tells docker to remove the container and its file system when it exits. We don’t need to keep the container around since the blog source is external to the image
–volume volume is used to mount folders on the host system into the container. This allow processes in the container to access the files as if they are local to the container. In this case two folders are mounted. The blog repository as /octopress and the local .ssh folder of the host user as /home/blogger/.ssh. The ssh keys are used by git to authenticate and encrypt traffic to and from github. Feel free to change this so that only the github keys are available in the container.
-ti Tells docker to create a pseudo TTY for the container and to keep the standard in open so you can send keystrokes to the container.
blog/octopress The name of the image to run. This is the image built earlier using docker build
/bin/bash The command to run when starting the container.


Docker will start the container, create a pseudo tty, open standard in and run /bin/bash so that your terminal is now effectively inside the container.

It is handy to place the command above in a script in the ~/bin folder of your user. For example create a file called ~/bin/blog and place the command in there. Then you can run blog from any terminal to immediately start and access the container.

Do blog stuff

Now run the Octopress blogging commands as you would if Ruby was installed on your local machine.

Install theme

If you created a new Octopress blog then you now have to install your theme. The following installs the default Octopress theme:

1
rake install

New post

1
rake new_post

Will ask for the post name and create the post file in source/_posts

Preview

To preview blog posts:

1
rake preview

Then in your browser navigate to http://localhost:4000 and you should see the preview of your blog.

Generate

1
rake generate

This will generate the blog into the public folder

Publish

1
rake deploy

This will commit the generated blog and push it to github.

Hazelcast, JCache and Spring Boot

Hazelcast is an in memory data grid which enables data sharing between nodes in a server cluster along with a full set of other data grid features. It also implements the JCache (JSR107) caching standard so it is ideal for building a data aggregation service.

In this post I’ll go through the motions of adding Hazelcast to a Spring Boot REST application and resolving the issues until we have a functioning REST service with its response cached in Hazelcast via JCache annotations.

TLDR; I suggest reading the post to understand the eventual solution, but if you are impatient see the solution on github:
* hazelcast-jcache option 1 and 2 and
* hazelcast-jcache option 3

UPDATE 1: It seems that this issue will be resolved soon due to the hard work of @snicoll over at Spring Boot and the Hazelcast community. See the issues:

UPDATE 2: This problem described in this post was fixed in Spring Boot release 1.5.3. Check this repository for a clean example based on Spring boot 1.5.3. I am leaving the post here since it is still interesting due to the different ways the problem could be worked around.

Versions

Dependency Version
Spring Boot 1.5.1
Hazelcast 3.7.5


Spring boot REST

I am going to assume a working knowledge of building REST services using Spring Boot so I won’t be going into too much detail here. Building a REST service in Spring is really easy and a quick Google will bring up a couple of tutorials on the subject.

This post will build on top of a basic REST app found on github. If you clone that you should be able to follow along.

Adding Hazelcast

To add Hazelcast to an existing Spring Boot project is very easy. All you have to do is add a dependency on Hazelcast, provide Hazelcast configuration and start using it.

Step 1

For maven add the following dependencies to your project pom.xml file:

Hazelcast dependencies in pom file
1
2
3
4
5
6
7
8
9
10
     <dependency>
        <groupId>com.hazelcast</groupId>
        <artifactId>hazelcast</artifactId>
    </dependency>

    <dependency>
        <groupId>com.hazelcast</groupId>
        <artifactId>hazelcast-spring</artifactId>
        <version>${hazelcast.version}</version>
    </dependency>

Step 2

I left the following Hazelcast configuration empty to keep things simple for now. Hazelcast will apply defaults for all the settings if you leave the configuration empty. You can either provide a hazelcast.xml file on the classpath (e.g. src/main/resources)

XML expample of default Hazelcast configuration
1
2
3
4
5
<hazelcast xsi:schemaLocation="http://www.hazelcast.com/schema/config hazelcast-config-3.7.xsd"
       xmlns="http://www.hazelcast.com/schema/config"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

</hazelcast>

or provide a com.hazelcast.config.Config bean by means of Spring Java configuration like this:

Java example of default Hazelcast configuration
1
2
3
4
5
6
7
8
@Configuration
public class HazelcastConfig {

    @Bean
    public Config getConfig() {
        return new Config();
    }
}

The hazelcast config can also be externalised from the application by passing the -Dhazelcast.config system property when starting the service.

Step 3

Hazelcast will not start up if you start the application now. The Spring magic happens because of the org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration configuration class which is conditionally loaded by Spring whenever the application context sees that:

  1. HazelcastInstance is on the classpath
  2. There is an unresolved dependency on a bean of type HazelcastInstance

To start using Hazelcast let’s create a special service that will wire in a Hazelcast instance. The service doesn’t do anything since it exists only to illustrate how Hazelcast is configured and started by Spring.

Illustrate starting Hazelcast by adding a dependency
1
2
3
4
5
6
7
@Service
public class MapService {

    @Autowired
    private HazelcastInstance instance;

}

If you start the application now and monitor the logs you will see that Hazelcast is indeed starting up. You should see something like:

[LOCAL] [dev] [3.7.5] Prefer IPv4 stack is true.                                                                                             
[LOCAL] [dev] [3.7.5] Picked [192.168.1.1]:5701, using socket ServerSocket[addr=/0:0:0:0:0:0:0:0,localport=5701], bind any local is true   
[192.168.1.1]:5701 [dev] [3.7.5] Hazelcast 3.7.5 (20170124 - 111f332) starting at [192.168.1.1]:5701                                     
[192.168.1.1]:5701 [dev] [3.7.5] Copyright (c) 2008-2016, Hazelcast, Inc. All Rights Reserved.                                             
[192.168.1.1]:5701 [dev] [3.7.5] Configured Hazelcast Serialization version : 1                                                            

and a bit further down:

Members [1] {                                                               
    Member [192.168.1.1]:5701 - f7225da2-a428-4849-944f-43abfb12063a this 
}                                                                           

This is great! Hazelcast running with almost no effort at all!

Add JCache

Next we want to start using Hazelcast as a JCache provider. To do this add a dependency on spring-boot-starter-cache in your pom file.

Spring Boot Caching dependency in pom file
1
2
3
4
<dependency>
    <groupId>org.springframework.boot</groupId>o
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

Then, in order to use the annotations, add a dependency on the JCache API

JCache dependency in pom file
1
2
3
4
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
</dependency>

Finally to tell Spring to now configure caching add the @EnableCaching annotation to the Spring boot application class. The Spring boot application class is the one that is currently annotated with @SpringBootApplication

Enable Caching
1
2
3
@SpringBootApplication
@EnableCaching
public class Application {

Now something unexpected happens. Starting the application creates two hazelcast nodes which, if your local firewall allows multicast, join up to form a cluster. If multicasting works then you should see the following:

Members [2] {
    Member [192.168.1.1]:5701 - 18383f04-43ac-41fc-a2bc-cd093a9706b6 this
    Member [192.168.1.1]:5702 - b654cb85-7b59-489d-b599-64ddd2dc0730
}

2017-02-16 08:41:46.154  INFO 14141 --- [ration.thread-0] c.h.internal.cluster.ClusterService      : [192.168.1.1]:5702 [dev] [3.7.5] 

Members [2] {
    Member [192.168.1.1]:5701 - 18383f04-43ac-41fc-a2bc-cd093a9706b6
    Member [192.168.1.1]:5702 - b654cb85-7b59-489d-b599-64ddd2dc0730 this
}

This is saying that there are two Hazelcast nodes running; One on port 5701 and another on port 5702 and they have joined to form a cluster.

This is an unexpected complication, but lets ignore the second instance for now.

Caching some results

Let’s see if the caching works. Firstly we have to provide some cache configuration. Add the following to the hazelcast.xml file.

Hazelcast Cache configuration
1
2
3
4
5
6
7
<cache name="sumCache">
    <expiry-policy-factory>
        <timed-expiry-policy-factory expiry-policy-type="CREATED"
                                     duration-amount="1"
                                     time-unit="MINUTES"/>
    </expiry-policy-factory>
</cache>

Next, to start caching change the sum function in the CalculatorService to:

Cached Sum Service
1
2
3
4
5
6
7
@CacheResult(cacheName="sumCache")
@RequestMapping(value = "/calc/{a}/plus/{b}", method = RequestMethod.GET)
public CalcResult sum(@PathVariable("a") Double a, @PathVariable("b") Double b) {
     System.out.println(String.format("******> Calculating %s + %s",a,b));

     return new CalcResult(a + b);
}
  1. On line 1 we added the @CacheResult annotation to indicate that we want to cache the result of this function and want to place them in the cache called sumCache
  2. On line 4 we print a message to standard out so we can see the caching in action

Start the application and call the CalculatorService to add two numbers together. You can use curl or simply navigate to the following URL in a browser window

http://localhost:8080/calc/200/plus/100 

Note: The port could be something other than 8080. Check the standard out when starting the application for the correct port number

You’ll get the following response

{"result":300.0}

and should see this output in standard out:

******> Calculating 200.0 + 100.0

Subsequent calls to the service will not print this line, but if you try again after a minute you’ll see the message again.

So caching works! But what about the second HazelcastInstance ?

Only one instance

So you may think that the second Hazelcast instance is due to the example MapService we added earlier. To test that we can disable it by commenting the @Service annotation. You can even delete the class if you like, but the application will still start two Hazelcast nodes.

My guess as to what is going on is: When the application context starts, a Hazelcast instance is created by the JCache configuration due to the @EnableCaching annotation but this instance is not registered with the Spring context. Later on a new instance is created by the HazelcastAutoConfiguration which is managed by Spring and can be injected into other components.

Solution

I have found two solutions to the ‘two instance problem’ so far. Each with its own drawbacks

Option 1

I got the following idea from Neil Stevenson over on stackoverflow.

Disable the Hazelcast auto configuration
1
2
3
4
@EnableAutoConfiguration(exclude = {
        // disable Hazelcast Auto Configuration, and use JCache configuration
    HazelcastAutoConfiguration.class, CacheAutoConfiguration
})

Drawback: You can’t use the Hazelcast instance created this way directly. Spring has no knowledge of it, so you can’t get it wired in anywhere.

Option 2

This as the same effect as option 1 above except that you can use the instance. You have to name the hazelcast instance in the config:

<instance-name>test</instance-name>

and then tell the Spring context to use it by getting it by name:

Bring the instance into Spring
1
2
3
4
@Bean
public HazelcastInstance getInstance() {
    return Hazelcast.getHazelcastInstanceByName("test");
}

Drawback: This relies on the order of bean creation so I can only say that it works in Spring Boot 1.5.1.

Option 3

The best solution so far is to set and instance name as in Option 2 above and then setting the spring.hazelcast.config=hazelcast.xml in the application.properties file.

see: hazelcast-jcache-option3

Conclusion

I personally think that option 3 is the best approach. That gives you the best of both worlds with minimum configuration.

Spring Boot can be a bit magical at times and doesn’t always do exactly what you would expect, but there is always a way to tell it to get out of the way and do it yourself. The people over at Spring are working hard to make everything ‘just work’ and I am confident that these things will be ironed out over time.