Docs
Production deploy
Production is a single Docker host running two containers: the Last Light agent and a Caddy reverse proxy that handles HTTPS automatically. State lives in one Docker volume. The footprint is intentionally small — this is meant to run on a cheap VPS you own, not your production cluster.
LASTLIGHT_SANDBOX=docker). While that
isolation is strong, it is early-stage software and may have undiscovered
weaknesses. Put it on a host that cannot reach your internal systems,
databases, or credentials beyond what it explicitly needs.
/dev/kvm available, the
native systemd deploy
runs the harness directly under systemd and uses gondolin for sandboxing —
no Docker required. Re-deploys are a git pull plus
sudo bash deploy/native/install.sh.
Quick setup (recommended)
The fastest path from a bare server to a running instance — the setup wizard handles environment files, secrets, Docker Compose, and optional Caddy TLS:
npx lastlight setup
It walks you through entering your GitHub App credentials, domain, the
repositories the bot manages, your model provider API key
(OpenAI / Anthropic / OpenRouter), and optional Slack integration. When done it
scaffolds your private deployment overlay at instance/ —
writing instance/config.yaml (your managed repos),
instance/secrets/.env, and your PEM at
instance/secrets/app.pem — then offers to build and start the containers.
Manual setup
1. Fork and clone
git clone https://github.com/YOUR-USER/lastlight.git
cd lastlight
git remote add upstream https://github.com/cliftonc/lastlight.git
Forking is recommended so you can tweak workflows, prompts, and agent context to
match your project. Pull from upstream when you want updates.
2. Lay out your deployment overlay (instance/)
Everything specific to your deployment — managed repos, config overrides,
agent-context, and secrets — lives in a single instance/ folder next
to docker-compose.yml. It's mounted read-only at /app/instance
(LASTLIGHT_OVERLAY_DIR=/app/instance) and is never committed to the
public repo or baked into the image — so it's the natural home for a private
config repo.
mkdir -p instance/secrets
cp deploy/.env.production.example instance/secrets/.env
cp /path/to/your-app.private-key.pem instance/secrets/app.pem
chmod 600 instance/secrets/.env instance/secrets/app.pem
# instance/config.yaml — at minimum the repos the bot manages:
printf 'managedRepos:\n - your-org/repo-one\n' > instance/config.yaml Edit instance/secrets/.env and fill in at minimum:
GITHUB_APP_ID,GITHUB_APP_INSTALLATION_ID,GITHUB_APP_PRIVATE_KEY_PATH(use./app.pem— the entrypoint symlinks it)WEBHOOK_SECRETDOMAIN— your public hostname, used by Caddy for automatic TLSOPENAI_API_KEY,ANTHROPIC_API_KEY, and/orOPENROUTER_API_KEY— must match whichever providerLASTLIGHT_MODELresolves to (defaultanthropic/claude-sonnet-4-6). One OpenRouter key covers most providers if you want a single billing surface.
Both the agent and caddy services read
instance/secrets/.env via env_file, and the entrypoint
sources it inside the container — so no repo-root .env is
needed. instance/config.yaml is merged over the public
config/default.yaml at startup (maps deep-merge, arrays like
managedRepos replace, env vars override). Edit any overlay file and
docker compose restart agent to apply — no rebuild.
See the Configuration reference for every
variable the harness understands, including optional knobs like
LASTLIGHT_MODEL, LASTLIGHT_MODELS,
LASTLIGHT_THINKING, LASTLIGHT_SANDBOX,
APPROVAL_GATES, ADMIN_PASSWORD, and the Slack
OAuth group.
OPENCODE_* env vars (OPENCODE_MODEL,
OPENCODE_MODELS, OPENCODE_VARIANT,
OPENCODE_VARIANTS) are still read as fallbacks for the matching
LASTLIGHT_* name. You can leave existing .env files
in place and rename at your leisure.
3. Build and start
# Point Caddy at your domain (in instance/secrets/.env)
echo "DOMAIN=lastlight.example.com" >> instance/secrets/.env
# Build and start both containers
docker compose build agent
docker compose up -d
# Tail the logs
docker compose logs -f agent
Caddy reads DOMAIN from the environment and provisions a Let's
Encrypt certificate on first start. DNS must already resolve to the host.
4. Verify it's running
curl https://lastlight.example.com/health
# { "status": "ok" }
# Invalid webhook signature — returns 401, confirms the listener works
curl -X POST https://lastlight.example.com/webhooks/github -d '{}'
Open https://lastlight.example.com/admin in a browser and check the
Home tab. You should see live activity stats, recent workflows, and resource usage.
5. Wire up the webhook
Back in your GitHub App settings:
- Webhook URL →
https://lastlight.example.com/webhooks/github - Webhook secret → same value as
WEBHOOK_SECRETininstance/secrets/.env - Make sure Active is enabled.
Create a test issue on a repo where the App is installed; Last Light should react within a few seconds and you'll see a new run appear on the dashboard Workflows tab.
State and persistence
All persistent state lives under /app/data inside the container,
mounted as a Docker volume:
lastlight.db— SQLite: executions, workflow runs, approvals, messaging sessions, rate limits, system status.agent-sessions/projects/— JSONL session files (Claude-SDK-style envelope written byevent-shim.ts— the full audit trail for every run, read by the dashboard). Override the location withLASTLIGHT_SESSIONS_DIR.sandboxes/— cloned repos, one per task (gondolin or docker).sandbox-data/— shared volume mounted into docker-mode sandboxes.logs/— structured harness logs.secrets/app.pem— mode-600 copy of the GitHub App key, used by sandbox containers via the shared volume.
Back this volume up if you care about the audit trail.
Updating
Updating lastlight itself (source, built-in assets) is a rebuild:
git fetch upstream
git merge upstream/main
docker compose build agent
docker compose up -d agent
Updating your overlay (anything in instance/ — managed
repos, config, agent-context) is just a restart, no rebuild:
# edit instance/config.yaml (or git pull in instance/), then:
docker compose restart agent Sessions and the database survive rebuilds because they live in the volume, not the image.