> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cycls.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Images

> Declare pip packages, system libraries, bundled files and build commands in Python. Cycls hashes the result into a cached Docker image.

`cycls.Image` is the container declaration. Chain methods to describe what the
container needs, then hand it to any decorator with `image=`.

```python theme={null}
image = (
    cycls.Image()
    .pip("pandas", "duckdb")
    .apt("ffmpeg", "libpq-dev")
    .copy("./utils.py")
    .copy("./models/", "app/models/")
    .run("curl -fsSL https://example.com/install.sh | sh")
)


@cycls.function(image=image)
def process(path):
    from utils import clean      # bundled by .copy()
    ...
```

## Methods

| Method                 | What it does                                               |
| ---------------------- | ---------------------------------------------------------- |
| `.pip(*packages)`      | install Python packages from PyPI                          |
| `.apt(*packages)`      | install system packages with apt-get                       |
| `.copy(src, dst=None)` | bundle local files or directories, `dst` defaults to `src` |
| `.run(command)`        | run a shell command during the build                       |
| `.rebuild()`           | skip the Docker cache for this image                       |

Every call returns a new `Image`, so a base image can be branched without
affecting the original.

```python theme={null}
base = cycls.Image().pip("httpx")
worker = base.pip("pandas")       # base is unchanged
```

## Bundling files

Copied paths land in `/app`, which is on `sys.path`, so bundled modules import
normally inside the function body.

```python theme={null}
(
    cycls.Image()
    .copy("./utils.py")                  # same path inside the container
    .copy("./models/", "app/models/")    # relocate
    .copy("/home/me/configs/app.json")   # absolute source
)
```

<Tip>
  **Splitting secrets by audience.** Keep `CYCLS_API_KEY` in `.env` on your machine
  and provider keys in `.providers.env`, then rename during the copy so the
  container sees only what it needs:

  ```python theme={null}
  image = cycls.Image().copy(".providers.env", ".env")
  ```

  The agent's dotenv loader finds `.env` at the expected name, and your deploy key
  never enters the image.
</Tip>

## Caching

Cycls hashes the declaration, including the contents of copied files, into a
deterministic Docker tag. Identical inputs reuse the cached build. Change one
package and only what depends on it rebuilds.

The first build of a new image takes a minute or two. After that it is cached
anywhere that config appears, on your machine and in the cloud.

```python theme={null}
image = cycls.Image().pip("numpy").rebuild()   # force a clean build
```

## What is already in the image

You do not need to declare the basics. Apps get FastAPI, hypercorn, PyJWT and
cryptography. Agents add the Anthropic and OpenAI SDKs, the MCP client, PDF
tooling, `ripgrep`, `jq`, `curl` and fonts. Declare what your own code needs on
top of that.

## Public static files

Files served at `/public` belong on [`cycls.Web`](/web/interface), not on the
image:

```python theme={null}
web = cycls.Web().copy_public("./assets/logo.png", "./downloads/")
```

They are then reachable at `https://your-app.cycls.ai/public/logo.png`.

## Next

<Card title="Volumes" icon="hard-drive" href="/build/volumes">
  Persistent storage that outlives any single deployment.
</Card>
