The Polycrate Container

The Polycrate container: concept and lifecycle, included tooling, mounts and environment variables, Dockerfile.poly for custom images, debugging with polycrate debug, container configuration, Docker native mode and troubleshooting.

The Polycrate Container

Container concept

Much of Polycrate's magic happens inside a Docker container that is started on the system running Polycrate every time an action is executed.

The container is based on a publicly available Docker image developed by ayedo (cargo.ayedo.cloud/library/polycrate) that bundles a large part of the best-practice toolchain for infrastructure as code and cloud-native development methods.

The container gives you access to a state-of-the-art DevOps runtime. Polycrate exports a snapshot of the workspace as a YAML file and makes this file available inside the container (path in POLYCRATE_WORKSPACE_SNAPSHOT_YAML), so the entire workspace configuration can easily be read by the tools inside the container.

Container lifecycle

sequenceDiagram
    participant User
    participant CLI as Polycrate CLI
    participant Docker
    participant Container
    participant Tools as Ansible/kubectl/Helm

    User->>CLI: polycrate run myblock install
    CLI->>CLI: Create snapshot
    CLI->>CLI: Generate transaction ID

    alt Custom Dockerfile.poly exists?
        CLI->>Docker: Build custom image
        Docker->>Docker: FROM polycrate:latest + custom layers
        Docker-->>CLI: Custom image ready
    end

    CLI->>Docker: docker run polycrate
    Docker->>Container: Start container
    CLI->>Container: Mount workspace → /workspace
    CLI->>Container: Mount SSH keys
    CLI->>Container: Inject environment variables
    CLI->>Container: Inject snapshot (YAML, path in POLYCRATE_WORKSPACE_SNAPSHOT_YAML)

    Container->>Container: Set working directory
    Container->>Tools: Run playbook/script
    Tools->>Tools: Read .snapshot.yml
    Tools->>Tools: Access /workspace

    alt Action successful
        Tools-->>Container: Exit 0
        Container-->>Docker: Success
        Docker-->>CLI: Container finished (Exit 0)
        CLI->>CLI: Write artifacts
        CLI-->>User: Success
    else Action failed
        Tools-->>Container: Exit 1
        Container-->>Docker: Failure
        Docker-->>CLI: Container finished (Exit 1)
        CLI-->>User: Failure with error output
    end

    CLI->>Docker: Cleanup container
    Docker->>Docker: Remove container

What is included in the container?

Base image: python:3.12-slim (Debian). Versions from the CLI build (Dockerfile + hack/download-tools.sh + python-requirements.txt, as of CLI 0.51.3).

Runtime & configuration management

Tool Version
Python 3.12
Ansible (PyPI) 13.2.0
kubernetes (Python) 32.0.1

Kubernetes & containers

Tool Version
kubectl 1.32.1
helm 3.18.6 (+ plugin helm-diff)
kustomize 5.8.0
cilium CLI 0.18.5
argocd 3.0.6
velero 1.16.1
docker / buildx from docker:dind / docker/buildx-bin (unpinned)
skopeo Debian package

Secrets, backup, encryption

Tool Version
bao (OpenBao) 2.5.1
age 1.2.1
restic 0.18.1
kopia 0.22.2

Utilities & clients

Tool Version / note
yq 4.45.1
jq, git, curl, wget, rsync, openssh-client, sshpass, envsubst, htpasswd Debian packages
mariadb-client, postgresql-client, sqlite3 Debian packages
dnsutils, procps, iputils-ping, bzip2 Debian packages

Ansible collections & roles

Package Note
Ansible collections (incl. community.general, community.docker) Part of ansible==13.2.0
geerlingguy.docker, geerlingguy.ntp Galaxy roles

Additional Python SDKs include hcloud, pynetbox, pyvmomi, cryptography, passlib.

Container environment

Mounts

When the container starts, the following paths are mounted:

