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

# One project, many needs: How Composable Image embraces real-world diversity

export const PostMeta = ({data = {}}) => {
  const {author, date} = data;
  const authors = Array.isArray(author) ? author : author ? [author] : [];
  const resolveAuthor = slug => {
    const entry = AUTHOR_MAP[slug] || ({});
    const name = entry.name || slug;
    const github = entry.github || null;
    const linkedin = entry.linkedin || null;
    const url = github ? `https://github.com/${github}` : linkedin || null;
    const avatarUrl = github ? `https://github.com/${github}.png?size=64` : null;
    return {
      name,
      url,
      avatarUrl
    };
  };
  const formattedDate = date ? new Date(date).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }) : null;
  if (authors.length === 0 && !formattedDate) return null;
  const AUTHOR_MAP = {
    "aaron-collier": {
      "name": "Aaron Collier"
    },
    "aaron-dudenhofer": {
      "name": "Aaron Dudenhofer"
    },
    "aaron-porter": {
      "name": "Aaron Porter"
    },
    "adriaan-odendaal": {
      "name": "Adriaan Odendaal"
    },
    "ajmal": {
      "name": "Ajmal Siddiqui"
    },
    "akalipetis": {
      "name": "Antonis Kalipetis"
    },
    "alexander-varwijk": {
      "name": "Alexander Varwijk"
    },
    "alicia-bevilacqua": {
      "name": "Alicia Bevilacqua"
    },
    "amelie-deguerry": {
      "name": "Amelie Deguerry"
    },
    "anacidre": {
      "name": "Ana Cidre",
      "linkedin": "https://www.linkedin.com/in/ana-cidre"
    },
    "andoni": {
      "name": "Andoni Auzmendi"
    },
    "andrei-taranu": {
      "name": "Andrei (Alex) Taranu",
      "linkedin": "https://www.linkedin.com/in/andrei-alex-taranu/"
    },
    "andrew-baxter": {
      "name": "Andrew Baxter"
    },
    "andrew-melck": {
      "name": "Andrew Melck"
    },
    "antoine-crochet-damais": {
      "name": "Antoine Crochet Damais"
    },
    "augustin-delaporte": {
      "name": "Augustin Delaporte",
      "linkedin": "https://www.linkedin.com/in/augustindelaporte/"
    },
    "branislav-bujisic": {
      "name": "Branislav Bujisic"
    },
    "carl-smith": {
      "name": "Carl Smith"
    },
    "caroline-leroy": {
      "name": "Caroline Leroy"
    },
    "cati-mayer": {
      "name": "Cati Mayer"
    },
    "catplat": {
      "name": "C Trinkwon"
    },
    "ceelolulu": {
      "name": "Celeste van der Watt"
    },
    "chadwcarlson": {
      "name": "Chad Carlson",
      "github": "chadwcarlson",
      "linkedin": "https://www.linkedin.com/in/chadwcarlson"
    },
    "chris-ward": {
      "name": "Chris Ward"
    },
    "chris-yates": {
      "name": "Chris Yates"
    },
    "christian-sieber": {
      "name": "Christian Sieber"
    },
    "christopher-lockheardt": {
      "name": "Christopher Lockheardt"
    },
    "christopher-skene": {
      "name": "Christopher Skene"
    },
    "chuck-morgan": {
      "name": "Chuck Morgan"
    },
    "corey-dockendorf": {
      "name": "Corey Dockendorf"
    },
    "crell": {
      "name": "Crell"
    },
    "damz": {
      "name": "Damz"
    },
    "dan-morrison": {
      "name": "Dan Morrison"
    },
    "davidbonachera": {
      "name": "David Bonachera",
      "github": "davidbonachera",
      "linkedin": "https://www.linkedin.com/in/davidbonachera"
    },
    "dereliahmet1": {
      "name": "Ahmet Faruk Dereli"
    },
    "devicezero": {
      "name": "Jonas Kröger",
      "github": "devicezero",
      "linkedin": "https://www.linkedin.com/in/jonaskroeger/"
    },
    "doug-goldberg": {
      "name": "Doug Goldberg"
    },
    "duncan-naves": {
      "name": "Duncan Naves",
      "github": "duncannaves",
      "linkedin": "https://www.linkedin.com/in/duncan-naves-a94423aa"
    },
    "erika-bustamante": {
      "name": "Erika Bustamante"
    },
    "fabpot": {
      "name": "Fabien Potencier"
    },
    "flovntp": {
      "name": "Florent Huck",
      "github": "flovntp",
      "linkedin": "https://www.linkedin.com/in/florenthuck"
    },
    "fred-plais": {
      "name": "Fred Plais"
    },
    "gauthier-garnier": {
      "name": "Gauthier Garnier"
    },
    "gilzow": {
      "name": "Paul Gilzow"
    },
    "gmoigneu": {
      "name": "Guillaume Moigneu",
      "github": "gmoigneu",
      "linkedin": "https://www.linkedin.com/in/guillaumemoigneu/"
    },
    "gregqualls": {
      "name": "Greg Qualls"
    },
    "guguss": {
      "name": "Augustin Delaporte"
    },
    "haylee-millar": {
      "name": "Haylee Millar"
    },
    "ivana-kotur": {
      "name": "Ivana Kotur"
    },
    "jackrabbithanna": {
      "name": "Mark Hanna"
    },
    "jared-wright": {
      "name": "Jared Wright",
      "github": "jww-sh",
      "linkedin": "https://www.linkedin.com/in/jaredwaynewright"
    },
    "jessica-orozco": {
      "name": "Jessica Orozco"
    },
    "joey-stanford": {
      "name": "Joey Stanford"
    },
    "john-grubb": {
      "name": "John Grubb"
    },
    "jonas-kruger": {
      "name": "Jonas Kruger"
    },
    "kathryn-frazer": {
      "name": "Kathryn Frazer"
    },
    "kemiojo": {
      "name": "Kemi Elizabeth Ojogbede"
    },
    "kieronsambrook-smith": {
      "name": "Kieronsambrook Smith"
    },
    "laurent-arnoud": {
      "name": "Laurent Arnoud",
      "linkedin": "https://www.linkedin.com/in/laurent-arnoud-861b44121/"
    },
    "letoya-boyne": {
      "name": "Letoya Boyne"
    },
    "lolautruche": {
      "name": "Jérôme Vieilledent"
    },
    "lyly-lepinay": {
      "name": "Lyly Lepinay"
    },
    "manauwar-alam": {
      "name": "Manauwar Alam"
    },
    "marc-antoine-porri": {
      "name": "Marc Antoine Porri"
    },
    "maria-antinkaapo": {
      "name": "Maria Antinkaapo"
    },
    "maria-de-anton": {
      "name": "Maria De Anton"
    },
    "mark-dorison": {
      "name": "Mark Dorison"
    },
    "markus-hausammann": {
      "name": "Markus Hausammann"
    },
    "mary-thomas": {
      "name": "Mary Thomas"
    },
    "mathias-bolt-lesniak": {
      "name": "Mathias Bolt Lesniak"
    },
    "mathieu-strauch": {
      "name": "Mathieu Strauch"
    },
    "matthias-van-woensel": {
      "name": "Matthias Van Woensel",
      "linkedin": "https://www.linkedin.com/in/matthias-van-woensel-267a069"
    },
    "maz-mohammadi": {
      "name": "Maz Mohammadi"
    },
    "michael-sharp": {
      "name": "Michael Sharp"
    },
    "mupsi": {
      "name": "Marine Gandy"
    },
    "natalie-harper": {
      "name": "Natalie Harper"
    },
    "ngommenginger": {
      "name": "Nicolas Gommenginger",
      "linkedin": "https://www.linkedin.com/in/nicolas-gommenginger"
    },
    "nicholas-bennison": {
      "name": "Nicholas Bennison"
    },
    "nicholas-vahalik": {
      "name": "Nicholas Vahalik"
    },
    "nick-hardiman": {
      "name": "Nick Hardiman"
    },
    "nickanderegg": {
      "name": "Nickanderegg"
    },
    "nicolas-grekas": {
      "name": "Nicolas Grekas",
      "github": "nicolas-grekas",
      "linkedin": "https://www.linkedin.com/in/nicolasgrekas/"
    },
    "niti-malwade": {
      "name": "Niti Malwade"
    },
    "opensocialteam": {
      "name": "Opensocialteam"
    },
    "ori-pekelman": {
      "name": "Ori Pekelman"
    },
    "otavio-santana": {
      "name": "Otavio Santana"
    },
    "palwandi": {
      "name": "Pawan Alwandi",
      "github": "pawpy",
      "linkedin": "https://www.linkedin.com/in/pawanalwandi"
    },
    "patrick-boest": {
      "name": "Patrick Boest"
    },
    "patrick-dawkins": {
      "name": "Patrick Dawkins",
      "github": "pjcdawkins",
      "linkedin": "https://www.linkedin.com/in/patrickdawkins"
    },
    "patrick-klima": {
      "name": "Patrick Klima"
    },
    "pjcdawkins": {
      "name": "Pjcdawkins"
    },
    "prineet-kaurbhurji": {
      "name": "Prineet Kaurbhurji"
    },
    "quentin-sinig": {
      "name": "Quentin Sinig"
    },
    "ralt": {
      "name": "Florian Margaine",
      "github": "ralt",
      "linkedin": "https://www.linkedin.com/in/florian-margaine-43971136"
    },
    "ramanathanramakrishnamurthy": {
      "name": "Ramanathanramakrishnamurthy"
    },
    "remi-lejeune": {
      "name": "Rémi Lejeune"
    },
    "ribel": {
      "name": "Taras Kruts"
    },
    "robert-douglass": {
      "name": "Robert Douglass"
    },
    "rudy-weber": {
      "name": "Rudy Weber"
    },
    "ryan-hicks": {
      "name": "Ryan Hicks"
    },
    "sabri-helal": {
      "name": "Sabri Helal"
    },
    "savannah-bergeron": {
      "name": "Savannah Bergeron"
    },
    "shannon-vettes": {
      "name": "Shannon Vettes"
    },
    "shawn-ogasawara": {
      "name": "Shawn Ogasawara",
      "linkedin": "https://www.linkedin.com/in/shawn-ogasawara-83a9a0/"
    },
    "shawna-spoor": {
      "name": "Shawna Spoor"
    },
    "shedrack-akintayo": {
      "name": "Shedrack Akintayo"
    },
    "simon-ruggier": {
      "name": "Simon Ruggier"
    },
    "sophie-van-der-kindere": {
      "name": "Sophie Van Der Kindere"
    },
    "stefanos-thampis": {
      "name": "Stefanos Thampis"
    },
    "stephen-weinberg": {
      "name": "Stephen Weinberg"
    },
    "sukhman-virk": {
      "name": "Sukhman Virk"
    },
    "sumaira-nazir": {
      "name": "Sumaira Nazir"
    },
    "sumer": {
      "name": "Sümer Cip"
    },
    "syed-raza": {
      "name": "Syed Raza"
    },
    "tamara-bacchia": {
      "name": "Tamara Bacchia"
    },
    "tara-arnold": {
      "name": "Tara Arnold"
    },
    "theosakamg": {
      "name": "Mickael Gaillard",
      "github": "theosakamg"
    },
    "thomasdiluccio": {
      "name": "Thomas di Luccio"
    },
    "tim-anderson": {
      "name": "Tim Anderson"
    },
    "tom-helmer-hansen": {
      "name": "Tom Helmer Hansen"
    },
    "tylermills": {
      "name": "Tyler Mills"
    },
    "upsun": {
      "name": "Upsun"
    },
    "veronika-tolkachova": {
      "name": "Veronika Tolkachova",
      "linkedin": "https://www.linkedin.com/in/veronika-tolkachova-169167a2"
    },
    "vince-parker": {
      "name": "Vince Parker"
    },
    "vinnie-russo": {
      "name": "Vincenzo Russo"
    },
    "vrobert78": {
      "name": "Vincent Robert",
      "github": "vrobert78",
      "linkedin": "https://www.linkedin.com/in/vincent-robert-498a883"
    },
    "yuriy-babenko": {
      "name": "Yuriy Babenko"
    },
    "yuriy-gerasimov": {
      "name": "Yuriy Gerasimov"
    }
  };
  return <div className="post-meta">
      {(authors.length > 0 || formattedDate) && <div className="post-meta-info">
          {authors.length > 0 && <div className="post-meta-authors">
              {authors.map(slug => {
    const {name, url, avatarUrl} = resolveAuthor(slug);
    const inner = <>
                    {avatarUrl && <img src={avatarUrl} alt={name} className="post-meta-avatar" />}
                    <span className="post-meta-author-name">{name}</span>
                  </>;
    return url ? <a key={slug} href={url} target="_blank" rel="noopener noreferrer" className="post-meta-author">
                    {inner}
                  </a> : <span key={slug} className="post-meta-author">{inner}</span>;
  })}
            </div>}
          {authors.length > 0 && formattedDate && <span className="post-meta-separator" aria-hidden="true">·</span>}
          {formattedDate && <span className="post-meta-date">{formattedDate}</span>}
        </div>}
    </div>;
};

