#53 - Extracting Image Metadata to JSON using ImageMagick

About Episode - Duration: 14 minutes, Published: 2019-01-23

In this episode, we are going to be building a simple web service to extract metadata from Image files. We will create an upload form using DropzoneJS, extract the metadata in JSON using ImageMagick, and we'll glue it all together using a Go backend wrapped in Docker.

Download: mp4 or webm

Get notified about future content via the mailing list, follow @jweissig_ on Twitter for episode updates, or use the RSS feed.

Links, Code, and Transcript

Introduction

In this episode, we are going to be building a fairly simple web application, where we can upload images via dragging and dropping them, and then get back useful metadata about these images, in JSON format. Finally, we will walk through the steps of containerizing this application using Docker.

Extracting Image Metadata to JSON

All right, you can see we have an area for dragging over the images here and then over on my desktop we have a bunch of image files. So, let’s drag over the first image and get a feel for how this application actually works. After we have uploaded the image, we automatically get back this JSON block, that contains all the information about what we uploaded. Things like, the image format, resolution, size, EXIF data, etc.

Extracting Image Metadata to JSON using ImageMagick

Let’s try another image. But, why would you want to do something like this? Well, this image happens to contain GPS coordinates, about where this photo was taken. Maybe you’d like to automate the extraction of this type of data, to get back useful information in JSON format, which could be pretty useful.

