Self-hosted service
openshift/source-to-image avatar
openshift/source-to-image

S2I still ends every build in a container commit, and its quick start points at CentOS 7

A tool for building artifacts from source and injecting into container images

2,542 stars703 forksGoApache-2.0

At a glance

What is it?
Source-to-Image builds an image by injecting source into a builder container, running that container's own assemble script, and committing the result. The build engine default is podman rather than docker, the dependency list now spans Docker, BuildKit and two moby modules, and the two commands in the write-up carry a typo and an end-of-life base image.
Who is it for?
S2I earns its place when the build environment is the thing you most need to pin, and when a language's dependency graph is too heavy to ship in a runtime image. It earns its place more cheaply when you accept committing a container as the build's last step, which is a different provenance story from a layered Dockerfile build and the reason the dependency list has grown a Podman and BuildKit path alongside the original Docker one.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • openshift/source-to-image2,542 ★ · -3
Star history
Repository2026-09-302026-11-13
openshift/source-to-image2.5k2.5k

-3 Stars in the last 44 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

The two commands in the write-up carry a typo and an end-of-life base image

The invitation to try the tool immediately comes with a shell transcript:

bash
s2i build https://github.com/sclorg/django-ex centos/python-35-centos7 hello-python
docker run -p 8080:8080 hello-python

Two things are wrong with that pair, and neither is a typo you can fix by reading it carefully. The repository argument reads django-ex with a space before centos, which splits the URL into a source that does not exist and a builder image name that does. And the builder image is centos/python-35-centos7, a CentOS 7 stack carrying Python 3.5, both long past their support windows. The rest of the sentence is still the correct summary of what the tool does: an image built from source in a git repository with no Dockerfile necessary, then browsed at http://localhost:8080. Nobody has updated this block, and it is the first thing a new user copies.

A builder image is four scripts and two of them are optional

The contract is small enough to hold in your head, which is the point of the design. An image qualifies as a builder if it supplies `assemble`, which builds or deploys the source, and `run`, which starts the assembled artifacts. Two more are marked optional: `save-artifacts`, which captures what a previous build produced so the next build can be incremental, and `usage`, which prints builder image usage information. On top of the scripts the write-up suggests the image have `/bin/sh` and `tar` available, for smoother operation of s2i itself. That is the entire interface between a language runtime and the tool. The Ruby walkthrough shows what the scripts do in practice: a builder image carries Ruby, Bundler, Rake, Apache and a compiler, the assemble step installs dependencies with Bundler and moves the source into the directory where Apache has been preconfigured to look for a `config.ru`, and the run script is what the committed image gets as its entrypoint. Two documents carry the full requirements, docs/builder_image.md and the tutorial at examples/nginx-centos7/README.md.

The build workflow finishes by committing the container

Five steps, and the last one is the one to understand. s2i creates a container from the build image and passes it a tar file containing the application source in `src`, with anything matched by `.s2iignore` already excluded, plus build artifacts in `artifacts` when incremental builds are in play. It then sets environment variables from `.s2i/environment` if the source provides them, starts the container, runs its `assemble` script, waits for the container to finish, and commits the container. The commit is where the run image is born: the CMD of the output image is set to the `run` script and the image is tagged with the name supplied on the command line. There is no layer-by-layer assembly here and no instruction parser. The unit of build is a whole container that ran to completion, which is also why the security argument in the write-up holds: because the build happens inside one container from an image you control, you decide what privileges and network access the builder gets, which is not true of a Dockerfile run without the usual operational controls, where the build usually runs as root with access to the container network.

Filtering the source tree is not recursive

`.s2iignore` sits in the root of the source repository and behaves in a way that catches people out. One rule per line, each line terminated in a newline character. Filepaths are appended to the absolute path of the root of the source tree, which is either the local directory you supplied or the destination of the clone s2i makes for a remote repository. Wildcards and file name expansion come from Go's `filepath.Match` and `filepath.Glob`. And then the catch: search is not recursive, so subdirectory paths have to be named explicitly. A rule that works like a .gitignore habit will quietly under-match, because the traversal stops where the rule stops. The walkthrough of the remaining rules continues past the point where this document ends, so treat the five points above as the documented set rather than the complete grammar. The same non-recursive assumption shapes the tar the build receives, since `src` is assembled before the container ever starts.

Compiled languages get two invocations and two images

For a dynamic language the build and run environments are the same, so one builder image and one commit is enough. For compiled languages the write-up argues the opposite, that dependencies needed for compilation can dwarf the artefacts you actually want to ship, and it enables a multiple-step build to keep runtime images slim. The Tomcat and Maven example runs it as four actions: build an image with OpenJDK and Tomcat that expects a WAR file injected, layer a second image on top of that one carrying Maven and standard dependencies and expecting a Maven project injected, invoke s2i with the Java source and the Maven image to produce the WAR, then invoke s2i a second time with that WAR and the original Tomcat image to produce the runtime image. The stated payoff is keeping the runtime environment close to the build environment, same JDK and same Tomcat JARs, without deploying build tools to production. The cost is that the artefact crossing the boundary between the two steps is a file on disk, so the pipeline has to carry it between invocations.

