diff --git a/apps/docs/src/content/docs/topics/provisioning-with-tofu.mdx b/apps/docs/src/content/docs/topics/provisioning-with-tofu.mdx index c64ec4bc..b11bb019 100644 --- a/apps/docs/src/content/docs/topics/provisioning-with-tofu.mdx +++ b/apps/docs/src/content/docs/topics/provisioning-with-tofu.mdx @@ -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. 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. @@ -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.", }, ]} /> @@ -118,6 +119,20 @@ Bootstrap (Docker install, monorepo clone, GHCR image pulls, first `compose up - output={[{ tone: "ok", text: "status: done" }]} /> + + `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 + ``` + + ## Verify - - TLS handshake failed; ACME has not issued yet: wait or check Traefik logs. + + 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 `). Stale OpenTofu language-server cache. Run `tofu init` once and re-open. diff --git a/infra/bootstrap/main.tf b/infra/bootstrap/main.tf index d9e5091d..b10e127d 100644 --- a/infra/bootstrap/main.tf +++ b/infra/bootstrap/main.tf @@ -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 diff --git a/infra/bootstrap/modules/bootstrap/main.tf b/infra/bootstrap/modules/bootstrap/main.tf index e316f991..b7fab90a 100644 --- a/infra/bootstrap/modules/bootstrap/main.tf +++ b/infra/bootstrap/modules/bootstrap/main.tf @@ -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 diff --git a/infra/bootstrap/modules/bootstrap/templates/compose.env.tftpl b/infra/bootstrap/modules/bootstrap/templates/compose.env.tftpl index 66fc211e..ee1c78b1 100644 --- a/infra/bootstrap/modules/bootstrap/templates/compose.env.tftpl +++ b/infra/bootstrap/modules/bootstrap/templates/compose.env.tftpl @@ -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} diff --git a/infra/bootstrap/modules/bootstrap/variables.tf b/infra/bootstrap/modules/bootstrap/variables.tf index e13178ca..60fa87f2 100644 --- a/infra/bootstrap/modules/bootstrap/variables.tf +++ b/infra/bootstrap/modules/bootstrap/variables.tf @@ -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 diff --git a/infra/bootstrap/terraform.tfvars.example b/infra/bootstrap/terraform.tfvars.example index 3589a16e..61f7b074 100644 --- a/infra/bootstrap/terraform.tfvars.example +++ b/infra/bootstrap/terraform.tfvars.example @@ -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) # diff --git a/infra/bootstrap/variables.tf b/infra/bootstrap/variables.tf index 310ec3da..e70661cb 100644 --- a/infra/bootstrap/variables.tf +++ b/infra/bootstrap/variables.tf @@ -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 @@ -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."