{
  "image": {
    "name": "/app/tmp/9eFSVHsaPB",
    "format": "JPEG",
    "formatDescription": "Joint Photographic Experts Group JFIF format",
    "mimeType": "image/jpeg",
    "class": "DirectClass",
    "geometry": {
      "width": 640,
      "height": 480,
      "x": 0,
      "y": 0
    },
    "resolution": {
      "x": "300",
      "y": "300"
    },
    "printSize": {
      "x": "2.1333333333333333037",
      "y": "1.6000000000000000888"
    },
    "units": "PixelsPerInch",
    "type": "TrueColor",
    "endianess": "Undefined",
    "colorspace": "sRGB",
    "depth": 8,
    "baseDepth": 8,
    "channelDepth": {
      "red": 8,
      "green": 8,
      "blue": 8
    },
    "pixels": 307200,
    "imageStatistics": {
      "all": {
        "min": "0",
        "max": "255",
        "mean": "120.794",
        "standardDeviation": "48.999",
        "kurtosis": "2.04928",
        "skewness": "0.518196"
      }
    },
    "channelStatistics": {
      "red": {
        "min": "10",
        "max": "255",
        "mean": "152.362",
        "standardDeviation": "49.3103",
        "kurtosis": "-0.459491",
        "skewness": "0.267462"
      },
      "green": {
        "min": "0",
        "max": "255",
        "mean": "132.752",
        "standardDeviation": "49.7499",
        "kurtosis": "-0.338396",
        "skewness": "0.29157"
      },
      "blue": {
        "min": "0",
        "max": "255",
        "mean": "77.2686",
        "standardDeviation": "47.918",
        "kurtosis": "2.32993",
        "skewness": "1.31755"
      }
    },
    "renderingIntent": "Perceptual",
    "gamma": 0.454545,
    "chromaticity": {
      "redPrimary": {
        "x": 0.64,
        "y": 0.33
      },
      "greenPrimary": {
        "x": 0.3,
        "y": 0.6
      },
      "bluePrimary": {
        "x": 0.15,
        "y": 0.06
      },
      "whitePrimary": {
        "x": 0.3127,
        "y": 0.329
      }
    },
    "backgroundColor": "#FFFFFF",
    "borderColor": "#DFDFDF",
    "matteColor": "#BDBDBD",
    "transparentColor": "#000000",
    "interlace": "None",
    "intensity": "Undefined",
    "compose": "Over",
    "pageGeometry": {
      "width": 640,
      "height": 480,
      "x": 0,
      "y": 0
    },
    "dispose": "Undefined",
    "iterations": 0,
    "compression": "JPEG",
    "quality": 75,
    "orientation": "TopLeft",
    "properties": {
      "date:create": "2019-01-12T02:51:28+00:00",
      "date:modify": "2019-01-12T02:51:28+00:00",
      "exif:ColorSpace": "1",
      "exif:ComponentsConfiguration": "1, 2, 3, 0",
      "exif:Contrast": "0",
      "exif:CustomRendered": "0",
      "exif:DateTime": "2008:11:01 21:15:07",
      "exif:DateTimeDigitized": "2008:10:22 16:28:39",
      "exif:DateTimeOriginal": "2008:10:22 16:28:39",
      "exif:DigitalZoomRatio": "0/100",
      "exif:ExifImageLength": "480",
      "exif:ExifImageWidth": "640",
      "exif:ExifOffset": "268",
      "exif:ExifVersion": "48, 50, 50, 48",
      "exif:ExposureBiasValue": "0/10",
      "exif:ExposureMode": "0",
      "exif:ExposureProgram": "2",
      "exif:ExposureTime": "4/300",
      "exif:FileSource": "3",
      "exif:Flash": "16",
      "exif:FlashPixVersion": "48, 49, 48, 48",
      "exif:FNumber": "59/10",
      "exif:FocalLength": "24/1",
      "exif:FocalLengthIn35mmFilm": "112",
      "exif:GainControl": "0",
      "exif:GPSAltitudeRef": "0",
      "exif:GPSDateStamp": "2008:10:23",
      "exif:GPSImgDirectionRef": null,
      "exif:GPSInfo": "926",
      "exif:GPSLatitude": "43/1, 28/1, 281400000/100000000",
      "exif:GPSLatitudeRef": "N",
      "exif:GPSLongitude": "11/1, 53/1, 645599999/100000000",
      "exif:GPSLongitudeRef": "E",
      "exif:GPSMapDatum": "WGS-84   ",
      "exif:GPSSatellites": "06",
      "exif:GPSTimeStamp": "14/1, 27/1, 724/100",
      "exif:ImageDescription": "                               ",
      "exif:InteroperabilityOffset": "896",
      "exif:ISOSpeedRatings": "64",
      "exif:LightSource": "0",
      "exif:Make": "NIKON",
      "exif:MakerNote": "78, 105, 107, 111, 110, 0, ..., 0, 0, 0, ",
      "exif:MaxApertureValue": "29/10",
      "exif:MeteringMode": "5",
      "exif:Model": "COOLPIX P6000",
      "exif:Orientation": "1",
      "exif:ResolutionUnit": "2",
      "exif:Saturation": "0",
      "exif:SceneCaptureType": "0",
      "exif:SceneType": "1",
      "exif:Sharpness": "0",
      "exif:Software": "Nikon Transfer 1.1 W",
      "exif:SubjectDistanceRange": "0",
      "exif:thumbnail:Compression": "6",
      "exif:thumbnail:InteroperabilityIndex": "R98",
      "exif:thumbnail:InteroperabilityVersion": "48, 49, 48, 48",
      "exif:thumbnail:JPEGInterchangeFormat": "4548",
      "exif:thumbnail:JPEGInterchangeFormatLength": "6702",
      "exif:thumbnail:ResolutionUnit": "2",
      "exif:thumbnail:XResolution": "72/1",
      "exif:thumbnail:YResolution": "72/1",
      "exif:UserComment": "65, 83, 67, 73, 73, 0, ...., 32, 32, 32, 0",
      "exif:WhiteBalance": "0",
      "exif:XResolution": "300/1",
      "exif:YCbCrPositioning": "1",
      "exif:YResolution": "300/1",
      "jpeg:colorspace": "2",
      "jpeg:sampling-factor": "2x1,1x1,1x1",
      "MicrosoftPhoto:Rating": "0",
      "signature": "55cbf121d52110cda7c785d97bf02f6a31bd0f5ac44c06f9b2f70c9c7d00ade4"
    },
    "profiles": {
      "exif": {
        "length": "11256"
      },
      "xmp": {
        "length": "4000"
      }
    },
    "artifacts": {
      "filename": "/app/tmp/9eFSVHsaPB"
    },
    "tainted": false,
    "filesize": "162KB",
    "numberPixels": "307K",
    "pixelsPerSecond": "15.36MB",
    "userTime": "0.010u",
    "elapsedTime": "0:01.020",
    "version": "ImageMagick 6.9.5-9 Q16 x86_64 2016-10-21 http://www.imagemagick.org"
  }
}

