---
description: >-
  The agent runbook that provisions self-hosting infrastructure (Docker host, network, database, TLS) before guided setup; AWS is the reference implementation.
---

> Part of the Watcher docs. Index of all pages, with summaries: https://watcher-docs.apolloresearch.ai/llms.txt
> Before upgrading Watcher, read the release pages and full changelogs for every version crossed: https://watcher-docs.apolloresearch.ai/releases/index.md
> Found an issue with this page? Report it: https://github.com/ApolloResearch/watcher-bin/issues/new?title=Docs%20feedback%3A%20Watcher%20Self-Host%3A%20Infrastructure%20Prerequisites&body=Page%3A%20Watcher%20Self-Host%3A%20Infrastructure%20Prerequisites%0AURL%3A%20https%3A%2F%2Fwatcher-docs.apolloresearch.ai%2Fself-hosting%2Fprompts%2Fprerequisites%2F%0A%0A

# Watcher Self-Host: Infrastructure Prerequisites

This prompt gets you from "nothing provisioned yet" to a running Docker host, ready for guided software setup. Give it to a coding agent (Claude Code or similar) working alongside whoever owns the cloud account or hosting environment, and drive it section by section.

The bundle's `prompts/setup.md` points to the docs index for its release line.
For Docker Compose, read "Watcher Self-Host: Guided Setup" from that index.
All setup-step references below refer to that runbook. Finish provisioning
the host here before starting the software setup.

Three patterns govern the agent's behavior here:

- **[A]: the agent runs it.** Any command that probes account state or provisions infrastructure (checking CLI installs, verifying a resource exists, running `terraform`) is something the agent executes directly and interprets the output itself. It doesn't narrate the command and wait for you to type it.
- **[B]: the agent asks you.** For values only you know and decisions only you can make: which network to deploy into, whether you want a managed database, whether you already have a TLS certificate.
- **[C]: you act outside the agent's reach.** For things the agent can't drive: creating a DNS validation record at your registrar, infrastructure your platform team owns and provisions on a separate timeline.

If you already have a Docker host ready and nothing else to provision, skip this file and start with `prompts/setup.md`. Use its docs index to find the guided setup runbook.

## 0. Confirm the hosting environment

**0.1: Which cloud or hosting environment? [B]**

Ask: "Where will Watcher run: AWS, another cloud provider (GCP, Azure, etc.), or your own bare-metal/on-prem infrastructure?"

**AWS is the reference implementation.** This bundle ships ready-to-use Terraform modules that work out of the box once you give them a network and subnet: `modules/watcher-self-host/` for the compute host, IAM, security groups, and an optional managed Postgres instance, plus an optional `modules/watcher-self-host-acm/` for a TLS certificate. If the answer is AWS, every section below has an "AWS (Terraform)" path: follow it as written.

**For any other provider**, Sections 1-6 still apply as a checklist. The underlying decisions (network, compute sizing, database, TLS, DNS) are the same everywhere, but there's no bundled module to run. Each section also has an "Any provider" path: read what needs to be true, then provision it with whatever IaC or console workflow you already use, substituting your provider's own tools (its CLI, its managed-database offering, its DNS-validated-certificate feature) for the AWS-specific ones. Skip the `aws` CLI commands and the two Terraform module blocks entirely on this path.

## 1. Tooling checks

Run these yourself before asking anything else: they tell you what's actually missing instead of guessing.

**1.1: Cloud CLI [A]**

AWS (Terraform):

```
aws --version
aws sts get-caller-identity
aws configure get region
```

Expected: a version string, a JSON identity block (account + ARN), and a non-empty region. If `aws --version` fails, the CLI isn't installed: point to https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html. If `sts get-caller-identity` fails, the CLI has no usable credentials: help configure `aws sso login`, `aws configure`, or an assumed role, whichever matches how this account is normally accessed. If region is empty, either set one (`aws configure set region <region>`) or confirm every command below will pass `--region` explicitly.

Any provider: run that provider's equivalent (`gcloud auth list` and `gcloud config get-value project`, `az account show`, etc.) and confirm both authentication and a working default project/subscription/region. There's no bundled check for this: use the provider's own CLI conventions.

**1.2: Terraform CLI [A]**

Needed only if you're using the AWS modules in this bundle, or writing comparable Terraform for another provider:

```
terraform version
```

Expected: `Terraform v1.13` or newer (matching `required_version` in the modules). If missing or too old, point to https://developer.hashicorp.com/terraform/install.

## 2. Network

**2.1: Confirm an existing network [B]**

Ask: "Do you have an existing network (VPC, VNet, or equivalent) to deploy Watcher into, with a subnet that has outbound internet access: a NAT gateway, or a public subnet with a public IP?"

AWS (Terraform): the included module is bring-your-own-network: it does not create one. Capture the `vpc_id` and `instance_subnet_id`.

