mehrancodes/laravel-harbor
Laravel Harbor helps you deploy Laravel applications with Docker, offering Artisan commands and configuration for building, provisioning, and releasing your app to a server in a repeatable container-based workflow.
Harbor is a dev-only CLI that spins up a throwaway Forge site per pull request, so reviewers get a working URL before merge. Treat it as CI infrastructure, not application code.
composer require mehrancodes/laravel-harbor --dev. Harbor v2 requires PHP 8.2+ — if your workflow uses kirschbaumdevelopment/laravel-test-runner, use :8.3; the older :8.1 examples in the docs will break.config/harbor.php and a GitHub Actions workflow. That workflow file is the real surface area — read it before you read the PHP.FORGE_API_TOKEN (and a Forge server/organisation already provisioned), plus GIT_TOKEN if you want Harbor to register deploy keys and post PR/MR comments for you. The default GITHUB_TOKEN in Actions is not sufficient for deploy keys.config/harbor.php (site naming, directory, server), then the CI log — Harbor's output is line-prefixed INFO / WARNING / FAIL, and the prefix tells you whether provisioning continued.pull_request (opened/synchronize/reopened), teardown on closed. Add the concurrency group from the v2.1.0 notes at the same time — see Gotchas.The two docs pages worth bookmarking: Git Providers and Announcement Comments.
Two git settings, not one. Since v2.1.0 Forge and Harbor are configured separately, and conflating them is the main source of confusion:
| Setting | Consumer | Purpose |
|---|---|---|
FORGE_GIT_PROVIDER |
Forge | how the site clones the repo (default github) |
FORGE_GIT_API_PROVIDER |
Harbor | which API Harbor uses for deploy keys + PR/MR comments |
FORGE_GIT_API_PROVIDER defaults to FORGE_GIT_PROVIDER. Keep it unset for plain GitHub/GitLab.com; set it explicitly only for self-hosted hosts.
Deploy keys: three modes, pick deliberately.
FORGE_DEPLOY_KEY=true plus a reachable GIT_TOKEN. Harbor generates a keypair, registers the public half on the provider, passes both halves to Forge, and removes the key on teardown. Cleanest for GitHub/GitLab.com.FORGE_DEPLOY_KEY=true with FORGE_DEPLOY_KEY_PUBLIC / FORGE_DEPLOY_KEY_PRIVATE (store the private half as a secret). Use when Harbor can't reach your Git API, e.g. an internal GitLab behind a firewall.GIT_TOKEN. Harbor prints the public key; someone adds it to the repo. Fine for a one-off, untenable across a team.Self-hosted GitLab, the canonical non-default setup:
FORGE_GIT_PROVIDER: custom # Forge clones via the custom URL
FORGE_GIT_REPOSITORY_URL: https://gitlab.example.com/group/app.git
FORGE_GIT_API_PROVIDER: gitlab-custom
GIT_API_URL: https://gitlab.example.com/api/v4
GIT_TOKEN: ${{ secrets.GITLAB_TOKEN }}
FORGE_DEPLOY_KEY: true
With FORGE_GIT_PROVIDER=custom Harbor infers the API from a github.com / gitlab.com repository URL, so the explicit provider is only needed for non-hosted domains.
Commenting on PRs/MRs needs four things together: a configured Git API provider, GIT_TOKEN, GIT_COMMENT_ENABLED=true, and GIT_ISSUE_NUMBER. On GitLab, GIT_ISSUE_NUMBER is the merge request iid (project-scoped), not the global id.
Migrate the env var now, not later. FORGE_GITHUB_DEPLOY_KEY still works as an alias but emits a deprecation warning and is slated for removal in a future major:
- FORGE_GITHUB_DEPLOY_KEY: true
+ FORGE_DEPLOY_KEY: true
Dry-run locally. Run ./vendor/bin/harbor provision from your machine with the same env vars against a scratch PR/branch to iterate on config without burning CI minutes — just remember it will create a real Forge site and a real deploy key, so tear down afterwards.
Add a per-PR concurrency group — this is a correctness fix, not a nicety. Two runs for the same PR (back-to-back pushes, or reopening a PR right after pushing) race to create the same site and one dies with site: The site must be installed before it can be deployed. Put it on both provision and teardown:
concurrency:
group: harbor-${{ github.event.pull_request.number }}
cancel-in-progress: false # false: cancelling mid-provision leaves an orphan site
403 Forbidden while registering the deploy key is a permissions problem, and the message names the fix. For a GitHub App token (actions/create-github-app-token): Repository permissions → Administration: Read & write, then accept the updated permissions on the installation — granting without accepting is the classic trap. For a PAT: repo scope. GITHUB_TOKEN can never do this.
"Deploy key not registered automatically" is a warning, not a failure. Harbor created the site with a valid keypair and printed the public key. Provisioning continues and the site works if you add the key to the repo. Cause is either a missing GIT_TOKEN or FORGE_GIT_PROVIDER=custom pointing somewhere Harbor can't infer an API for. Don't retry — configure the API or paste the key.
Provider validation is now strict and fails fast (before Forge is called). FORGE_GIT_PROVIDER must be one of github, gitlab, gitlab-custom, bitbucket, custom; custom additionally requires FORGE_GIT_REPOSITORY_URL. A typo that used to leak into a confusing Forge error now aborts early — that's an improvement, but it will surface latent typos on upgrade.
Silent-comment debugging order: API provider set? GIT_TOKEN valid for that host? GIT_COMMENT_ENABLED=true? GIT_ISSUE_NUMBER present and, on GitLab, the MR iid? Any one missing gives you a WARNING … no comment was posted and nothing else.
Teardown must run. Deploy keys and Forge sites are created eagerly; if the teardown job never fires (PR closed by a deleted branch, cancelled workflow), you own a dangling site and a deploy key on the repo. Sweep periodically, and never use cancel-in-progress: true on provision.
Extension points are thin on purpose. The site directory is now configurable (PR #131) and site naming lives in config/harbor.php; beyond that, customisation happens by shaping the env vars per job (e.g. a matrix over providers) rather than subclassing anything. If you find yourself wanting to fork internals, you probably want a second workflow instead.
How can I help you explore Laravel packages today?