October 3, 2022

Simplifying Multi-Step Dockerfile with Drone Pipeline

Table of Contents

Over the past few years, many organizations have begun adopting cloud-native architectures. Despite the adoption of these architectures, many companies haven’t achieved optimal results. But why is that? One reason is our adherence to traditional methods of building and deploying applications. As we live in the cloud-native era, every application that we build should be deployable to any cloud through containerization. 

To demonstrate what I mean in this tutorial, I’ll use the next.js with-docker-example. You can clone this demo source from my blog demo sources

```shell
git clone https://github.com/kameshsampath/nextjs-with-drone
export PROJECT_HOME=$(pwd)/nextjs-with-drone
cd $PROJECT_HOME
````

The goal here is to deploy this application onto a cloud platform. These are the steps to do that:

  1. Containerize the application using the Dockerfile
  2. Push them to any Container Registry, such as Github, Quay.io, etc.
  3. Deploy the containerized applications to a Cloud platform, such as Kubernetes, AWS Fargate, Google Cloud Run, etc.

Getting Started

Let’s start analyzing our code starting with Dockerfile:


```dockerfile
FROM node:16-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

RUN yarn build

FROM node:16-alpine AS runner
WORKDIR /app

ENV NODE_ENV production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json

COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static
./.next/static

USER nextjs

EXPOSE 3000
ENV PORT 3000

CMD ["node", "server.js"]
```

This is a multi-step build, which can be difficult for people newer to these processes. Furthermore, if one of the steps fails, then it becomes difficult for us to know why it failed. But let’s stop and think for a second: don’t we feel that all of these commands should flow in a sequence with each step running in a defined order, one after the other? If you also came to this conclusion, then you’re with me on your first step toward continuous integration (CI).

Why Build is Not the Same as  CI

We often get confused between builds and CI. To add fuel to fire, the tools in the market can also be quite confusing, offering plugins and extensions to the build tools, e.g., yarn or Apache Maven, to mix the build with CI. I’m a big fan of the Single Responsibility principle. When we apply that to the application build and deploy process, the build in a cloud-native application should build the application to a container image and then hand off the other steps (such as pushing to the registry, deploying to cloud-native platform, etc.) to CI tools.

With this in mind, when we think about CI, I’ve already introduced you to two important  pieces of nomenclature: 

  • Step – Something that only does one thing, such as build an application, push to registry, etc.
  • Pipeline – Putting multiple steps together to embrace CI 

Before I go further into how you can move from multistep dockerfile builds to a CI pipeline, let’s meet an open source tool, Drone, a cloud-native self-service CI platform. Although it’s 10 years old, Drone still offers a mature CI system harnessing the scaling and fault tolerance characteristics of cloud-native architectures. Drone is known for its simple, decoupled, and declarative features, enabling us to define some robust pipelines with an ease of understanding.

Adding Dockefile to the Pipeline

Let’s start moving our multistep Dockerfile to a Drone pipeline. This is just like how Dockerfile Drone uses a YAML file called .drone.yml, which usually resides in the root of the project sources. You can read more about Drone on the documentation page. In upcoming sections, we’ll start putting together the .drone.yml.

For the first step in writing a .drone.yml, we need the following information:

  • What is the name of the application? nextjs-with-drone
  • What type of build will we use? Docker
  • What kind of Drone resource will we use? We’ll use Pipeline, as we have multiple steps.
  • What kind of platform will we be building on? For most container cases, the platform will be Linux, and the OS architecture will be amd64 or arm64.

With this information, our .drone.yml will look as follows:

```yaml ---kind: pipeline type: docker name: nextjs-with-drone platform: os: linux   arch: arm64 ```

So now we have the first few lines of the Pipeline filled up. The next task is to identify our pipeline steps from the Dockerfile, and we can infer that we have the following three steps:

  1. deps – builds the nodejs dependencies
  2. builder – builds the node.js application
  3. runner – the package for the application as linux container image 

Now let’s begin adding each of these steps as Drone pipeline steps. The Drone pipeline step at minimum requires the following details:

  • What is the name of the step?
  • What container image will this step run?
  • What commands will execute it within the container?

We have all of this required information from the Dockerfile, so let’s start adding them to the .drone.yml as Drone steps:

```yaml
---
kind: pipeline
type: docker
name: nextjs-with-drone

platform:  
os: linux  
arch: arm64

steps:  
- name: yarn-install    
image: docker.io/node:lts-alpine    
commands:  
- apk add --no-cache libc6-compat  
- cd /app-build  
- cp /drone/src/package.json ./  
- cp /drone/src/yarn.lock ./  
- yarn install --frozen-lockfile  
- cp -r /drone/src/* .  
- yarn build    
volumes:  
- name: app-build-dir    
path: /app-build  
- name: build-image   
  image: gcr.io/kaniko-project/executor:debug    
commands:  
- >    
/kaniko/executor    
--context /app-build    
--dockerfile Dockerfile    
--destination ttl.sh/nextjswithdrone/my-app:1h     
volumes:  
- name: app-build-dir    
path: /app-build
volumes:  
- name: app-build-dir     
temp: {}
```

As you’ve likely noticed, the commands attribute of the step is just RUN, COPY instructions in Dockerfile. These are translated into equivalent Linux commands. We have added an extra attribute called volumes, which is essentially used to mount any directories or files into the container. In this case, we used it to share the build artifacts of one step with another.

Final Steps: Cleaning Up

One last step is cleaning up the Dockerfile so that we have just one step to build to the final container image. The cleaned-up Dockerfile looks like this:

```dockerfile
FROM node:lts-alpine
LABEL org.opencontainers.image.source
https://github.com/kameshsampath/fruits-app-ui

WORKDIR /app

ENV NODE_ENV production

RUN adduser --system --uid 1001 nextjs

RUN cp -r /app-build/.next/standalone/. ./ \   
&& cp -r /app-build/public ./ \   
&& cp /app-build/package.json ./package.json \   
&& cp -r /app-build/.next/static ./.next/

RUN chown -R 1001:0 /app \  
&& chmod -R g+=wrx /app

RUN ls -ltra /app

USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["yarn", "start"]
```

You’re now all set to do your first CI build of your application! Download the Drone CLI and add it to your $PATH. If it all went well when running the following command, then you should see the output like drone version 1.5.0:

```shell
drone --version
```

Let us run the pipeline: 

```shell
drone exec
```

If it all went well, then your container image would have been pushed to the ttl.sh repository. Let’s run the container locally to see if our build works:

```shell
docker run -it --rm -p 3000:3000 ttl.sh/nextjswithdrone/my-app:1h
```

When you open http://localhost:3000 in your browser, you should see the welcome screen:

Voilà! You have taken your first step toward practicing CI employing software delivery best practices. Now you can improve or add additional steps to the Pipeline to make it deploy to container cloud platforms. 

Ready to get started with Drone Desktop? Download the free trial today. 

Continuous Integration