Building OCI-Compliant Container Images

You can build OCI-compliant container images natively on RWX. Building images on RWX is substantially faster and more ergonomic than building images with docker build due to techniques detailed in the original proposal.

However, note that building with docker build on RWX is also supported.

Simple Example

Here is a simple example that builds a container from a base of ubuntu:24.04. It creates a script and then configures the container command to run the script.

base:
  image: ubuntu:24.04
  config: none

tasks:
  - key: image
    run: |
      cat <<'EOF' > script.sh
      #!/usr/bin/env bash
      echo "hello from a container built on RWX"
      EOF
      chmod +x script.sh
      echo "$PWD/script.sh" | tee $RWX_IMAGE/command

To use the container image, you'll want to push it to a container registry by using the rwx cli. You'll need the task ID from the image task and you'll need to specify the remote tag.

rwx push the-task-id --to the-remote-image-tag

For example, if the ID of the image task is bf3c1467e60eb5601ef2a43b840a0d96 and you want to push the image to 123456789012.dkr.ecr.us-east-2.amazonaws.com/your_repository:your_tag, you would run:

rwx push bf3c1467e60eb5601ef2a43b840a0d96 \
  --to 123456789012.dkr.ecr.us-east-2.amazonaws.com/your_repository:your_tag

For more details see the reference docs on pushing.

Full Node.js Example on RWX

https://github.com/rwx-cloud/rwx-image-example

Here is a more fully featured example of building a container image to run a Node.js application, pushing it directly from an RWX run, and then testing it within an RWX run.

High Level Approach

We're going to define three run definitions using embedded runs:

  • build the container
  • push the container
  • test the container
on:
  github:
    push:
      init:
        commit-sha: ${{ event.git.sha }}

tasks:
  - key: build-image
    call: ${{ run.dir }}/build-image.yml
    init:
      commit-sha: ${{ init.commit-sha }}

  - key: push-image
    call: ${{ run.dir }}/push-image.yml
    init:
      image-task-id: ${{ tasks.build-image.tasks.image.id }}
      image-tag: ${{ init.commit-sha }}

  - key: test-image
    after: push-image
    call: ${{ run.dir }}/test-image.yml
    init:
      image-tag: ${{ init.commit-sha }}

Building the Container

https://github.com/rwx-cloud/rwx-image-example/blob/main/.rwx/build-image.yml

This example is defined in .rwx/build-image.yml

We'll use a base image of node:24.11.0-trixie-slim

base:
  image: node:24.11.0-trixie-slim
  config: none

Next, we'll define a system task to install curl and jq. Those utilities are needed by the git/clone package that we'll use later. You can also use this task to install any other packages that you need for your image.

tasks:
  - key: system
    run: |
      apt-get -y update
      apt-get -y install curl jq
      apt-get -y clean

We'll then use the git/clone package to clone the code.

- key: code
  use: system
  call: git/clone 1.8.0
  with:
    repository: https://github.com/rwx-cloud/rwx-image-example.git
    ref: ${{ init.commit-sha }}

We'll define another task to install npm packages. We'll use a filter so that this task will always be cached unless package.json or package-lock.json changes. Note that this is different than a normal Dockerfile where you have to COPY the files into the image first. On RWX, you can start with all of the files, and then filter individual commands. For more on the differences, see the docs on building images.

- key: npm-install
  use: code
  run: npm ci --omit=dev
  filter:
    - package.json
    - package-lock.json

Finally, we'll define a task for the final image configuration. We'll set the $RWX_IMAGE/user to run the image as the node user instead of root. We'll also configure the default image command as node server.js.

- key: image
  use: npm-install
  run: |
    # drop root privileges and run as the node user
    echo "node" | tee $RWX_IMAGE/user

    echo "node server.js" | tee $RWX_IMAGE/command
  env:
    NODE_ENV: production

For more documentation, see the reference docs on building images.

Pushing the Image from RWX

https://github.com/rwx-cloud/rwx-image-example/blob/main/.rwx/push-image.yml

Let's also define a run in .rwx/push-image.yml to push the image. We're defining this as a separate workflow since pushing an image requires authenticating into a registry and installing tools that we may not want to have included in our container image.

For this run, we'll use the recommended ubuntu:24.04 base configured with rwx/base 1.0.0

base:
  image: ubuntu:24.04
  config: rwx/base 1.0.0

