AstroCraft — home
AstroCraft
Buy a licence
All documentation

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.

Last updated

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:

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.

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.