Testiment

Caddy

Deploy Testiment behind the built-in Caddy reverse proxy with automatic HTTPS.

Use this guide when you want the repo's install wizard to configure Testiment for a VPS with:

  • Caddy handling TLS and reverse proxying
  • one public domain, such as app.example.com
  • Docker Compose running the stack

This guide follows the actual prompts from ./scripts/setup.sh.

What This Deployment Uses

  • docker-compose.yml for the main stack
  • docker-compose.caddy.yml for the bundled Caddy container
  • Caddyfile in the repo root for the bundled Caddy config
  • ./scripts/setup.sh for the install wizard
  • .env, apps/server/.env, and apps/web/.env generated or rewritten by the wizard

Before You Start

  • A Linux VPS with Docker Engine and Docker Compose v2 installed
  • A public IPv4 address for the VPS
  • One DNS name pointing at that server, for example app.example.com
  • Ports 80 and 443 open to the internet
  • S3-compatible object storage credentials

Recommended baseline:

  • Ubuntu 22.04+
  • 2 vCPU
  • 4 GB RAM
  • 40 GB SSD

DNS Layout

Create the DNS record before you start the stack. Do not rely on creating it after Caddy is already trying to issue certificates.

Example:

app.example.com  ->  your VPS public IP

If your DNS provider supports proxying, start with plain DNS-only records until Caddy has issued certificates successfully.

Wait for public DNS propagation before you answer yes to the wizard's final "Start Testiment now with Docker Compose" prompt.

Recommended check:

dig @1.1.1.1 +short app.example.com

The command should return your VPS public IP.

Connect to the VPS and Clone the Repo

git clone https://github.com/redpangilinan/testiment
cd testiment

Run everything else from the repository root.

Run the Install Wizard

./scripts/setup.sh

The wizard validates the repo layout, reads any existing env files as defaults, and creates timestamped backups before rewriting them.

If older env files already exist, the wizard prints that it will use the current values as defaults and back them up before rewrite.

Answer the Domain Prompt

The wizard asks for:

Public domain

Example answer:

Public domain: app.example.com

Rules enforced by the wizard:

  • the value must be a hostname, not a full URL
  • Caddy mode requires a real public domain, not localhost or a raw IP

From that value, the wizard auto-builds:

  • NEXT_PUBLIC_APP_URL=https://app.example.com
  • NEXT_PUBLIC_SITE_URL=https://testiment.dev
  • NEXT_PUBLIC_SERVER_URL=https://app.example.com
  • BETTER_AUTH_URL=https://app.example.com
  • CORS_ORIGINS=https://app.example.com

For new installs, BETTER_AUTH_COOKIE_DOMAIN defaults to the same public host. If you already use a custom cookie domain, the wizard preserves it.

Enable Caddy in the Wizard

The next important prompt is:

Enable built-in Caddy reverse proxy with automatic HTTPS [Y/n]

Answer yes.

The wizard then asks for:

Email for Caddy TLS certificates

Use a real email address. Caddy uses it for ACME certificate management.

In Caddy mode, the wizard automatically keeps the internal app bindings private by default:

  • WEB_PORT=127.0.0.1:3001
  • SERVER_PORT=127.0.0.1:3000

That means only Caddy needs to be public on 80 and 443.

Choose the Database Mode

The wizard asks:

Use an external PostgreSQL database [y/N]

Choose one:

  • no: the repo uses the bundled Postgres service from docker-compose.yml
  • yes: the wizard switches to docker-compose.external-db.yml and asks for Database URL

If you are just getting started on one VPS, bundled Postgres is the simplest path.

Finish the Required App Questions

The wizard always asks for storage settings because uploads need S3-compatible storage:

Storage bucket name
Storage access key ID
Storage secret access key
Storage endpoint URL (leave blank for AWS S3)
Storage region
Storage public URL (optional)

It also asks whether you want to configure these optional integrations now:

  • Google OAuth sign-in
  • Resend email delivery
  • Upstash Redis for capture rate limiting
  • Cloudflare Turnstile for capture bot protection
  • PostHog client analytics

If you do not have those ready yet, answer no and add them later.

Review the Files the Wizard Writes

When the prompts are complete, the wizard writes:

  • .env
  • apps/server/.env
  • apps/web/.env

The repo-owned Caddyfile reads its values from those env files. Caddy mode settings include:

CRIKKET_PROXY_MODE=caddy
CADDY_HTTP_PORT=80
CADDY_HTTPS_PORT=443
CADDY_ACME_EMAIL=you@example.com
NEXT_PUBLIC_APP_URL=https://app.example.com
NEXT_PUBLIC_SERVER_URL=https://app.example.com

The committed Caddyfile looks like this shape:

{
	email {$CADDY_ACME_EMAIL}
}

{$NEXT_PUBLIC_APP_URL} {
  encode gzip zstd

  @api path /api/* /rpc/*
  handle @api {
    reverse_proxy server:3000
  }

  handle {
    reverse_proxy server:3001
  }
}

In this stack, web shares the server network namespace, so one public hostname can serve the web app at / and the API at /api and /rpc.

Start the Stack

At the end, the wizard asks:

Start Testiment now with Docker Compose [Y/n]

Answer yes if:

  • DNS already points to this VPS
  • ports 80 and 443 are open
  • Docker is installed and the daemon is running

The wizard then runs:

  • Docker Compose config validation
  • image pulls
  • up -d
  • ps

If you answer no, you can start it yourself later.

Preferred command:

./scripts/start.sh

start.sh reads the generated env files and automatically picks the correct Compose file set for:

  • bundled vs external PostgreSQL
  • built-in Caddy vs no built-in proxy

Use raw docker compose only if you intentionally want to bypass the wrapper.

Point the Domain to the VPS

In your DNS provider:

  • create an A record for app.example.com pointing to the VPS public IPv4

If you use IPv6, add a matching AAAA record only if the VPS is actually reachable over IPv6.

Then make sure your cloud firewall or host firewall allows:

  • TCP 80
  • TCP 443

Keep these private unless you have a specific reason to expose them:

  • 3000
  • 3001
  • 5432

Verify HTTPS and Container Health

Check DNS first:

dig +short app.example.com

Run the built-in health check:

./scripts/healthcheck.sh

healthcheck.sh verifies:

  • expected services are running
  • app and auth session URLs are configured consistently
  • bundled Postgres is reachable when you use bundled database mode
  • the public app URL and /api/auth/get-session respond successfully

For targeted live logs, use Docker Compose directly:

docker compose logs -f caddy
docker compose logs -f server

Quick HTTPS checks:

curl -I https://app.example.com
curl https://app.example.com/api/auth/get-session

Expected result:

  • the hostname resolves to the VPS
  • Caddy provisions certificates successfully
  • app.example.com loads the web app
  • https://app.example.com/api/auth/get-session returns null when no session cookie is present

Common Failure Points

  • DNS still points somewhere else, so ACME validation never reaches your VPS
  • ports 80 or 443 are blocked by the provider firewall, security group, or ufw
  • storage credentials are missing, so uploads fail later even if the app boots

If You Need to Restart Later

./scripts/restart.sh
./scripts/healthcheck.sh

Use restart.sh only for routine restarts. It does not recreate containers, so it is not enough after:

  • env file changes
  • image updates
  • Compose config changes
  • Caddyfile changes that require container recreation instead of a live reload

For those cases, use:

./scripts/update.sh

On this page