<PostMeta data={{ author: ["thomasdiluccio"], date: "2025-12-11T10:00:00+00:00", image: "/images/posts/releases/composable-image-embraces-real-world-diversity/composable-image.webp" }} />

Applications rarely fit inside a single tidy runtime box. They evolve through experiments, architectural shifts, quick MVP spikes that outlast their intended lifespan, and the continuous layering of new business needs.

The result? Most apps are **hybrids**, part PHP or Node, part Python, part “we need this binary to generate PDFs,” part “our frontend is static but built with three tools,” and part “this tiny script powers an entire workflow.”

This is the reality we developers live in today. And this is exactly the reality that Composable Image was designed for.

Composable Image, now fully GA, offers a simple promise:

**Build exactly the environment you need: no more, no less, without the complexity of custom Dockerfiles or the limitations of single-runtime images.**

Let’s explore the three main patterns where Composable Image shines and why it’s becoming the preferred choice for modern Upsun applications.

## 1. Multiple runtimes in the same application container

This is the most common and most transformative use case.

Apps today routinely combine a primary runtime with secondary ones. A PHP application with a Node build toolchain. A Node backend enriched with a Python script for ML preprocessing. A Python API that relies on Ruby or Go utilities for legacy workflows.

Composable Image makes these multi-runtime setups **declarative, predictable, and frictionless**:

