# Deploying your code

One command deploys the directory it's run in. It works the same from your computer, from a coding agent and from CI: there is no connection to GitHub or any other host to set up, and nothing about your repository is stored here but the code you push.

```bash
alf launch     # the first time: sets the directory up and deploys it
alf deploy     # every time after
```

That's the [command line](/docs/cli). It doesn't wait for the deployments: once the code is sent it says the project is being deployed and prints its link, where the deploy is followed live (`--wait`, or CI, waits). Where it isn't installed (CI, an agent's sandbox, a server), [`deploy.sh`](#without-the-command-line) does the same with nothing to install.

## What it deploys

The command looks at the directory:

| It finds | It deploys |
| --- | --- |
| An [`alf.toml`](/docs/alf-toml) that lists services | Those, with the settings it gives them, and the databases, buckets and computers it names. |
| A docker-compose file (`docker-compose.yml`, `docker-compose.yaml`, `compose.yml`, `compose.yaml`) | Every service in it: `build:` ones are built from your code, `image:` ones run that image. See [docker-compose stacks](/docs/compose). |
| Otherwise a `Dockerfile` | One service, built from it, named after the directory (or `--service <name>`). |

Services the project already has get the new code. Missing ones are created: public on the port the image declares, unless the compose file says otherwise. Without an alf.toml, a push brings code, not settings: variables, domains, scaling and everything else you set in the dashboard stay as they are. With one, the settings it names are set too, and those it doesn't name stay. Nothing is ever deleted by a push.

Before anything is sent, the command prints the plan:

```
Project: shop
  ~ web: deploy the pushed code (built from web/Dockerfile)
  + worker: new service, built from worker/Dockerfile, private
  = db: unchanged (postgres:17)
```

For what's new or resized, the plan also says what it costs: its size, so much an hour awake (and, for one that never sleeps, a month), and nothing while asleep.

Then it packs the code, sends it, and waits. From the [command line](/docs/cli), only the files that changed since the services' last push are sent (nothing at all when none did), and their server puts the code together again: a deploy of a large repository after a small change sends a few kilobytes. Your server builds the images; the build log is live in each service's **Deployments** tab, and a preview's services are built on the servers its project's are, where their build cache is. When a deployment fails, the command prints why, with the container's last output, and exits with an error.

**What's sent.** In a git repository: the repository's files, including uncommitted changes, without ignored files (so `node_modules`, build output and `.env` files in your `.gitignore` stay home). Elsewhere: the directory, without `.git` and `node_modules`. `alf deploy` also sends the files of submodules that are checked out (a site's theme, say), leaves out what the builds' `.dockerignore` does (Docker wouldn't see it anyway), and names any file of 50 MB or more that goes up, so data dumps and the like can be left out. Large code is fine: it goes up in parts, up to 8 GB compressed. (Gigabytes of "code" are usually data, models or build output: what the image doesn't need is better left out, and what it does need makes every deploy as slow as that upload. For the heaviest cases, build the image in CI, push it to a registry and deploy the image.) Paths are from the repository's root, so a monorepo works from any subdirectory.

## From your computer

```bash
cd ~/code/shop
alf deploy                 # what alf.toml (or the compose file, or the Dockerfile) has
alf deploy web             # only the service web
alf deploy --project shop  # without an alf.toml: which project
```

In a project, **+ Create → Your code** (or, for one service, **Deployments → Deploy new code**) shows the command for it.

### Without the command line

The same page has **a one-time line, nothing to install**: `deploy.sh` with a link good for 15 minutes, so no token is stored on the machine. It needs `sh`, `curl` and `tar`:

```bash
curl -fsSL https://runalf.com/deploy.sh | sh -s -- "https://api.runalf.com/api/v1/push/…"
```

A service's own link always deploys to that service, whatever else the directory holds. [Coding agents](/docs/agents) get this line from their `push_code` and `upload_code` tools.

## From CI

On GitHub, one command in the code's directory sets it up:

```bash
alf ci github
```

It writes the two workflows below and, once you say yes, makes a deploy token for the project for the repository's secret `SMARTALFRED_TOKEN`: saved there with the GitHub CLI when it's installed and signed in (the token is never shown), else shown once with the page to paste it in. See [the command line](/docs/cli#deploying-from-github).

By hand, or in another CI: CI uses a **deploy token** instead of a link. In **+ Create → Your code → From CI → Another CI, or by hand**, create one for the project (or make one under **Agents**: deploy access, limited to the project) and save it in your CI as the secret `SMARTALFRED_TOKEN`. In CI the command is `deploy.sh`, which needs nothing installed.

### GitHub Actions

```yaml
# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Alf
        run: curl -fsSL https://runalf.com/deploy.sh | sh
        env:
          SMARTALFRED_TOKEN: ${{ secrets.SMARTALFRED_TOKEN }}
          SMARTALFRED_PROJECT: shop
```

Every push to `main` is deployed; the job fails when the deployment does, and its summary shows what happened. The deployment carries the commit and its message.

### A preview for every pull request

```yaml
# .github/workflows/preview.yml
name: Preview on Alf
on:
  pull_request:
    types: [opened, synchronize, reopened, closed]
concurrency:
  group: smartalfred-preview-${{ github.event.number }}
jobs:
  preview:
    # Pull requests from forks get no secrets, so they're skipped.
    if: github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest
    # The comment is written with the job's own token: nothing to install on GitHub.
    permissions:
      contents: read
      pull-requests: write
    env:
      SMARTALFRED_TOKEN: ${{ secrets.SMARTALFRED_TOKEN }}
      SMARTALFRED_PROJECT: shop
    steps:
      - uses: actions/checkout@v4
        if: github.event.action != 'closed'
      - name: Deploy the preview
        id: deploy
        if: github.event.action != 'closed'
        run: curl -fsSL https://runalf.com/deploy.sh | sh -s -- --preview pr-${{ github.event.number }}
      - name: Say where it is on the pull request
        if: github.event.action != 'closed' && steps.deploy.outputs.urls != ''
        env:
          GH_TOKEN: ${{ github.token }}
          URLS: ${{ steps.deploy.outputs.urls }}
          SHA: ${{ github.event.pull_request.head.sha }}
        run: |
          marker='<!-- smartalfred-preview -->'
          body="$(printf '%s\n' "$marker"; printf '**Preview** of %s, live on Alf:\n\n%s\n' "${SHA:0:7}" "$(printf '%s\n' "$URLS" | sed '/^$/d; s/^\([^ ]*\) \(.*\)$/- **\1**: \2/')")"
          comments="repos/$GITHUB_REPOSITORY/issues/${{ github.event.number }}/comments"
          id="$(gh api --paginate "$comments" --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n 1)"
          if [ -n "$id" ]; then gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -f body="$body" >/dev/null
          elif [ "${{ github.event.action }}" != closed ]; then gh api "$comments" -f body="$body" >/dev/null; fi
      - name: End the preview
        if: github.event.action == 'closed'
        run: curl -fsSL https://runalf.com/deploy.sh | sh -s -- --end-preview pr-${{ github.event.number }}
      - name: Say it ended on the pull request
        if: github.event.action == 'closed'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          marker='<!-- smartalfred-preview -->'
          body="$(printf '%s\n' "$marker"; printf 'The preview of this pull request ended when it closed.\n')"
          comments="repos/$GITHUB_REPOSITORY/issues/${{ github.event.number }}/comments"
          id="$(gh api --paginate "$comments" --jq ".[] | select(.body | startswith(\"$marker\")) | .id" | head -n 1)"
          if [ -n "$id" ]; then gh api -X PATCH "repos/$GITHUB_REPOSITORY/issues/comments/$id" -f body="$body" >/dev/null
          elif [ "${{ github.event.action }}" != closed ]; then gh api "$comments" -f body="$body" >/dev/null; fi
```

`--preview` deploys to a [preview](/docs/previews) of the project with that name, and makes it the first time: a copy of the project at its own addresses. The steps after it put the addresses (`deploy.sh` hands them on as the output `urls`) in a comment on the pull request, one that's updated with each push and says when the preview ended, with the job's own token: nothing to install on GitHub. Pull requests from forks get no secrets, so they're skipped.

### Any other CI

The command is the same everywhere: check the code out, set `SMARTALFRED_TOKEN`, run it.

```yaml
# .gitlab-ci.yml
deploy:
  image: alpine/git
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  script:
    - apk add --no-cache curl
    - curl -fsSL https://runalf.com/deploy.sh | sh -s -- --project shop
```

It needs `sh`, `curl`, `tar` and (in a repository) `git`.

## Options

Each option can also be set in the environment, as `SMARTALFRED_` and its name: `--keep-staged` is `SMARTALFRED_KEEP_STAGED=1`.

| Option | |
| --- | --- |
| `--project <name>` | The project: its name, address part or id. Not needed with a link, a token made for one project, or an `alf.toml` that names it. |
| `--service <name>` | With a Dockerfile: the service to deploy to (a new name creates it). With a compose file: only that service of it. |
| `--config <path>` | Use this project file instead of `alf.toml`. |
| `--no-config` | Leave `alf.toml` out. |
| `--compose <path>` | Use this compose file instead of the one found. |
| `--profile <names>` | The compose file's [profiles](/docs/compose#profiles) to deploy, comma-separated. Its services in other profiles are left out, as compose does. |
| `--dockerfile <path>` | Use this Dockerfile; a compose file and `alf.toml` are ignored. |
| `--context <dir>` | The directory to build in. Default: the Dockerfile's. |
| `--preview <name>` | Deploy to the project's preview of that name, made if it isn't there. |
| `--end-preview <name>` | End that preview, and do nothing else. |
| `--candidate` | Start new versions [beside the live ones](/docs/deployments#candidates-try-a-version-before-it-s-live) and wait to be promoted. |
| `--keep-staged` | Leave [staged variable and secret changes](/docs/variables-and-secrets) staged. By default they go live with the deployment. |
| `--config-wins` | Settings changed in the dashboard since `alf.toml` set them go back to the file's. By default [they stay](/docs/alf-toml#what-a-deploy-does-with-it), and the deploy says which. |
| `--group <name>` | Show newly created services as one [group](/docs/one-click#groups). |
| `--message <text>` | Shown with the deployment. Default: the last commit's subject. |
| `--plan` | Print the plan and stop: nothing is sent or changed. |
| `--no-wait` | Don't wait for the deployments. |

A directory can be given last: `… | sh -s -- --project shop ./apps/web`.

## Where the Dockerfile is

For a new service it's the `Dockerfile` in the directory the command runs in, built in that directory. After the first push the service remembers where its Dockerfile is, so pushing from another directory of the same repository still builds the right one. `--dockerfile` (and `--context`) change it; so do the **Dockerfile** and **Build directory** fields in the service's settings.

## A service that waits for its code

**+ Create → Service → Your code** makes a service without deploying anything: useful when you want to set its variables, secrets or volumes first. It's deployed with the first push to it.

## Tokens and safety

- A deploy token made for one project can't touch another. Tokens can carry [limits](/docs/agent-safety), and a project that requires approval for agents' changes refuses pushes with tokens; a person's link still works.
- A push shows up in the token's activity under **Agents**.
- The link or token authorizes the push; after that, the push has its own one-hour address for sending the code and asking how it went.