Any provider: capture whatever your provider calls the equivalent (VPC, VNet, subnetwork) and confirm it has egress; you'll pass this to your own IaC or console.

If there's no network yet, pause here: provisioning one is outside any of this bundle's modules and outside what an LLM session can safely improvise. Point to the platform/infra team, or the provider's own quick-start.

**2.2: Verify it exists [A]**

AWS (Terraform):

```
aws ec2 describe-vpcs --vpc-ids <their-vpc-id>
aws ec2 describe-subnets --subnet-ids <their-subnet-id>
```

Expected: both return a single matching resource. A `NotFound` error means the wrong ID, wrong account, or wrong `--region`/`AWS_REGION`: resolve before continuing.

Any provider: run the provider CLI's equivalent describe/list command, or verify directly in the console if that's simpler.

## 3. Postgres: bundled or managed

Ask whether the operator wants bundled or managed Postgres. Read Step 1.8 of the fetched setup runbook for the operational-ownership tradeoffs. Provision based on that choice:

- **Bundled** (default): nothing to provision here, since the docker-compose stack brings its own Postgres.
- **Managed** (AWS RDS, GCP Cloud SQL, Azure Database for PostgreSQL, or similar):
  - AWS (Terraform): set `create_rds = true` and provide `rds_subnet_ids` (two subnets in different AZs) on the compute module in Section 6.
  - Any provider: provision the managed instance with your own IaC or console, and have its connection string ready for setup runbook Step 4.

## 4. TLS certificate

Ask: "Do you want infrastructure-as-code to provision your TLS certificate, or are you handling it yourself (existing cert, Let's Encrypt, a corporate CA)?" If the latter, skip to Section 5: setup runbook Step 3.1 covers the fully manual path for any provider.

**AWS (Terraform):** use `modules/watcher-self-host-acm/`. ACM certificates
are regional: request it with your AWS provider configured for the region
where the load balancer will live (setup runbook Step 3.2), or the ALB
won't be able to select it:

```hcl
module "watcher_cert" {
  source = "./modules/watcher-self-host-acm"

  domain_name    = "watcher.example.com"
  hosted_zone_id = "Z0123456789ABCDEFGHIJ"  # omit if your DNS isn't in Route53
}

output "watcher_certificate_arn" {
  value = module.watcher_cert.certificate_arn
}

output "manual_validation_records" {
  value = module.watcher_cert.manual_validation_records
}
```

**4.1: Is the hostname's DNS in Route53? [B]** If yes, provide the `hosted_zone_id` and the module creates the validation record and waits for the certificate to become `ISSUED` in the same `terraform apply`: no manual step.

**4.2: If not in Route53 [C]:** omit `hosted_zone_id`. After `terraform apply`, read the validation records Terraform couldn't create itself:

```
terraform output -json manual_validation_records
```

Set each `name`/`type`/`value` as a DNS record at whatever provider hosts this hostname's DNS (Cloudflare, Route53, a registrar's DNS panel, corporate DNS, etc.): check that provider's documentation for how to add a raw DNS record of the given `type` (typically `CNAME`), since the exact UI/CLI steps vary by provider. Certificate issuance can take several minutes to a few hours after the record propagates. Confirm it's issued before using it:

```
aws acm describe-certificate --certificate-arn <arn> --query 'Certificate.Status' --output text
```

Expected: `ISSUED`. No follow-up `terraform apply` is needed: nothing in the module consumes the status; the check above is the source of truth.

**Any provider:** use that provider's own DNS-validated certificate service (e.g. GCP-managed SSL certificates, Azure App Service Certificates) or a general-purpose option like Let's Encrypt: the mechanics mirror Section 4.2 above (request, add a validation record, wait for issuance) even without a bundled module to drive it.

Whichever path, keep the certificate identifier (ARN or equivalent): it's an input to whatever reverse proxy or load balancer gets configured in setup runbook Step 3.2.

## 5. DNS access

Confirm (if not already, from Section 4): "Do you have access to create DNS records for this hostname?" Setup runbook Step 3.3 needs this for the final A/ALIAS/CNAME record; if certificate validation also needed a record (Section 4.2), this access is needed earlier than Step 3.3 implies.

## 6. Provision the compute host

**AWS (Terraform):** once Sections 1-5 are settled, call `modules/watcher-self-host/` with `vpc_id`, `instance_subnet_id`, `ami_id`, and (if applicable) `create_rds`/`rds_subnet_ids`. The full variable reference is `modules/watcher-self-host/variables.tf` in this bundle; the `terraform apply` walkthrough is on the AWS infrastructure page of the published Watcher documentation.

**Confirm the image source and host IAM role before applying [B].**

For Apollo's ECR registry, the host starts with a role in the customer's AWS
account. That source role calls `sts:AssumeRole` on
`arn:aws:iam::456354208905:role/WatcherSelfHostPuller`, the destination role
in Apollo's account. Passing the destination ARN to `--role-arn` does not
select the source role.