The default container engine is podman and the default image tag is latest

The Makefile answers the container engine question without prose. `CONTAINER_ENGINE = podman` is the default, and the build-container target uses that variable rather than naming docker, building an image tagged `localhost/source-to-image/s2i:${VERSION}` from images/release/Dockerfile with `VERSION = latest` as the default value. So the local container build produces a tag called latest unless you override it, which is a sharp edge for anything that resolves images by tag. The same Makefile exports `S2I_BUILD_TAGS = "exclude_graphdriver_btrfs"` for every build it drives, so the btrfs graphdriver is compiled out of the binary rather than merely unused. That tag travels through `make`, `make all` and `make build`, which both call hack/build-go.sh, and cross-compilation goes through hack/build-cross.sh. The output directory is _output. Two distribution-flavoured Dockerfiles sit at the root, rhel8.Dockerfile and rhel9.Dockerfile, alongside an images/ directory.

The dependency list now spans Docker, BuildKit and two moby modules

The go.mod file reads like a migration in progress, which is the honest summary of a tool that outlived the container API it started against. The module declares go 1.26.7 and requires github.com/docker/docker at v28.5.2+incompatible, the incompatible marker still attached to the original Docker client, alongside github.com/moby/buildkit v0.33.0 and two separate moby modules, github.com/moby/moby/api v1.56.0 and github.com/moby/moby/client v0.6.0, plus github.com/moby/docker-image-spec v1.3.1 and github.com/opencontainers/image-spec v1.1.1 for image formats. Container image handling goes through go.podman.io/image/v5, pinned to a pseudo-version rather than a release tag, and the CLI is cobra v1.10.2 with pflag v1.0.10, which is the reason bash completion is something the build verifies. The dependency graph is vendored, with a vendor/ directory at the root, imports are organised by go-imports-organizer with a goio.yaml alongside it, and renovate.json is present for automated update runs.

make verify gates formatting, dependencies, completion and import order

The Makefile documents its own targets in comments before defining them, which makes the intended workflow readable without leaving the file. `make check` is the umbrella, and it runs `verify` and then `test`. `verify` depends on the cross build and then runs four scripts in order: hack/verify-gofmt.sh, hack/verify-deps.sh, hack/verify-bash-completion.sh and hack/verify-imports.sh. Tests go through hack/test-go, with WHAT and TESTS accepted as directory names to narrow the run and TESTFLAGS passed through to that script. There is also an `imports` target, which does not shell out to a hack script but runs the vendored organizer directly with `go run ./vendor/github.com/go-imports-organizer/goio`. The release side lives elsewhere: a .tekton/ directory and a .ci-operator.yaml describe a Red Hat CI operator pipeline, and CONTRIBUTING.md, OWNERS, AUTHORS and doc.go sit at the root. Top-level housekeeping covers .dockerignore, .gitignore, contrib/, tools/, test/ and a test_apprepo/ fixture.

Editorial conclusion

S2I earns its place when the build environment is the thing you most need to pin, and when a language's dependency graph is too heavy to ship in a runtime image. It earns its place more cheaply when you accept committing a container as the build's last step, which is a different provenance story from a layered Dockerfile build and the reason the dependency list has grown a Podman and BuildKit path alongside the original Docker one. Three things to check first. The quick start in the write-up is stale in two ways, a broken repository name and a CentOS 7 image carrying Python 3.5, so treat it as syntax rather than as a recipe. The btrfs graphdriver is excluded by a build tag, so builds on btrfs-backed storage are not what CI exercises. And the release path runs through a Tekton pipeline described by .ci-operator.yaml, not through a workflow file in the repository.

Frequently asked questions

How does openshift source to image work?

s2i creates a container from the builder image, hands it a tar of the application source in src plus any build artifacts, optionally sets environment variables from .s2i/environment, runs the container's assemble script, waits for it to finish, and commits the container with CMD set to the run script.

what is source to image in openshift

A toolkit and workflow for building container images from source without a Dockerfile. Source is injected into a builder image, that image's own assemble script prepares it, and the resulting container is committed as the runnable image.

Does source-to-image require a Dockerfile?

No. The write-up describes building and running a new container image from source code in a git repository with no Dockerfile necessary, because the build logic lives inside the builder image as an assemble script.

Which scripts must a source-to-image builder image provide?

assemble and run are the required ones, with save-artifacts and usage marked optional. Having /bin/sh and tar available in the image is suggested for smoother operation of s2i itself.

How does .s2iignore filter the source tree?

One regular expression per line, each line ending in a newline, with filepaths appended to the absolute path of the source tree root. Wildcards use Go's filepath.Match and filepath.Glob, and the search is not recursive, so subdirectory paths have to be named explicitly.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. openshift/source-to-image on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/openshift-source-to-image.svg)](https://hysenlabs.com/projects/openshift-source-to-image)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/openshift-source-to-image.svg)](https://hysenlabs.com/projects/openshift-source-to-image)

Was this useful?