Before we jump into the guts of how this actually works. Let’s try one more image. This image happens to contain a bunch of data about the camera that took the photo along with its lens settings. Most modern cameras and phones automatically include this type of information when snapping a photo.

But, why?

You’re likely wondering. Why did I create this, and why am I doing an episode about it? Well, this application works into a larger idea that I’ve been thinking about, I wanted to create a few example web applications so that we can containerize them, and deploy them into production. Obviously, the UI needs quite a bit of tweaking on this, but the foundation is there, and this will force us to work through all the steps and problems associated with actually getting an example application configured and deployed into production.

So, I’m thinking we take this concept and create a handful of image utility websites, things like extracting all the metadata and showing it to the user, maybe image optimization, or extracting the GPS points and giving a Google Maps link.

Basically, I want something that goes beyond the simple hello world type applications where we can walk through the end-to-end process of actually running containerized applications in production. Not only this, but then we can walk through what an automated deployment pipeline looks like, with logging, monitoring, etc. It should be a really useful teaching tool. These will be real website, serving live traffic, helping people out on the Internet, and you’ll get a complete picture of how it works behind the scenes.

How does it work?

I created a few diagrams that will hopefully help to explain what’s going on here. First, the HTML form accepts uploads via DropzoneJS, which is an awesome JavaScript library for handling drag-and-drop uploads in your browser. From there, the uploaded file is sent to our HTTP server written in Go, this server writes the uploaded file to a temporary file. Then, we call ImageMagick, with a reference to this newly created temporary file, that we saved to disk. We then, extract metadata about this image in JSON format. Finally, we capture the output from ImageMagick, by way of our Go app, and sent it back to the user. We’re also wrapping all of this in a Docker container, with Alpine Linux as the base image, and installing ImageMagick.

Extracting Image Metadata to JSON using ImageMagick

Why use a Container?

But, you might be asking why use Docker for this? Well, it just makes it so simple to run this application, or send it to other people, if they want to run it. Maybe think of it this way, what would happen if I didn’t use a container? If you wanted to use this particular application, a dependency is having ImageMagick installed. Also, you would have to download the source code, and compile the Go app for your particular architecture, which potentially complicates things. Containers just make life so much easier. So why not use them?

Now that you have a general idea of how things flow. Let’s jump back to the browser for a minute and walk through each piece. First, there’s DropzoneJS, which is absolutely great. If you have any projects where you need drag-and-drop upload functionality, I’d highly recommend it. Next, we have ImageMagick, and it’s very much like the Swiss army knife of command line image editing. Like it says here, it supports over 200 different image formats, and you can do all sorts of cool stuff with it.

Command to extract metadata in JSON

Let’s jump over to the command line for a second, and I’ll show you what it looks like to extract this JSON data, in a standalone version without the web app. So, I have an example image here, and I’m going to use the “convert” command, to extract the metadata from the image. Typically, you’d use this “convert” command, to do things like converting between one format and another, or maybe resizing the image, but lucky for us, this “convert” command also supports extracting metadata in a variety of different formats.

We just need to run “convert example.png json:”.

convert example.png json:

And here’s the output. There’s lots of different things about this image we can see, things like the format, resolution, image properties, etc. Basically, just what we saw in the web app.

