Slimming a Containerized NuxtJS App with Slim.AI

Nuxt

Container optimization is a hard problem to solve, especially for a wide variety of use cases, languages, and frameworks.

We’re working on bringing the Container Optimization capabilities of our open-source project, DockerSlim, to our Slim SaaS portal.

Slimming a container provides plenty of benefits — up to 30X reduction in image size, fewer vulnerabilities, and faster scan times, to name a few. Automated container optimization requires a combination of static and dynamic analysis to understand what’s running in a given application or microservice and to rebuild a container with just those parts. It’s a complex process and not easily understood.

Since we use VueJS and NuxtJS for most of our developer-facing frontends, we thought it’d be best to start with a Nuxt-based demo app to see how we can automatically optimize it for production.

Here’s how we built it and what we learned. In this post, we’ll cover:

  1. What is Container Optimization?
  2. Building and Containerizing a Sample Nuxt Application
  3. Minifying the Nuxt Container using Slim
  4. Size, Security, and Performance Results

DiscordInvite

What is Container Optimization?

Containers, most notably Docker, have made it considerably easier to ship applications to the cloud and scale effectively with technologies like Kubernetes or serverless. Containers help developers ship code without being experts in infrastructure and operations. However, they also create new challenges.

Containers used in development tend to be large (1 GB+) and include tooling – like shells and package managers – that make them unsafe to use in production contexts. Container optimization, also known as “container slimming” or “container hardening”, is a process by which you make containers ready for production.

It can be tedious and time consuming, and one of our goals at Slim.AI is to make it easy for any developer to quickly create a production-ready container with minimal effort. Manually slimming a container can take hours and be highly specialized.

Our latest Container Optimization feature allows users to slim containers in an easy-to-use and consistent web environment. Slimming images via the Slim SaaS takes only minutes, and creates a repeatable, trackable process that can be used every time you make a code change.

The feature is currently available to selected beta users (register here for early access), with plans of rolling it out more broadly in late Spring 2022.

Building and Containerizing a Sample Nuxt Application

Here at Slim.AI, we use VueJS and its web-development framework NuxtJS in most of our developer-facing frontend web applications.

VueJS is simple, scalable, and makes for an easy onboarding experience for most developers with a bit of Javascript background. NuxtJS provides an excellent starting point for any web application with a ton of out-of-the-box tooling for content, media, and forms and an intuitive project structure.

For this demo app, we’ll create a simple Nuxt blog-type site capable of hosting our Docs content, and put it in a container using the Docker Official node image as the base image.

The App

NuxtJS takes the pain out of rigging up a web application, providing out-of-the-box features and functionality for Vue apps. It also comes with a lot of baked-in magic in the way of Nuxt Modules, making things like rendering Markdown or adding a grid-system simple and intuitive. We’ll add the Vuetify framework to make building UI components easy and consistent.

Code is available in our Examples repo here.

Building a local Nuxt application is easy if you have the requisite libraries installed already. We’ll use npm as our package manager for this install, but there are similar capabilities in yarn.

$ npm init nuxt-app slim-demo

This will enter you into the Nuxt app creation workflow. Choose the following options (or tune it to your preferences; required choices in bold.).

? Project name: slim-demo
? Programming language: **JavaScript**
? Package manager: **Npm**
? UI framework: Vuetify.js
? Nuxt.js modules: Progressive Web App (PWA)
? Linting tools: ESLint
? Rendering mode: Universal (SSR / SSG)*
? Deployment target: **Server (Node.js hosting)**
? Development tools: jsconfig.json
? Continuous integration: None
? Version control system: Git

This gives us a project structure that looks like the following.

|- app 
|-- assets
|-- components
|-- content # where our documents live
|-- layouts
|-- pages
|-- static # where our images live
|-- store
|-- test 

Using Vuetify, it’s easy to build a simply framed and responsive set of pages. All we need to do is add our blog pages to items in the default.vue layout and update some copy and CSS in the index.vue page, and we’re ready to start adding content.

Adding Content using nuxt-content

Nuxt Content is a Nuxt module that makes it easy to render Markdown files (and many other content types) quickly without the need for a third-party integrations. This is great, as our writers provide markdown for all of our docs.

Following the tutorial here, we can put our Markdown files in our content folder and store all of our images in the /static/blog folder.

Under our pages folder, we create a /blog folder and add a new Vue component called _slug.vue. This is a trick of the Vue Router, which will now make the URL Slug (say /blog/quickstart) into a parameter we can access in our component.

Our _slug.vue component looks like this:

<template>
    <nuxt-content :document="article" />
