Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions apps/docs/src/content/docs/topics/provisioning-with-tofu.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ import FaqItem from "../../../components/FaqItem.tsx";
{ value: "5 min", label: "Provisioning time" },
]}
>
One declarative `tofu apply` provisions a Hetzner VPS, configures Cloudflare
DNS firewalls, installs Docker runtime, and bootstraps your full-stack
docker-compose environment.
One declarative `tofu apply` provisions a Hetzner VPS, configures
Cloudflare DNS and a Hetzner firewall scoped to Cloudflare's IP ranges,
installs Docker runtime, and bootstraps your full-stack docker-compose
environment.
</PageIntro>

BoringStack's [Deployment](/topics/deployment/) path is manual: SSH into a VPS, install Docker, clone the monorepo, `compose pull && compose up -d`. Some operators prefer this.
Expand Down Expand Up @@ -100,7 +101,7 @@ flowchart LR
{ tone: "ok", text: "Success! The configuration is valid." },
{
tone: "ok",
text: "Apply complete! Resources: 5 added, 0 changed, 0 destroyed.",
text: "Apply complete! Resources: 9 added, 0 changed, 0 destroyed.",
},
]}
/>
Expand All @@ -118,6 +119,20 @@ Bootstrap (Docker install, monorepo clone, GHCR image pulls, first `compose up -
output={[{ tone: "ok", text: "status: done" }]}
/>

<DocCallout type="note" title="`status: done` reflects cloud-init, not bootstrap.sh">
`cloud-init status` only knows whether its own steps ran — it goes
`done` even if the bootstrap script underneath failed mid-way (GHCR
pull error, `compose up` crash, etc.). If `curl /health` doesn't
return 200 after a few minutes, the real story is in:

```bash
ssh root@$(tofu output -raw vps_ipv4)
tail -200 /var/log/cloud-init-output.log # bootstrap.sh stdout/stderr
docker compose -f /opt/boringstack/infra/compose/compose/docker-compose.yml ps
docker compose logs traefik api ui # whichever isn't healthy
```
</DocCallout>

## Verify

<CommandRun
Expand Down Expand Up @@ -350,8 +365,14 @@ Wipes the Hetzner server, removes the Cloudflare records, deletes the firewall a
Origin not responding: check `docker compose logs traefik api` on the
server.
</FaqItem>
<FaqItem title="Site returns 525 from Cloudflare">
TLS handshake failed; ACME has not issued yet: wait or check Traefik logs.
<FaqItem title="Site returns 525 from Cloudflare on first apply">
Expected for the first 2–5 minutes after `apply`. Traefik needs DNS
to propagate globally before Let's Encrypt can complete the HTTP-01
challenge; until the cert lands, Cloudflare can't validate the
origin and serves 525. Traefik retries on its own — confirm with
`docker compose logs traefik | grep -i acme`. If it's still
failing past ~10 min, check that the apex A/AAAA records resolve
publicly (`dig +short @1.1.1.1 <domain>`).
</FaqItem>
<FaqItem title="Unexpected attribute errors in the editor">
Stale OpenTofu language-server cache. Run `tofu init` once and re-open.
Expand Down
30 changes: 16 additions & 14 deletions infra/bootstrap/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,22 @@ module "bootstrap" {
acme_email = var.acme_email

# Optional integrations
email_provider = var.email_provider
email_from = var.email_from
cloudflare_account_id = var.cloudflare_account_id
cloudflare_email_api_token = var.cloudflare_email_api_token
resend_api_key = var.resend_api_key
sendgrid_api_key = var.sendgrid_api_key
google_oauth_client_id = var.google_oauth_client_id
google_oauth_client_secret = var.google_oauth_client_secret
github_oauth_client_id = var.github_oauth_client_id
github_oauth_client_secret = var.github_oauth_client_secret
stripe_secret_key = var.stripe_secret_key
stripe_webhook_secret = var.stripe_webhook_secret
sentry_dsn = var.sentry_dsn
vite_sentry_dsn = var.vite_sentry_dsn
email_provider = var.email_provider
email_from = var.email_from
cloudflare_account_id = var.cloudflare_account_id
cloudflare_email_api_token = var.cloudflare_email_api_token
resend_api_key = var.resend_api_key
sendgrid_api_key = var.sendgrid_api_key
google_oauth_client_id = var.google_oauth_client_id
google_oauth_client_secret = var.google_oauth_client_secret
github_oauth_client_id = var.github_oauth_client_id
github_oauth_client_secret = var.github_oauth_client_secret
linkedin_oauth_client_id = var.linkedin_oauth_client_id
linkedin_oauth_client_secret = var.linkedin_oauth_client_secret
stripe_secret_key = var.stripe_secret_key
stripe_webhook_secret = var.stripe_webhook_secret
sentry_dsn = var.sentry_dsn
vite_sentry_dsn = var.vite_sentry_dsn

# Backups
backups_enabled = var.backups_enabled
Expand Down
56 changes: 29 additions & 27 deletions infra/bootstrap/modules/bootstrap/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -33,33 +33,35 @@ locals {
ui_image_name = "${local.repo_name}-ui"

compose_env_values = {
stack = "prod"
public_ui_host = var.domain
acme_email = var.acme_email
image_owner = local.image_owner
api_image_name = local.api_image_name
ui_image_name = local.ui_image_name
postgres_password = var.postgres_password
valkey_password = var.valkey_password
jwt_secret = var.jwt_secret
email_provider = var.email_provider
email_from = var.email_from
cloudflare_account_id = var.cloudflare_account_id
cloudflare_email_api_token = var.cloudflare_email_api_token
resend_api_key = var.resend_api_key
sendgrid_api_key = var.sendgrid_api_key
google_oauth_client_id = var.google_oauth_client_id
google_oauth_client_secret = var.google_oauth_client_secret
github_oauth_client_id = var.github_oauth_client_id
github_oauth_client_secret = var.github_oauth_client_secret
stripe_secret_key = var.stripe_secret_key
stripe_webhook_secret = var.stripe_webhook_secret
sentry_dsn = var.sentry_dsn
vite_sentry_dsn = var.vite_sentry_dsn
superuser_email = var.superuser_email
superuser_password = var.superuser_password
ghcr_username = var.ghcr_username
ghcr_token = var.ghcr_token
stack = "prod"
public_ui_host = var.domain
acme_email = var.acme_email
image_owner = local.image_owner
api_image_name = local.api_image_name
ui_image_name = local.ui_image_name
postgres_password = var.postgres_password
valkey_password = var.valkey_password
jwt_secret = var.jwt_secret
email_provider = var.email_provider
email_from = var.email_from
cloudflare_account_id = var.cloudflare_account_id
cloudflare_email_api_token = var.cloudflare_email_api_token
resend_api_key = var.resend_api_key
sendgrid_api_key = var.sendgrid_api_key
google_oauth_client_id = var.google_oauth_client_id
google_oauth_client_secret = var.google_oauth_client_secret
github_oauth_client_id = var.github_oauth_client_id
github_oauth_client_secret = var.github_oauth_client_secret
linkedin_oauth_client_id = var.linkedin_oauth_client_id
linkedin_oauth_client_secret = var.linkedin_oauth_client_secret
stripe_secret_key = var.stripe_secret_key
stripe_webhook_secret = var.stripe_webhook_secret
sentry_dsn = var.sentry_dsn
vite_sentry_dsn = var.vite_sentry_dsn
superuser_email = var.superuser_email
superuser_password = var.superuser_password
ghcr_username = var.ghcr_username
ghcr_token = var.ghcr_token
}

# Docker Compose treats single-quoted .env values literally, avoiding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ GOOGLE_OAUTH_CLIENT_ID=${google_oauth_client_id}
GOOGLE_OAUTH_CLIENT_SECRET=${google_oauth_client_secret}
GITHUB_OAUTH_CLIENT_ID=${github_oauth_client_id}
GITHUB_OAUTH_CLIENT_SECRET=${github_oauth_client_secret}
LINKEDIN_OAUTH_CLIENT_ID=${linkedin_oauth_client_id}
LINKEDIN_OAUTH_CLIENT_SECRET=${linkedin_oauth_client_secret}

# Billing
STRIPE_SECRET_KEY=${stripe_secret_key}
Expand Down
9 changes: 9 additions & 0 deletions infra/bootstrap/modules/bootstrap/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ variable "github_oauth_client_secret" {
sensitive = true
}

variable "linkedin_oauth_client_id" {
type = string
}

variable "linkedin_oauth_client_secret" {
type = string
sensitive = true
}

variable "stripe_secret_key" {
type = string
sensitive = true
Expand Down
22 changes: 22 additions & 0 deletions infra/bootstrap/terraform.tfvars.example
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,28 @@ acme_email = "ops@example.com" # real address; Let's Encrypt rejects exa

# monorepo_repo = "https://github.com/boringstack-xyz/boringstack"

# ----------------------------------------------------------------------------
# Private GHCR auth (optional; required if your fork pushes images to a
# private GitHub Container Registry — leave empty for public images.)
#
# GitHub PAT (classic) with `read:packages` scope. Set both or neither;
# the bootstrap renders a `docker login ghcr.io` only when both are set.
# ----------------------------------------------------------------------------

# ghcr_username = ""
# ghcr_token = ""

# ----------------------------------------------------------------------------
# First-boot superuser seed (optional)
#
# When both are set, the api-migrate job creates this user as an admin on
# first boot. Skip both to register through the UI yourself after apply.
# Rotate the password from the UI's password-reset flow once you're in.
# ----------------------------------------------------------------------------

# superuser_email = ""
# superuser_password = "" # min length enforced by the API env validator

# ----------------------------------------------------------------------------
# Email provider (optional; leave empty to disable)
#
Expand Down
25 changes: 24 additions & 1 deletion infra/bootstrap/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ variable "github_oauth_client_id" {

variable "github_oauth_client_secret" {
type = string
description = ""
description = "GitHub OAuth — paired with github_oauth_client_id."
default = ""
sensitive = true

Expand All @@ -323,6 +323,29 @@ variable "github_oauth_client_secret" {
}
}

variable "linkedin_oauth_client_id" {
type = string
description = "LinkedIn OAuth — leave empty to disable LinkedIn login."
default = ""

validation {
condition = length(regexall("[\r\n]", var.linkedin_oauth_client_id)) == 0
error_message = "linkedin_oauth_client_id must fit on one line."
}
}

variable "linkedin_oauth_client_secret" {
type = string
description = "LinkedIn OAuth — paired with linkedin_oauth_client_id."
default = ""
sensitive = true

validation {
condition = length(regexall("[\r\n]", var.linkedin_oauth_client_secret)) == 0
error_message = "linkedin_oauth_client_secret must fit on one line."
}
}

variable "stripe_secret_key" {
type = string
description = "Stripe secret key. Leave empty to disable billing."
Expand Down
Loading