-
Notifications
You must be signed in to change notification settings - Fork 0
Harden production deployment configuration #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -148,19 +148,21 @@ INVOLUTE_IMAGE_NAMESPACE=fakechris INVOLUTE_IMAGE_TAG=latest pnpm compose:pull:u | |||||||||||||||||||
|
|
||||||||||||||||||||
| ## VPS deployment (fresh install) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| This is the recommended first production path: one VPS, Docker Compose, Postgres, the Node API, the static web container, and Caddy terminating HTTPS on a single domain. | ||||||||||||||||||||
| This is the recommended first production path: one VPS, Docker Compose, Postgres, the Node API, and the static web container from published Docker Hub images. Terminate HTTPS with either the host reverse proxy or the optional compose Caddy profile, but keep one env file: `.env.production`. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Status: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| - the deployment files and automation are in place | ||||||||||||||||||||
| - the Tailscale-only path has already been exercised | ||||||||||||||||||||
| - the remaining production work is to validate the same stack on a public domain with real Google OAuth callback and one backup/restore drill | ||||||||||||||||||||
| - production should run through `.env.production` and `docker-compose.prod.images.yml` | ||||||||||||||||||||
| - deployments should use Ansible Vault for secrets, not hand-edited env files | ||||||||||||||||||||
| - every deploy runs smoke checks for `/health`, `/auth/session`, and `/auth/google/start` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Files involved: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| - [`docker-compose.prod.yml`](./docker-compose.prod.yml) | ||||||||||||||||||||
| - [`docker-compose.prod.images.yml`](./docker-compose.prod.images.yml) | ||||||||||||||||||||
| - [`Caddyfile`](./Caddyfile) | ||||||||||||||||||||
| - [`.env.production.example`](./.env.production.example) | ||||||||||||||||||||
| - [`scripts/prod-smoke.sh`](./scripts/prod-smoke.sh) | ||||||||||||||||||||
| - [`scripts/postgres-backup.sh`](./scripts/postgres-backup.sh) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Assumptions: | ||||||||||||||||||||
|
|
@@ -181,8 +183,10 @@ cp .env.production.example .env.production | |||||||||||||||||||
| APP_DOMAIN=involute.example.com | ||||||||||||||||||||
| APP_ORIGIN=https://involute.example.com | ||||||||||||||||||||
| POSTGRES_PASSWORD=... | ||||||||||||||||||||
| DATABASE_URL=postgresql://involute:<same-url-safe-password>@db:5432/involute?schema=public | ||||||||||||||||||||
| AUTH_TOKEN=... | ||||||||||||||||||||
| VIEWER_ASSERTION_SECRET=... | ||||||||||||||||||||
| REQUIRE_GOOGLE_OAUTH=true | ||||||||||||||||||||
| ADMIN_EMAIL_ALLOWLIST=you@example.com | ||||||||||||||||||||
| GOOGLE_OAUTH_CLIENT_ID=... | ||||||||||||||||||||
| GOOGLE_OAUTH_CLIENT_SECRET=... | ||||||||||||||||||||
|
|
@@ -198,22 +202,24 @@ pnpm compose:prod:up | |||||||||||||||||||
| 4. Smoke check it: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ```bash | ||||||||||||||||||||
| docker compose --env-file .env.production -f docker-compose.prod.yml ps | ||||||||||||||||||||
| curl -I https://involute.example.com | ||||||||||||||||||||
| curl https://involute.example.com/health | ||||||||||||||||||||
| docker compose --env-file .env.production -f docker-compose.prod.images.yml ps | ||||||||||||||||||||
| pnpm smoke:prod https://involute.example.com | ||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| 5. If you need to re-assert the first admin explicitly: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ```bash | ||||||||||||||||||||
| docker compose --env-file .env.production -f docker-compose.prod.yml run --rm \ | ||||||||||||||||||||
| docker compose --env-file .env.production -f docker-compose.prod.images.yml run --rm \ | ||||||||||||||||||||
| --entrypoint /bin/sh server -lc \ | ||||||||||||||||||||
| 'pnpm --filter @turnkeyai/involute-server run admin:bootstrap you@example.com' | ||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Operational notes: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| - production compose keeps Postgres internal; only Caddy exposes `80/443` | ||||||||||||||||||||
| - production compose passes `DATABASE_URL` directly to the API and migration containers; do not rely on ad-hoc URL concatenation | ||||||||||||||||||||
| - use a URL-safe Postgres password so `DATABASE_URL` and `POSTGRES_PASSWORD` stay identical | ||||||||||||||||||||
| - by default, `docker-compose.prod.images.yml` exposes API/web on `SERVER_BIND_ADDRESS:4200` and `WEB_BIND_ADDRESS:4201` for a host reverse proxy | ||||||||||||||||||||
| - to let compose Caddy own `80/443`, run with the `caddy` profile and ensure host nginx/apache is not bound to those ports | ||||||||||||||||||||
| - `server-init` runs `prisma migrate deploy` before the API starts | ||||||||||||||||||||
| - `SEED_DATABASE` defaults to `false` in production; turn it on only for a fresh demo seed | ||||||||||||||||||||
| - the web container is the static production build, not the Vite dev server | ||||||||||||||||||||
|
|
@@ -265,7 +271,17 @@ For the current Tailscale-only test phase, use: | |||||||||||||||||||
| - `involute_bind_address: <tailscale-ip>` | ||||||||||||||||||||
| - `involute_app_origin: http://<tailscale-ip>:4201` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| When the public domain and OAuth are ready, switch the inventory to `production` and use [`docker-compose.prod.yml`](./docker-compose.prod.yml). | ||||||||||||||||||||
| When the public domain and OAuth are ready, switch the inventory to `production` and use [`docker-compose.prod.images.yml`](./docker-compose.prod.images.yml). | ||||||||||||||||||||
|
|
||||||||||||||||||||
| For local Ansible deploys, keep secrets in an encrypted vault file: | ||||||||||||||||||||
|
|
||||||||||||||||||||
| ```bash | ||||||||||||||||||||
| cp ops/ansible/group_vars/all/vault.yml.example ops/ansible/group_vars/all/vault.yml | ||||||||||||||||||||
| ansible-vault encrypt ops/ansible/group_vars/all/vault.yml | ||||||||||||||||||||
| ANSIBLE_VAULT_PASSWORD_FILE=ops/ansible/vault-password.txt pnpm deploy:prod | ||||||||||||||||||||
|
Comment on lines
+279
to
+281
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Include the vault password-file creation step. This flow encrypts Docs patch cp ops/ansible/group_vars/all/vault.yml.example ops/ansible/group_vars/all/vault.yml
-ansible-vault encrypt ops/ansible/group_vars/all/vault.yml
+printf '%s\n' '<choose-a-vault-password>' > ops/ansible/vault-password.txt
+chmod 600 ops/ansible/vault-password.txt
+ANSIBLE_VAULT_PASSWORD_FILE=ops/ansible/vault-password.txt \
+ ansible-vault encrypt ops/ansible/group_vars/all/vault.yml
ANSIBLE_VAULT_PASSWORD_FILE=ops/ansible/vault-password.txt pnpm deploy:prod📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||
| ``` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| `ops/ansible/group_vars/all/vault.yml` and `ops/ansible/vault-password.txt` are ignored by git. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| GitHub Actions can run the same deployment path from [`.github/workflows/deploy.yml`](./.github/workflows/deploy.yml). Configure these repository secrets before enabling it: | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -278,7 +294,8 @@ GitHub Actions can run the same deployment path from [`.github/workflows/deploy. | |||||||||||||||||||
| - `INVOLUTE_VIEWER_ASSERTION_SECRET` | ||||||||||||||||||||
| - `INVOLUTE_BIND_ADDRESS` for `tailscale` | ||||||||||||||||||||
| - `INVOLUTE_APP_DOMAIN` and `INVOLUTE_POSTGRES_PASSWORD` for `production` | ||||||||||||||||||||
| - optional: `INVOLUTE_ADMIN_EMAIL_ALLOWLIST`, `INVOLUTE_GOOGLE_OAUTH_CLIENT_ID`, `INVOLUTE_GOOGLE_OAUTH_CLIENT_SECRET`, `INVOLUTE_GOOGLE_OAUTH_REDIRECT_URI` | ||||||||||||||||||||
| - `INVOLUTE_GOOGLE_OAUTH_CLIENT_ID`, `INVOLUTE_GOOGLE_OAUTH_CLIENT_SECRET`, `INVOLUTE_GOOGLE_OAUTH_REDIRECT_URI` when `REQUIRE_GOOGLE_OAUTH=true` | ||||||||||||||||||||
| - optional: `INVOLUTE_ADMIN_EMAIL_ALLOWLIST`, `INVOLUTE_IMAGE_TAG` | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Recommended repository variables: | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # Copy this file to vault.yml, replace values, then encrypt it: | ||
| # | ||
| # ansible-vault encrypt ops/ansible/group_vars/all/vault.yml | ||
| # | ||
| # Keep vault.yml and the vault password file out of git. | ||
|
|
||
| involute_auth_token: replace-with-a-long-random-token | ||
| involute_viewer_assertion_secret: replace-with-a-long-random-secret | ||
| involute_admin_email_allowlist: you@example.com | ||
|
|
||
| # Use a URL-safe password because DATABASE_URL is built from this value. | ||
| involute_postgres_password: replace-with-a-long-url-safe-random-password | ||
|
|
||
| involute_google_oauth_client_id: replace-with-google-client-id | ||
| involute_google_oauth_client_secret: replace-with-google-client-secret | ||
| involute_google_oauth_redirect_uri: https://involute.example.com/auth/google/callback |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,13 @@ | ||
| all: | ||
| hosts: | ||
| involute_tailscale: | ||
| involute_production: | ||
| ansible_host: tailnet-host.example.ts.net | ||
| ansible_user: root | ||
| involute_stack_profile: production | ||
| involute_bind_address: 100.x.y.z | ||
| involute_stack_profile: tailscale | ||
| involute_app_origin: http://100.x.y.z:4201 | ||
| involute_seed_database: true | ||
| involute_admin_email_allowlist: "first-admin@example.com" | ||
| involute_auth_token: change-me | ||
| involute_viewer_assertion_secret: change-me | ||
| involute_app_domain: involute.example.com | ||
| involute_app_origin: https://involute.example.com | ||
| involute_smoke_base_url: https://involute.example.com | ||
| involute_seed_database: false | ||
| involute_require_google_oauth: true | ||
| # Put real secrets in ops/ansible/group_vars/all/vault.yml. | ||
|
Comment on lines
+3
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Example still carries Tailscale-style bind address under the
Either drop 🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid defaulting production deploys to mutable
latest.Line 43 makes the deployed image depend on whatever
latestpoints to at deploy time, which can race Docker publishing or redeploy an unintended build. Prefer requiringINVOLUTE_IMAGE_TAGforproduction, or deploy an immutable tag such as the publishedsha-<short-sha>tag.Suggested direction
Then fail validation for production when it is unset, or set it from a workflow that runs after the image publish completes.
🤖 Prompt for AI Agents