How to License and Distribute a Private Docker Image
Monday, December 13th 2021
Continuing our series on licensing and distributing software products using Keygen — today, we're going to be diving into private Docker images. We're going to be taking advantage of Keygen's API, for licensing and distribution, as well as Keygen's brand new CLI, for publishing application releases.
At the end of this post, we'll be securely distributing a private Docker image to licensed users, without having to run and manage a full-blown Docker registry.
It has become increasingly common for Independent Software Vendors (or ISVs) to ship on-premise and multi-prem software solutions using Docker containers. Containerization allows on-prem software applications to be easily installed by customers and other end-users, while giving the ISV control over the application environment. This not only reduces the support burden on the ISV, but it also decreases onboarding efforts since Docker containers are relatively plug-and-play in modern enterprise environments.
Today, we'll be working with Dockerizing a Node application, but the same could be done for most other types of applications. From Go, to Ruby on Rails, to Python's Django — the same principles apply — and that's why Docker is great for shipping on-prem.
Building our Docker image
To keep it simple, we're going to create a "Hello, world!" Node app, packaged up into a Docker image, which we'll distribute to our licensed users using Keygen.
Add a package.json
file
To kick things off — let's create a base package.json
file. In it, we'll define our main
and add a script for running the app.
cat << EOF > package.json { "private": true, "main": "main.js", "scripts": { "start": "node main.js" } }EOF
Create our Express app
Next, we'll add our main.js
file. This is where our app's logic will live.
touch main.js
Then we'll add a couple dependencies, such as express
, and morgan
for logging.
yarn add express morgan
Then we'll add a simple "Hello, world!" Express app that runs on port 8080
.
const { PORT = 8080 } = process.env const express = require('express')const morgan = require('morgan')const app = express() app.use(morgan('combined')) app.get('/', (req, res) => res.send('Hello, world!')) app.listen(PORT, () => { console.log(`Server is running on port ${port}`)})
Add a Dockerfile
Then we'll add our Dockerfile
.
touch Dockerfile
We'll keep it super simple and use Alpine Node as our base image. Then we'll install dependencies
with yarn
and boot our Express app on port 8080
.
FROM node:alpine WORKDIR /usr/src/appCOPY . . RUN yarn install EXPOSE 8080CMD ["yarn", "start"]
Add a .dockerignore
file
Finally, we'll create a .dockerignore
alongside our other files.
touch .dockerignore echo 'node_modules/' >> .dockerignoreecho '.git/' >> .dockerignoreecho '*.log' >> .dockerignore
This will prevent any locally installed Node modules and as well as any debug logs from being copied onto our final Docker image.
Running our Docker image
To review what we're done so far — we've created a small "Hello, world!" Express app, with a
minimalistic Dockerfile
, and a .dockerignore
to prevent certain local files from being
included in our final image.
Now that we've got all that out of the way — let's test our app! First, let's build our image
and then boot it. Docker will expose it on port 49160
of localhost
.
docker build . -t keygen/hello_world# => Sending build context to Docker daemon 21.5kB# Step 1/6 : FROM node:alpine# ...# Successfully built df58ca39ab02# Successfully tagged keygen/hello_world:latest docker run -p 49160:8080 -d keygen/hello_world# => 01aa53edb95783e2ea786c4cf95a82e2cc0e3264d5d1a8df437d3a32e3b254a1 docker ps# => CONTAINER ID IMAGE STATUS PORTS# 01aa53edb957 keygen/hello_world Up 1 minute 0.0.0.0:49160->8080/tcp docker logs 01aa53edb957# => Server is running on port 8080
Now that it's booted, let's fire off an HTTP request using curl
and make sure everything's
working properly. Our app should be on localhost:49160
.
curl http://localhost:49160# => Hello, world!
Next, let's package our image.
Packing our Docker image
Keygen doesn't run a full Docker registry, so tagging and pushing an image with docker push
isn't supported. Instead, Keygen's distribution API acts more like a private S3 bucket, but
only accessible by licensed users. We'll be packing our image into a tar
file, which we'll
publish later on using the keygen
command line tool.
To tag and pack a Docker image into a tarball, we'll use the docker save
command.
docker image ls# => REPOSITORY TAG IMAGE ID CREATED SIZE# keygen/hello_world latest df58ca39ab02 9 minutes ago 174MB# node alpine bb1fcdaff936 10 days ago 170MB docker tag df58ca39ab02 keygen/hello_world:v1.0.0# => Tagged image: keygen/hello_world:v1.0.0 docker save keygen/hello_world:v1.0.0 | gzip > hello_world.tar.gz
We should then have a packed Docker image, ready for upload.
ls -sh hello_world.tar.gz# => 49M hello_world.tar.gz
Publishing our Docker image
Next, we'll want to sign up for a free Keygen account. Once we've done that, we'll want to use the Dashboard to create a Product resource, and also generate a Token for our new product. This will allow us to authenticate with Keygen's API and authorize Keygen's CLI to upload releases on your account.
Create a product
On your account Dashboard, the first thing we'll need to do is create a new Product. In the Products section, click New Product and enter the name of your product and click Create Product. This will generate a product ID that we'll pass to Keygen's CLI.
Create a product token
Next on the Dashboard, we'll want to create a Token to authenticate and authorize the CLI so that
we can publish releases. You'll want to click on the Tokens
section, click New Product Token and
create a new token. For security reasons, Keygen does not store your tokens. Copy it and store it
somewhere secure.
Never share this token. It gives full access to your product, including the ability to create and read licenses, and publish new releases.
Publishing a release
If you haven't already, you'll want to download the keygen
CLI. Once you've done that,
we'll use the keygen
CLI to publish our v1.0.0 release. We'll pass in our previously created
values, our Account ID, Product ID, and our Product Token.
# Export env variables for the CLI (or you could pass via flags e.g. --account)export KEYGEN_ACCOUNT_ID="<YOUR_KEYGEN_ACCOUNT_UUID>"export KEYGEN_PRODUCT_ID="<YOUR_KEYGEN_PRODUCT_UUID>"export KEYGEN_PRODUCT_TOKEN="<YOUR_KEYGEN_PRODUCT_API_TOKEN>" # Create a new releasekeygen new --version '1.0.0' # Upload the artifactkeygen upload ./hello_world.tar.gz --platform 'docker' --release '1.0.0' # Publish the releasekeygen publish --release '1.0.0'
You can find your account ID in your settings page.
Downloading our Docker image
Now that we have our Docker image published, on our Dashboard, we can navigate to the Releases section and see our new v1.0.0 release. Since by default, Keygen only allows releases to be downloaded by licensed users, we'll need to create a new license.
(Typically, license creation would be performed using the API, tied to your purchasing flow, but today we'll be doing things manually.)
Create a policy
On your account Dashboard, the next thing we'll need to do, if you haven't already, is create a Policy. These control the different types of products you offer. For this example, we'll be creating a simple 1 year timed license.
In the Policies section, click New Policy and enter a descriptive name and then select a 1
year Duration and set the policy's Authentication Strategy to LICENSE
. Everything else can
be left blank, but feel free to customize. Finally, click Create Policy.
Create a license
Next, navigate on over to the Licenses section and create a new License using the policy. The license will inherit most of its rules from its policy, but feel free to give it a descriptive name to keep things organized. When you're ready, click Create License.
You'll use the license's Key to authenticate with Keygen's API.
Download our release
Finally, let's download our Docker image! We'll download our image tarball from Keygen's API, using our
license key for authentication. Then we'll run the counterpart to the docker save
command,
docker load
.
curl -sSLO https://get.keygen.sh/demo/1.0.0/hello_world.tar.gz \ -u "license:6F4FF7-AA8DC8-9B138D-EE76D6-E5BEF5-V3" docker load < hello_world.tar.gz# => Loaded image: keygen/hello_world:v1.0.0
Listing our images, we should now be able to see v1 of our hello_world
image.
docker image ls# => REPOSITORY TAG IMAGE ID CREATED SIZE# keygen/hello_world v1.0.0 df58ca39ab02 About a minute ago 174MB
The image can then be run and utilized as you would normally.
docker run -p 49160:8080 -d keygen/hello_world:v1.0.0# => Server is running on port 8080 curl http://localhost:49160# => Hello, world!
In conclusion
I hope this article has helped shed some light on how to use Keygen to distribute private Docker images to your customers. We've covered creating a minimalistic Docker image, packing the Docker image into a tarball, and then finally distributing that image to our customers. Keygen's API will automatically assert that releases are only accessible by users with a valid license.
These same concepts of "packing" a Docker image and publishing the resulting tarball can be used to manage releases using other services as well, such as for hosting private Docker images on AWS S3. This is usually much simpler, operationally, than running and maintaining a full customer-facing Docker registry on your own infrastructure.
The immediate benefit of using Keygen over S3 is that you're able assert artifacts are only accessible to licensed users, making things like timed trials much easier to manage compared to managing IAM policies in S3 for each customer, or manually adding and removing customers from a private Docker Hub repository. *shudder*
After that, you can then go on to add licensing logic into your application itself, to support more fine-grained entitlement controls, such as activation limits, feature flags and more, by integrating with our software licensing API.
Until next time.
If you find any errors in my code, or if you can think of ways to improve things, ping me via Twitter.