graph LR
    Host[Host system] --> WS[Workspace root]
    Host --> SSH[SSH keys]
    Host --> Docker[Docker socket optional]

    WS -->|mount| CWS[Container: /workspace]
    SSH -->|mount| CSSH[Container: ~/.ssh/]
    Docker -->|mount| CDocker[Container: /var/run/docker.sock]

Host → container mappings:

  • $WORKSPACE_ROOT → /workspace (read/write)
  • $WORKSPACE_ROOT/id_rsa → ~/.ssh/id_rsa (read-only)
  • /var/run/docker.sock → /var/run/docker.sock (optional, for Docker-in-Docker)

Environment variables

Polycrate sets considerably more variables in the container than just the POLYCRATE_* names: these include Ansible defaults (ANSIBLE_*), Kubernetes (KUBECONFIG, K8S_AUTH_KUBECONFIG), possibly SSH (SSH_AUTH_SOCK, ANSIBLE_SSH_PRIVATE_KEY_FILE) and further runtime values. The complete list is available at runtime in the workspace snapshot under env: (same content as the variables passed to the container).

Excerpt: POLYCRATE_* and central runtime variables (current CLI):

Variable Meaning
POLYCRATE_WORKSPACE Path to the workspace (in the container usually the value of config.containerroot, typically /workspace)
POLYCRATE_BLOCK Name of the current block
POLYCRATE_BLOCK_WORKDIR Container path to the block workdir (if configured)
POLYCRATE_ACTION Name of the current action
POLYCRATE_WORKFLOW / POLYCRATE_STEP During workflow execution
POLYCRATE_VERSION / POLYCRATE_CLI_VERSION CLI version
POLYCRATE_IMAGE_REFERENCE / POLYCRATE_IMAGE_VERSION Configured base image (config.image)
POLYCRATE_FORCE Corresponds to --force
POLYCRATE_HOST_UID / POLYCRATE_HOST_GID UID/GID of the user on the host
POLYCRATE_WORKSPACE_SNAPSHOT_YAML Path to the temporary snapshot file (mounted into the container)
POLYCRATE_RUNTIME_SCRIPT_PATH Path to the executing wrapper script of the action
IN_CONTAINER 1 in the container, 0 with --local
IN_CI Set by the CLI (true)

The workspace name is not available as POLYCRATE_WORKSPACE_NAME, but in the snapshot under workspace.name. The CLI does not set a fixed file POLYCRATE_SNAPSHOT_FILE=/workspace/.snapshot.yml; the path is in POLYCRATE_WORKSPACE_SNAPSHOT_YAML (temporary path, identical in the container via mount).

You can set additional variables with extraenv or -e KEY=value (see Container configuration).

Host user context (since 0.29.16)

The variables POLYCRATE_HOST_UID and POLYCRATE_HOST_GID contain the UID and GID of the user running polycrate run on the host system.

Variable Description
POLYCRATE_HOST_UID User ID of the executing host user (e.g. "1000")
POLYCRATE_HOST_GID Group ID of the executing host user (e.g. "1000")

Use case: permission fixing

Since actions run as root in the container, newly created files belong to the root user. With these variables, blocks can correct ownership at the end of an action:

# At the end of a playbook
- name: Fix ownership for host user
  ansible.builtin.file:
    path: "{{ workspace.path }}/artifacts"
    owner: "{{ lookup('env', 'POLYCRATE_HOST_UID') }}"
    group: "{{ lookup('env', 'POLYCRATE_HOST_GID') }}"
    recurse: yes
  when: lookup('env', 'POLYCRATE_HOST_UID') | default('') != ''

This ensures that the host user has write access to the generated files after the action has run.

Working directory

The working directory is automatically set to the block path:

# For a local block
WORKDIR=/workspace/blocks/my-block

# For an inherited block (from: parent-block)
WORKDIR=/workspace/blocks/parent-block

See Inheritance for details on the working directory of inherited blocks.

Dockerfile.poly

By default Polycrate looks for a Dockerfile in the workspace. The file name is configurable in workspace.poly under config.dockerfile (default in new workspaces: Dockerfile.poly). Alternatively, set the path via the CLI: --dockerfile $FILENAME (bound to workspace.config.dockerfile).

