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.ymlfor the main stackdocker-compose.caddy.ymlfor the bundled Caddy containerCaddyfilein the repo root for the bundled Caddy config./scripts/setup.shfor the install wizard.env,apps/server/.env, andapps/web/.envgenerated 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
80and443open 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 IPIf 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.comThe command should return your VPS public IP.
Connect to the VPS and Clone the Repo
git clone https://github.com/redpangilinan/testiment
cd testimentRun everything else from the repository root.
Run the Install Wizard
./scripts/setup.shThe 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 domainExample answer:
Public domain: app.example.comRules enforced by the wizard:
- the value must be a hostname, not a full URL
- Caddy mode requires a real public domain, not
localhostor a raw IP
From that value, the wizard auto-builds:
NEXT_PUBLIC_APP_URL=https://app.example.comNEXT_PUBLIC_SITE_URL=https://testiment.devNEXT_PUBLIC_SERVER_URL=https://app.example.comBETTER_AUTH_URL=https://app.example.comCORS_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 certificatesUse 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:3001SERVER_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 fromdocker-compose.ymlyes: the wizard switches todocker-compose.external-db.ymland asks forDatabase 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:
.envapps/server/.envapps/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.comThe 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
80and443are open - Docker is installed and the daemon is running
The wizard then runs:
- Docker Compose config validation
- image pulls
up -dps
If you answer no, you can start it yourself later.
Preferred command:
./scripts/start.shstart.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
Arecord forapp.example.compointing 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:
300030015432
Verify HTTPS and Container Health
Check DNS first:
dig +short app.example.comRun the built-in health check:
./scripts/healthcheck.shhealthcheck.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-sessionrespond successfully
For targeted live logs, use Docker Compose directly:
docker compose logs -f caddy
docker compose logs -f serverQuick HTTPS checks:
curl -I https://app.example.com
curl https://app.example.com/api/auth/get-sessionExpected result:
- the hostname resolves to the VPS
- Caddy provisions certificates successfully
app.example.comloads the web apphttps://app.example.com/api/auth/get-sessionreturnsnullwhen no session cookie is present
Common Failure Points
- DNS still points somewhere else, so ACME validation never reaches your VPS
- ports
80or443are blocked by the provider firewall, security group, orufw - storage credentials are missing, so uploads fail later even if the app boots
If You Need to Restart Later
./scripts/restart.sh
./scripts/healthcheck.shUse 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
Caddyfilechanges that require container recreation instead of a live reload
For those cases, use:
./scripts/update.sh