</template>
<script>
export default { 
    async asyncData({$content, params}) {
        const article = await $content('articles',params.slug).fetch()

        const slug = params.slug
        return { slug, article } 
    }
}
</script>

Nuxt-content handles fetching and parsing the markdown files, and the params.slug works to identify the file, so long as the URL slug and the filename are the same.

We can test locally by running npm run dev from our root folder.

Containerizing the app

Now, we need to Dockerize this application. Creating a basic Dockerfile is easy and straightforward, with the minor note that Nuxt requires an lts (long-term support) version of the Node container. In this case, we can use node:lts or pin a specific Node version, like node:14.19.0.

Nuxt requires two environment variables to be set in order to operate correctly: NUXT_HOST and NUXT_PORT.

FROM node:lts

COPY app/package.json /app/package.json
WORKDIR /app 
RUN npm install

COPY app /app
RUN npm run build 

USER node
EXPOSE 3000 
ENV NUXT_HOST=0.0.0.0
ENV NUXT_PORT=3000

ENTRYPOINT ["npm","start"]

We can build and run the container, finding our docs files being served from the Nuxt container.

$ docker build -t slim-demo . 
... building ...
$ docker run --rm -p 3000:3000 slim-demo 
... running ...

Minifying the Nuxt Container

Here is where the fun begins. First, let’s run docker images to see what our container looks like:

$ docker images
REPOSITORY     TAG           IMAGE ID       CREATED          SIZE
slim-demo      latest        956f13cef18e   44 minutes ago   1.29GB

Woof! One GIGABYTE+ for a couple of markdown files and some CSS!

That’s comically huge. Furthermore, if we run a vulnerability scanner on this image (say, Grype or Snyk), we see that the image contains 1,983 vulnerabilities. Even though we’ve applied container best practices to our Dockerfile, it still takes upwards of three to five minutes to build. Not great if you are doing a lot of iterative development or shipping it through a CI pipeline.

How can we make the container smaller and make those vulnerabilities more manageable?

Container Optimization in DockerSlim - Benefits and Challenges

There are lots of ways to optimize a container, including DockerSlim, the open source project created by Slim.AI founder Kyle Quest. Tens of thousands of developers use DockerSlim to automate container slimming every day, but getting it working can take some doing, especially for complex applications.

Nuxt and Vue are built for the modern web, incorporating a lot of recent front-end innovations to create smooth client-side interactions. These benefits, however, can make slimming a containerized Nuxt application a bit nuanced, as we’ve learned slimming our own front-end apps.

For instance, if we just use docker-slim build right out of the box:

$ docker-slim build --target slim-demo 

This appears to succeed. Indeed, we get a 96 MB container. However, when we go to run it, we see that docker-slim has nuked the /static/images folder and our codi_trophy.png image, since it isn’t loaded in the testing of the container using default settings. Our UX team won’t be happy about this.

There’s a magical incantation for docker-slim that we could figure out to prevent this from happening (in reality, it’s not that complicated), but our goal at Slim.AI is to make cloud-native application development easier for every developer, and, understandably, not everyone wants to learn a new open-source tool just to ship a frontend app.

Container Optimization in the Slim Developer Portal

With our latest feature on the Slim Platform, we bring Container Optimization to our SaaS product. In this Nuxt demo, we can enter optimization workflow from the banner on the homepage. Here, we click through a series of screens to configure our app, with defaults for Nuxt applications pre-filled in this Demo use case (future iterations will offer more advance configuration capabilities).

We do simple steps, like providing a custom tag for the new image, confirming our base application folder, verifying that our app is served from the root of the container, and voila, the system builds an optimized container for us.

The new image is just 122 MB, that’s 11X smaller. What’s more, if we re-run our vulnerability scanner, we now have only *64 vulnerabilities (7 critical)**, a much more manageable number. Being considerably smaller, the new image is faster to scan as it goes through CI/CD as well (52 seconds vs 175 seconds), meaning you can ship faster.

The container image is stored in the Slim Cloud (our proprietary storage system) and available for Download. Future iterations will allow you to pull it via our CLI or push it to your own Connected Registry.

As a final step, you can see what changed in the container image by clicking the Diff button. This will do a side-by-side comparison of the original image to the slim image, so you can see exactly which files were removed and which still remain.

And as an added bonus, if you download and run the container image, you’ll have a working copy of our docs and free virtual Codi sticker.

Beta Invite

Results Summary

Test Original Image Slim Image Improvement
Size 1.3 GB 122 MB 90% smaller
Total vulnerabilities 1,983 64 (7 critical) 96.7% fewer
Critical vulnerabilities 37 7 81% fewer
Time to Scan 175 sec 52 sec 70% faster
2 Likes