If the configured file exists in the workspace, Polycrate builds an image from it and starts the container with that tag instead of the unmodified reference image.

The base image (pull/tag) comes from config.image in the workspace:

config:
  image:
    reference: cargo.ayedo.cloud/library/polycrate
    version: "0.31.0"   # Example: CLI/image version
  dockerfile: Dockerfile.poly

Via the CLI: --image-ref and --image-version (see CLI reference).

With this custom image, workspace customizations such as installing packages or libraries in the Polycrate container can be persisted.

By convention, Dockerfile.poly should be based on the official Polycrate image so as not to break existing functionality:

FROM cargo.ayedo.cloud/library/polycrate:latest

RUN pip install hcloud==1.16.0

Custom Dockerfile.poly examples

Example 1: installing Python packages

FROM cargo.ayedo.cloud/library/polycrate:latest

# Python packages for custom Ansible modules
RUN pip install \
    hcloud==1.16.0 \
    proxmoxer==2.0.1 \
    requests==2.31.0

Example 2: adding Node.js tools

FROM cargo.ayedo.cloud/library/polycrate:latest

# Install Node.js and NPM (Ubuntu-based!)
RUN apt-get update && apt-get install -y --no-install-recommends \
    nodejs npm \
    && rm -rf /var/lib/apt/lists/*

# Global NPM packages
RUN npm install -g \
    cdktf-cli \
    serverless

Example 3: custom CLIs and tools

FROM cargo.ayedo.cloud/library/polycrate:latest

# Tailscale CLI
RUN wget https://pkgs.tailscale.com/stable/tailscale_1.56.1_amd64.tgz && \
    tar xzf tailscale_1.56.1_amd64.tgz && \
    mv tailscale_1.56.1_amd64/tailscale /usr/local/bin/ && \
    rm -rf tailscale_*

# Velero (Kubernetes backup tool)
RUN wget https://github.com/vmware-tanzu/velero/releases/download/v1.12.0/velero-v1.12.0-linux-amd64.tar.gz && \
    tar xzf velero-v1.12.0-linux-amd64.tar.gz && \
    mv velero-v1.12.0-linux-amd64/velero /usr/local/bin/ && \
    rm -rf velero-*

Example 4: adding Ansible collections

FROM cargo.ayedo.cloud/library/polycrate:latest

# Custom Ansible collections
RUN ansible-galaxy collection install \
    community.hashi_vault:4.0.0 \
    community.vmware:3.9.0 \
    netbox.netbox:3.13.0

Example 5: multi-cloud setup

FROM cargo.ayedo.cloud/library/polycrate:latest

# Oracle Cloud CLI
RUN pip install oci-cli

# DigitalOcean CLI
RUN wget -O /usr/local/bin/doctl https://github.com/digitalocean/doctl/releases/download/v1.100.0/doctl-1.100.0-linux-amd64 && \
    chmod +x /usr/local/bin/doctl

# Linode CLI
RUN pip install linode-cli

Image building

If a Dockerfile.poly exists in the workspace:

# Polycrate builds the image automatically before every action
polycrate run myblock install

# Internally this corresponds to:
docker build -f Dockerfile.poly -t polycrate-workspace:latest .
docker run polycrate-workspace:latest ...

The image is only rebuilt if Dockerfile.poly has changed.

Container debugging

polycrate debug command

The easiest way to start an interactive debug shell in the Polycrate container:

# Start a debug container in the current workspace
polycrate debug

# With explicit workspace path
polycrate debug -w /path/to/workspace

# With additional mounts
polycrate debug -m /local/path:/container/path

# With additional environment variables
polycrate debug -e MY_VAR=value

How it works:

  1. The current workspace is loaded (including all blocks)
  2. The workspace image is pulled (or built if a Dockerfile.poly exists)
  3. The container starts with all mounts, environment variables and network configuration as in an action run
  4. An interactive Bash shell opens

Example session:

$ polycrate debug
✓ Pulling image cargo.ayedo.cloud/library/polycrate:0.51.3

Starting debug container: polycrate-debug-a1b2c3d4
Workspace: /home/user/my-workspace
Image: cargo.ayedo.cloud/library/polycrate:0.51.3
Type 'exit' to leave the container

root@polycrate:/workspace# ls -la
drwxr-xr-x 5 root root 4096 Dec 15 10:00 blocks
-rw-r--r-- 1 root root  512 Dec 15 10:00 workspace.poly

root@polycrate:/workspace# env | grep POLYCRATE
POLYCRATE_WORKSPACE=/workspace
POLYCRATE_VERSION=0.51.3

root@polycrate:/workspace# exit
$

Use cases:

  • Playbook debugging: run and test Ansible playbooks manually
  • Environment inspection: check which variables are set in the container
  • Path validation: verify that mounts arrive correctly in the container
  • Tool availability: test whether required tools are present in the image
  • Network debugging: check whether network connections work from the container

→ See CLI reference for all options.

Showing container logs

By default Polycrate shows the complete container output:

polycrate run myblock install
# Shows stdout/stderr from the container

Manual interactive shell

Alternatively, you can start a shell in the container manually:

# With the default image
docker run -it --rm \
  -v $(pwd):/workspace \
  cargo.ayedo.cloud/library/polycrate:latest \
  /bin/bash

# With a custom Dockerfile.poly
docker build -f Dockerfile.poly -t polycrate-workspace:latest .
docker run -it --rm \
  -v $(pwd):/workspace \
  polycrate-workspace:latest \
  /bin/bash

Inspecting the snapshot

In the container, POLYCRATE_WORKSPACE_SNAPSHOT_YAML points to the mounted snapshot file (temporary path, often under /tmp/). You find the same content in the snapshot under env:.

# Inside the container
SNAP="${POLYCRATE_WORKSPACE_SNAPSHOT_YAML:?}"
cat "$SNAP"
yq eval '.block.config' "$SNAP"

Container configuration

The CLI does not support free additional docker run arguments (no config.container.docker_flags). You control image, Dockerfile, additional environment variables and mounts as follows.

Image, Dockerfile and paths (config)

At workspace level (in workspace.poly) the following fields, among others, are relevant:

name: my-workspace

config:
  image:
    reference: cargo.ayedo.cloud/library/polycrate
    version: "0.31.0"
  dockerfile: Dockerfile.poly
  # Further paths: blocksroot, artifactsroot, containerroot, …

See Configuration for the complete config overview.

Additional environment variables and mounts

At workspace level (not under config):

name: my-workspace

extraenv:
  - "MY_CUSTOM_VAR=value"
  - "API_KEY=secret-key"

extramounts:
  - "/var/run/docker.sock:/var/run/docker.sock"

Corresponds to the global flags -e / --env and -m / --mount (see CLI reference).

Docker socket for the Docker CLI in the container

Typical pattern: mount the Docker socket from the host:

extramounts:
  - "/var/run/docker.sock:/var/run/docker.sock"

Example usage:

# Block with Docker CLI in the container (socket mounted)
actions:
  - name: build-and-push
    playbook: build-and-push.yml

Docker native mode

Since version 0.28.0 Polycrate offers an optional native Docker mode that uses the Docker Go SDK directly instead of calling the Docker CLI.

Activation

# Via flag
polycrate --docker-native run myblock install
polycrate -N run myblock install

# Via environment variable
export POLYCRATE_DOCKER_NATIVE=true
polycrate run myblock install

# Via configuration file (~/.polycrate/config.yml)
# docker:
#   native: true

Advantages

Aspect Docker CLI Docker native
Docker CLI installation Required Not required
Performance ~50-100ms overhead per call Direct API calls
Error handling Text parsing Typed errors
Output streaming Cumbersome Native streams

Spinner feedback

In native mode Polycrate shows animated spinners during Docker operations:

⠹ Pulling image cargo.ayedo.cloud/library/polycrate:0.51.3...
✓ Pulling image cargo.ayedo.cloud/library/polycrate:0.51.3

⠼ Building container image my-workspace:0.51.3...
✓ Building container image my-workspace:0.51.3

⠧ Cleaning up containers...
✓ Cleaning up containers

At a higher log level (--loglevel 3) spinners are disabled and the full output is streamed.

Podman compatibility

The native Docker mode is also compatible with Podman. Podman can provide a Docker-compatible API socket:

# 1. Start the Podman API service (rootless)
podman system service --time=0 &

# 2. Point DOCKER_HOST at the Podman socket
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock

# 3. Use Polycrate with the native backend
polycrate -N run myblock myaction
polycrate -N debug

Best practices

1. Pin image versions

# ✅ Good: specific version
FROM cargo.ayedo.cloud/library/polycrate:1.2.3

# ❌ Bad: latest (can break)
FROM cargo.ayedo.cloud/library/polycrate:latest

2. Minimize layers

# ✅ Good: combined RUN statements (Ubuntu-based!)
FROM cargo.ayedo.cloud/library/polycrate:latest
RUN apt-get update && apt-get install -y --no-install-recommends curl wget && \
    pip install --no-cache-dir boto3 requests && \
    rm -rf /var/lib/apt/lists/*

# ❌ Bad: many separate RUN statements
FROM cargo.ayedo.cloud/library/polycrate:latest
RUN apt-get update && apt-get install -y curl
RUN apt-get install -y wget
RUN pip install boto3
RUN pip install requests

3. Clean up in the same layer

FROM cargo.ayedo.cloud/library/polycrate:latest

RUN wget https://example.com/tool.tar.gz && \
    tar xzf tool.tar.gz && \
    mv tool /usr/local/bin/ && \
    rm -rf tool*  # Cleanup in the same layer

4. Version your Dockerfile.poly

# In the workspace
git add Dockerfile.poly
git commit -m "Add custom Python packages to container"
git push

5. Test custom images locally

# Build the image
docker build -f Dockerfile.poly -t test-polycrate .

# Test interactively
docker run -it --rm test-polycrate /bin/bash

# Test tools
docker run --rm test-polycrate ansible --version
docker run --rm test-polycrate kubectl version --client

Troubleshooting

Image build fails

# Check the Dockerfile
cat Dockerfile.poly

# Build manually for better error output
docker build -f Dockerfile.poly -t polycrate-workspace:latest .

Package installation fails

# Update the APT cache (Ubuntu-based!)
FROM cargo.ayedo.cloud/library/polycrate:latest
RUN apt-get update && apt-get install -y --no-install-recommends my-package \
    && rm -rf /var/lib/apt/lists/*

Container does not start

# Check container logs
docker ps -a
docker logs <container-id>

# Inspect the image
docker images | grep polycrate
docker inspect cargo.ayedo.cloud/library/polycrate:latest

Permission problems with mounts

If you have permission problems with mounted directories, there are two approaches:

Option 1: fix ownership in the playbook (recommended)

Use the environment variables POLYCRATE_HOST_UID and POLYCRATE_HOST_GID to correct ownership at the end of the action:

- name: Fix ownership for host user
  ansible.builtin.file:
    path: "{{ workspace.path }}/dist"
    owner: "{{ lookup('env', 'POLYCRATE_HOST_UID') }}"
    group: "{{ lookup('env', 'POLYCRATE_HOST_GID') }}"
    recurse: yes
  when: lookup('env', 'POLYCRATE_HOST_UID') | default('') != ''

Option 2: dedicated user in the container

The Polycrate CLI currently sets no --user override for the action container. If you need to run a process as non-root, start it specifically in the playbook (e.g. become: false / dedicated command) or use a customized image; there is no docker_flags list in workspace.poly.

Relation to other concepts

  • Snapshot: mounted into the container as a YAML file (POLYCRATE_WORKSPACE_SNAPSHOT_YAML)
  • Actions: are executed in the container
  • Workspaces: are mounted into the container
  • Integrations: all tools run in the container