<Warning>
  **Important: Keep your Nix channel up to date**

  Nix releases new channels twice a year, and only the current channel receives active support and security updates. When a new channel is released, the previous one is immediately deprecated.

  Always verify you're using a [currently supported Nix channel](https://docs.upsun.com/create-apps/app-reference/composable-image.html#supported-nix-channels) before deploying or updating your configuration.
</Warning>

```yaml {filename=".upsun/config.yaml"} theme={null}
type: "composable:25.11"
stack:
  runtimes:
    - "php@8.4"
    - "nodejs@24"
    - "python@3.13"
  packages:
    - yarn
    - python313Packages.yq
    - python313Packages.jupyterlab
    - package: wkhtmltopdf
      channel: unstable

```

**No Dockerfile**.<br />\
**No multi-stage build arcana**.<br />\
**No risk of environment drift** between development, CI, and production.

The first declared runtime is the primary one; all are available inside your container. Each runtime is built in isolation, ensuring there are **no hidden system dependencies** and **no side effects**.

This solves one of the biggest pains of modern app development: your environment finally reflects the *actual* complexity of your app, without compromising clarity.

## 2. Zero runtimes for pure static applications

Not every project needs PHP, Node, Python, or any interpreter. Some applications are simply **static sites**, a collection of HTML, CSS, JS, and assets that don’t require a runtime to execute.