{
  "image": {
    "name": "/app/tmp/lehRDrkKJl",
    "format": "PNG",
    "formatDescription": "Portable Network Graphics",
    "mimeType": "image/png",
    "class": "PseudoClass",
    "geometry": {
      "width": 200,
      "height": 125,
      "x": 0,
      "y": 0
    },
    "resolution": {
      "x": "0.39000000000000001332",
      "y": "0.39000000000000001332"
    },
    "printSize": {
      "x": "512.8205128205128176",
      "y": "320.51282051282049679"
    },
    "units": "PixelsPerCentimeter",
    "type": "Palette",
    "endianess": "Undefined",
    "colorspace": "sRGB",
    "depth": 8,
    "baseDepth": 8,
    "channelDepth": {
      "gray": 8
    },
    "pixels": 25000,
    "channelStatistics": {
      "gray": {
        "min": "191",
        "max": "191",
        "mean": "191",
        "standardDeviation": "0",
        "kurtosis": "0",
        "skewness": "0"
      }
    },
    "colormapEntries": 1,
    "colormap": [
        "#BFBFBF"
    ],
    "renderingIntent": "Perceptual",
    "gamma": 0.454545,
    "chromaticity": {
      "redPrimary": {
        "x": 0.64,
        "y": 0.33
      },
      "greenPrimary": {
        "x": 0.3,
        "y": 0.6
      },
      "bluePrimary": {
        "x": 0.15,
        "y": 0.06
      },
      "whitePrimary": {
        "x": 0.3127,
        "y": 0.329
      }
    },
    "backgroundColor": "#FFFFFF",
    "borderColor": "#DFDFDF",
    "matteColor": "#BDBDBD",
    "transparentColor": "#000000",
    "interlace": "None",
    "intensity": "Undefined",
    "compose": "Over",
    "pageGeometry": {
      "width": 200,
      "height": 125,
      "x": 0,
      "y": 0
    },
    "dispose": "Undefined",
    "iterations": 0,
    "compression": "Zip",
    "orientation": "Undefined",
    "properties": {
      "date:create": "2019-01-11T06:45:22+00:00",
      "date:modify": "2019-01-11T06:45:22+00:00",
      "png:IHDR.bit-depth-orig": "1",
      "png:IHDR.bit_depth": "1",
      "png:IHDR.color-type-orig": "3",
      "png:IHDR.color_type": "3 (Indexed)",
      "png:IHDR.interlace_method": "0 (Not interlaced)",
      "png:IHDR.width,height": "200, 125",
      "png:pHYs": "x_res=39, y_res=39, units=1",
      "png:PLTE.number_colors": "1",
      "png:sRGB": "intent=0 (Perceptual Intent)",
      "png:tIME": "2019-01-09T15:02:54Z",
      "signature": "d608d7ef70405530b85b838ecf1079a8f124e5c86b4a681a5fa56409bb1f2325"
    },
    "artifacts": {
      "filename": "/app/tmp/lehRDrkKJl"
    },
    "tainted": false,
    "filesize": "179B",
    "numberPixels": "25K",
    "pixelsPerSecond": "0B",
    "userTime": "0.000u",
    "elapsedTime": "0:01.000",
    "version": "ImageMagick 6.9.5-9 Q16 x86_64 2016-10-21 http://www.imagemagick.org"
  }
}

There’s one other piece to this puzzle, and that is highlight.js, which basically takes that JSON block, and apply syntax highlighting to it. This is not essential, but it just improves the readability of the block that we get back. The reason I’m explaining this in detail, is that I want to use this application as a foundation for a bunch of other applications in the future, so that’s why I’m kind of going into detail here, and there hopefully you don’t mind.

The Code

So, now I thought we’d just jump over and look through the code really quickly, and then build the Docker container. So, you can see the folder structure here. Actually, I should mention, that I’ve uploaded this code to GitHub, and I’ve posted the Docker Images to Docker Hub, so the container and the code are all available.

