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

# Deploy without Git

> Build and deploy a hosted server from an uploaded source archive.

Horizon can build a hosted server from a gzipped source archive instead of a Git
repository. Archive deployments work well for prototypes, one-off utilities, and
servers that do not need repository automation.

Each version records the archive's SHA-256 digest, so the build always refers to
one exact set of uploaded bytes. You can move the project to a
[Git source](/platform/build-system#git-sources) later without changing its
serving URL.

You create archive versions and deployments through the Horizon API. After a
deployment is created, the project's **Deployments** page shows its build and
deployment status, source, logs, and rollback actions.

## Prerequisites

You need `curl`, `jq`, `tar`, and `shasum`, plus a Horizon
[API key](/api-keys) with the `write:projects` organization permission. Use the
[generated API reference](https://horizon.prefect.io/api/v0/docs) for current
request and response schemas.

Collect these values before starting:

* organization slug
* project slug and display name
* source directory
* entrypoint, including the server object when needed
* dependency file, when automatic discovery is insufficient
* deployment target, normally `production`

Inspect the entrypoint locally and run the project's checks before uploading its
source:

```bash theme={null}
fastmcp inspect path/to/server.py:mcp --json
```

## Create the project

Use the API reference's organization operations to select the organization by
its exact slug and confirm that the API key has `write:projects`. Horizon
conceals unauthorized writes with `404`, so check the permission before changing
an endpoint that appears to be missing.

Use the project operations to find a project with the requested slug. Reuse it
when it exists. Otherwise, create one hosted project. The project slug determines
the serving hostname, and Horizon creates a production target with the project.
Keep the project and target IDs for future updates.

## Build the archive

From the server's source directory, create the archive in a temporary directory.
Exclude credentials, dependencies, build output, caches, and local agent state.
Inspect the result before uploading it.

```bash theme={null}
WORKDIR=$(mktemp -d)
trap 'rm -rf "$WORKDIR"' EXIT
ARCHIVE="$WORKDIR/source.tar.gz"

tar -czf "$ARCHIVE" \
  --exclude='.env' \
  --exclude='.env.*' \
  --exclude='.git' \
  --exclude='.venv' \
  --exclude='node_modules' \
  --exclude='dist' \
  --exclude='coverage' \
  --exclude='__pycache__' \
  --exclude='.pytest_cache' \
  --exclude='.agents' \
  --exclude='.pi' \
  .

if tar -tzf "$ARCHIVE" | grep -Eq '(^|/)\.env($|[./])'; then
  echo "refusing to upload an archive containing an environment file" >&2
  exit 1
fi

SIZE=$(wc -c < "$ARCHIVE" | tr -d ' ')
SHA=$(shasum -a 256 "$ARCHIVE" | cut -d' ' -f1)
```

The archive size and hexadecimal SHA-256 identify the upload. Keep the temporary
archive until Horizon accepts it, then let the shell cleanup remove it.

## Upload the archive

Use the project's source-upload operation to request a presigned URL with the
archive size and digest. Upload the archive bytes to that URL before creating a
version.

The upload response defines the signed request. Send every returned header
without changing or recalculating it. Horizon converts the hexadecimal digest to
the checksum value expected by storage.

## Build the version

Use the version operation to create an archive-source version with:

* the uploaded archive's SHA-256 digest
* the server entrypoint
* the dependency file, when one is required

Creating the version starts the build immediately, which is why the archive must
already be available. Wait for the version to become `ready` or `failed`. A ready
version includes the manifest Horizon discovered from the entrypoint. If the
build fails, inspect its error and build logs before creating another version.

## Deploy the version

Set required runtime values through the project's
[environment variables](/environment-variables) before the first production
deployment. These values stay outside the source archive.

Use the target operations to select the requested target, then create a
deployment for the ready version. A deployment is active while its status is
`queued` or `deploying`. It is complete when it leaves those states:

* `succeeded` means the target serves the new version
* `failed` means the deployment did not replace the serving version
* `superseded` means a newer deployment replaced it before it started

The **Deployments** page shows the same result and links to the deployment and
build logs.

## Verify the server

Read the serving URL from the target after the deployment succeeds. Protected
deployments accept the Horizon API key as a bearer credential. List the server's
tools, then call one safe tool and check its result.

```python theme={null}
import asyncio
import os

from fastmcp import Client


async def main():
    async with Client(
        os.environ["HORIZON_SERVING_URL"],
        auth=os.environ["HORIZON_API_KEY"],
    ) as client:
        print([tool.name for tool in await client.list_tools()])


asyncio.run(main())
```

## Update the server

Reuse the project and target. Create and upload a new archive, build a new
version, deploy that version, and verify the serving URL again. Each version
remains tied to its archive digest, and the project keeps the same serving URL.
