Podman: How do I make a new image from a base?

This article is part of a series.

So the example image based on httpd that I made is actually a pretty bad image. I manually changed the index.html file in htdocs/ and committed it directly instead of creating a new project with just the changed files.

Thankfully the folks offering up the httpd container also offer pretty easy advice on how to extend it.

Building on that advice, here’s a demo:

cd ~/to/place/to/store/projects
mkdir simple_site
mkdir simple_site/public_html
touch simple_site/public_html/index.html
echo -e '<html><body>
    <h1>Hello!</h1>
    <p>This is from a containerized project.</p>
</body></html>' > simple_site/public_html/index.html
touch simple_site/Containerfile
echo -e 'FROM httpd:2.4
COPY public_html /usr/local/apache2/htdocs/' > simple_site/Containerfile
podman build -f simple_site/Containerfile -t my-apache2-demo simple_site
podman run -dit -p 8080:80 my-apache2-demo
curl localhost:8080

The Containerfile

podman and docker will now both recognize Containerfile as well as Dockerfile so in my demo I chose to use Containerfile.

FROM httpd:2.4
COPY ./public-html/ /usr/local/apache2/htdocs/

That’s crazy simple, but it’s on top of a much more elaborate file from the base container which includes a call to start running the apache server, i.e. the required for most cases CMD call.

EXPOSE 80
CMD ["httpd-foreground"]

For complete Containerfile/Dockerfile novices here is some video help:

podman build

A new command in that script is podman build, which creates an image and launches a container.

podman build 
# where is the Containerfile relative to the pwd 
-f simple_site/Containerfile 
# tag (--tag) for the IMAGE that will be created by this build
# `podman images` will show a localhost/my-apache2-dem
-t my-apache2-demo 
## context, what should the working directory be for the commands
## run in the container file.
simple_site

If the script above had cd’d into simple_site to run the build command it would have looked like:

podman build -t my-apache2-demo .

Podman would have found the Containerfile and the context would be the pwd.

Another alternative would have been to tag it with our local registry and then the new image could be pushed there!

## can tag it even if registry is not available. 
## Registry isn't verified at the "tag" step.
podman build -t localhost:5050/my-apache2-demo .
## make sure its running
# podman ps -a
# podman start my_registry # or use ID number
podman push localhost:5050/my-apache2-demo

If one didn’t care about having an image of the build around, from the directory with the Containerfile:

podman build .

And a nameless image would be created that could be easily removed with prune once the container is wound down or by using --rm and --rmi in the run command. (also see podman container cleanup)

The Volume approach

The httpd docs point out that one doesn’t HAVE to have a Containerfile for such a simple situation. Instead it’s possible to just map the files using the –volume (-v) explored in the previous post.

podman run -dit --name my-apache-app -p 8080:80 -v "$PWD":/usr/local/apache2/htdocs/ httpd:2.4

There would be no image to push.

Wrapping a Binary

All of this is trying to move me towards understanding the swift-container-plugin so let’s do one more simplistic example, that honestly could have a lot of problems that I’m not aware of, but it works.

Let’s say one has a statically compiled binary that one knows works on most basic Linux machines.

I have one that came in at about 197MB (debug build) based on a simple Hummingbird server. The base code is only slightly edited from an example in the swift-container-plugin examples folder.

Using the swift-container-plugin I got an image that clocked in at almost 500MB, pressing the limit of my free Digital Ocean account. I want SMALLER.

So I tried this one and it worked to get an image closer to 200MB. It borrowed heavily from examples in the Hummingbird examples repo, but uses Alpine as the base instead of Ubuntu. Alpine will take the musl based binary I built. I also stripped out the build steps and a lot of the niceties. Including some

# Most swift examples use Ubuntu. 
FROM alpine

# Create a hummingbird user and group with /app as its home directory
RUN addgroup \
    -S \
    hbGroup \
&& adduser \
    -S \
    hbUser \
    -h /app/ \
    -k /dev/null \
    -G hbGroup

# Switch to the new home directory
WORKDIR /app

# give the binary to the hummingbird user
COPY --chown=hbUser:hbGroup ./binary /app/

# Ensure all further commands run as the hummingbird user
USER hbUser:hbGroup

# Let Docker bind to port 8080
EXPOSE 8080

# Start the Hummingbird service when the image is run, default to listening on 8080 in production environment
ENTRYPOINT ["./hello-world"]
CMD ["--hostname", "0.0.0.0", "--port", "8080"]

The following script assumes the binary for HelloServer has already been cross-compiled, which I’ll cover in a different post.

cd ~/to/place/to/store/projects
mkdir -p HelloWrapper
mkdir -p HelloWrapper/binary
cp HelloServer/.build/x86_64-swift-linux-musl/debug/hello-world HelloWrapper/binary/hello-world
touch HelloWrapper/Containerfile
# add contents to containerfile
cd HelloWrapper
podman build -t hellowrapper .
podman run -d -p 8080:8080 localhost/hellowrapper
curl localhost:8080
podman images
podman image inspect localhost/hellowrapper

That last “inspect” command is new, podman inspect provides a number of detains about the entities podman manages: containers, images, volumes, network, pods. One can limit it by providing a subtype. In this case images with podman image inspect

Summary

These were two VERY basic Containerfiles that would need improvement before going into production… but they work! Improving them and actually getting the site up on Digital Ocean is part of the next few posts. One avenue to investigate is starting from the base scratch (Thanks jheck!)

Appendix: The Swift Code

For posterity the swift code. Again this code it largely identical to:

Which is Apache-2.0’d to Apple Inc. and the SwiftContainerPlugin project authors.

Package.swift

import PackageDescription

let package = Package(
    name: "hello-world",
    platforms: [.macOS(.v14)],
    dependencies: [
        .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.1.0"),
        // This was here from when the example was just a main.swift?
        // .package(path: "../.."),
        .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"),
        .package(url: "https://github.com/apple/swift-container-plugin", from: "1.0.0"),
    ],
    targets: [
        .executableTarget(
            name: "hello-world",
            dependencies: [
                .product(name: "Hummingbird", package: "hummingbird"),
                .product(name: "ArgumentParser", package: "swift-argument-parser"),
            ]
        )
    ]
)

App.swift

import ArgumentParser

@main
struct Hello: AsyncParsableCommand {
    @Option(name: .shortAndLong)
    var hostname: String = "0.0.0.0"

    @Option(name: .shortAndLong)
    var port: Int = 8080

    func run() async throws {
        let app = buildApplication(
            configuration: .init(
                address: .hostname(hostname, port: port),
                serverName: "HelloServer"
            )
        )
        try await app.runService()
    }
}

Application+build.swift

import Foundation
import Hummingbird
import Logging

let myos = ProcessInfo.processInfo.operatingSystemVersionString

func buildApplication(configuration: ApplicationConfiguration) -> some ApplicationProtocol {
    let router = Router()
    router.addMiddleware { LogRequestsMiddleware(.info) }
    router.get("/") { _, _ in
        "Hello World, from Hummingbird on \(myos)\n"
    }

    let app = Application(
        router: router,
        configuration: configuration,
        logger: Logger(label: "HelloWorldHummingbird")
    )

    return app
}

This article is part of a series.