Deployment
This guide covers deploying Orchard in a production environment.
Preparation
Before deploying, ensure you have:
- A server meeting the system requirements
- A domain name pointing to your server
- TLS termination (a reverse proxy such as Caddy or nginx with Let's Encrypt)
- PostgreSQL running and accessible
- Fence built and ready to run — required for a production deployment, not optional
DNS Configuration
Create DNS records pointing to your server:
orchard.example.com. A 203.0.113.10
If you want per-grove SSH subdomains, add a wildcard:
*.orchard.example.com. A 203.0.113.10
TLS with a Reverse Proxy
The Orchard API server runs plain HTTP on port 8080. Use a reverse proxy to terminate TLS:
Caddy example (Caddyfile):
orchard.example.com {
reverse_proxy localhost:8080
}
nginx example:
server {
listen 443 ssl;
server_name orchard.example.com;
ssl_certificate /etc/letsencrypt/live/orchard.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/orchard.example.com/privkey.pem;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}Authentication (Fence)
The core API defaults to authentication disabled, which is only appropriate for local development. A production deployment always runs Fence — Orchard's own OAuth2/OIDC authorization server (the fence module) — alongside the core API, so you don't need to stand up a separate identity provider just to enable it. It's a separate process, published as a JAR on each release — run both:
curl -LO https://github.com/orchard-cde/orchard/releases/latest/download/fence.jar
java -jar fence.jar --server.port=7779Fence needs a few settings pointed at your real deployment before it's production-ready:
| Env var | Purpose | Default |
|---|---|---|
FENCE_ISSUER | Public URL Fence identifies itself as. Must match what you set as issuer-uri on the core API — the default (http://localhost:<port>) only works for local/single-host setups. | http://localhost:7779 |
FENCE_SIGNING_KEY_PATH | Where Fence's JWT signing key is stored. Generated automatically on first boot if the file doesn't exist. Must be on durable storage — if this path doesn't survive a restart (e.g. it's inside an ephemeral container filesystem), Fence generates a new key every time it restarts, invalidating every previously-issued token and forcing all users to log in again. | ~/.fence/signing-key.jwk |
FENCE_REDIRECT_URI | Where Fence sends the browser back to after sign-in — your Canopy UI's callback URL. | http://localhost:3000/callback |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET | OAuth client credentials for the upstream identity provider (Google by default) that Fence delegates the actual sign-in step to. Required unless running in standalone mode. | — |
By default Fence delegates the human sign-in step to an upstream OIDC provider (Google, via the credentials above) rather than storing passwords itself. If you'd rather run Fence without wiring up an upstream provider or a full OAuth2 client registry — for local testing or a single-operator deployment — set fence.standalone.enabled: true, which trades that off for a simpler, permissive login flow. This is not recommended for a production deployment exposed to a network.
SSH Gateway
Direct SSH to a seedling (trowel grove connect, sshConnectionString) is only appropriate for local development. A production deployment always runs the SSH gateway (the gateway module), so no seedling's sshd is exposed to users. Cultivators connect to a single gateway endpoint, which authenticates them and relays the session to their grove's seedling:
ssh -i ~/.orchard/keys/default -p 2222 <grove-id>@gateway.your-domain.comThe gateway accepts either a key registered with trowel ssh-key add or a short-lived gateway token entered as the password. Orchard doesn't block direct seedling SSH on its own. The gateway is required because it's the only production setup Orchard supports. It's a separate process, published as a JAR on each release, and needs Fence and the core API running:
curl -LO https://github.com/orchard-cde/orchard/releases/latest/download/gateway.jar
java -jar gateway.jar| Env var | Purpose | Default |
|---|---|---|
GATEWAY_SSH_PORT | Port cultivators connect to. | 2222 |
GATEWAY_ADMIN_PORT | Admin/actuator port (/actuator/health). Keep it internal. | 8081 |
GATEWAY_HOST_KEY_PATH | The gateway's SSH host key. Generated on first boot if missing. Must be on durable storage, or the host key changes on every restart and clients get host-key-mismatch warnings. | ~/.orchard/gateway-host-key |
GATEWAY_INTERNAL_SSH_KEY_PATH | Private key the gateway uses to log in to seedlings. The gateway won't start if it can't read this file. | ~/.ssh/orchard_ed25519 |
GATEWAY_FENCE_ISSUER_URI | Your Fence issuer URL (same value as FENCE_ISSUER). | http://localhost:7779 |
GATEWAY_OAUTH2_CLIENT_ID / GATEWAY_OAUTH2_CLIENT_SECRET | Credentials the gateway uses to get its service token from Fence. | orchard-gateway / empty |
GATEWAY_TRELLIS_BASE_URL | Core API URL. | http://localhost:8080 |
Client secret. Fence registers the gateway as a confidential client from FENCE_GATEWAY_CLIENT_ID / FENCE_GATEWAY_CLIENT_SECRET. These must match the gateway's GATEWAY_OAUTH2_CLIENT_ID / GATEWAY_OAUTH2_CLIENT_SECRET, or the gateway can't authenticate to the core API. Both secrets default to empty, so set a strong shared secret on both sides.
Internal key. The gateway logs in to seedlings with its internal key, so that key must pair with the public key Orchard installs on seedlings. The simplest setup points GATEWAY_INTERNAL_SSH_KEY_PATH at the same private key as your provider's ssh-key-path:
- QEMU:
orchard.qemu.ssh-key-path. Seedlings getorchard.qemu.ssh-public-key(orORCHARD_SSH_PUBLIC_KEY) if it's set, otherwise<ssh-key-path>.pub. - AWS EC2:
orchard.nursery.aws.ssh-key-path. Seedlings get<ssh-key-path>.pub.
All three settings default to ~/.ssh/orchard_ed25519. Keep the private key on durable storage and readable only by the Orchard and gateway service accounts.
Network. The gateway connects to each seedling at the IP address and SSH port Orchard records for it:
- QEMU: seedlings are recorded as
127.0.0.1:<forwarded port>, so run the gateway on the same host as the core API. QEMU's port forwards listen on all interfaces, so firewall thessh-port-range(default49152–49999) from outside the host. - AWS EC2: the gateway host must reach instances on port 22. Restrict the instance security group to the Orchard server and gateway hosts.
Expose only the gateway's SSH port to cultivators.
Production Configuration
Edit application.yml (or override via environment variables) before starting:
orchard:
security:
oauth2:
enabled: true # Always enable in production
qemu:
ssh-port-range-start: 49152
ssh-port-range-end: 49999
enable-kvm: true
ssh-public-key: <your-public-key>
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://fence.your-domain.com # your Fence instanceIf you already run an external OAuth2/OIDC identity provider (Keycloak, Auth0, Okta, etc.) and prefer to use it instead of Fence, issuer-uri accepts any standards-compliant issuer — the resource server config isn't tied to Fence specifically. See API Authentication for how Trowel authenticates against Fence.
Health Checks
Monitor your deployment with the health endpoint:
curl https://orchard.example.com/api/healthExpected response:
{
"status": "healthy",
"name": "Orchard",
"version": "0.4.0"
}Check provider readiness (e.g. QEMU availability):
curl https://orchard.example.com/api/health/readyReturns 200 when the configured seedling provider is available, 503 otherwise.
Updates
Stop the running processes, update each one, then start them again. The bootRun and java -jar commands run in the foreground until stopped, so run each process in its own terminal or under a process manager such as systemd. Start the core API and Fence before the gateway, which needs both.
Core API:
git pull
./gradlew :trellis:bootRunFence:
curl -LO https://github.com/orchard-cde/orchard/releases/latest/download/fence.jar
java -jar fence.jar --server.port=7779SSH gateway:
curl -LO https://github.com/orchard-cde/orchard/releases/latest/download/gateway.jar
java -jar gateway.jar