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

> ## Agent Instructions
> These are the docs for Sevalla, a platform for deploying applications, databases, static sites, object storage and load balancers. The dashboard is at https://app.sevalla.com.
> Bold text in steps is an exact dashboard label.
> To act on a Sevalla account, prefer the Sevalla MCP server (https://mcp.sevalla.com/mcp, see https://docs.sevalla.com/ai-agents/overview.md), then the Sevalla CLI (https://docs.sevalla.com/developer-tools/cli.md), then the REST API (https://api-docs.sevalla.com, OpenAPI spec at https://api.sevalla.com/v3/openapi.json).
> Every page index is at https://docs.sevalla.com/llms.txt.

# Deploy a Medusa application

> Deploy a Medusa 2 commerce server and admin to Sevalla: PostgreSQL, Redis, a background worker in worker mode, migrations in a job, and product images in object storage.

A Medusa project runs on Sevalla as one application with three processes from the same image. [Ingot](https://docs.sevalla.com/applications/builds/ingot.md) builds the Medusa backend in the project's `apps/backend` workspace with `medusa build`. The web process runs `medusa start` in server mode, which serves the store and admin APIs and the admin dashboard on the port in the `PORT` environment variable; a background worker runs `medusa start` in worker mode for subscribers and scheduled jobs; and a job applies the database migrations before each deployment. Medusa keeps its data in a Sevalla PostgreSQL database, its sessions, events and workflows in a Sevalla Redis database, and uploaded images in an object storage bucket with public access.

## At a glance

| | |
| - | - |
| Source | A Git repository |
| Build | [Ingot](https://docs.sevalla.com/applications/builds/ingot.md) (Node.js 24, npm workspaces, the `apps/backend` package that `ingot.json` names), or the [Dockerfile](#deploy-with-a-dockerfile) |
| Start command | `cd .medusa/server && exec medusa start`, from `start` in `ingot.json` |
| Port | The `PORT` environment variable, `8080` by default |
| Processes | Web process (server mode); `Worker` background worker (`cd .medusa/server && MEDUSA_WORKER_MODE=worker exec medusa start`); `Migrate` job (`cd .medusa/server && medusa db:migrate`, **Before deployment**) |
| Services | PostgreSQL 18 and Redis 8.6 over internal connections; a bucket with public access for product images |
| Disk | None |
| Health check | `GET /health` returns `200` and `OK` (readiness probe) |
| Size | **S1** for the web process, the worker and the job (Medusa needs more than the 300 MB of **H1**), **DB1** for each database |

### Environment variables

| Variable | Required | Available during | Secret | Value |
| - | - | - | - | - |
| `DATABASE_URL` | Yes | Runtime | Yes | `DB_URL` from the PostgreSQL internal connection, renamed: `postgres://<user>:<password>@<host>:<port>/<database>` |
| `REDIS_URL` | Yes | Runtime | Yes | Added by the Redis internal connection: `redis://:<password>@<host>:<port>/0` |
| `JWT_SECRET` | Yes | Runtime | Yes | Generate with `openssl rand -hex 32` |
| `COOKIE_SECRET` | Yes | Runtime | Yes | Generate with `openssl rand -hex 32` |
| `MEDUSA_WORKER_MODE` | Yes | Runtime | No | `server`. The `Worker` process's start command sets `worker` for itself |
| `S3_FILE_URL` | Yes | Runtime | No | `https://` and the bucket's public domain, such as `https://storefront-media-q8w2e.sevalla.storage` |
| `S3_ENDPOINT` | Yes | Runtime | No | The bucket's **Endpoint**, such as `https://<account-id>.r2.cloudflarestorage.com` |
| `S3_BUCKET` | Yes | Runtime | No | The bucket's **Bucket name**, such as `storefront-media-q8w2e` |
| `S3_ACCESS_KEY_ID` | Yes | Runtime | Yes | The bucket's **Access key** |
| `S3_SECRET_ACCESS_KEY` | Yes | Runtime | Yes | The bucket's **Secret key** |
| `STORE_CORS`, `AUTH_CORS` | No | Runtime | No | Only for a storefront on another domain: its URL, such as `https://shop.example.com` |
| `PORT` | No | Runtime | No | Set by Sevalla to the web process port |

Environment variables belong to the application, so the web process, the worker and the job all get them. The internal connections also add `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`, `REDIS_HOST`, `REDIS_PORT` and `REDIS_PASSWORD`, which Medusa doesn't read.

## Before you begin

* A Sevalla company with a payment method, and a GitHub, GitLab or Bitbucket account for the repository. See [Git providers](https://docs.sevalla.com/git/overview.md).
* Node.js 24 on your computer. The Medusa 2.22 project needs Node.js 22.23.4 or later.

## Create the project

Create a Medusa project with npm and without the Next.js storefront:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
yes n | npx create-medusa-app@latest storefront --use-npm --skip-db --no-browser
cd storefront
```

* `create-medusa-app` asks whether to install the storefront and has no option to answer no, so `yes n` answers it. `--skip-db` skips creating a local database; the `Migrate` job sets up the one on Sevalla.
* The project is an npm workspace with the Medusa backend in `apps/backend`. Medusa 2.22.1 is the latest release when this guide was tested.
* `apps/backend/.env` holds development values, including secrets, and `.gitignore` keeps it out of the repository.

## Prepare the project for Sevalla

### Configure Medusa for production

Replace `apps/backend/medusa-config.ts` with a configuration that reads the worker mode, keeps sessions, events, workflows, locks and the cache in Redis, and stores uploads in the bucket:

```ts apps/backend/medusa-config.ts theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import { loadEnv, defineConfig } from '@medusajs/framework/utils'

loadEnv(process.env.NODE_ENV || 'development', process.cwd())

const redisUrl = process.env.REDIS_URL

module.exports = defineConfig({
  projectConfig: {
    databaseUrl: process.env.DATABASE_URL,
    redisUrl,
    workerMode: (process.env.MEDUSA_WORKER_MODE || 'shared') as 'shared' | 'worker' | 'server',
    http: {
      storeCors: process.env.STORE_CORS!,
      adminCors: process.env.ADMIN_CORS!,
      authCors: process.env.AUTH_CORS!,
      jwtSecret: process.env.JWT_SECRET,
      cookieSecret: process.env.COOKIE_SECRET,
    }
  },
  admin: {
    disable: process.env.MEDUSA_WORKER_MODE === 'worker',
  },
  modules: [
    {
      resolve: '@medusajs/medusa/caching',
      options: {
        providers: [
          { resolve: '@medusajs/medusa/caching-redis', id: 'caching-redis', is_default: true, options: { redisUrl } },
        ],
      },
    },
    { resolve: '@medusajs/medusa/event-bus-redis', options: { redisUrl } },
    { resolve: '@medusajs/medusa/workflow-engine-redis', options: { redis: { redisUrl } } },
    {
      resolve: '@medusajs/medusa/locking',
      options: {
        providers: [
          { resolve: '@medusajs/medusa/locking-redis', id: 'locking-redis', is_default: true, options: { redisUrl } },
        ],
      },
    },
    ...(process.env.S3_BUCKET ? [{
      resolve: '@medusajs/medusa/file',
      options: {
        providers: [
          {
            resolve: '@medusajs/medusa/file-s3',
            id: 's3',
            options: {
              file_url: process.env.S3_FILE_URL,
              access_key_id: process.env.S3_ACCESS_KEY_ID,
              secret_access_key: process.env.S3_SECRET_ACCESS_KEY,
              region: 'auto',
              bucket: process.env.S3_BUCKET,
              endpoint: process.env.S3_ENDPOINT,
            },
          },
        ],
      },
    }] : []),
  ],
})
```

| Setting | What it does |
| - | - |
| `workerMode` | `server` answers HTTP requests only, `worker` runs subscribers and scheduled jobs only, and `shared`, the default, does both. The web process runs with `MEDUSA_WORKER_MODE=server` and the `Worker` process with `worker`, so a slow job never holds up a request. |
| `admin.disable` | The worker doesn't serve the admin dashboard. The web process does, at `/app`. |
| `redisUrl` | Keeps sessions in Redis instead of the instance's memory, so every web instance knows every signed-in user. |
| `modules` | Medusa's Redis modules for the cache, the event bus, the workflow engine and locks. The event bus carries events from the web process to the worker through Redis; with Medusa's default in-memory modules, the worker would never see them. The four modules come with `@medusajs/medusa`. |
| `file-s3` | Medusa's S3 file provider uploads product images to the bucket and stores their URLs on `S3_FILE_URL`, the bucket's public domain. Sevalla buckets run on Cloudflare R2, which ignores the region, so `region` is `auto`. Without `S3_BUCKET`, such as on your computer, Medusa keeps uploads in a local folder. |

The Redis modules read `REDIS_URL` with ioredis, which accepts `redis://` URLs. A Sevalla Valkey database works too, but its internal connection adds a `REDIS_URL` that starts with `valkey://`, which ioredis doesn't parse: replace `valkey://` with `redis://` in the variable's value.

### Log the events the worker handles

To see the worker at work, add a subscriber that logs every new product:

```ts apps/backend/src/subscribers/product-created.ts theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
import type { SubscriberArgs, SubscriberConfig } from '@medusajs/framework'

export default async function productCreatedHandler({
  event: { data },
  container,
}: SubscriberArgs<{ id: string }>) {
  const logger = container.resolve('logger')
  logger.info(`Product created: ${data.id}`)
}

export const config: SubscriberConfig = {
  event: 'product.created',
}
```

The web process emits `product.created` when you create a product, and the worker runs the subscriber.

### Tell Ingot which package to build

The repository holds an npm workspace, so Ingot needs to know which package is the application, and how Medusa starts after `medusa build`. Create `ingot.json` at the root of the repository:

```json ingot.json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{
  "app": "apps/backend",
  "start": "cd .medusa/server && exec medusa start"
}
```

* `app` makes Ingot install the whole workspace from the root's `package-lock.json`, build `apps/backend` with Turborepo, remove the devDependencies, and run the processes from `apps/backend`.
* `medusa build` compiles the backend and the admin dashboard into `apps/backend/.medusa/server`, and Medusa runs from there: `medusa start` and `medusa db:migrate` load the compiled `medusa-config.js` and serve the admin from `public/admin` in the directory they run in. `exec` makes Medusa the container's main process, so it receives `SIGTERM` and shuts down cleanly.

### Pin the Node.js version

Pin Node.js 24 in the root `package.json`, where Ingot reads it, so the build doesn't change when a new Ingot release moves its default line:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
npm pkg set engines.node="24.x"
```

| File | Why Sevalla needs it |
| - | - |
| `package.json` | The workspace, and `engines.node`, which pins Node.js 24 |
| `package-lock.json` | Ingot installs exactly these versions with `npm ci` |
| `ingot.json` | The package to build and the start command |
| `apps/backend/medusa-config.ts` | Reads the database, Redis, worker mode, secrets and bucket from the environment |

Commit everything and push it to your Git provider:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
git init
git add .
git commit -m "Prepare for Sevalla"
git branch -M main
git remote add origin https://github.com/acme/storefront.git
git push -u origin main
```

## Deploy to Sevalla

These steps build with [Ingot](https://docs.sevalla.com/applications/builds/ingot.md). To build from your own Dockerfile instead, see [Deploy with a Dockerfile](#deploy-with-a-dockerfile).

**1. Create the databases**

In the sidebar, click **Databases**, then click **Create database**. Under **Database**, choose **PostgreSQL** and the newest version. Enter `storefront-db` as the **Display name**, choose a **Data center** and a **Size**, then click **Create database**. Create a second database the same way with **Redis**, the newest version and the **Display name** `storefront-redis`, in the same **Data center**. See [Create a database](https://docs.sevalla.com/databases/create.md) for every field.

**2. Create the bucket**

In the sidebar, click **Object storage**, then click **Create object storage**. Enter `storefront-media` as the **Name**, choose the **Location** closest to the databases, and click **Create object storage**. On the bucket's **Settings** tab, under **Public access**, click **Enable public access** and confirm. See [Create a bucket](https://docs.sevalla.com/object-storage/create.md).

**3. Create the application**

In the sidebar, click **Applications**, then click **Create** > **Application**. Under **Source**, click **Private repository**, then choose the **Git provider**, the **Repository** and the **Branch**. Under **Location**, choose the same **Data center** as the databases. Choose **S1** (1 GB of memory) as the **Size**, then click **Create application**. The application opens on its **Overview** tab with the status **Not deployed yet**: creating an application doesn't deploy it.

**4. Connect the databases**

Click the **Networking** tab. Under **Connected services**, click **Add internal connection**. Choose `storefront-db` as the **Service** and turn on **Add environment variables to the application**. In the list of variables, change the key `DB_URL` to `DATABASE_URL`, then click **Add internal connection**. Add a second internal connection to `storefront-redis` the same way, and keep its keys: Medusa reads `REDIS_URL`. See [Connect an application](https://docs.sevalla.com/databases/connect.md#connect-an-application).

**5. Add the environment variables**

Click the **Environment variables** tab, then click **Add environment variables**. Add these variables and click **Add environment variables**:

| Variable | Value |
| - | - |
| `JWT_SECRET` | A random string that signs sign-in tokens. Generate one with `openssl rand -hex 32`. |
| `COOKIE_SECRET` | A second random string, for session cookies. Changing either secret signs every user out. |
| `MEDUSA_WORKER_MODE` | `server` |
| `S3_FILE_URL` | `https://` followed by the bucket's **Public domain**, such as `https://storefront-media-q8w2e.sevalla.storage` |
| `S3_ENDPOINT` | The bucket's **Endpoint**, from its **Settings** tab |
| `S3_BUCKET` | The bucket's **Bucket name**, such as `storefront-media-q8w2e`, not its display name |
| `S3_ACCESS_KEY_ID` | The bucket's **Access key** |
| `S3_SECRET_ACCESS_KEY` | The bucket's **Secret key** |

See [Connect with the S3 API](https://docs.sevalla.com/object-storage/connect.md#connection-values) for where each bucket value is.

**6. Create the migration job**

Click the **Processes** tab, then click **Create process**. Choose **Job**, enter `Migrate` as the **Name** and `cd .medusa/server && medusa db:migrate` as the **Custom start command**. Keep the **Start policy** at **Before deployment**, choose **S1** as the **Resource**, then click **Create process**.

**7. Create the background worker**

Click **Create process** again. Choose **Background worker**, enter `Worker` as the **Name** and `cd .medusa/server && MEDUSA_WORKER_MODE=worker exec medusa start` as the **Custom start command**. Choose **S1** as the **Resource**, keep the instance count at 1, then click **Create process**.

**8. Turn on the readiness probe**

On the **Processes** tab, open the web process's menu and click **Update process**. Under **Health checks**, turn on **Readiness probe** with the **Path** `/health`, then click **Update process**.

**9. Deploy**

Click the **Overview** tab, then click **Deploy now** under **Recent deployments**. Check the **Branch** and click **Deploy**.

**10. Turn on auto-deploy**

To deploy every push to the branch automatically, click the **Settings** tab, then click **Update auto-deploy** in the **Auto-deploy** section. Turn on **Auto-deploy** and click **Update auto-deploy**. See [Deployments](https://docs.sevalla.com/applications/deployments.md#deploy-automatically-on-push).

![The Create process sheet with Background worker selected, and the Name, Custom start command, Resource and Scaling fields](https://assets.sevalla.com/screenshots/light/app-processes-worker-create.png)

Each custom start command runs with `/bin/sh -l -c` from `apps/backend`, so `cd .medusa/server` moves to the built application first. `MEDUSA_WORKER_MODE=worker` before `exec` sets the variable for the worker only, and `exec` makes Medusa the container's main process, which receives `SIGTERM`. The `Migrate` job runs after the build and before the new version gets traffic: `medusa db:migrate` creates or updates Medusa's tables, syncs the links between modules and runs the project's migration scripts, which seed a store, regions and demo products the first time. If it fails, the deployment fails and the previous version keeps serving. The readiness probe sends traffic to a new web instance only once Medusa answers `/health`, about 10 seconds after it starts. See [Health checks](https://docs.sevalla.com/applications/processes.md#health-checks).

## Verify the deployment

1. On the **Deployments** tab, click the deployment. Its **Build log** shows Ingot's [build plan](https://docs.sevalla.com/applications/builds/ingot.md#read-the-build-plan): the `apps/backend` package from `ingot.json`, Node.js 24 from `engines.node`, npm from `packageManager`, the Turborepo build, the start command from `ingot.json`, and dev dependencies removed after the build.

   ```text Build log theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   ╭─ ■ Node.js 24 application ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
   │ App                apps/backend                                                                             "app" in ingot.json or INGOT_APP       │
   │ Node.js            24                                                                                       package.json engines "24.x"            │
   │ Package manager    npm 11.19.0                                                                              package.json packageManager            │
   │ Workspace package  @dtc/backend                                                                             apps/backend/package.json              │
   │ Build              TURBO_TELEMETRY_DISABLED=1 node_modules/.bin/turbo run build --filter='@dtc/backend...'  turbo.json                             │
   │ Start              cd .medusa/server && exec medusa start                                                   start in ingot.json or INGOT_START_CMD │
   │ DevDependencies    removed after the build                                                                                                         │
   ├─ Image ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
   │ Build              node:24-trixie                                                                                                                  │
   │ Runtime            node:24-trixie-slim                                                                                                             │
   │ User               1000:1000                                                                                                                       │
   │ Init               tini                                                                                                                            │
   │ Port               $PORT, default 8080                                                                                                             │
   ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Ingot v0.4.1 ─╯
   ```

   During the rollout, the build log shows `Migrate has been successful`, `Web has been deployed successfully.` and `Worker has been deployed successfully.` On the **Logs** tab, the `Migrate` process's output ends with `Migration scripts completed`.

2. Wait for the deployment's status to change to **Deployed**. The application's status next to its name changes to **Running**.

3. Check the server and the admin dashboard from a terminal:

   ```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   curl -s https://storefront-x7k2m.sevalla.app/health; echo
   curl -s -o /dev/null -w '%{http_code} %{content_type}\n' https://storefront-x7k2m.sevalla.app/app
   ```

   ```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   OK
   200 text/html; charset=utf-8
   ```

4. Create the first admin user. Click the **Web terminal** tab, and in the shell that opens in the web process, run this with your email and a password:

   ```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
   cd .medusa/server && medusa user -e ana@example.com -p '<password>'
   ```

   The command prints `User created successfully.` See [Web terminal](https://docs.sevalla.com/applications/web-terminal.md).

5. Open the application's domain followed by `/app`, for example `https://storefront-x7k2m.sevalla.app/app`, and sign in with the email and password from step 4. In **Products**, create a product and add an image under **Media**. The image is on the bucket's **Objects** tab, and its URL is on the bucket's public domain, such as `https://storefront-media-q8w2e.sevalla.storage/mug-01M65XE7WVCTWW96FAPMVT6WQE.png`.

6. On the application's **Logs** tab, choose the `Worker` process. It logs `Product created: prod_...` with the new product's ID: the web process emitted the event, and the worker ran the subscriber. See [Runtime logs](https://docs.sevalla.com/applications/logs.md).

To check from a terminal, for example as an agent, use Medusa's admin API instead of steps 5 and 6. With `mug.png` in the current folder and the password from step 4:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
APP_URL=https://storefront-x7k2m.sevalla.app
PASSWORD='<your password>'
TOKEN=$(curl -s $APP_URL/auth/user/emailpass -H "Content-Type: application/json" \
  -d "{\"email\":\"ana@example.com\",\"password\":\"$PASSWORD\"}" | jq -r .token)
IMAGE_URL=$(curl -s $APP_URL/admin/uploads -H "Authorization: Bearer $TOKEN" -F "files=@mug.png" | jq -r '.files[0].url')
curl -s -o /dev/null -w '%{http_code} %{content_type}\n' "$IMAGE_URL"
curl -s $APP_URL/admin/products -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"title\":\"Coffee mug\",\"images\":[{\"url\":\"$IMAGE_URL\"}],\"options\":[{\"title\":\"Size\",\"values\":[\"One size\"]}]}" \
  | jq -c '{title: .product.title, image: .product.images[0].url}'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
200 image/png
{"title":"Coffee mug","image":"https://storefront-media-q8w2e.sevalla.storage/mug-01M65XE7WVCTWW96FAPMVT6WQE.png"}
```

With the API, [Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) returns the deployment's status, [Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) its build log, and [Get application runtime logs](https://api-docs.sevalla.com/v3/applications/get-application-runtime-logs) the worker's lines; see [Deploy with the API](#deploy-with-the-api).

![The application header with its name, Running status, repository, branch and the domain that opens the application](https://assets.sevalla.com/screenshots/light/app-overview-address.png)

If the deployment fails, read its build log, then see [Troubleshoot](#troubleshoot) and [Troubleshoot Ingot builds](https://docs.sevalla.com/applications/builds/ingot.md#troubleshoot-ingot-builds).

## Deploy with a Dockerfile

Ingot needs no Dockerfile. Write your own when you want full control of the image: the base image, the system packages and every layer. Add a `Dockerfile` and a `.dockerignore` to the root of the repository:

```dockerfile Dockerfile theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
FROM node:24-trixie-slim AS build
WORKDIR /app
COPY package.json package-lock.json .npmrc ./
COPY apps/backend/package.json apps/backend/
RUN npm ci
COPY . .
RUN npm run build -w apps/backend && npm prune --omit=dev

FROM node:24-trixie-slim
# Debian's packages get the base image's security updates, so they aren't pinned
# hadolint ignore=DL3008
RUN apt-get update \
  && apt-get install -y --no-install-recommends tini \
  && rm -rf /var/lib/apt/lists/*
ENV NODE_ENV=production PORT=8080 PATH=/app/node_modules/.bin:$PATH
WORKDIR /app/apps/backend
COPY --from=build /app/node_modules /app/node_modules
COPY --from=build /app/apps/backend/.medusa/server .medusa/server
USER 1000:1000
EXPOSE 8080
ENTRYPOINT ["tini", "--"]
CMD ["/bin/sh", "-c", "cd .medusa/server && exec medusa start"]
```

| Line or choice | Why |
| - | - |
| `node:24-trixie-slim` | The official Node.js 24 image on Debian 13, the line `engines.node` pins. The tag follows Node.js 24's patch releases, so each build gets its security fixes; a digest would stay on one release. |
| `package.json` files and `package-lock.json` first | `npm ci` installs the whole workspace from the lock file. Copying the manifests before the code lets Docker reuse the installed dependencies when only the code changes. `.npmrc` comes from the template. |
| Two stages | The first stage runs `medusa build` and removes the devDependencies. The final stage copies only the dependencies and `.medusa/server`, the compiled backend and admin dashboard, without the TypeScript sources. |
| `WORKDIR /app/apps/backend` | The same directory Ingot's image runs from, so the `Migrate` job and the `Worker` keep their `cd .medusa/server` commands with either build strategy. |
| `PATH=/app/node_modules/.bin:$PATH` | Lets the start commands run `medusa` by name. |
| `USER 1000:1000` | Medusa runs as the image's unprivileged `node` user. The application stays owned by root, so Medusa can read it but not change it. Uploads go to the bucket, so Medusa needs no writable folder. |
| `tini` | Runs as the first process and passes `SIGTERM` on to Medusa, which closes its connections and stops with `Gracefully shutting down server`. |

```text .dockerignore theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
.git
node_modules
apps/*/node_modules
apps/*/.medusa
.turbo
**/.env*
npm-debug.log*
Dockerfile
.dockerignore
```

Commit and push both files. Ingot doesn't use the `Dockerfile`, so you can keep it in a repository that builds with Ingot, but it adds the `.dockerignore` patterns to its own, and its build plan lists them in an `Ignored` row. Then switch the application to the Dockerfile build strategy:

**1. Choose the Dockerfile build strategy**

On the application's **Settings** tab, in the **Build strategy** section, click **Update build strategy**. Click **Dockerfile**, keep the **Dockerfile path** `Dockerfile` and the **Docker context** `.`, then click **Update build strategy**.

**2. Deploy**

Click the **Overview** tab, then click **Deploy now** under **Recent deployments**. Check the **Branch** and click **Deploy**.

The deployment's **Build log** shows `📦 Building and pushing docker image using Dockerfile...` instead of Ingot's build plan, then `Migrate has been successful`, and its **Dockerfile** tab shows your `Dockerfile`. When the deployment is **Deployed**, the checks in [Verify the deployment](#verify-the-deployment) print the same lines. See [Dockerfile](https://docs.sevalla.com/applications/builds/dockerfile.md) for build arguments and private base images, and [Deploy with the API](#build-from-the-dockerfile-with-the-api) to switch the build strategy with the API.

## Deploy changes

With **Auto-deploy** on, every push to the branch starts a deployment; otherwise, click **Deploy now** for each change. Each deployment builds the image, runs the `Migrate` job, then replaces the instances of the web process and the worker. When you add or change a data model in a custom module, generate its migration on your computer with `npx medusa db:generate <module>` in `apps/backend` and commit it with the code. Events that the web process emits during a deployment wait in Redis until a worker takes them. See [Deployments](https://docs.sevalla.com/applications/deployments.md).

## Use environment variables

Add configuration on the application's **Environment variables** tab and read it with `process.env` in `medusa-config.ts` and in server code. Variables apply at the next deployment. Admin customizations can only read variables that start with `VITE_`, and `medusa build` builds them into the admin dashboard, so they must be **Available during buildtime**, and a change needs a new build. See [Environment variables](https://docs.sevalla.com/applications/environment-variables.md).

## Add a custom domain

To serve the store API and the admin dashboard from your own domain, such as `shop-api.example.com`, add the domain on the application's **Domains** tab and create the DNS records that Sevalla shows. The web process runs on **S1**, larger than **H1**, as custom domains need. See [Custom domains](https://docs.sevalla.com/domains/overview.md). A storefront on another domain calls the store API from the browser: add its URL, such as `https://shop.example.com`, to `STORE_CORS` and `AUTH_CORS`, then deploy.

## Troubleshoot

| Symptom | Cause | Fix |
| - | - | - |
| A process stops at start, and its runtime logs show `Error in loading config: Cannot find module '/app/apps/backend/medusa-config'` | The command runs Medusa from `apps/backend`, where only the TypeScript configuration is, and the image has no TypeScript loader after the build | Start the command with `cd .medusa/server && `, as in [Deploy to Sevalla](#deploy-to-sevalla). |
| The web process stops at start with `Error: [config] ⚠️ http.jwtSecret not found.` | `JWT_SECRET` isn't set. In production, Medusa doesn't fall back to a default secret | Add `JWT_SECRET` and `COOKIE_SECRET`, then deploy. |
| The runtime logs repeat `[ioredis] Unhandled error event: Error: connect ENOENT //:<password>@<host>:6379/0` | `REDIS_URL` starts with `valkey://`, from a Valkey database's internal connection, and ioredis reads it as a file path | Replace `valkey://` with `redis://` in `REDIS_URL`, then deploy. |
| The web process stops at start with `Error starting server: Access key ID and secret access key are required when using access key authentication` | `S3_BUCKET` is set, but `S3_ACCESS_KEY_ID` or `S3_SECRET_ACCESS_KEY` isn't | Add both keys from the bucket's **Settings** tab, then deploy. |
| The `Worker` doesn't log `Product created`, and another application's worker does | The two applications are connected to the same Redis database, so they share Medusa's event queue, and either worker takes an event | Give each application, such as a staging copy, its own Redis database. |

## Deploy with the API

This sequence creates the same deployment as [Deploy to Sevalla](#deploy-to-sevalla) through the [Sevalla API](https://docs.sevalla.com/developer-tools/api.md), so an agent or a script can run it as is. Each step names the operation, shows the `curl` command, and gives the matching [CLI](https://docs.sevalla.com/developer-tools/cli.md) command and [MCP server](https://docs.sevalla.com/ai-agents/overview.md) tool.

You need:

* An [API key](https://docs.sevalla.com/developer-tools/api-keys.md) with the **Company developer** role, in the `SEVALLA_API_KEY` environment variable. The CLI reads the same key from `SEVALLA_API_TOKEN`.
* `curl`, `jq` and `openssl`, the project pushed to GitHub as in [Prepare the project for Sevalla](#prepare-the-project-for-sevalla), and `mug.png`, any PNG image, in the current folder for the last check.
* A GitHub account that can read the repository, connected to Sevalla. See [Git providers](https://docs.sevalla.com/git/overview.md). The API can't list connected accounts: when none can read the repository, [Create application](https://api-docs.sevalla.com/v3/applications/create-application) fails with `400` and `Cannot access repository https://github.com/acme/storefront: Repository not found or you don't have access.`

The sequence creates four services: an application, two databases and a bucket. The API allows 20 creations of each kind per hour per company. A call that fails returns `status` and `message` instead of the resource, and `jq -r .id` prints `null`: stop and read the message.

**1. Look up the location and the sizes**

[List clusters](https://api-docs.sevalla.com/v3/resources/list-clusters) (`listClustersV3`) returns every location your company can use, with its region ID. This guide uses Frankfurt, Germany, `eu-frankfurt-1`. [List process resource types](https://api-docs.sevalla.com/v3/resources/list-process-resource-types) (`listProcessResourceTypesV3`) and [List database resource types](https://api-docs.sevalla.com/v3/resources/list-database-resource-types) (`listDatabaseResourceTypesV3`) return the sizes.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
CLUSTER_ID=$(curl -s https://api.sevalla.com/v3/resources/clusters \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[] | select(.location == "eu-frankfurt-1") | .id')
S1_ID=$(curl -s https://api.sevalla.com/v3/resources/process-resource-types \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[] | select(.name == "s1") | .id')
DB_SIZE_ID=$(curl -s https://api.sevalla.com/v3/resources/database-resource-types \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[] | select(.name == "db1") | .id')
```

CLI: `sevalla resources clusters`, `sevalla resources process-types` and `sevalla resources db-types`. MCP: `get_platform_options`.

**2. Create the application**

[Create application](https://api-docs.sevalla.com/v3/applications/create-application) (`createApplicationV3`) creates the application with a web process at **S1**, the size this guide needs, and doesn't deploy it.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
APP_ID=$(curl -s https://api.sevalla.com/v3/applications \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<JSON | jq -r .id
{
  "display_name": "storefront",
  "cluster_id": "$CLUSTER_ID",
  "source": "privateGit",
  "git_type": "github",
  "repo_url": "https://github.com/acme/storefront",
  "default_branch": "main"
}
JSON
)
```

CLI: `sevalla apps create --name storefront --cluster "$CLUSTER_ID" --source privateGit --git-type github --repository https://github.com/acme/storefront --branch main`. MCP: `create_application` with `deploy` set to `false`.

**3. Create the databases**

[Create database](https://api-docs.sevalla.com/v3/databases/create-database) (`createDatabaseV3`) starts creating a PostgreSQL 18 database and a Redis 8.6 database, each with a password you generate. [List database versions](https://api-docs.sevalla.com/v3/resources/list-database-versions) (`listDatabaseVersionsV3`) lists the versions of a location.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DB_PASSWORD=$(openssl rand -hex 24)
DB_ID=$(curl -s https://api.sevalla.com/v3/databases \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<JSON | jq -r .id
{
  "display_name": "storefront-db",
  "type": "postgresql",
  "version": "18",
  "cluster_id": "$CLUSTER_ID",
  "resource_type_id": "$DB_SIZE_ID",
  "db_name": "storefront",
  "db_user": "storefront",
  "db_password": "$DB_PASSWORD"
}
JSON
)
REDIS_PASSWORD=$(openssl rand -hex 24)
REDIS_ID=$(curl -s https://api.sevalla.com/v3/databases \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<JSON | jq -r .id
{
  "display_name": "storefront-redis",
  "type": "redis",
  "version": "8.6",
  "cluster_id": "$CLUSTER_ID",
  "resource_type_id": "$DB_SIZE_ID",
  "db_name": "storefront",
  "db_password": "$REDIS_PASSWORD"
}
JSON
)
```

CLI: `sevalla databases create --name storefront-db --type postgresql --db-version 18 --cluster "$CLUSTER_ID" --resource-type "$DB_SIZE_ID" --db-name storefront --db-user storefront --db-password "$DB_PASSWORD"`, and the same with `--type redis --db-version 8.6` for `storefront-redis`. MCP: `create_database`, which generates the password and doesn't return it; with `connect_to_application_id`, it also makes the internal connection.

**4. Connect the databases**

[Create internal connection](https://api-docs.sevalla.com/v3/databases/create-internal-connection) (`createDatabaseInternalConnectionV3`) connects each database to the application over the private network. Unlike the dashboard, it adds no environment variables: a later step adds `DATABASE_URL` and `REDIS_URL`.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for ID in $DB_ID $REDIS_ID; do
  curl -s https://api.sevalla.com/v3/databases/$ID/internal-connections \
    -H "Authorization: Bearer $SEVALLA_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"target_id\": \"$APP_ID\", \"target_type\": \"app\"}" | jq -r .id
done
```

CLI: `sevalla databases connections internal-create "$DB_ID" --target-id "$APP_ID" --target-type app`, once per database. MCP: `sevalla_api_create` with `createDatabaseInternalConnectionV3`.

**5. Create the bucket**

[Create object storage bucket](https://api-docs.sevalla.com/v3/object-storages/create-object-storage-bucket) (`createObjectStorageV3`) creates the bucket in Western Europe (`weur`), close to Frankfurt, with public access on, so browsers load product images from its public domain.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
BUCKET_ID=$(curl -s https://api.sevalla.com/v3/object-storage \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"display_name": "storefront-media", "location": "weur", "public_access": true}' | jq -r .id)
```

CLI: `sevalla object-storage create --name storefront-media --location weur`, then `sevalla object-storage cdn-domain enable "$BUCKET_ID"` for public access. MCP: `sevalla_api_create` with `createObjectStorageV3` and the same body.

**6. Read the database and bucket details**

[Get database](https://api-docs.sevalla.com/v3/databases/get-database) (`getDatabaseV3`) returns each database's internal hostname and port, once its `status` is `ready`. [Get object storage bucket](https://api-docs.sevalla.com/v3/object-storages/get-object-storage-bucket) (`getObjectStorageV3`) returns the bucket's public domain, endpoint, name and keys.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for ID in $DB_ID $REDIS_ID; do
  for i in $(seq 1 60); do
    [ "$(curl -s https://api.sevalla.com/v3/databases/$ID -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.status // empty')" = ready ] && break
    sleep 10
  done
done
DB=$(curl -s https://api.sevalla.com/v3/databases/$DB_ID -H "Authorization: Bearer $SEVALLA_API_KEY")
REDIS=$(curl -s https://api.sevalla.com/v3/databases/$REDIS_ID -H "Authorization: Bearer $SEVALLA_API_KEY")
BUCKET=$(curl -s https://api.sevalla.com/v3/object-storage/$BUCKET_ID -H "Authorization: Bearer $SEVALLA_API_KEY")
```

CLI: `sevalla databases get "$DB_ID" --json` and `sevalla object-storage get "$BUCKET_ID" --json`. MCP: `get_database`, and `sevalla_api_read` with `getObjectStorageV3`.

**7. Add the environment variables**

[Create environment variable](https://api-docs.sevalla.com/v3/applications/create-environment-variable) (`createEnvironmentVariableV3`) adds one variable per call. `add_var` sends each one as available during runtime only, since the build needs none. `DATABASE_URL` and `REDIS_URL` have the formats the dashboard's internal connections use:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
add_var() {
  curl -s https://api.sevalla.com/v3/applications/$APP_ID/env-vars \
    -H "Authorization: Bearer $SEVALLA_API_KEY" \
    -H "Content-Type: application/json" \
    -d "$(jq -n --arg key "$1" --arg value "$2" '{key: $key, value: $value, is_runtime: true, is_buildtime: false}')" | jq -r .key
}
add_var DATABASE_URL "postgres://$(jq -r .db_user <<< "$DB"):$DB_PASSWORD@$(jq -r .internal_hostname <<< "$DB"):$(jq -r .internal_port <<< "$DB")/$(jq -r .db_name <<< "$DB")"
add_var REDIS_URL "redis://:$REDIS_PASSWORD@$(jq -r .internal_hostname <<< "$REDIS"):$(jq -r .internal_port <<< "$REDIS")/0"
add_var JWT_SECRET "$(openssl rand -hex 32)"
add_var COOKIE_SECRET "$(openssl rand -hex 32)"
add_var MEDUSA_WORKER_MODE server
add_var S3_FILE_URL "https://$(jq -r .domain <<< "$BUCKET")"
add_var S3_ENDPOINT "$(jq -r .endpoint <<< "$BUCKET")"
add_var S3_BUCKET "$(jq -r .bucket_name <<< "$BUCKET")"
add_var S3_ACCESS_KEY_ID "$(jq -r .access_key <<< "$BUCKET")"
add_var S3_SECRET_ACCESS_KEY "$(jq -r .secret_key <<< "$BUCKET")"
```

[List environment variables](https://api-docs.sevalla.com/v3/applications/list-environment-variables) (`listEnvironmentVariablesV3`) returns the values later; the MCP server's tools don't.

CLI: `sevalla apps env-vars create "$APP_ID" --key MEDUSA_WORKER_MODE --value server`, once per variable; the CLI makes each one available during the build too. MCP: `set_environment_variables` with all the variables in one call, each with `runtime` `true` and `buildtime` `false`.

**8. Turn on the readiness probe**

[List processes](https://api-docs.sevalla.com/v3/applications/list-processes) (`listProcessesV3`) returns the web process, and [Update process](https://api-docs.sevalla.com/v3/applications/update-process) (`updateProcessV3`) gives it a readiness probe on `/health`. Send `timeoutSeconds`: without it, the API sets 120 seconds.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
WEB_ID=$(curl -s https://api.sevalla.com/v3/applications/$APP_ID/processes \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.data[] | select(.type == "web") | .id')
curl -s -X PATCH https://api.sevalla.com/v3/applications/$APP_ID/processes/$WEB_ID \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"readiness_probe": {"httpGet": {"path": "/health", "port": 8080, "scheme": "HTTP"}, "initialDelaySeconds": 0, "periodSeconds": 10, "timeoutSeconds": 1, "successThreshold": 1, "failureThreshold": 3}}' \
  | jq -c '{resource_type_name, readiness: .readiness_probe.httpGet.path}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"resource_type_name":"s1","readiness":"/health"}
```

CLI: `sevalla apps processes list --app-id "$APP_ID"`; the CLI can't set probes. MCP: `get_application`, then `sevalla_api_update` with `updateProcessV3` and the same body.

**9. Create the migration job and the worker**

[Create process](https://api-docs.sevalla.com/v3/applications/create-process) (`createProcessV3`) adds the `Migrate` job and the `Worker` background worker. Every process needs `scaling_strategy`, and a job needs `job_start_policy`: `beforeDeployment` is the dashboard's **Before deployment**.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s https://api.sevalla.com/v3/applications/$APP_ID/processes \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<JSON | jq -r .display_name
{
  "display_name": "Migrate",
  "type": "job",
  "entrypoint": "cd .medusa/server && medusa db:migrate",
  "resource_type_id": "$S1_ID",
  "scaling_strategy": {"type": "manual", "config": {"instanceCount": 1}},
  "job_start_policy": "beforeDeployment"
}
JSON
curl -s https://api.sevalla.com/v3/applications/$APP_ID/processes \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<JSON | jq -r .display_name
{
  "display_name": "Worker",
  "type": "worker",
  "entrypoint": "cd .medusa/server && MEDUSA_WORKER_MODE=worker exec medusa start",
  "resource_type_id": "$S1_ID",
  "scaling_strategy": {"type": "manual", "config": {"instanceCount": 1}}
}
JSON
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
Migrate
Worker
```

CLI: `sevalla apps processes create "$APP_ID" --name Migrate --type job --resource-type "$S1_ID" --command "cd .medusa/server && medusa db:migrate" --job-start-policy beforeDeployment`, and the same with `--name Worker --type worker` and the worker's command. MCP: `sevalla_api_create` with `createProcessV3` and the same bodies.

**10. Turn on auto-deploy**

[Update application](https://api-docs.sevalla.com/v3/applications/update-application) (`updateApplicationV3`) with `auto_deploy` deploys every push to the branch.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PATCH https://api.sevalla.com/v3/applications/$APP_ID \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"auto_deploy": true}' | jq .auto_deploy
```

CLI: `sevalla apps update "$APP_ID" --data '{"auto_deploy": true}'`. MCP: `sevalla_api_update` with `updateApplicationV3`.

**11. Deploy**

[Trigger deployment](https://api-docs.sevalla.com/v3/applications/trigger-deployment) (`triggerDeploymentV3`) builds the latest commit of the branch and rolls it out.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DEPLOYMENT_ID=$(curl -s https://api.sevalla.com/v3/applications/$APP_ID/deployments \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}' | jq -r .id)
```

CLI: `sevalla apps deployments trigger "$APP_ID"`. MCP: `trigger_deployment`.

**12. Wait for the deployment**

[Get deployment](https://api-docs.sevalla.com/v3/applications/get-deployment) (`getDeploymentV3`) returns the deployment's `status`: `waiting`, then `inProgress`, then `success` or `failed`. The loop polls every 15 seconds for up to 30 minutes and keeps polling through a failed request. The first deployment takes about 6 minutes.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for i in $(seq 1 120); do
  STATUS=$(curl -s "https://api.sevalla.com/v3/applications/$APP_ID/deployments/$DEPLOYMENT_ID" \
    -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.status // empty')
  case $STATUS in success|failed|cancelled|skipped) break ;; esac
  sleep 15
done
echo "$STATUS"
```

CLI: `sevalla apps deployments get "$DEPLOYMENT_ID" --app-id "$APP_ID"`. MCP: `get_deployment`.

**13. Check the deployment**

[Get application deployment logs](https://api-docs.sevalla.com/v3/applications/get-application-deployment-logs) (`getApplicationDeploymentLogsV3`) returns the build log, which starts with the build plan in [Verify the deployment](#verify-the-deployment) and, on a failed deployment, ends with the error; `grep` keeps the rollout's result lines. [Get application runtime logs](https://api-docs.sevalla.com/v3/applications/get-application-runtime-logs) (`getApplicationRuntimeLogsV3`) returns what the `Migrate` job printed. [List application domains](https://api-docs.sevalla.com/v3/applications/list-application-domains) (`listApplicationDomainsV3`) returns the application's domain:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s "https://api.sevalla.com/v3/applications/$APP_ID/deployments/$DEPLOYMENT_ID/logs?limit=5000" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[].message' | grep -E 'Migrate has|Web has|Worker has'
curl -s -G https://api.sevalla.com/v3/applications/$APP_ID/runtime-logs --data-urlencode "search=Migration scripts completed" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[].message'
APP_URL=https://$(curl -s https://api.sevalla.com/v3/applications/$APP_ID/domains \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.data[] | select(.is_primary) | .name')
curl -s $APP_URL/health; echo
curl -s -o /dev/null -w '%{http_code} %{content_type}\n' $APP_URL/app
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
✅ Migrate has been successful
🚀 Web has been deployed successfully.
🚀 Worker has been deployed successfully.
Migration scripts completed
OK
200 text/html; charset=utf-8
```

The build log's messages carry terminal color codes around the text.

CLI: `sevalla apps deployments logs "$DEPLOYMENT_ID" --app-id "$APP_ID"`, `sevalla apps logs runtime "$APP_ID" --search "Migration scripts completed"` and `sevalla apps domains list --app-id "$APP_ID"`. MCP: `get_deployment_logs`, `search_runtime_logs` and `get_application`.

**14. Create the admin user**

[Run a command in a process](https://api-docs.sevalla.com/v3/applications/run-a-command-in-a-process) (`execProcessCommandV3`) runs `medusa user` in the web process, as step 4 of [Verify the deployment](#verify-the-deployment) does in the web terminal. The call has no shell, so the command is `sh -c`; it waits up to 60 seconds.

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
PASSWORD="Aa1$(openssl rand -hex 12)"
curl -s -X POST https://api.sevalla.com/v3/applications/$APP_ID/processes/$WEB_ID/exec \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$(jq -n --arg pw "$PASSWORD" '{command: ["sh", "-c", "cd .medusa/server && medusa user -e ana@example.com -p \($pw)"], timeout: 60}')" \
  | jq -r '.stdout, .exit_code' | grep -o -E 'User created successfully.|^[0-9]+$'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
User created successfully.
0
```

Keep `PASSWORD` to sign in as `ana@example.com`. CLI: `sevalla apps processes exec "$WEB_ID" --app-id "$APP_ID" --timeout 60 -- sh -c "cd .medusa/server && medusa user -e ana@example.com -p $PASSWORD"`. MCP: `sevalla_api_update` with `execProcessCommandV3`.

**15. Add a product with an image**

Medusa's admin API signs in, uploads `mug.png` to the bucket and creates a product with it. The worker's subscriber logs the new product, which [Get application runtime logs](https://api-docs.sevalla.com/v3/applications/get-application-runtime-logs) finds:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
TOKEN=$(curl -s $APP_URL/auth/user/emailpass -H "Content-Type: application/json" \
  -d "{\"email\":\"ana@example.com\",\"password\":\"$PASSWORD\"}" | jq -r .token)
IMAGE_URL=$(curl -s $APP_URL/admin/uploads -H "Authorization: Bearer $TOKEN" -F "files=@mug.png" | jq -r '.files[0].url')
curl -s -o /dev/null -w '%{http_code} %{content_type}\n' "$IMAGE_URL"
PRODUCT_ID=$(curl -s $APP_URL/admin/products -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d "{\"title\":\"Coffee mug\",\"images\":[{\"url\":\"$IMAGE_URL\"}],\"options\":[{\"title\":\"Size\",\"values\":[\"One size\"]}]}" \
  | jq -r .product.id)
sleep 10
curl -s -G https://api.sevalla.com/v3/applications/$APP_ID/runtime-logs --data-urlencode "search=Product created: $PRODUCT_ID" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[].message'
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
200 image/png
Product created: prod_01M65XE90SZ5W701GT693DHHF5
```

CLI: `sevalla apps logs runtime "$APP_ID" --search "Product created: $PRODUCT_ID"`. MCP: `search_runtime_logs`.

### Build from the Dockerfile with the API

After you push the `Dockerfile` from [Deploy with a Dockerfile](#deploy-with-a-dockerfile), [Update application](https://api-docs.sevalla.com/v3/applications/update-application) (`updateApplicationV3`) switches the build strategy. Then repeat the **Deploy**, **Wait for the deployment** and **Check the deployment** steps:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PATCH https://api.sevalla.com/v3/applications/$APP_ID \
  -H "Authorization: Bearer $SEVALLA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."}' \
  | jq -c '{build_type, dockerfile_path, docker_context}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."}
```

CLI: `sevalla apps update "$APP_ID" --data '{"build_type":"dockerfile","dockerfile_path":"Dockerfile","docker_context":"."}'`. MCP: `sevalla_api_update` with `updateApplicationV3`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.