With `create_iam = true` (the default), the module creates the source role
named `watcher-self-host-instance`. It grants that role permission to assume
Apollo's pull role when `configure_apollo_ecr = true` (the default).
Confirm Apollo's pull role trusts
`arn:aws:iam::<customer-account-id>:role/watcher-self-host-instance`.
That trust can be set before the role exists.

If the customer changes `instance_role_name`, use the new role's ARN.
If they set `create_iam = false`, they must supply
`existing_instance_profile_name`. Get the full ARN of the role behind that
profile from the customer's IAM owner, including any role path. The IAM
owner must grant it `sts:AssumeRole` on Apollo's destination ARN, and that
exact source ARN must be trusted by Apollo's pull role. Trust for the
default source role does not cover another role in the same account.

For images already copied into an internal registry, set
`configure_apollo_ecr = false` and use the customer's registry login flow.
This omits the Apollo ECR policy and refresh timer. The host does not need
Apollo role access on this path.

Terraform does not surface a module's outputs at the root, and `terraform output` reads state, so declare a root output for every value you will need later **in the same configuration you apply here**. Declaring them afterwards leaves them out of state, and reading them then reports that each output is not found:

```hcl
output "watcher_instance_id" {
  value = module.watcher.instance_id
}

output "watcher_instance_role_arn" {
  value = module.watcher.instance_role_arn
}

output "watcher_rds_endpoint" {
  value = module.watcher.rds_endpoint
}

output "watcher_rds_database_name" {
  value = module.watcher.rds_database_name
}

output "watcher_rds_master_username" {
  value = module.watcher.rds_master_username
}

output "watcher_rds_master_password" {
  value     = module.watcher.rds_master_password
  sensitive = true
}
```

The four RDS outputs are `null` when `create_rds` is false. The role output is
`null` when `create_iam = false`; use the ARN from the customer's IAM owner.
If you have already applied without these outputs, add them and apply again;
the plan should record only the new outputs and no resource changes.

Fetch the `ami_id` value (the current Ubuntu 24.04 AMI for the deployment's region) with:

```
aws ssm get-parameter \
  --name /aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id \
  --query Parameter.Value --output text
```

The lookup above is for **new deployments only**. If you are supplying `ami_id` for an instance that already exists (typically after updating the module to a version that requires it), pin the AMI the instance already runs, so your first plan is a no-op instead of a replacement:

```
aws ec2 describe-instances --instance-ids <your instance id> \
  --query 'Reservations[].Instances[].ImageId' --output text
```

The module pins this AMI. Changing `ami_id` later replaces the instance and destroys its root volume (host configuration, admin audit log, and the bundled Postgres data if you use it), so treat any change to it as a scheduled operation: back up the database, every file you created on the host, and the audit log volume first, following the backup and restore procedures on the Operations page of the published Watcher documentation, and the instance-replacement procedure on its AWS infrastructure page.

**Any provider:** provision a comparable VM with your own IaC or console. Setup runbook Step 1.1 gives sizing guidance (a 4-vCPU / 8 GB host is a reasonable starting point) that applies regardless of provider. Make sure it has Docker Engine and the Compose plugin, or a path to install them, and that whoever will run `prompts/setup.md` has shell access to it (SSH, or your provider's session-manager equivalent).

## 7. Hand off to guided setup

Once the host exists:

- **Apollo ECR:** carry the source IAM role ARN into setup. For a
  module-created role, read `terraform output -raw watcher_instance_role_arn`
  from the Terraform working directory. This is the ARN Apollo must trust.
  Run `aws sts get-caller-identity --region eu-west-2` inside the host shell
  to check its account and role name. Its `arn:aws:sts::...:assumed-role/...`
  result is a temporary session ARN, not the IAM role ARN. It omits any IAM
  role path, so do not use it as the allowlist entry. Complete Step 1.2 of
  the guided setup prompt to test role access before pulling images.
- **AWS (Terraform):** note the EC2 instance id (`terraform output -raw watcher_instance_id`) for connecting via SSM in setup runbook Step 1.1. If `create_rds = true`, read the four connection values for setup runbook Step 4's `DATABASE_URL` from the root outputs you declared in Section 6. `-raw` is required for the password, which is sensitive and is redacted by a bare `terraform output`:

```sh
terraform output -raw watcher_rds_endpoint
terraform output -raw watcher_rds_master_username
terraform output -raw watcher_rds_database_name
terraform output -raw watcher_rds_master_password
```

The endpoint already carries `host:port`. Store the password in the customer's secret manager before the session ends. If a certificate was provisioned in Section 4, note its ARN for Step 3.2's reverse-proxy configuration.
- **Any provider:** note however you'll reach the host (its address and access method), the managed-database connection string if applicable, and the certificate identifier if applicable: same three things, gathered your own way.

Start Step 1 of "Watcher Self-Host: Guided Setup", found through the docs index
linked in the bundle's `prompts/setup.md`.