In this example we're going to push to an AWS ECR registry, but you can push to any container registry that you want.

We'll need to install the AWS CLI and the RWX CLI.

tasks:
  - key: aws-cli
    call: aws/install-cli 1.0.7

  - key: rwx-cli
    call: rwx/install-cli 2.0.2

Next, we'll use the aws/assume-role package to facilitate assuming a role that we've configured with permissions to push into our registry.

- key: aws
  use: aws-cli
  call: aws/assume-role 2.0.5
  with:
    region: us-east-2
    role-to-assume: arn:aws:iam::537248441529:role/ecr-demo
    role-duration-seconds: 900

Finally, we'll define a task to push the image.

- key: push-image
  use: [rwx-cli, aws]
  run: |
    aws ecr get-login-password --region us-east-2 \
      | docker login --username AWS --password-stdin $REGISTRY_HOST
    rwx push $IMAGE_TASK_ID --to $REGISTRY_HOST/demo:$REMOTE_TAG
  env:
    AWS_OIDC_TOKEN: ${{ vaults.default.oidc.ecr-demo }}
    IMAGE_TASK_ID: ${{ init.image-task-id }}
    REGISTRY_HOST: 537248441529.dkr.ecr.us-east-2.amazonaws.com
    REMOTE_TAG: ${{ init.image-tag }}

Using the Image in RWX

https://github.com/rwx-cloud/rwx-image-example/blob/main/.rwx/test-image.yml

Here's an example of pulling the image and using it within a run.

We'll create this run definition as .rwx/test-image.yml and use the recommended base of ubuntu:24.04 configured with rwx/base 1.0.0

base:
  image: ubuntu:24.04
  config: rwx/base 1.0.0

To pull the image for this example, we'll need to configure AWS to pull from our ECR repository. You can use any container repository that you want though.

tasks:
  - key: aws-cli
    call: aws/install-cli 1.0.7

  - key: aws
    use: aws-cli
    call: aws/assume-role 2.0.5
    with:
      region: us-east-2
      role-to-assume: arn:aws:iam::537248441529:role/ecr-demo
      role-duration-seconds: 900

Next, we'll define a task to pull the image. We'll use the docker: preserve-true option (see docs) to store the docker data in the RWX cache.

- key: docker-images
  use: aws
  docker: preserve-data
  run: |
    aws ecr get-login-password --region us-east-2 \
      | docker login --username AWS --password-stdin $REGISTRY_HOST
    docker pull $REGISTRY_HOST/demo:$IMAGE_TAG
  env:
    AWS_OIDC_TOKEN: ${{ vaults.default.oidc.ecr-demo }}
    IMAGE_TAG: ${{ init.image-tag }}
    REGISTRY_HOST: 537248441529.dkr.ecr.us-east-2.amazonaws.com

Finally, we can use the image. We'll run it as a background process. Configuring a health check is recommended to make sure the background process is fully ready before the run command is executed.

- key: test-image
  docker: true
  use: docker-images
  background-processes:
    - key: app
      run: |
        docker run --init -p 3000:3000 \
          --health-cmd="curl -f http://localhost:3000" \
          --health-interval=1s \
          --health-retries 10 \
          $REGISTRY_HOST/demo:$IMAGE_TAG
      ready-check: rwx-docker-ready-check
  run: curl -fsS http://localhost:3000
  env:
    AWS_SKIP_AUTH: true
    IMAGE_TAG: ${{ init.image-tag }}
    REGISTRY_HOST: 537248441529.dkr.ecr.us-east-2.amazonaws.com

Bringing It All Together

https://github.com/rwx-cloud/rwx-image-example/blob/main/.rwx/push.yml

Here is a full run definition configured to run on a GitHub push trigger that builds the image, pushes it to a registry, and then uses it in another workflow.

on:
  github:
    push:
      init:
        commit-sha: ${{ event.git.sha }}

tasks:
  - key: build-image
    call: ${{ run.dir }}/build-image.yml
    init:
      commit-sha: ${{ init.commit-sha }}

  - key: push-image
    call: ${{ run.dir }}/push-image.yml
    init:
      image-task-id: ${{ tasks.build-image.tasks.image.id }}
      image-tag: ${{ init.commit-sha }}

  - key: test-image
    after: push-image
    call: ${{ run.dir }}/test-image.yml
    init:
      image-tag: ${{ init.commit-sha }}

More Documentation

See the references docs on building images and pushing images.