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

# Deployment and hosting

> Put a Store Runtime on Railway, on Vercel with Neon, or on your own Docker host, and know what to check before you point a domain at it.

<Warning>
  **In development.** 86d is being built in the open. Every capability is Experimental until it earns evidence, so check [maturity levels](/docs/resources/versioning) before you rely on anything here.
</Warning>

86d.store ships with a `Dockerfile` and a `docker-compose.yml`, and it runs on Railway or Vercel without modification. Pick the tab that matches where you want it. All three paths are you hosting a [Store Runtime](/docs/concepts/architecture) yourself, which means you also own the database, the backups, and the uptime. Those host names appear here only because you chose them.

A deployment created through [86d.app](/docs/introduction) is a different arrangement: 86d provisions the machines, runs the deployment, and bills you as a [Managed Deployment](/docs/resources/glossary#86d-cloud). That path never names the suppliers behind managed hosting, DNS, or payments. You operate 86d Cloud. See [86d Cloud plans](/docs/resources/cloud-plans) and [architecture](/docs/concepts/architecture#what-you-see-from-86d).

<Tabs>
  <Tab title="Docker (self-hosted)">
    Compose starts PostgreSQL 16, MinIO object storage, and the Store app. Copy the environment template:

    ```bash theme={null}
    cp .env.example .env
    ```

    Generate an auth secret:

    ```bash theme={null}
    openssl rand -base64 32
    ```

    Put the printed value into `.env`, replacing the placeholder:

    ```bash .env theme={null}
    BETTER_AUTH_SECRET=replace_with_generated_value
    ```

    Then start it:

    ```bash theme={null}
    docker compose up
    ```

    ### Run a release image

    Once an operator publishes a release, pull the immutable version tag for the 86d.store Store Runtime image from either registry:

    ```bash theme={null}
    docker pull ghcr.io/86d-app/store:VERSION
    # or
    docker pull docker.io/86dapp/store:VERSION
    ```

    Replace `VERSION` with the exact release version. Start the selected image with the same PostgreSQL, storage, and Store services without rebuilding source:

    ```bash theme={null}
    STORE_IMAGE=ghcr.io/86d-app/store:VERSION docker compose up -d --no-build
    # or
    STORE_IMAGE=docker.io/86dapp/store:VERSION docker compose up -d --no-build
    curl --fail http://127.0.0.1:3000/api/health
    ```

    The release path promotes `latest` only after both immutable registry tags resolve to the same multi-platform image. Pin a version or digest for a production deployment because `latest` moves on the next release.

    First boot waits for PostgreSQL, applies `internals/docker/init.sql` (nanoid/pgcrypto), runs Drizzle migrations, compiles curated Module tables, seeds demo data with `AUTO_SEED=true`, and starts the Next.js server on port 3000. Seed writes only curated Modules that compile to tables (`stripe` remains tier-none). A second container restart with `AUTO_SEED=true` must upsert the same seed-owned keys without wiping schemas or duplicating rows.

    Health is served at `/api/health` after boot. Four services come up:

    | Service       | Default port |
    | ------------- | ------------ |
    | Store app     | 3000         |
    | PostgreSQL    | 5432         |
    | MinIO API     | 9000         |
    | MinIO Console | 9001         |

    Compose sets `NODE_ENV=production`. In production the runtime rejects a placeholder secret, a known default, anything shorter than 32 characters, and anything with too little entropy, so the app will not start until you paste a real value. Paste the output of `openssl`, not the command itself.

    Uploads go to MinIO by default and are served from your own origin at `/uploads/...`, so the bucket never has to be public.

    Port conflicts are overridable:

    ```bash theme={null}
    POSTGRES_PUBLISH_PORT=5433 \
    STORE_PUBLISH_PORT=3001 \
    MINIO_API_PUBLISH_PORT=9002 \
    MINIO_CONSOLE_PUBLISH_PORT=9003 \
    docker compose up
    ```
  </Tab>

  <Tab title="Railway">
    Railway runs the Store as a container and gives you managed PostgreSQL. Use the **Deploy on Railway** button on the [GitHub repository](https://github.com/86d-store/86d).

    Two variables are required:

    | Variable             | Where it comes from                                       |
    | -------------------- | --------------------------------------------------------- |
    | `DATABASE_URL`       | Filled in by Railway when you attach a PostgreSQL service |
    | `BETTER_AUTH_SECRET` | You generate it with `openssl rand -base64 32`            |

    Set `BETTER_AUTH_SECRET` under **Variables** before the first deploy, or the container will not boot.

    For uploads, add a Railway object storage bucket and set:

    ```bash theme={null}
    STORAGE_CLIENT=s3
    S3_ENDPOINT=https://railway-bucket-endpoint.example
    S3_BUCKET=store-assets
    S3_ACCESS_KEY=your_access_key_here
    S3_SECRET_KEY=your_secret_key_here
    S3_VIRTUAL_HOSTED_STYLE=true
    ```

    <Tip>
      Railway sets `RAILWAY_PUBLIC_DOMAIN` at runtime, so you can write `APP_URL=https://$RAILWAY_PUBLIC_DOMAIN` instead of hardcoding the hostname.
    </Tip>
  </Tab>

  <Tab title="Vercel and Neon">
    Vercel hosts the Next.js app and Neon supplies PostgreSQL. Use the **Deploy on Vercel** button on the [GitHub repository](https://github.com/86d-store/86d).

    | Variable                       | Value                                              |
    | ------------------------------ | -------------------------------------------------- |
    | `DATABASE_URL`                 | Neon pooled connection string                      |
    | `DATABASE_URL_UNPOOLED`        | Neon direct connection string, used for migrations |
    | `BETTER_AUTH_SECRET`           | `openssl rand -base64 32`                          |
    | `STORAGE_CLIENT`               | `vercel`                                           |
    | `BLOB_READ_WRITE_TOKEN`        | From your Vercel Blob store                        |
    | `VERCEL_BLOB_STORAGE_HOSTNAME` | Your Blob store's public hostname                  |

    <Info>
      Neon hands you two connection strings. The pooled one is for the app and the direct one is for migrations. Using the pooled string for migrations fails in ways that are hard to read, so set both.
    </Info>
  </Tab>
</Tabs>

## Variables you will actually set

The full list is in [Environment variables](/docs/configuration/environment-variables) and in `.env.example`. These are the ones every deployment needs:

| Variable                | Required    | What it does                                                                                  |
| ----------------------- | ----------- | --------------------------------------------------------------------------------------------- |
| `DATABASE_URL`          | Yes         | PostgreSQL connection string                                                                  |
| `DATABASE_URL_UNPOOLED` | Yes         | Direct connection string, used for migrations                                                 |
| `BETTER_AUTH_SECRET`    | Yes         | Signs sessions. Generate with `openssl rand -base64 32`                                       |
| `STORE_ID`              | Recommended | Data isolation boundary for a standalone Store. Defaults to a fixed development UUID if unset |
| `STORAGE_CLIENT`        | No          | `local`, `vercel`, or `s3`. Defaults to `local`                                               |
| `APP_URL`               | No          | Your Store's public URL, for example `https://example.com`                                    |

### Values 86d Cloud supplies

A [Managed Deployment](/docs/resources/glossary#86d-cloud) receives `86D_STORE_ID`, `86D_API_URL`, and `86D_WORKLOAD_CREDENTIAL` together. The runtime trades the opaque credential at the [Control Plane](/docs/concepts/architecture) for short-lived, Store-scoped access before it fetches any managed configuration. The credential can be rotated and revoked, it carries no provider secrets, and it cannot authenticate a person.

Do not set these on a Store you host yourself. Standalone Stores use `STORE_ID` for local data isolation and read their configuration from the active [Template](/docs/concepts/templates).

Human sign-in to a managed Store Admin uses a separate pair, `86D_ADMIN_OAUTH_CLIENT_ID` and `86D_ADMIN_OAUTH_CLIENT_SECRET`. Keep every machine credential and client secret out of browser code, logs, agent output, Templates, and Store Admin settings. See [managed identity](/docs/concepts/architecture#managed-identity).

## Where uploaded files go

`STORAGE_CLIENT` decides where Product images, PDFs, and other uploads live.

| Value    | Use it for                                    | Also set                                                                  |
| -------- | --------------------------------------------- | ------------------------------------------------------------------------- |
| `local`  | Local development, self-hosted Docker         | `STORAGE_LOCAL_DIR` (default `./uploads`)                                 |
| `vercel` | Vercel deployments                            | `BLOB_READ_WRITE_TOKEN`, `VERCEL_BLOB_STORAGE_HOSTNAME`                   |
| `s3`     | MinIO, AWS S3, Cloudflare R2, Railway buckets | `S3_ENDPOINT`, `S3_BUCKET`, `S3_REGION`, `S3_ACCESS_KEY`, `S3_SECRET_KEY` |

<Warning>
  `local` writes to the container filesystem. On Vercel and other stateless hosts every deploy wipes it, which means your Product images disappear. Use `vercel` or `s3` there.
</Warning>

[Configure storage](/docs/configuration/storage) has the per-provider detail.

## Before you point a domain at it

Work through this list after the first deploy and before anyone can reach the [Storefront](/docs/concepts/storefront):

* Replace the seeded Store Admin password
* Confirm `BETTER_AUTH_SECRET` is a random value, not the placeholder
* Set `APP_URL` to the public domain
* Configure one sandbox payment provider and its webhook verification values, per [Payment integrations](/docs/guides/payment-integrations)
* Upload a Product image in Store Admin to prove storage works
* Run [`86d doctor`](/docs/cli/commands#86d-doctor) and clear every failure

<Warning>
  Finishing that list does not make this release safe for real money. No [Checkout](/docs/concepts/commerce-model), [Payment](/docs/modules/payments), tax, [Shipping](/docs/modules/shipping), [Inventory](/docs/modules/inventory), or webhook path has recorded evidence yet. Read [Versioning and maturity](/docs/resources/versioning) before you accept an Order from a stranger.
</Warning>

## Related pages

* [Environment variables](/docs/configuration/environment-variables)
* [Configure storage](/docs/configuration/storage)
* [Authentication](/docs/configuration/authentication)
* [Secure a Store Runtime](/docs/operations/security)
* [Test the Store Runtime](/docs/operations/testing)
* [Troubleshooting](/docs/operations/troubleshooting)
* [Glossary](/docs/resources/glossary)