Composable Image supports these projects cleanly: you can deploy a fully static site with **no runtime declared at all**.

```yaml {filename=".upsun/config.yaml"} theme={null}
type: "composable:25.11"
stack: []
```

For many teams, that’s all they need: a fast, predictable deployment of static assets.

But even static applications sometimes rely on **non-runtime build tools** during the build phase. Not full programming languages, just command-line utilities powered by Nix packages. For example:

* `html-tidy` for HTML cleanup
* `jpegoptim` or `pngquant` for image compression
* `curl` or `jq` for fetching and massaging external data

```yaml {filename=".upsun/config.yaml"} theme={null}
type: "composable:25.11"
stack:
  runtimes: []
  packages:
    - html-tidy
    - pngquant
    - jpegoptim
    - curl
    - jq
```

No interpreters.\
No language runtime.\
Just the utilities your static project requires, nothing more.

Behind the scenes, Nix provides each tool in isolation, with no hidden dependencies and no risk of drift across environments. This gives static projects the same benefits as complex apps: **reproducibility, clarity, and control**, with an absolute minimum footprint.

## 3. Local env setup

Below is a beginner-friendly, **step-by-step tutorial** (no flakes) to install Nix locally and reproduce your Upsun toolchain **(curl, jq, tidy, pngquant, jpegoptim, html-tidy)**.

### 3.1 Install Nix

The most reliable installer is Determinate Systems is to run:

```bash {filename="Terminal"} theme={null}
curl -L https://install.determinate.systems/nix | sh -s -- install |
```

When it finishes, **close your terminal and open a new one**, you can verify the installation:

```bash {filename="Terminal"} theme={null}
nix --version 
```

### 3.2 Create a project folder (or go to your repo)

Go to the repository where you want the tools available:

```bash {filename="Terminal"} theme={null}
cd /path/to/your/repo 
```

### 3.3 Create a `shell.nix` file (no flakes)

Create a file named `shell.nix` at the root of your repo:

```bash {filename="Terminal"} theme={null}

cat > shell.nix <<'EOF'
let
  # Pin nixpkgs for reproducible installs
  nixpkgs=builtins.fetchTarball{ url="https://releases.nixos.org/nixos/25.11/nixexprs.tar.xz"; };
  pkgs = import nixpkgs {};
in
pkgs.mkShell {
  packages = with pkgs; [
    curl jq html-tidy pngquant jpegoptim
  ];
}
EOF
```

