This page shows you how to setup a project using pod. It assumes a reasonable understanding of Podman and how to write containerfiles.

Start by running pod init—this will create a directory for your project, and let you run any language-specific initialisers to scaffold your project.

$ pod init
Project name [Projects] my-cool-project
Base image for development:
Trying to pull
Getting image source signatures
Copying blob 96ac260f719c skipped: already exists
Copying blob 9d19ee268e0d skipped: already exists
Copying blob e798b0d318d7 skipped: already exists
Copying blob 76639df9a8a9 skipped: already exists
Copying config 3cd9bc411b done
Writing manifest to image destination
Storing signatures
Enter container to setup project now? [Y/n]
root@aec5d6273248:/my-cool-project# ls
root@aec5d6273248:/my-cool-project# crystal init app .
    create  /my-cool-project/.gitignore
    create  /my-cool-project/.editorconfig
    create  /my-cool-project/LICENSE
    create  /my-cool-project/
    create  /my-cool-project/shard.yml
    create  /my-cool-project/src/
    create  /my-cool-project/spec/
    create  /my-cool-project/spec/
Initialized empty Git repository in /my-cool-project/.git/
root@aec5d6273248:/my-cool-project# exit
Setup complete? [Y/n]
Removing container used for setup: my-cool-project-setup

 [1] .git
 [2] spec
 [3] src
 [4] None of these

Which directory has the source files? [3] 3
Initialised project in /home/will/Projects/my-cool-project

pod init will ask for:

During setup pod will run a shell using image you specified. Use this to use the build tool of your language to create a project—like cargo init or npm init. In the example above I use crystal init app ..

If you later want to run a shell to get access to the inner build tools (to do something like run a code generator) you can run pod enter shell. This is configured in the entrypoints section of the pods.yaml file.

We’ve now got two containerfiles, and a pods.yaml file that tells pod what to do. Pod makes some guesses about your project to setup the containerfiles, but you should check that they make sense before building an image. will be used to create a development image, where we can bind-mount our source code in. This gives us a containerised environment without paying a large overhead of rebuilding an image every time we change our code (or even allowing live reloading, depending on the language we’re using). This is the default file for a Crystal project:

COPY shard.yml .
RUN shards install
ENTRYPOINT ["shards", "run", "--error-trace", "--"]

This installs any dependencies and uses shards run to run the default target of our project. We will need to rebuild the image if our dependencies change, but that shouldn’t happen too often. Once you’re happy with the contents of, you can build a dev image:

$ pod build dev
STEP 1/5: FROM AS builder
STEP 2/5: WORKDIR /src
--> Using cache a61f4f23a678caed77fd07ad3f619e807153d4e77b5776caa5bb89261528b286
--> a61f4f23a67
STEP 3/5: COPY shard.yml .
--> 7f568ea75c3
STEP 4/5: RUN shards install
Resolving dependencies
Writing shard.lock
--> 911e98c4dee
STEP 5/5: ENTRYPOINT ["shards", "run", "--error-trace", "--"]
COMMIT my-project:dev-latest
--> 6d89adbf519
Successfully tagged localhost/test-project:dev-latest
Built dev in 4.0s

Before we run it, open pods.yaml and find the section. It has a bunch of default values to illustrate the options you might need—like passing some arguments through to your program, or exposing ports. For our dev image, critical thing is:

  # binding the source directory lets us re-run changes without rebuilding
  src: /src/src

Without this, our container wouldn’t have any code to run! You might need to change this for other languages that put their code in a different directory. For example if you’re using Swift you might do Sources: /src/Sources (/src is the default working directory, which is set with WORKDIR in our containerfile).

We’re almost ready to run our image. I edited the main file of my Crystal project to print “Hello pods!” so I know it’s actually done the right thing. We can run it with:

$ pod run dev
Dependencies are satisfied
Building: test-project
Executing: test-project
Hello pods!


you can set and in pods.yaml to specify the default target to build and run, which saves you from specifying it every time. This shortens the above command to just pod r.

Now we can do the easy part—write a useful application. You go off and do that, then we can continue and make a production image. can be very similar to our dev version, but for compiled languages we can use a two-stage containerfile to make our final image smaller. For Crystal projects I use a containerfile that looks something like:

FROM AS builder
COPY . .
RUN shards install
RUN shards build --error-trace --release --progress --static

COPY --from=builder /src/bin/my-project /bin/my-project
ENTRYPOINT ["/bin/my-project"]

The first image (named builder) uses the crystal:latest-alpine image to produce a statically linked release binary. For Rust we might do something like cargo build --release here. The second image just uses an unadorned alpine image to run the program, copying just the executable from the previous image. If your program relies on some static assets, you’ll need to copy them into this image.

We build our production image just like the dev one:

$ pod build prod
[2/2] COMMIT my-project:prod-latest
--> 98cca2532dd
Successfully tagged localhost/my-project:prod-latest
Built prod in 14s

You can define a repository and tag that the image should get pushed to using the push attribute on the image definition. auto_push: true means this will be done after every successful build.

We could just use pod run prod to run a container from our image, but that’s boring. Instead we’ll use pod update prod to compare with currently running images and apply changes.

Run pod update prod and you should see that pod will start a new container since there isn’t one running yet. (I’ve removed some of the default options from containers in pods.yaml, I’m assuming that you’ve altered the config for your job).

$ pod update prod
Starting my-project-prod

Now let’s make a change to the pods.yaml config file, I’m going to pass a flag into my program by adding to flags in the container definition. I can update my container:

$ pod update -d prod
update: my-project-prod (arguments changed)
same image: localhost/my-project:prod-latest (ce7aa8f92c2d) 2023-10-05 12:40:49 UTC
+   --enable_feature=true
Container started at 2023-10-05 12:41:58 UTC (up 2m56s)
update? [y/N]

This will tell me some useful information; I’m going to be running the same image (I haven’t changed any code or built a new one), I’m not changing any of the flags to podman, just adding a new flag to my program, and finally the existing container has been running for just under three minutes. If I type y and press enter, pod will stop the existing container, and start a new one in its place.

pod update can update multiple containers, even across multiple machines by specifying remote on the container configuration.

You should now be able to setup a project using pod, build an image for development, iterate quickly without having to wait for image rebuilds, and create a production image that can be quickly pushed using pod update.