Extracting Image Metadata to JSON using ImageMagick

The links are in the episode notes above. I’m not going to walk through this in too much detail, in that if you’re really interested, you can just go to GitHub and take a look for yourself.

Well, we have our high-level directory structure here, with static files here, which has our JavaScript and CSS files. Then, we have the template directory, which has our index.html file lives. This is our upload form. Actually, let’s open up the index.html file. I’ll just show you what the form looks like. There’s honestly not much to this file.

Basically. We have a forum where drop zone is connected and then down at the bottom here.

      <form class="dropzone" id="my-awesome-dropzone">
      </form>

We have this script block. This is where we’re saying on any successful DropZone result create a new code block where we’re going to dump the JSON and then highlight the code.

  <script>
  // https://highlightjs.org/usage/
  hljs.initHighlightingOnLoad();

  // http://api.jquery.com/prepend/
  Dropzone.autoDiscover = false;
  var myDropzone = new Dropzone("#my-awesome-dropzone", {
      maxFilesize: 1024, // MB
      init: function() {
          this.on("success", function(file, responseText) {
              $( ".inner" ).prepend( "<pre><code class=\"json\">" + responseText + "</pre>" );
              $( ".inner" ).prepend( "<p>" + file.name + "</p>" );
              // re-init hljs for newly added content
              hljs.initHighlighting.called = false;
              hljs.initHighlighting();
          });
      }
  });
  </script>

Then, let’s jump over to our web server program written in Go. We just jump down to the bottom here. It’s actually very simple. We have a couple lines that handle serving static files. This is our JavaScript and style sheets. Then we have a couple lines here. The first one serves up the index.html template and the second one here handles uploading files.

func main() {

    fs := http.FileServer(http.Dir("static"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))

    http.HandleFunc("/", indexHandler)
    http.HandleFunc("/upload", uploadHandler)

    log.Println("Listening 5000...")
    http.ListenAndServe(":5000", nil)

}

You can see there’s nothing to the indexHandler at all. It basically grabs our index file and returns it to the user.

func indexHandler(w http.ResponseWriter, r *http.Request) {
    // parse and execute the template
 t, _ := template.ParseFiles("templates/index.html")
    t.Execute(w, nil)
}

The uploadHandler, is a little more interesting, as you can see here. I added some short notes to myself, in research that I was doing, trying to figure out how to handle uploads. But, the flow basically goes something like this. I generate a random file name that I’m going to use to save this file as my general rule of thumb is that you never trust user input.

