> ## Documentation Index
> Fetch the complete documentation index at: https://getoverlay.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Docker on one host

> Run a narrow Overlay sanity check on one EC2 instance with a self-hosted Convex backend, external PostgreSQL for Better Auth, and Redis.

Use this guide for the first customer-controlled deployment. It proves that the
release can start in the customer's VPC, reach a self-hosted Convex backend,
PostgreSQL, and Redis, complete OIDC sign-in, persist a conversation, and
recover after a container restart.

> [!WARNING]
>
> This is a disposable sanity check, not the pilot production architecture. It
> runs one web container and one Convex backend on one EC2 host. Move to
> [Deploy On AWS](https://getoverlay.io/docs/self-hosting/install/aws.md) after this check passes.

## What this runs

```text theme={null}
Operator browser
  -> SSH tunnel to localhost:3000
  -> EC2 Docker host
       -> Overlay web
       -> convex-backend (local volume persistence for the sanity check)
       -> RDS PostgreSQL: overlay_auth (Better Auth only)
       -> ElastiCache Redis over TLS
       -> Google Workspace OIDC
       -> approved AI Gateway model
```

The sanity profile deliberately disables files, memory, knowledge, vector
search, automations, webhooks, integrations, browser use, sandboxes, analytics,
error reporting, and billing. Enable those only in the qualified AWS deployment.

## Before you start

You need:

| Input | Requirement |
| - | - |
| AWS | A disposable nonproduction account or VPC, an approved Region, and permission to use EC2, RDS, and ElastiCache. |
| EC2 | Ubuntu 24.04, at least 20 GB of encrypted gp3 storage, and `t3.large` or another 2-vCPU instance when pulling the release image. Use at least 40 GB and 4 vCPU only for an optional source build. |
| PostgreSQL | One private RDS PostgreSQL instance with an `overlay_auth` database and login role for Better Auth. Application data lives in the self-hosted Convex backend. |
| Redis | A TLS ElastiCache endpoint reachable from the EC2 security group. |
| Identity | A Google Workspace organization and Google Cloud OAuth web client that permits the localhost callback shown below. |
| Models | An approved `AI_GATEWAY_API_KEY` and model ID. |
| Source | The customer repository URL and an approved Overlay release tag or commit. |

Use security-group references, not public database endpoints:

* EC2 inbound: SSH only from the operator's current IP. Do not expose port 3000.
* RDS inbound: PostgreSQL `5432` only from the EC2 security group.
* Redis inbound: TLS Redis `6379` only from the EC2 security group.
* EC2 outbound: HTTPS plus the approved RDS and Redis destinations.

## Install

**1. Connect to the EC2 instance**

Connect through Session Manager when available. Otherwise use the temporary,
IP-restricted SSH rule:

```bash theme={null}
ssh ubuntu@<EC2_PUBLIC_DNS>
```

**2. Install Docker, Git, and PostgreSQL tools**

Review the Docker installation script before running it if customer policy
requires source inspection.

```bash theme={null}
sudo apt-get update
sudo apt-get install -y ca-certificates curl git jq postgresql-client
curl -fsSL https://get.docker.com -o /tmp/get-docker.sh
less /tmp/get-docker.sh
sudo sh /tmp/get-docker.sh
sudo usermod -aG docker "$USER"
newgrp docker
docker version
docker compose version
```

Install the AWS RDS CA bundle for administrative `psql` commands:

```bash theme={null}
mkdir -p "$HOME/.postgresql"
curl -fsSL \
  https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem \
  -o "$HOME/.postgresql/root.crt"
chmod 600 "$HOME/.postgresql/root.crt"
```

**3. Create the Better Auth database with the RDS administrator**

Run this once with a temporary administrative connection. The Better Auth role
must not retain administrative membership. Replace all angle-bracket values
before running it.

```bash theme={null}
export PGHOST='<writer>.rds.amazonaws.com'
export PGPORT=5432
export PGUSER='<rds-admin-role>'
export PGSSLMODE=verify-full
export PGSSLROOTCERT="$HOME/.postgresql/root.crt"
read -rsp 'RDS admin password: ' PGPASSWORD; echo; export PGPASSWORD
read -rsp 'Better Auth password: ' OVERLAY_AUTH_DB_PASSWORD; echo

psql --dbname=postgres -v ON_ERROR_STOP=1 \
  -v admin_role="$PGUSER" \
  -v auth_password="$OVERLAY_AUTH_DB_PASSWORD" <<'SQL'
CREATE ROLE overlay_auth LOGIN PASSWORD :'auth_password';
GRANT overlay_auth TO :"admin_role";
SQL

psql --dbname=postgres -v ON_ERROR_STOP=1 \
  -c 'CREATE DATABASE overlay_auth OWNER overlay_auth;'

psql --dbname=postgres -v ON_ERROR_STOP=1 -v admin_role="$PGUSER" <<'SQL'
REVOKE overlay_auth FROM :"admin_role";
SQL
unset PGPASSWORD OVERLAY_AUTH_DB_PASSWORD
```

If the role or database already exists, inspect it instead of blindly
rerunning the creation statements.

**4. Clone the approved release**

Clone the public repository for the initial sanity check. If your organization
maintains an approved private mirror, set `OVERLAY_REPOSITORY_URL` to that
mirror instead. The commands below default to the tested `v0.1.0-rc.1`
release; change `OVERLAY_RELEASE` only when you have approved another published
release.

```bash theme={null}
export OVERLAY_REPOSITORY_URL="${OVERLAY_REPOSITORY_URL:-https://github.com/LayerNorm/overlay-web.git}"
export OVERLAY_RELEASE="${OVERLAY_RELEASE:-v0.1.0-rc.1}"

git clone "$OVERLAY_REPOSITORY_URL" overlay-web
cd overlay-web
git fetch --tags --force
git checkout --detach "$OVERLAY_RELEASE"
git status --short --branch
```

Record the exact commit and tag:

```bash theme={null}
git rev-parse HEAD
git tag --points-at HEAD
```

Published releases are listed at
[github.com/LayerNorm/overlay-web/releases](https://github.com/LayerNorm/overlay-web/releases).

**5. Create the runtime files**

The real environment file is ignored by Git.

```bash theme={null}
cp deploy/ec2/.env.example deploy/ec2/.env
cp docs/config/ec2-sanity.example.json deploy/ec2/overlay.config.json
chmod 600 deploy/ec2/.env
```

Edit `deploy/ec2/.env` and replace every `replace_me` or `replace-me`
value. Generate every Overlay-owned secret independently:

```bash theme={null}
openssl rand -hex 32
```

Do not reuse a generated value. Do not paste secret values into chat,
tickets, logs, screenshots, Git, or the coding agent's final report.

Database passwords containing reserved URL characters must be URL-encoded
in `BETTER_AUTH_DATABASE_URL`. `NEXT_PUBLIC_CONVEX_URL` points at the
convex-backend service on the same host; `INTERNAL_API_SECRET` must match the
value set on the backend deployment.

**6. Configure Google Workspace login**

In a customer-controlled Google Cloud project, configure Google Auth Platform
with an **Internal** audience and create a **Web application** OAuth client.
Register these exact values:

```text theme={null}
Callback URL:
http://localhost:3000/api/better-auth/sso/callback/workspace

Web origin:
http://localhost:3000
```

In `deploy/ec2/.env`, replace `example.edu` with the Workspace domain and
set `BETTER_AUTH_CONNECTION_CLIENT_ID` and
`BETTER_AUTH_CONNECTION_CLIENT_SECRET`. Keep the connection ID as
`workspace` so it matches the callback path.

Follow [Google Workspace](https://getoverlay.io/docs/self-hosting/configure/authentication/recipes/google-workspace.md)
for the complete console flow. If Auth0 is the approved broker instead,
follow [Auth0](https://getoverlay.io/docs/self-hosting/configure/authentication/recipes/auth0.md) and use connection ID
`auth0`. Cognito, Okta, and Keycloak use [Generic OIDC](https://getoverlay.io/docs/self-hosting/configure/authentication/recipes/generic-oidc.md).

**7. Validate and pull the release image**

Define a short helper for the remaining commands:

```bash theme={null}
compose() {
  docker compose \
    --env-file deploy/ec2/.env \
    -f deploy/ec2/docker-compose.yml \
    "$@"
}

OVERLAY_ENV_FILE=.env \
OVERLAY_CONFIG_PATH=./overlay.config.json \
compose config --quiet
compose pull web backend migrate
```

`OVERLAY_WEB_IMAGE` must name the approved versioned release, such as
`ghcr.io/layernorm/overlay-web:0.1.0-rc.1`. Do not use `latest` for a
customer qualification run. The web and migration roles use the same immutable
image; `backend` uses the pinned official `convex-backend` image.

To build inspected or customized source instead, use the build override:

```bash theme={null}
compose_source() {
  docker compose \
    --env-file deploy/ec2/.env \
    -f deploy/ec2/docker-compose.yml \
    -f deploy/ec2/docker-compose.build.yml \
    "$@"
}

compose_source build web
```

Continue with `compose_source` instead of `compose` for the remaining steps
when using a source build. The override builds the tested enterprise runtime,
including the web server and migration tooling.

**8. Start the Convex backend and run migrations once**

Start the backend and wait for its `/version` health check:

```bash theme={null}
compose up -d backend
compose ps
curl -fsS http://127.0.0.1:3210/version
```

Then run the Better Auth migration once:

```bash theme={null}
compose --profile tools run --rm migrate
```

The command must exit with status `0` and report that Better Auth migrations
are current. Do not run migrations from every web container. Deploy the Convex
functions and schema to the backend once per release with the bootstrap flow
from
[Self-hosted Convex backend](https://getoverlay.io/docs/self-hosting/install/self-hosted-convex.md), which also
sets `INTERNAL_API_SECRET` and `INTERNAL_SERVICE_AUTH_SECRET` on the deployment.

**9. Start the web service**

```bash theme={null}
compose up -d web
compose ps
compose logs --tail=100 web backend
```

Wait for `web` to become healthy. Backend logs should show the persistence
lease held and regular function traffic without restart loops.

**10. Open the SSH tunnel**

From the operator's computer, open a second terminal. The tunnel must carry
both the app and the Convex client/realtime port:

```bash theme={null}
ssh -N -L 3000:127.0.0.1:3000 -L 3210:127.0.0.1:3210 -L 3211:127.0.0.1:3211 ubuntu@<EC2_PUBLIC_DNS>
```

Keep that terminal open and visit
[http://localhost:3000](http://localhost:3000). The browser, IdP, and Convex
client see localhost origins while the application runs inside the customer
VPC.

## Verify the sanity check

Run these commands on EC2:

```bash theme={null}
curl -fsS http://127.0.0.1:3000/api/v1/capabilities | jq
curl -fsS http://127.0.0.1:3000/api/auth/session | jq
```

Then verify in the browser:

1. A signed-out user sees one `Continue with Google` option on a single line.
2. Sign-in returns to `/app/chat`.
3. `/api/auth/session` reports `authenticated: true` and `emailVerified: true`.
4. Create a conversation and send one model request.
5. Rename the conversation and refresh the page.
6. Restart only the web container with `compose restart web`.
7. Refresh again and confirm the conversation and title persist.
8. Sign out and confirm the session is cleared.
9. Confirm the browser network and container logs reach only the local Convex backend URL.

Expected capability signals include `provider: "convex"`. Features disabled by
the sanity profile should not appear as usable navigation or routes.

## Troubleshooting

| Symptom | Check |
| - | - |
| Web container is unhealthy | `compose logs web`; validate every placeholder in `.env`; confirm the config file is mounted. |
| Migration cannot connect | RDS security group, database hostname, URL-encoded password, CA hostname verification, and database ownership. |
| Web cannot reach the backend | `NEXT_PUBLIC_CONVEX_URL`, backend `/version` health, `INTERNAL_API_SECRET` matching on both sides, and compose network resolution. |
| Backend restarts or loses its lease | `compose logs backend`, free disk for the persistence volume, and `POSTGRES_URL`/`INSTANCE_SECRET` if external persistence is used. |
| Better Auth reports canonical and legacy connections together | Use `BETTER_AUTH_CONNECTION_*` for the connection registry and remove legacy `BETTER_AUTH_DEFAULT_SSO_*` and `BETTER_AUTH_OIDC_*` values from both the environment and JSON config. |
| Redis requests fail closed | ElastiCache endpoint, TLS URL beginning with `rediss://`, security groups, and authentication token. |
| OIDC returns a callback error | Exact callback, provider ID, issuer, client ID/secret, trusted origin, and SSH tunnel. |
| Chat has no available model | `AI_GATEWAY_API_KEY`, approved model ID, outbound HTTPS, and model-provider configuration. |
| A container restarts repeatedly | `compose ps`, `compose logs --tail=200 <service>`, and available disk/memory. |

## Stop and remove the sanity host

```bash theme={null}
compose down
docker image rm overlay-web:ec2-sanity
shred -u deploy/ec2/.env 2>/dev/null || rm -f deploy/ec2/.env
```

Delete the disposable EC2 instance, databases, Redis deployment, security-group
rules, snapshots, and OIDC callback when they are no longer needed. Retain only
the redacted result, release commit, and approved evidence.

## Paste into a coding agent

Paste the following only after authenticating the agent through the customer's
approved AWS mechanism:

```text theme={null}
Deploy the approved Overlay release by following docs/self-hosting/install/docker.mdx.

Objective:
- Complete a disposable EC2 sanity check using a self-hosted Convex backend,
  external RDS PostgreSQL for Better Auth, and ElastiCache Redis.
- Prove OIDC sign-in, Convex persistence, one chat response, and container
  restart recovery.

Hard rules:
- Work only in the approved nonproduction AWS account and Region.
- Begin with `aws sts get-caller-identity` and report the account and role.
- Do not use root, create long-lived AWS keys, expose port 3000 publicly, or make
  RDS/Redis public.
- Do not print, echo, commit, upload, or include secret values in your report.
- Ask me to populate missing provider credentials in deploy/ec2/.env; set mode 0600.
- Use the auth-scoped database and the exact approved source commit.
- Stop before any destructive action or when an architecture input is ambiguous.

Execution:
1. Inspect the guide and repository artifacts before changing AWS.
2. Inventory existing VPC, subnet, security-group, RDS, Redis, OIDC, and source inputs.
3. Present the minimal proposed changes and wait for approval.
4. Provision or reuse only approved disposable resources.
5. Clone the approved release, configure the ignored files, validate Compose,
   build one image, start the Convex backend, deploy Convex functions, run the
   Better Auth migration once, and start web.
6. Run all command-line checks in the guide. Pause for my interactive OIDC login.
7. Complete persistence and restart QA and inspect logs for expected Convex traffic.
8. Return a redacted report with resource IDs, Git SHA, image ID, test results,
   failures, cleanup steps, and a PASS or FAIL decision. Never return secrets.
```

> **[Next: Deploy On AWS](https://getoverlay.io/docs/self-hosting/install/aws.md)**
>
> Replace the single EC2 host with the qualified ECS, RDS, Redis, S3, Secrets
> Manager, and CloudWatch architecture.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.