# Deploying the CMS

> Deploy AstroCraft as one container holding a real clone, plus two branch builds of your public site. Notes for Dokploy, Coolify and Railway.

Source: https://editor.astrocraftthemes.com/docs/deployment/

Knowing which of two things you are deploying is most of this page.

**One container runs the admin.** It holds a real, full clone of your repository, checked out on `staging`, because the CMS is a git layer: a save is a commit, a landed commit auto-pushes `staging`, and Publish fast-forwards your publish branch. Point `cms.<your-domain>` at it — the invite and verification links the CMS mails are built on that host.

**Two branch builds are your public site**, on whatever static platform you already use. The **publish branch with no flags** is production. The **staging branch built with `PREVIEW=1`** is the preview that shows drafts. Both rebuild on push, which is why the CMS needs no deploy API: publishing _is_ a fast-forward of the branch production watches.

The target shape is the self-hosted PaaS row — Dokploy, Coolify, Railway — which are the same thing wearing three UIs: build a Docker image, attach a persistent volume, inject environment.

## Once, before the first boot

Create the staging branch if you do not have one. The clone asks for it by name:

```bash
git push origin main:staging
```

Mint the push credential: a **fine-grained PAT** scoped to this one repository with _Contents: read and write_. That is the one form all three platforms share. An SSH deploy key works instead.

If invites and sign-up verification should mail, have your Resend key and a verified sender ready.

## The container

Build from the repository's `Dockerfile`. The image holds the toolchain and an entrypoint; the **checkout lives on the volume** at `/data/site`, so unpushed commits and the accounts store survive every redeploy together.

First boot clones and builds — give it a few minutes. Later boots reuse the clone and rebuild.

| Variable                                     | Required               | What it is                                                   |
| :------------------------------------------- | :--------------------- | :----------------------------------------------------------- |
| `GIT_URL`                                    | yes                    | your repository's HTTPS clone URL                            |
| `GIT_PUSH_TOKEN`                             | one of these two       | the fine-grained PAT                                         |
| `GIT_SSH_COMMAND`                            |                        | e.g. `ssh -i /run/secrets/deploy_key`, with an SSH `GIT_URL` |
| `GIT_COMMITTER_NAME` / `GIT_COMMITTER_EMAIL` | yes                    | the deployment's committer identity                          |
| `RESEND_API_KEY` / `MAIL_FROM`               | no                     | the two mail paths                                           |
| `GIT_BRANCH`                                 | no (default `staging`) | the branch the checkout lives on                             |
| `ASTROCRAFT_DB_URL` / `ASTROCRAFT_DB_TOKEN`  | no                     | move the store into a database                               |
| `PORT` / `HOST`                              | no (4321 / 0.0.0.0)    | the standalone server                                        |

Commits are **authored** by the signed-in account and **committed** by that identity, which is what keeps `git log` and `git blame` worth reading.

Attach a volume at **`/data`**. Map your `cms.` subdomain to the container's port. Create the first admin with the accounts CLI inside the container, or sign up and promote.

> Run that CLI **from inside the checkout** (`/data/site`). The `.accounts/` store is resolved against the process's working directory, so running it from `/` writes an account into a store the server will never read — and the server will still offer first-admin sign-up. The entrypoint starts the server inside the checkout for the same reason.

The push credential reaches git through the environment, never through argv and never written into `.git/config`.

## The public site's two builds

Configure two deploys of the same repository:

- **Publish branch**, plain `pnpm build` → production. Point `workspace.domain` at it.
- **Staging branch**, same command plus `PREVIEW=1` → the preview. Paste its URL into Settings → _Staging preview_.

Nothing to configure for the live check, as long as your `/` page carries the build stamp.

## What lives where

Two facts are deliberately **not** environment variables: the publish branch and the staging preview URL. Both are settings an admin sets on screen, living on the volume with everything else. A deployment restart must not be the way you change which branch you publish to.

With `ASTROCRAFT_DB_URL` set, the volume holds only the checkout — which the entrypoint re-clones anyway — so the machine becomes disposable. See [Bring a database](/docs/database/).

## The platforms

**Dokploy** — an _Application_ from the git repository, build type Dockerfile; env from the table; a _Volume Mount_ at `/data`; the `cms.` domain on port 4321. The two public deploys are two more applications watching `main` and `staging`, the second with `PREVIEW=1`.

**Coolify** — new resource → _Dockerfile_ from the repository; env from the table; _Persistent Storage_ at `/data`; domain on 4321. Branch builds as two _Static_ resources with the same split.

**Railway** — new service from the repo (it detects the Dockerfile); variables from the table; a _Volume_ at `/data`; a domain on 4321. Branch builds via two more services or your static host.

## The fresh-machine check

From nothing but the repository URL, this is the loop closing:

1. The container boots and `cms.<domain>/admin/` signs in.
2. A save commits — the entry's History tab shows it — and the commit reaches the remote on its own within a few seconds. The site panel's unpushed count drains.
3. The **staging deploy shows the draft and production does not**.
4. Publish fast-forwards the publish branch, and _"Your changes are live ✓"_ appears when the production build lands.
5. After a forced redeploy of the container, the same account signs straight back in and the settings read what was chosen — the store survived.

When that closes on one platform, the deployment is done.

## Troubleshooting

**Clone fails with _Remote branch staging not found_.** The one-time `git push origin main:staging` above.

**Publish answers "no identity".** `GIT_COMMITTER_NAME` / `GIT_COMMITTER_EMAIL` are unset.

**A push is rejected.** The next step is on screen — _Update from the site's repository_. Only genuine divergence, where both sides hold commits the other lacks, still needs a terminal.

**The container's own copy of the public site is stale.** It rebuilds **at boot**, not on push. If you serve the public site from the container rather than from branch builds, restart it on publish — all three platforms can watch the publish branch — or accept that its public copy trails until the next boot. The admin, the previews and the live check are unaffected either way.

**Everybody was logged out by a redeploy, and settings reset.** The volume did not survive, or is not mounted at `/data`. The store lives inside the checkout unless you moved it to a database.

**The build fails inside the publish gate but works locally.** The gate builds into a throwaway directory under `node_modules/`, deliberately — a build written to `/tmp` fails, because Astro runs its prerender entry from inside the output directory and Node then walks up looking for `node_modules` and finds none.