Skip to main content

Public Server Deployment

Deploy a fully functional, publicly reachable ProjectAchilles install on a single Linux server with your own domain and an automatically-issued TLS certificate. Works on a DigitalOcean droplet or any Ubuntu 22.04/24.04 host (Hetzner, Linode, EC2, bare metal).

For an internal/private network install, see On-Prem Server.

Architecture

A single Caddy reverse proxy terminates TLS and serves one public origin — UI at /, API at /api, and agents on the same host. Backend, frontend, and Elasticsearch stay on the internal Docker network.

ComponentExposedRole
Caddy:80, :443TLS termination + reverse proxy, automatic Let's Encrypt
frontendinternalnginx SPA + /api proxy to backend
backendinternalExpress, SQLite, Go agent builds
elasticsearchinternalOptional bundled analytics (ES_MODE=self)

Defined by docker-compose.server.yml + deploy/caddy/, driven by the scripts in scripts/.

Sizing

ElasticsearchRecommendedNotes
Self-hosted (ES_MODE=self)4 GB RAM / 2 vCPU (s-2vcpu-4gb)ES wants ~2 GB.
Elastic Cloud (ES_MODE=cloud)2 GB RAM (s-1vcpu-2gb)ES is external.

Prerequisites

  • A domain you control and the ability to create an A record.
  • A server (or a DigitalOcean account + doctl to create one).
  • Clerk application keys.
  • (Optional) Elastic Cloud deployment; a GitHub token for private repos.

Step 1: Configure

cp deploy.config.env.example deploy.config.env
$EDITOR deploy.config.env

Minimum fields:

ACHILLES_DOMAIN=achilles.example.com
ACME_EMAIL=admin@example.com
TLS_MODE=acme-http
CLERK_PUBLISHABLE_KEY=pk_live_xxx
CLERK_SECRET_KEY=sk_live_xxx
ES_MODE=self # or: cloud (then set ELASTICSEARCH_CLOUD_ID/API_KEY)

The installer is hybrid: it reads deploy.config.env and interactively prompts for any required value left blank. A complete config runs fully unattended — ideal for driving from an LLM / agentic coding tool.

Step 2: Deploy

Pick one path:

# A — DigitalOcean: create droplet + firewall (+ optional DNS), then install.
# Fill the DO_* section first (API token, region, size, SSH key fingerprint).
./scripts/deploy-do.sh

# B — Existing server you can SSH to (point the A record at it first).
./scripts/deploy-remote.sh root@<server-ip>

# C — Running directly on the server.
./scripts/deploy-server.sh

Step 3: DNS

Create a single A record pointing your subdomain at the server's public IP:

achilles.example.com.   A   203.0.113.10

For acme-http, DNS must resolve and ports 80/443 must be reachable before Caddy can issue the certificate. Watch progress:

docker compose -f docker-compose.server.yml logs -f caddy

Step 4: Verify

curl -s https://achilles.example.com/api/health
# {"status":"ok","service":"ProjectAchilles",...}

Then open https://achilles.example.com/ and confirm Clerk login works.

Enrolling agents

Agents connect to the same public host over HTTPS. In Endpoints → enrollment, generate a token and run the printed command on the endpoint. Because the certificate is publicly trusted, no extra CA configuration is needed — AGENT_SERVER_URL was already set to your public URL by the installer.

Day-2 operations

docker compose -f docker-compose.server.yml ps         # status
docker compose -f docker-compose.server.yml logs -f # logs

# Upgrade (idempotent):
git pull && ./scripts/deploy-server.sh

Persistent state lives in Docker volumes: achilles-data (SQLite, encrypted settings, certs), esdata (self-hosted ES), caddy-data (TLS certs). Back up achilles-data regularly.

Troubleshooting

SymptomCheck
TLS cert not issuedlogs -f caddy; DNS resolves? 80/443 open? Let's Encrypt rate-limits — test with a staging email.
502 from the UIBackend starting/unhealthy: logs -f backend, curl -sk https://DOMAIN/api/health.
Agents can't connectAGENT_SERVER_URL matches the public URL; firewall allows 443.
ES errorsES_MODE=self needs ≥4 GB RAM, or switch to cloud.
Full reference

The complete guide — including the configuration reference, backups, and security notes — is in docs/deployment/SELF_HOSTED_SERVER.md.