I recently had a need to manually load some container images into a Linux system running containerd
(instead of Docker) as the container runtime. I say “manually load some images” because this system was isolated from the Internet, and so simply running a container and having containerd
automatically pull the image from an image registry wasn’t going to work. The process for working around the lack of Internet access isn’t difficult, but didn’t seem to be documented anywhere that I could readily find using a general web search. I thought publishing it here may help individuals seeking this information in the future.
For an administrator/operations-minded user, the primary means of interacting with containerd
is via the ctr
command-line tool. This tool uses a command syntax very similar to Docker, so users familiar with Docker should be able to be productive with ctr
pretty easily.
In my specific example, I had a bastion host with Internet access, and a couple of hosts behind the bastion that did not have Internet access. It was the hosts behind the bastion that needed the container images preloaded. So, I used the ctr
tool to fetch and prepare the images on the bastion, then transferred the images to the isolated systems and loaded them. Here’s the process I followed:
On the bastion host, first I downloaded (pulled) the image from a public registry using ctr image pull
(the example I’ll use here is for the Calico node container image, used by the Calico CNI in Kubernetes clusters):
(Note that sudo
may be needed for all these ctr
commands; that will depend on your system configuration.)
If you have a system (like your local laptop) running Docker, then you can use docker pull
here instead; just note that you may need to adjust the path/URL to the image/image registry.
Still on the bastion host, I exported the pulled images to standalone archives:
The general format for this command looks like this:
If you don’t know what the image name (according to containerd
) is, use ctr image ls
.
After transferring the standalone archives to the other systems (using whatever means you prefer; I used scp
), then load (or import) the images into containerd
with this command:
Repeat as needed for additional images. It appears, by the way, that using wildcards in the ctr image import
command won’t work; I had to manually specify each individual file for import.
ctr -n=k8s.io images import <filename-from-previous-step>
Verify that the image(s) are present and recognized by containerd
using ctr image ls
.
package main
import (
"context"
"fmt"
"log"
"syscall"
"time"
"github.com/containerd/containerd"
"github.com/containerd/containerd/cio"
"github.com/containerd/containerd/oci"
"github.com/containerd/containerd/namespaces"
)
func main() {
if err := redisExample(); err != nil {
log.Fatal(err)
}
}
func redisExample() error {
// create a new client connected to the default socket path for containerd
client, err := containerd.New("/run/containerd/containerd.sock")
if err != nil {
return err
}
defer client.Close()
// create a new context with an "example" namespace
ctx := namespaces.WithNamespace(context.Background(), "example")
// pull the redis image from DockerHub
image, err := client.Pull(ctx, "docker.io/library/redis:alpine", containerd.WithPullUnpack)
if err != nil {
return err
}
// create a container
container, err := client.NewContainer(
ctx,
"redis-server",
containerd.WithImage(image),
containerd.WithNewSnapshot("redis-server-snapshot", image),
containerd.WithNewSpec(oci.WithImageConfig(image)),
)
if err != nil {
return err
}
defer container.Delete(ctx, containerd.WithSnapshotCleanup)
// create a task from the container
task, err := container.NewTask(ctx, cio.NewCreator(cio.WithStdio))
if err != nil {
return err
}
defer task.Delete(ctx)
// make sure we wait before calling start
exitStatusC, err := task.Wait(ctx)
if err != nil {
fmt.Println(err)
}
// call start on the task to execute the redis server
if err := task.Start(ctx); err != nil {
return err
}
// sleep for a lil bit to see the logs
time.Sleep(3 * time.Second)
// kill the process and get the exit status
if err := task.Kill(ctx, syscall.SIGTERM); err != nil {
return err
}
// wait for the process to fully exit and print out the exit status
status := <-exitStatusC
code, _, err := status.Result()
if err != nil {
return err
}
fmt.Printf("redis-server exited with status: %d\n", code)
return nil
}