Stand up your own software factory: fork the repo, drop in your secrets, docker compose up. Caddy handles TLS automatically.
For the authoritative step-by-step — including every optional env var, Slack OAuth login, approval gates, and the dashboard password — read Production deploy in the docs.
Last Light is a powerful autonomous software factory. While it runs all code in isolated per-phase sandboxes (gondolin micro-VMs by default, Docker containers on opt-in), it is early-stage software that may have undetected security issues. Deploy it on an isolated host — a cheap VPS, a separate cloud project, or a dedicated VM — where it cannot reach sensitive internal systems, databases, or credentials beyond what it explicitly needs.
Fork Last Light so you can customize skills, tweak prompts, and track your own changes. Pull upstream updates whenever you want.
# Fork on GitHub first, then clone your fork
git clone https://github.com/YOUR-USERNAME/lastlight.git
cd lastlight
# Add upstream so you can pull updates later
git remote add upstream https://github.com/nearform/lastlight.git Your skills, deployment config, and Dockerfile are yours to modify. The skills/ directory is where all the behavior lives — edit freely.
Everything deployment-specific — managed repos, config overrides, and secrets — lives in a single instance/ folder, gitignored and mounted read-only at /app/instance. It's never baked into the image, so it's the natural home for a private config repo. (npx lastlight setup scaffolds all of this for you.)
# Secrets (gitignored, host-only)
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
# Overlay config — at minimum the repos the bot manages
printf 'managedRepos:\n - your-org/repo-one\n' > instance/config.yaml Edit instance/secrets/.env with your:
managedRepos in instance/config.yaml (the public default ships empty)./app.pem)localhost for dev)OPENAI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY matching your LASTLIGHT_MODEL (Last Light runs on any model the pi coding agent supports — Claude, GPT, Gemini, Llama, and more; default anthropic/claude-sonnet-4-6; one OpenRouter key reaches most of them)LASTLIGHT_SANDBOX selects gondolin (default — QEMU micro-VM), docker, or noneLASTLIGHT_MODELS as JSON to route architect/executor/reviewer phases to different provider/model stringsLASTLIGHT_THINKING as the default (off / minimal / low / medium / high / xhigh), or LASTLIGHT_THINKINGS JSON for per-phase overridesAPPROVAL_GATES=post_architect,post_reviewer to pause at human-in-the-loop checkpointsADMIN_PASSWORD and a stable ADMIN_SECRET for persistent login sessionsSLACK_BOT_TOKEN and SLACK_APP_TOKEN for the in-process chat skill (Socket Mode)SLACK_OAUTH_CLIENT_ID, SLACK_OAUTH_CLIENT_SECRET, SLACK_OAUTH_REDIRECT_URI, and optionally SLACK_ALLOWED_WORKSPACE to restrict login to one teamSee the Configuration reference for every variable the harness reads, with defaults and descriptions.
The harness Docker image bundles agentic-pi as an npm dep; all skills and dependencies are baked in, and GitHub tooling is built into the agent (no separate MCP process to run). Secrets are injected at runtime via the mounted volume.
On a Linux host with /dev/kvm exposed, 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. Most managed container hosts (Cloud Run, Fly Machines, shared-CPU VMs) don't expose nested virt — stay on the Docker deploy below in those cases.
# DOMAIN lives in instance/secrets/.env (read by Caddy via env_file) —
# no repo-root .env needed.
# Build and start
docker compose up -d
# Watch the logs
docker compose logs -f agent No interactive login step — pi-ai reads the OPENAI_API_KEY / ANTHROPIC_API_KEY / OPENROUTER_API_KEY you put in instance/secrets/.env and forwards them into each sandbox.
What happens:
DOMAIN=localhost — Caddy serves HTTP only, no TLSVerify it's running:
# Health check
curl https://lastlight.example.com/health
# Or locally
curl http://localhost/health If you already run Caddy, Nginx, or another reverse proxy on the host, the bundled Caddy container will fail with "address already in use" on ports 80/443. Instead, disable it and point your existing proxy at the agent:
1. Create a docker-compose.override.yml to disable the Caddy service and expose the agent port locally:
# docker-compose.override.yml
services:
agent:
ports:
- "127.0.0.1:8644:8644"
caddy:
profiles:
- disabled 2. Add a reverse proxy entry in your existing config. For Caddy:
lastlight.example.com {
reverse_proxy localhost:8644
} For Nginx:
server {
listen 443 ssl;
server_name lastlight.example.com;
location / {
proxy_pass http://127.0.0.1:8644;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
} 3. Start only the agent: docker compose up -d agent
In your GitHub App settings, set the webhook URL and subscribe to events.
https://lastlight.example.com/webhooks/githubWEBHOOK_SECRET in your instance/secrets/.envInstall the GitHub App on the repos (or org) you want Last Light to manage. Test by opening an issue — the bot should triage it within seconds.
Which image you need depends on the sandbox backend. The default gondolin backend runs each phase in a QEMU micro-VM, so it needs QEMU in the image and /dev/kvm on the node — use the lastlight-agent-qemu variant. The base lastlight-agent image ships no QEMU: it targets the docker backend, which wants a Docker socket that containerd-based clusters don't expose.
A complete, kubectl apply -k-able example lives in apps/server/deploy/k8s/ — namespace, the KVM device plugin, Deployment, PVC, and Service. It exposes /dev/kvm to an otherwise-unprivileged pod via the squat/generic-device-plugin DaemonSet (resource devic.es/kvm), adds the one SYS_RESOURCE capability gondolin's QEMU needs (which requires the namespace to run at privileged PodSecurity), and keeps the guest-image cache on the PVC. Follow its README for the edits it needs (image tag, Secret, StorageClass, ingress, managed repos).
Until #210 Gap 2 lands, the gondolin guest image lacks gitleaks/semgrep, so security-review falls back to a manual pass.
Create the K8s Secret from your local files: kubectl create secret generic lastlight-secrets --namespace lastlight --from-file=.env=instance/secrets/.env --from-file=app.pem=instance/secrets/app.pem
Pull upstream changes, rebuild the image, restart:
git fetch upstream
git merge upstream/main
docker compose build
docker compose up -d This also pulls the latest agentic-pi (an npm dependency baked into the image at build time). To pick up the latest agentic-pi without any other Last Light changes, just rebuild:
docker compose build --no-cache agent
docker compose up -d agent GitHub App tokens expire hourly. The harness mints fresh per-phase installation tokens on every workflow run, so refresh is automatic — no action needed. If you see auth errors, restart the agent container.
GitHub App installations get 5,000 API calls/hour. For most repos that's plenty. If you hit limits, increase cron intervals or reduce repos per installation.
Each triage or review is ~1 agent session. Build requests use more tokens. Costs depend on your configured model provider's API usage (OpenAI / Anthropic / OpenRouter / whichever LASTLIGHT_MODEL points at). Route cheap models to high-volume phases (triage, screen) via LASTLIGHT_MODELS to keep the bill down.
Session logs are saved as JSON trajectories. View them with docker compose logs agent or inspect the agent-logs volume.
One instance handles all repos where the GitHub App is installed. No per-repo config needed — skills apply universally.