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.
- https://hub.docker.com/_/httpd
- https://www.docker.com/blog/how-to-use-the-apache-httpd-docker-official-image/
- https://github.com/docker-library/httpd
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:
- docker crash course by TechWorld with Nana starting at 49:10 https://youtu.be/pg19Z8LL06w?si=hYbiGuqQzW0Zj1KT&t=2950
- the podman zero to hero video, starting at 22:32 https://youtu.be/YXfA5O5Mr18?si=sWsvxCabGD_EkBnf&t=1352
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
}