> ## 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.

# Upsun deployment hooks and commands: which one to use, and when

> Build hooks, deploy hooks, pre_start, start, and post_start all shape how Upsun deploys your app. Learn what each one does and when to reach for it.

export const PostMeta = ({data = {}}) => {
  const {author, date} = data;
  const authors = Array.isArray(author) ? author : author ? [author] : [];
  const toSlug = value => String(value).toLowerCase().trim().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
  const resolveAuthor = slug => {
    const entry = AUTHOR_MAP[slug] || ({});
    const name = entry.name || slug;
    const github = entry.github || null;
    const url = `/posts/authors/${toSlug(slug)}`;
    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",
      "github": "jackrabbithanna"
    },
    "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} 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={{ date: "2026-06-04T09:00:00.000Z", author: ["ralt"], date: "2026-06-04T09:00:00.000Z" }} />

Have you ever stared at an Upsun configuration file wondering whether your migration belongs in the `deploy` hook or the `post_deploy` hook? You're not alone.

Upsun gives you 6 places to run a command during a deployment: a `build` hook, `pre_start`, `start`, `post_start`, a `deploy` hook, and a `post_deploy` hook. Each one has its own page in the [docs](/docs/configure-apps/hooks), and each page is good. The part the docs can't answer for you is the one you actually care about: which one do you reach for, for the task in front of you?

That answer depends on context. It depends on when the command needs to run, whether it needs a database, whether it should block traffic, and even on the kind of mount you write to. None of that jumps out when you read the reference pages one at a time. So this post walks through each command and hook, then shows how they fit together with a few real examples.

## Two phases: build, then deploy

Every deployment has 2 parts: a [build, then a deploy](/docs/core-concepts/build-deploy). They run in different worlds, and that split explains most of the rest.

The build runs in isolation. No database, no services, no other containers. Upsun checks out your code, installs dependencies, runs the build flavor for your language, runs your `build` hook, then freezes the result into a read-only image.

The deploy takes that frozen image and makes it live. Now services are reachable, mounts are attached, and the file system is read-only apart from those mounts. If you use [zero-downtime deployments](/docs/core-concepts/build-deploy#zero-downtime-deployments), Upsun starts the new container next to the old one and removes the old one only once the new one is ready, so live traffic never drops.

Keep that one line in mind: the build is isolated and reusable, the deploy is live and connected. Where a command belongs almost always comes down to which side of that line it sits on.

## The build hook has nothing to do with runtime

The `build` hook, your [build dependencies](/docs/configure-apps/app-reference/single-runtime-image#dependencies), and the build flavor all affect build time, not runtime. They run while your app is being assembled, before any database exists.

Good fits: compiling assets, installing packages, generating code, bundling a frontend. Anything that turns your source into the artifact you want to ship and needs nothing more than your code plus write access to disk.

The `build` hook is also the one hook that can stop a deploy. If it fails, the build is aborted and nothing ships. The other hooks don't behave that way: if they fail, the deploy still happens. Add `set -e` at the top so the hook fails on the first failing command instead of only the last.

```yaml {filename=".upsun/config.yaml"} theme={null}
applications:
  app:
    type: "nodejs:22"
    source:
      root: "/"
    hooks:
      build: |
        set -e
        npm ci
        npm run build
```

One more thing the build buys you: the result is tied to your commit and reused. If a container has no changes, Upsun skips its build entirely. The `build` hook is the cheapest place to do expensive, deterministic work, because you pay for it once per commit, not once per deploy.

## pre\_start, start, and post\_start run every time your app starts

These 3 commands wrap your app process, in order: `pre_start`, then `start`, then `post_start`. Here's the part that's easy to miss: unlike the hooks, which run once per deploy, this trio runs every time the `start` process starts. And it starts again every time it's restarted, after a deploy, after host maintenance, and any time `start` exits and the supervisor brings it back. You configure them under [`web.commands`](/docs/configure-apps/image-properties/web#web-commands).

### start: the supervised one

[`start`](/docs/configure-apps/image-properties/web#start-command) launches your app and keeps it running. Upsun supervises it: if it ever exits, it gets restarted right away. That's the detail that matters. `start` is not a script that runs once and finishes, it's a long-running process expected to stay up.

Two things follow from that. Don't background it with `&`: the supervisor reads a terminating command as "start another copy", and you land in a loop until the container crashes. And `start` has to *be* that long-running process, so setup work that needs to run and then finish belongs in `pre_start` instead.

On most languages you write a `start` command. On PHP you can leave it out, and Upsun runs PHP-FPM for you.

### pre\_start: setup that runs and then finishes

[`pre_start`](/docs/configure-apps/image-properties/web#pre_start-command) runs right before `start`, every time the app starts. The difference from `start` is semantic, not about how often each runs: `pre_start` is a command you expect to run to completion and exit, while `start` is the supervised process that stays up. So `pre_start` is where setup that has to finish before your app launches goes.

Take a PHP app. You want the default PHP-FPM start command, but you also want to clear and warm the cache first, so OPcache loads the right code from the first request. You can't do that inside `start` without replacing the default with a wrapper script. `pre_start` lets you keep the default `start` and still run the prep beforehand. Same idea for a Python app that generates templates before serving: generate in `pre_start`, run the app in `start`.

```yaml {filename=".upsun/config.yaml"} theme={null}
applications:
  app:
    type: "php:8.3"
    source:
      root: "/"
    web:
      commands:
        pre_start: "php bin/console cache:warmup"
        # start is omitted: PHP-FPM is the default
```

### post\_start: wait until the app is actually ready

[`post_start`](/docs/configure-apps/image-properties/web#post_start-command) runs after `start`, before the container is added to the router. Its job is to make the orchestration wait until your app is genuinely ready to serve, not merely started.

This matters most during zero-downtime deployments and horizontal scaling. When you add an instance, you don't want traffic routed to it before it can answer. A `post_start` that polls `localhost` until the app responds holds the handover until then, so the old container stays up and requests aren't sent too early. The payoff is no dropped requests while scaling.

Whether you need it depends on your stack. A `start` command returns almost instantly: the process is up, but the app inside may still be warming. PHP serves on the first request, so it rarely needs `post_start`. Python apps tend to start fast enough to skip it. Java apps are the classic case where it earns its place: the JVM takes a moment to come up, and `post_start` gives it that moment before traffic arrives.

```yaml {filename=".upsun/config.yaml"} theme={null}
applications:
  app:
    type: "java:21"
    source:
      root: "/"
    web:
      commands:
        start: "java -jar target/app.jar"
        post_start: |
          curl -sS --retry 20 --retry-delay 1 --retry-connrefused localhost -o /dev/null
```

## The deploy hook runs once, before traffic

The [`deploy` hook](/docs/configure-apps/hooks/hooks-comparison#deploy-hook) runs after the container has started but before it accepts requests. It runs once, on a single web instance, with services reachable and the file system read-only apart from mounts. While it runs, incoming requests are held.

That makes it the right home for work that has to happen exactly once and exclusively, against code that matches what's on disk. Database schema migrations are the textbook case. You don't want live requests hitting the database mid-migration unless your app was built specifically for that, and if it was, you already know to go for it. For everyone else, the `deploy` hook is where migrations belong.

Keep it short. Held requests can wait out a few seconds, but a long `deploy` hook turns into dropped connections. Anything that can run safely while requests flow should move to `post_deploy` instead.

## The post\_deploy hook runs once, after traffic

The [`post_deploy` hook](/docs/configure-apps/hooks/hooks-comparison#post-deploy-hook) is the `deploy` hook's mirror image: same once-only, services-available behavior, but it runs after the app is open to the world and alongside live traffic. Reach for it when the work doesn't need exclusive access and can happen in a lazy fashion: clearing a CDN cache, warming caches that tolerate concurrent reads, kicking off a content import.

It comes with one rule. `post_deploy` is the only hook that runs on a redeploy with no code changes, so its scripts need to be idempotent, or safe to run more than once. Write them as if they will run twice, because eventually they will.

## Your mount type changes the answer

Here's the part that catches people out, and it's worth slowing down for. The kind of [mount](/docs/configure-apps/image-properties/mounts) you write to influences which command you should pick.

A `storage` mount is shared across all instances of your app over the network. An `instance` mount is local to each instance: with 3 instances behind horizontal scaling, each gets its own local disk with nothing shared between them.

That distinction decides where a file-generating command belongs:

* If you write to an `instance` mount, a `deploy` hook only populates one node, because it runs on a single instance. Files generated there never reach the others. To put per-instance files on every node, generate them in `pre_start` or `post_start`, which run on each instance.
* If you write to a `storage` mount, the `deploy` or `post_deploy` hook works, because every instance sees the same shared directory. Pick between the two based on whether the work should block traffic.
* If you rely on `/tmp` or other ephemeral storage, plan for it to be empty when the container starts again. The `deploy` and `post_deploy` hooks run once per deploy, but a container can restart on its own without a new deploy, and only `pre_start`, `start`, and `post_start` run on those restarts. So anything you need in ephemeral storage has to be regenerated in `pre_start`, or it won't be there.

Disk performance pulls in the same direction. Local instance disks are faster than the shared network storage. If you generate something heavy, say an e-commerce catalog rendered for a dozen languages across a million products, doing it once on a shared mount in a `deploy` hook can drag out the deployment. Generating it on the fast local disk in `pre_start` can be the better trade. It isn't micro-optimization, it's infrastructure optimization, and at that scale it adds up.

## A quick decision guide

When you're not sure, these questions usually settle it:

| If you need to...                                         | Reach for                                                                     |
| --------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Compile, bundle, or install, with no services             | [`build`](/docs/configure-apps/app-reference/single-runtime-image#build) hook |
| Set up something per instance, on every start             | [`pre_start`](/docs/configure-apps/image-properties/web#pre_start-command)    |
| Run and keep your app process alive                       | [`start`](/docs/configure-apps/image-properties/web#start-command)            |
| Wait until the app is ready before routing traffic        | [`post_start`](/docs/configure-apps/image-properties/web#post_start-command)  |
| Run something once, before traffic, with exclusive access | [`deploy`](/docs/configure-apps/hooks/hooks-comparison#deploy-hook) hook      |
| Run something once, after traffic, that's safe to repeat  | [`post_deploy`](/docs/configure-apps/hooks/hooks-comparison#post-deploy-hook) |

There's no single right configuration. Each application makes its own calls based on what it needs. If you run Symfony, its Upsun integration already wires up sensible defaults for all of these, so you may rarely touch them by hand. For everything else, the questions above are the ones to ask.

Once you can map a task to the moment it should run, the configuration writes itself. That's the part the reference pages leave to you, and it's the part worth getting right.

Want to put this into practice? [Create a free Upsun account](https://upsun.com) and read the full [build and deploy hooks guide](/docs/configure-apps/hooks) to see each one in context.