func uploadHandler(w http.ResponseWriter, r *http.Request) {

    // generate semi-random filename
 rand.Seed(time.Now().UnixNano())
    name := randSeq(10)

    r.ParseMultipartForm(32 << 20) // 32MB is the default used by FormFile
 file, handler, err := r.FormFile("file")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()

    // log some data to console
 fmt.Printf("Processing: %v\n", handler.Header)

    // create the directory path if needed
 os.MkdirAll("./tmp/", 0755)

    // save the file
 f, err := os.OpenFile("./tmp/"+name, os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer f.Close()

    io.Copy(f, file)

    // probe file
 probedata := probe(name)

    fmt.Fprintf(w, "%v", probedata)

}

So, I wouldn’t necessarily want to save the file with what the user provided. I rather, generate my own, and then we can go off that, then we grab the uploaded file. This is the form data that DropzoneJS passing us. Then, I print a little debug message, just with the HTTP headers. This is on the server side, but just helps to make sure things are going smoothly. Next make sure the tmp directory exists, and then we go ahead and save the file to our temp directory, with our random name that we generated. Then, we call this probe function which actually calls the convert command. I’ll show you that in a second, and then finally returned the results of this probe command, back to the users browser.

One thing you might be thinking is well. This is kind of hacked together. Hey, he’s not doing any error checking! You’re right, for a lot of prototype type applications, I don’t focus on are checking, or making sure things are totally streamlined. In that, I just want to prove the idea, and then if it actually is successful, I’ll go back and polish things up. So, for me, for this type of application, it’s totally normal.

I find this kind of funny, in that the heart of this application, is actually this probe function. And, all we’re doing here, is we’re basically running that “convert” command, that I showed you at the command line earlier. So it’s “convert example.png json:”, and then we’re returning the output of that command back to the user.

func probe(filename string) string {

    // convert example.png json:
 cmd := exec.Command("convert", "/app/tmp/"+filename, "json:")
    response, err := cmd.CombinedOutput()

    if err != nil {
        return "ERROR in cmd.CombinedOutput()"
    }

    // hopefully return json
 return string(response)

}

So most of the stuff in this program is sort of boilerplate in setting up the web server and making sure that it all works correctly. I guess.

The Dockerfile

The only thing left is to have a look at our Dockerfile.

FROM alpine:latest
RUN apk add --no-cache imagemagick bash
RUN mkdir -p /app && mkdir -p /app/static && mkdir -p /app/templates && mkdir -p /app/tmp
ADD static /app/static
ADD templates /app/templates
ADD web /app/web
WORKDIR /app
EXPOSE 5000
ENTRYPOINT ["/app/web"]

So, I’m using Alpine as the base image. Then, I’m installing ImageMagick and Bash. Installing Bash is more of a convenience. Say that I need to connect to this container, to do some debugging, it just makes things feel quicker when I don’t have to type out the complete commands, I like to use use tab completion. Next, I’m setting up the folder structure within the container. This is where our Go binary will sit, things like the JavaScript and stylesheets, our template and then the temp directory.

These lines here, are basically taking files from our local machine, and copying it inside the container, to the folder structure that we created earlier. So, this is our JavaScript and stylesheets, the index.html template, and then the Go binary, which I’ll show you how to create a second. These three lines down here, basically define the environment how our application should run. So we’re setting up the work directory, that’s /app, where our app lives. Then, we’re exposing port 5000, that’s the port that our Go web servers listening on. Then, we’re setting the entry point, so when the container fires up it’ll execute our Go web server.

The README file

If you check out the code on GitHub, you’ll actually find one other file, that’s the readme file. In there, you’ll find the commands that you need if you want to play around with this. Personally, I like creating these type of files. Even if I don’t share it with other people, in that if I come back to this a few months from now, am I going to remember what these command are? These are pretty generic, but in general, that’s what I like to do.

All right, so this command is what I’m using to build the Go web server, you’ll notice that it’s really long. Typically, you might just run “go run” or “go build”. In this case, I’m actually running on a Mac, and I want to build this container for the Linux architecture, because that’s what I’m running inside my container.

CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-w' -o web ./main.go

So let’s jump over to a command line for a second and if I list the directory contents, you can see there’s no web binary file here. So I’m just going to paste that command that I copied from the readme file and that’ll build our golang web server. And then if I list the directory contents again, you can see that the binary is here.

bash-3.2$ ls -l
total 24
-rw-r--r--  1 jweissig  staff   267  8 Jan 17:36 Dockerfile
-rw-r--r--  1 jweissig  staff   841 23 Jan 19:21 README.md
-rw-r--r--  1 jweissig  staff  2247  9 Jan 18:11 main.go
drwxr-xr-x  9 jweissig  staff   306  8 Jan 17:23 static
drwxr-xr-x  4 jweissig  staff   136 15 Jan 16:50 templates
drwxr-xr-x  3 jweissig  staff   102 11 Jan 15:03 tmp
bash-3.2$
bash-3.2$ CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-w' -o web ./main.go
bash-3.2$
bash-3.2$ ls -l
total 14272
-rw-r--r--  1 jweissig  staff      267  8 Jan 17:36 Dockerfile
-rw-r--r--  1 jweissig  staff      841 23 Jan 19:21 README.md
-rw-r--r--  1 jweissig  staff     2247  9 Jan 18:11 main.go
drwxr-xr-x  9 jweissig  staff      306  8 Jan 17:23 static
drwxr-xr-x  4 jweissig  staff      136 15 Jan 16:50 templates
drwxr-xr-x  3 jweissig  staff      102 11 Jan 15:03 tmp
-rwxr-xr-x  1 jweissig  staff  7294695 23 Jan 19:48 web
bash-3.2$ file web
web: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

Then let’s flip back to the README file. You can see that I have the command to build the container and then also push the container up to Docker Hub.

docker build -t jweissig/alpine-imagemagick-detect .
docker push jweissig/alpine-imagemagick-detect

So, I’m just going to copy the build step here, then we’ll flip over to the command line, and then I’ll paste it. So what this command is actually doing, is we’re saying “docker build -t”, which is tagging it, and we’re going to tag it as, “jweissig/alpine-imagemagick-detect”, and then “.”. So, we’re saying, docker build whatever is in this directory, and tag it with this particular tag. This tag, is the tag that’s going to get pushed up to Docker Hub.

This basically walks through each of those steps in our Dockerfile that we looked at earlier. So it’s taking that Alpine image, them installing ImageMagick and Bash, setting up our directory structure, copying over our file contents. Then, the final steps, are to set the work directory, expose the port, and then configure our entry point, so that when the container boots, it’s pointing at our Go web server.

bash-3.2$ docker build -t jweissig/alpine-imagemagick-detect .
Sending build context to Docker daemon  7.818MB
Step 1/9 : FROM alpine:latest
 ---> 4e38e38c8ce0
Step 2/9 : RUN apk add --no-cache imagemagick bash
 ---> Using cache
 ---> bbb7f6bb6f32
Step 3/9 : RUN mkdir -p /app && mkdir -p /app/static && mkdir -p /app/templates && mkdir -p /app/tmp
 ---> Using cache
 ---> 47e7007cbf88
Step 4/9 : ADD static /app/static
 ---> Using cache
 ---> 4683064f58b1
Step 5/9 : ADD templates /app/templates
 ---> f2f45becc96d
Removing intermediate container ff1abaf0caa3
Step 6/9 : ADD web /app/web
 ---> b08238e4bda7
Removing intermediate container c4160186d82c
Step 7/9 : WORKDIR /app
 ---> 03e14efb96ca
Removing intermediate container 63b6aba1cb98
Step 8/9 : EXPOSE 5000
 ---> Running in 95e2118245fb
 ---> 8ec9d984ee23
Removing intermediate container 95e2118245fb
Step 9/9 : ENTRYPOINT /app/web
 ---> Running in 6a0592c20130
 ---> bf063f230fd2
Removing intermediate container 6a0592c20130
Successfully built bf063f230fd2
Successfully tagged jweissig/alpine-imagemagick-detect:latest

What’s kind of cool about this, is now we have a totally self-contained program, that we can send to other people. They don’t need to install any dependencies. They just need to do “docker run” and away they go.

Running the Container

To run the container, use these commands:

docker pull jweissig/alpine-imagemagick-detect
docker run -it -p 5000:5000 --rm jweissig/alpine-imagemagick-detect

And then connect to:

http://localhost:5000/

I did add a few debugging commands if you’re interested. Basically, this will allow you to connect to the container interactively, and then you can run the commands, if you’re interested in debugging, or just want to learn about it.

docker ps | grep imagemagick
docker exec -it <CONTAINER ID> bash
convert example.png json:

Cool, so that’s how we build a fairly simple end-to-end application and containerize it.

Alright, that’s it for this episode. Hopefully you found it useful. I’ll see you later. Bye.


Comments (0)

Comments are enabled for subscribers only.

If you are interested in joining the discussion, and getting weekly members only content, please signup.