<Note>
  **Note**: you can add in this packages list any useful package you want to install from [https://search.nixos.org/packages](https://search.nixos.org/packages)
</Note>

### 3.4 Enter the Nix environment

From the repo directory, run:

```bash {filename="Terminal"} theme={null}
nix-shell
```

This drops you into a shell where the tools are available (without installing them globally on your system).

### 3.5 Verify the tools

Still inside `nix-shell`, run:

```bash {filename="Terminal"} theme={null}
tidy -v
pngquant --version
jpegoptim --version
curl --version | head -n 1
jq --version
```

If all commands respond, you’re good.

### 3.6 Use the tools (example commands)

Inside `nix-shell`, you can use:

HTML tidy:

```bash {filename="Terminal"} theme={null}
tidy -q -e index.html
```

Optimize PNG:

```bash {filename="Terminal"} theme={null}
pngquant --force --output image.png --quality=65-80 image.png
```

Optimize JPEG:

```bash {filename="Terminal"} theme={null}
jpegoptim --strip-all --max=85 photo.jpg
```

### 3.7 Exit the environment

When you’re done:

```bash {filename="Terminal"} theme={null}
exit
```

## 4. Embedding specific or niche packages and binaries

Every app eventually hits the need for “that one CLI tool”:

* `wkhtmltopdf` for invoices
* `ffmpeg` for videos
* `ghostscript` for documents
* `imagemagick` for media processing
* a low-level CLI for LDAP, or cryptographic utilities
* even niche or experimental packages from the Nix ecosystem

Traditionally, this meant custom Dockerfiles, artisanal system package installs, or awkward workarounds.

With Composable Image, adding such dependencies is trivial:

```yaml {filename=".upsun/config.yaml"} theme={null}
type: "composable:25.11"
stack:
  packages:
    - wkhtmltopdf
    - ffmpeg
    - ghostscript
```

Need the very last version available on Nix? Also trivial:

```yaml {filename=".upsun/config.yaml"} theme={null}
type: "composable:25.11"
stack:
  packages:
    - package: jupyterlab
      channel: unstable
```

Because Upsun’s Composable Image is built on Nix, **each package is isolated and reproducible**. There are no hidden dependencies, no unexpected version conflicts, and no surprises at runtime.

You get the tools you need, exactly as you declared them, clean, explicit, and portable.

## Why This Matters: Clarity, Control, Confidence

Composable Image is not just a feature. It’s a shift in how developers think about application environments:

**Clarity:** Your entire environment is visible in a single `config.yaml`. No more guessing what’s inside your container.

**Control**: You decide which runtimes, versions, binaries, and extensions your app needs. Upsun simply builds it for you.

**Confidence**: Thanks to Upsun’s reproducible build model, what you declare runs exactly as you expect, in dev and in production.

## **Limitations and Responsibilities**

Composable Image offers a high degree of flexibility and control. But, as with any powerful tool, it also comes with responsibilities that teams should be aware of.

### Nix channels evolve quickly.

Composable Image relies on a single Nix channel per version. When a new channel is released, the previous one enters deprecation and is eventually decommissioned. This means teams using Composable Image must **monitor new Nix channel releases** and update their configuration accordingly.

Upsun ensures new channels are supported shortly after their official release, but the update action remains on the project owner’s side.

### Deprecated channels stop receiving updates.

Once a Nix channel is deprecated, its packages may no longer receive feature updates or security patches from upstream maintainers. Staying on an outdated channel exposes applications to potential security risks or missing improvements.

Regularly upgrading to the latest channel ensures your application benefits from the newest features and safest versions of your dependencies.

### More flexibility means more maintenance.

Unlike Single Runtime images, where Upsun fully manages runtime updates, security patches, and version lifecycles, Composable Image gives you the keys to your environment.

The trade-off is that **you must track and maintain the versions you declare**. If you need an environment that updates itself seamlessly without configuration changes, Single Runtime images may still be the best option.

In short:

**Composable Image gives teams maximum control, but with great power comes the responsibility to keep configurations up to date and secure.**

## A platform built for real applications

Composable Image embraces the diversity that naturally emerges in real-world projects.

It acknowledges that applications evolve, stacks grow, and tools accumulate, and instead of forcing developers into rigid constraints, it empowers them to shape their environment as needed, with clarity and predictability.

Whether you’re running multiple runtimes, deploying a lightweight static site, or adding niche packages to power specialized features, Composable Image gives you the flexibility you need without compromising simplicity.

**Your application is unique.**

**Now your runtime can be, too.**
