Compare commits
1 Commits
f7fdf3930c
...
feat/dokpl
| Author | SHA1 | Date | |
|---|---|---|---|
| 57bca19aff |
@@ -2,6 +2,7 @@ APP_NAME=Amare
|
|||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
|
# Staging/production: set APP_URL to the public HTTPS origin (e.g. https://staging.example.com).
|
||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
|
|
||||||
APP_LOCALE=pt_BR
|
APP_LOCALE=pt_BR
|
||||||
@@ -38,6 +39,10 @@ SESSION_LIFETIME=120
|
|||||||
SESSION_ENCRYPT=false
|
SESSION_ENCRYPT=false
|
||||||
SESSION_PATH=/
|
SESSION_PATH=/
|
||||||
SESSION_DOMAIN=null
|
SESSION_DOMAIN=null
|
||||||
|
# Staging/production behind HTTPS (Dokploy Traefik): SESSION_SECURE_COOKIE=true
|
||||||
|
SESSION_SECURE_COOKIE=false
|
||||||
|
SESSION_HTTP_ONLY=true
|
||||||
|
SESSION_SAME_SITE=lax
|
||||||
|
|
||||||
BROADCAST_CONNECTION=log
|
BROADCAST_CONNECTION=log
|
||||||
FILESYSTEM_DISK=local
|
FILESYSTEM_DISK=local
|
||||||
|
|||||||
82
.github/workflows/deploy-staging.yml
vendored
Normal file
82
.github/workflows/deploy-staging.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
name: Deploy staging
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: [CI]
|
||||||
|
types: [completed]
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-staging
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: publish-and-deploy-staging
|
||||||
|
if: >-
|
||||||
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
github.event.workflow_run.event == 'push' &&
|
||||||
|
github.event.workflow_run.head_branch == 'main'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout deployed SHA
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
|
||||||
|
- name: Set image metadata
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
SHA="${{ github.event.workflow_run.head_sha }}"
|
||||||
|
SHORT_SHA="${SHA:0:7}"
|
||||||
|
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||||
|
IMAGE="$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
echo "sha=${SHA}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and push SHA + staging tags
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.sha }}
|
||||||
|
${{ steps.meta.outputs.image }}:staging
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Deploy staging on Dokploy
|
||||||
|
env:
|
||||||
|
DOKPLOY_URL: ${{ secrets.DOKPLOY_URL }}
|
||||||
|
DOKPLOY_API_KEY: ${{ secrets.DOKPLOY_API_KEY }}
|
||||||
|
DOKPLOY_COMPOSE_ID: ${{ secrets.DOKPLOY_STAGING_COMPOSE_ID }}
|
||||||
|
DEPLOY_TITLE: "staging ${{ steps.meta.outputs.short_sha }}"
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/deploy/dokploy-deploy.sh
|
||||||
|
./scripts/deploy/dokploy-deploy.sh
|
||||||
|
|
||||||
|
- name: Smoke staging
|
||||||
|
env:
|
||||||
|
SMOKE_BASE_URL: ${{ secrets.STAGING_URL }}
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/deploy/smoke.sh
|
||||||
|
./scripts/deploy/smoke.sh
|
||||||
84
.github/workflows/promote-production.yml
vendored
Normal file
84
.github/workflows/promote-production.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
name: Promote production
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
sha:
|
||||||
|
description: Full git SHA already published to GHCR (same digest used by staging)
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
confirm:
|
||||||
|
description: Type PRODUCTION to confirm promotion of the given SHA
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-production
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
promote:
|
||||||
|
name: promote-production
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Guard confirmation
|
||||||
|
run: |
|
||||||
|
if [ "${{ inputs.confirm }}" != "PRODUCTION" ]; then
|
||||||
|
echo "Confirmation must be exactly PRODUCTION" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Checkout repository scripts
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set image metadata
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
SHA="${{ inputs.sha }}"
|
||||||
|
SHORT_SHA="${SHA:0:7}"
|
||||||
|
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||||
|
IMAGE="$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
echo "sha=${SHA}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Point :production at existing SHA digest (no rebuild)
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag "${{ steps.meta.outputs.image }}:production" \
|
||||||
|
"${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.sha }}"
|
||||||
|
|
||||||
|
- name: Deploy production on Dokploy
|
||||||
|
env:
|
||||||
|
DOKPLOY_URL: ${{ secrets.DOKPLOY_URL }}
|
||||||
|
DOKPLOY_API_KEY: ${{ secrets.DOKPLOY_API_KEY }}
|
||||||
|
DOKPLOY_COMPOSE_ID: ${{ secrets.DOKPLOY_PRODUCTION_COMPOSE_ID }}
|
||||||
|
DEPLOY_TITLE: "production ${{ steps.meta.outputs.short_sha }}"
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/deploy/dokploy-deploy.sh
|
||||||
|
./scripts/deploy/dokploy-deploy.sh
|
||||||
|
|
||||||
|
- name: Smoke production
|
||||||
|
env:
|
||||||
|
SMOKE_BASE_URL: ${{ secrets.PRODUCTION_URL }}
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/deploy/smoke.sh
|
||||||
|
./scripts/deploy/smoke.sh
|
||||||
@@ -125,3 +125,10 @@ Após `php artisan db:seed`:
|
|||||||
- [SPEC.md](SPEC.md) — especificação do produto
|
- [SPEC.md](SPEC.md) — especificação do produto
|
||||||
- [docs/adr/](docs/adr/) — ADRs aceitas
|
- [docs/adr/](docs/adr/) — ADRs aceitas
|
||||||
- [docs/conventions/php-strict-types.md](docs/conventions/php-strict-types.md) — convenção de strict types
|
- [docs/conventions/php-strict-types.md](docs/conventions/php-strict-types.md) — convenção de strict types
|
||||||
|
- [docs/deployment/dokploy.md](docs/deployment/dokploy.md) — deploy staging/produção no Dokploy + GHCR
|
||||||
|
|
||||||
|
## Deploy (Dokploy)
|
||||||
|
|
||||||
|
Staging publica automaticamente após CI verde em `main` (imagem GHCR por SHA + alias `:staging`). Produção promove a **mesma digest** com workflow manual `Promote production` (sem rebuild).
|
||||||
|
|
||||||
|
Ver runbook completo: [docs/deployment/dokploy.md](docs/deployment/dokploy.md).
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
//
|
// Trust Traefik/Dokploy (and local reverse proxies) for X-Forwarded-* headers.
|
||||||
|
$middleware->trustProxies(at: '*');
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
$exceptions->shouldRenderJsonWhen(
|
$exceptions->shouldRenderJsonWhen(
|
||||||
|
|||||||
59
docker-compose.deploy.yml
Normal file
59
docker-compose.deploy.yml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Shared Compose for Dokploy staging and production.
|
||||||
|
# Both stacks use the same file with different env:
|
||||||
|
# APP_IMAGE=ghcr.io/<owner>/<repo>
|
||||||
|
# IMAGE_TAG=staging|production|<git-sha>
|
||||||
|
# PostgreSQL is a separate Dokploy database service (not defined here).
|
||||||
|
# Traefik/Dokploy domains should target service `web` port 8000.
|
||||||
|
|
||||||
|
services:
|
||||||
|
migrate:
|
||||||
|
image: ${APP_IMAGE}:${IMAGE_TAG}
|
||||||
|
pull_policy: always
|
||||||
|
restart: "no"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
command: ["php", "artisan", "migrate", "--force", "--no-interaction"]
|
||||||
|
|
||||||
|
web:
|
||||||
|
image: ${APP_IMAGE}:${IMAGE_TAG}
|
||||||
|
pull_policy: always
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
migrate:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
expose:
|
||||||
|
- "8000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/up"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 40s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
queue:
|
||||||
|
image: ${APP_IMAGE}:${IMAGE_TAG}
|
||||||
|
pull_policy: always
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
migrate:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
command: ["php", "artisan", "queue:work", "--sleep=2", "--tries=3", "--max-time=3600"]
|
||||||
|
stop_grace_period: 60s
|
||||||
|
stop_signal: SIGTERM
|
||||||
|
|
||||||
|
scheduler:
|
||||||
|
image: ${APP_IMAGE}:${IMAGE_TAG}
|
||||||
|
pull_policy: always
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
migrate:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
command: ["php", "artisan", "schedule:work"]
|
||||||
|
stop_grace_period: 30s
|
||||||
|
stop_signal: SIGTERM
|
||||||
@@ -7,5 +7,8 @@ Mesma imagem, comandos distintos:
|
|||||||
| web | `frankenphp run --config /etc/caddy/Caddyfile` |
|
| web | `frankenphp run --config /etc/caddy/Caddyfile` |
|
||||||
| queue | `php artisan queue:work --sleep=2 --tries=3` |
|
| queue | `php artisan queue:work --sleep=2 --tries=3` |
|
||||||
| scheduler | `php artisan schedule:work` |
|
| scheduler | `php artisan schedule:work` |
|
||||||
|
| migrate | `php artisan migrate --force` (one-shot no Compose de deploy) |
|
||||||
|
|
||||||
FrankenPHP em **modo regular** (ADR-006). Worker mode proibido no MVP.
|
FrankenPHP em **modo regular** (ADR-006). Worker mode proibido no MVP.
|
||||||
|
|
||||||
|
Deploy Dokploy (staging/produção): ver [`docker-compose.deploy.yml`](../docker-compose.deploy.yml) e [docs/deployment/dokploy.md](../docs/deployment/dokploy.md).
|
||||||
|
|||||||
236
docs/deployment/dokploy.md
Normal file
236
docs/deployment/dokploy.md
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
# Deploy Dokploy (staging → production)
|
||||||
|
|
||||||
|
Runbook for operating Amare on a VPS with Dokploy connected to GitHub, publishing immutable images to GHCR.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
CI (main) → build FrankenPHP image → GHCR :<sha> + :staging
|
||||||
|
→ Dokploy staging compose.deploy
|
||||||
|
→ smoke /up / /admin/login
|
||||||
|
|
||||||
|
Promote (manual) → retag same digest as :production (no rebuild)
|
||||||
|
→ Dokploy production compose.deploy
|
||||||
|
→ smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
| Piece | Detail |
|
||||||
|
|---|---|
|
||||||
|
| Compose file | [`docker-compose.deploy.yml`](../../docker-compose.deploy.yml) |
|
||||||
|
| Processes | `migrate` (one-shot) → `web` / `queue` / `scheduler` |
|
||||||
|
| Image | `ghcr.io/<owner>/<repo>:<sha>` (+ aliases `:staging`, `:production`) |
|
||||||
|
| Database | Dokploy PostgreSQL **per environment** (not in the app image) |
|
||||||
|
| Media | Cloudflare R2 (`FILESYSTEM_DISK=r2`), separate buckets per environment |
|
||||||
|
| Mail | Resend (`MAIL_MAILER=resend`) |
|
||||||
|
| Proxy | Dokploy Traefik → service `web` port `8000` |
|
||||||
|
|
||||||
|
## Prerequisites (manual)
|
||||||
|
|
||||||
|
1. Dokploy installed on the VPS; GitHub provider connected.
|
||||||
|
2. GHCR registry in Dokploy (`ghcr.io`) with a PAT that can **read** packages (`read:packages`). Prefer a dedicated bot/token; do not store write tokens on the VPS.
|
||||||
|
3. Two PostgreSQL services in Dokploy (staging + production), private (no public port).
|
||||||
|
4. Two R2 buckets (or prefixes) and Resend credentials for each environment as needed.
|
||||||
|
5. Domains (or temporary Dokploy/traefik.me hosts) pointing at the VPS with TLS.
|
||||||
|
|
||||||
|
## Create Compose stacks
|
||||||
|
|
||||||
|
Create **two** Dokploy Compose services (same repo, same compose path):
|
||||||
|
|
||||||
|
| Stack | Compose path | `IMAGE_TAG` | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| staging | `docker-compose.deploy.yml` | `staging` | Auto-deployed after CI on `main` |
|
||||||
|
| production | `docker-compose.deploy.yml` | `production` | Manual promotion only |
|
||||||
|
|
||||||
|
Dokploy Environment for each stack must set:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
APP_IMAGE=ghcr.io/<owner>/<repo>
|
||||||
|
IMAGE_TAG=staging # or production
|
||||||
|
```
|
||||||
|
|
||||||
|
Point Dokploy domain(s) at service **`web`**, port **`8000`**. Do not publish PostgreSQL or host ports for app processes.
|
||||||
|
|
||||||
|
Source can be GitHub (so Dokploy clones the compose file) or Raw paste of `docker-compose.deploy.yml`. Prefer GitHub + fixed compose path so updates stay in sync with `main`.
|
||||||
|
|
||||||
|
## Required Laravel env (Dokploy only)
|
||||||
|
|
||||||
|
Set these in Dokploy Environment UI (written to `.env` next to the compose file). **Never** put them in GitHub Actions secrets or image layers.
|
||||||
|
|
||||||
|
```env
|
||||||
|
APP_NAME=Amare
|
||||||
|
APP_ENV=staging # or production
|
||||||
|
APP_KEY=base64:... # unique per environment — generate with php artisan key:generate --show
|
||||||
|
APP_DEBUG=false
|
||||||
|
APP_URL=https://staging.example.com
|
||||||
|
|
||||||
|
APP_LOCALE=pt_BR
|
||||||
|
APP_FALLBACK_LOCALE=pt_BR
|
||||||
|
APP_TIMEZONE=America/Fortaleza
|
||||||
|
|
||||||
|
DB_CONNECTION=pgsql
|
||||||
|
DB_HOST=<dokploy-postgres-host>
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_DATABASE=amare_staging
|
||||||
|
DB_USERNAME=...
|
||||||
|
DB_PASSWORD=...
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_SECURE_COOKIE=true
|
||||||
|
SESSION_HTTP_ONLY=true
|
||||||
|
SESSION_SAME_SITE=lax
|
||||||
|
CACHE_STORE=database
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
FILESYSTEM_DISK=r2
|
||||||
|
R2_ACCESS_KEY_ID=...
|
||||||
|
R2_SECRET_ACCESS_KEY=...
|
||||||
|
R2_BUCKET=...
|
||||||
|
R2_ENDPOINT=https://<account_id>.r2.cloudflarestorage.com
|
||||||
|
R2_URL=https://media-staging.example.com
|
||||||
|
|
||||||
|
MAIL_MAILER=resend
|
||||||
|
RESEND_API_KEY=...
|
||||||
|
MAIL_FROM_ADDRESS=noreply@example.com
|
||||||
|
MAIL_FROM_NAME=Amare
|
||||||
|
|
||||||
|
LOG_LEVEL=warning
|
||||||
|
```
|
||||||
|
|
||||||
|
Trusted proxies are configured in `bootstrap/app.php` so Traefik `X-Forwarded-*` headers work for HTTPS cookies and URLs.
|
||||||
|
|
||||||
|
## GitHub Actions secrets
|
||||||
|
|
||||||
|
Repository secrets used by workflows:
|
||||||
|
|
||||||
|
| Secret | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `DOKPLOY_URL` | Base URL of the Dokploy panel (no trailing slash) |
|
||||||
|
| `DOKPLOY_API_KEY` | API key from Dokploy profile → API/CLI |
|
||||||
|
| `DOKPLOY_STAGING_COMPOSE_ID` | Staging compose id |
|
||||||
|
| `DOKPLOY_PRODUCTION_COMPOSE_ID` | Production compose id |
|
||||||
|
| `STAGING_URL` | Public origin for staging smoke (e.g. `https://staging.example.com`) |
|
||||||
|
| `PRODUCTION_URL` | Public origin for production smoke |
|
||||||
|
|
||||||
|
`GITHUB_TOKEN` (automatic) publishes to GHCR with `packages:write`. No Laravel/`APP_KEY`/DB/R2/Resend secrets belong in GitHub for this pipeline.
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
### Staging (automatic)
|
||||||
|
|
||||||
|
[`.github/workflows/deploy-staging.yml`](../../.github/workflows/deploy-staging.yml)
|
||||||
|
|
||||||
|
1. Waits for workflow `CI` success on push to `main`.
|
||||||
|
2. Builds once; pushes `:<full-sha>` and `:staging`.
|
||||||
|
3. Calls Dokploy `compose.deploy` and polls until done.
|
||||||
|
4. Runs [`scripts/deploy/smoke.sh`](../../scripts/deploy/smoke.sh) against `STAGING_URL`.
|
||||||
|
|
||||||
|
### Production (manual)
|
||||||
|
|
||||||
|
[`.github/workflows/promote-production.yml`](../../.github/workflows/promote-production.yml)
|
||||||
|
|
||||||
|
1. Operator runs **Actions → Promote production**.
|
||||||
|
2. Inputs: full `sha` already on GHCR; `confirm` must be exactly `PRODUCTION`.
|
||||||
|
3. Retags the **same digest** as `:production` (no rebuild).
|
||||||
|
4. Deploys production compose + smoke.
|
||||||
|
|
||||||
|
Private repos on GitHub Free do not get Environment required reviewers; human approval is the explicit `workflow_dispatch` + confirmation string. GitHub Pro Environment reviewers are optional later.
|
||||||
|
|
||||||
|
## First admin (no `db:seed` in production)
|
||||||
|
|
||||||
|
Seed credentials are local-only. For staging/production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From Dokploy → staging/production → Open terminal on `web` (or one-off run)
|
||||||
|
php artisan tinker --execute="
|
||||||
|
\\App\\Models\\User::query()->updateOrCreate(
|
||||||
|
['email' => 'admin@example.com'],
|
||||||
|
[
|
||||||
|
'name' => 'Admin',
|
||||||
|
'password' => bcrypt('use-a-strong-password'),
|
||||||
|
'role' => 'admin',
|
||||||
|
'is_active' => true,
|
||||||
|
'email_verified_at' => now(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
Adjust field names if the model evolves. Never reuse `admin@amare.local` / `password`.
|
||||||
|
|
||||||
|
## Backup and restore
|
||||||
|
|
||||||
|
Policy (SPEC §16.3): daily PostgreSQL backup, retention ≥ 14 days, RPO ≤ 24h, RTO ≤ 4h.
|
||||||
|
|
||||||
|
### Configure (Dokploy)
|
||||||
|
|
||||||
|
1. Settings → Destinations: add S3-compatible destination (AWS S3, R2, etc.).
|
||||||
|
2. Open each PostgreSQL service → Backup:
|
||||||
|
- Destination: the S3 destination
|
||||||
|
- Schedule: cron e.g. `0 3 * * *`
|
||||||
|
- Prefix: `amare/staging` or `amare/production`
|
||||||
|
- Enabled: on
|
||||||
|
3. Click **Test** and verify the object appears in the bucket.
|
||||||
|
4. Prefer Dokploy alerts/webhooks for backup failure if configured.
|
||||||
|
|
||||||
|
### Restore (staging rehearsal before first production promote)
|
||||||
|
|
||||||
|
1. Create a scratch database or restore into a disposable Postgres service.
|
||||||
|
2. Database → Backup → **Restore**: pick destination + backup file + target database name.
|
||||||
|
3. Point a temporary compose env at the restored DB and confirm `/up` + `/admin/login`.
|
||||||
|
4. Document the timestamp of the successful rehearsal.
|
||||||
|
|
||||||
|
Do **not** promote to production until staging restore has been proven once.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
No rebuild. Move the environment alias to a previous SHA digest and redeploy.
|
||||||
|
|
||||||
|
### Staging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Locally or in a one-off Actions shell with GHCR login
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag ghcr.io/<owner>/<repo>:staging \
|
||||||
|
ghcr.io/<owner>/<repo>:<previous-sha>
|
||||||
|
|
||||||
|
# Then trigger Dokploy deploy (UI Deploy, or):
|
||||||
|
DOKPLOY_URL=... DOKPLOY_API_KEY=... DOKPLOY_COMPOSE_ID=... \
|
||||||
|
./scripts/deploy/dokploy-deploy.sh
|
||||||
|
|
||||||
|
SMOKE_BASE_URL=https://staging.example.com ./scripts/deploy/smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
Same pattern with `:production` tag and production compose id / URL. Prefer re-running **Promote production** with the previous SHA and confirmation `PRODUCTION`.
|
||||||
|
|
||||||
|
If a migration is not backward-compatible, fix forward with a new SHA; keep migrations reversible when possible.
|
||||||
|
|
||||||
|
## Smoke checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SMOKE_BASE_URL=https://staging.example.com ./scripts/deploy/smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expects HTTP 200 for `/up`, `/`, and `/admin/login`.
|
||||||
|
|
||||||
|
## Domain checklist before production promote
|
||||||
|
|
||||||
|
- [ ] Final hostname DNS → VPS
|
||||||
|
- [ ] Dokploy TLS certificate issued
|
||||||
|
- [ ] `APP_URL` matches public HTTPS origin
|
||||||
|
- [ ] `SESSION_SECURE_COOKIE=true`
|
||||||
|
- [ ] Staging smoke green on the SHA to promote
|
||||||
|
- [ ] Staging backup + restore rehearsed
|
||||||
|
- [ ] Production Postgres backup schedule enabled
|
||||||
|
- [ ] Production R2 bucket + Resend domain ready
|
||||||
|
- [ ] First admin created without seed
|
||||||
|
|
||||||
|
## Local validation of Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
APP_IMAGE=ghcr.io/<owner>/<repo> IMAGE_TAG=staging \
|
||||||
|
docker compose -f docker-compose.deploy.yml config
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires a `.env` file present (Dokploy creates it from Environment UI). For local config checks, an empty `.env` is enough.
|
||||||
@@ -15,15 +15,17 @@ Fases 0–1 e provedores de produção (Resend + R2) estão em `main` com CI ver
|
|||||||
|
|
||||||
- Staging em Dokploy com a mesma imagem FrankenPHP por SHA para web, queue e scheduler.
|
- Staging em Dokploy com a mesma imagem FrankenPHP por SHA para web, queue e scheduler.
|
||||||
- Deploy automático em `main` após CI: build → GHCR → Dokploy API → migrate → health → smoke.
|
- Deploy automático em `main` após CI: build → GHCR → Dokploy API → migrate → health → smoke.
|
||||||
- Compose local com app FrankenPHP + Postgres.
|
- Produção via promoção manual da mesma digest SHA (alias `:production`), sem rebuild.
|
||||||
- PHP 8.4 canônico em docs/Docker/CI.
|
- Backup PostgreSQL diário (retenção ≥14 dias), restore documentado e rollback por SHA.
|
||||||
- Gates com `npm audit` e cobertura Domain/Application ≥ 80%.
|
- Compose local com app FrankenPHP + Postgres (pendente fora da fatia deploy).
|
||||||
- E-mail verificado + reset de senha seguros no painel (ADM-01 / SPEC §12.1).
|
- PHP 8.4 canônico em docs/Docker/CI (pendente).
|
||||||
- Strict types em PHP próprio faltante.
|
- Gates com `npm audit` e cobertura Domain/Application ≥ 80% (pendente).
|
||||||
|
- E-mail verificado + reset de senha seguros no painel (ADM-01 / SPEC §12.1) (pendente).
|
||||||
|
- Strict types em PHP próprio faltante (pendente).
|
||||||
|
|
||||||
**Non-Goals:**
|
**Non-Goals:**
|
||||||
|
|
||||||
- Produção, promoção humana, provisionamento de VPS para clientes.
|
- Deploy automático em produção; provisionamento de VPS para clientes.
|
||||||
- Fase 2 (briefing/CRM/E2E-01/02).
|
- Fase 2 (briefing/CRM/E2E-01/02).
|
||||||
- Redis, worker mode, CDN automation, signed private media.
|
- Redis, worker mode, CDN automation, signed private media.
|
||||||
|
|
||||||
@@ -31,11 +33,13 @@ Fases 0–1 e provedores de produção (Resend + R2) estão em `main` com CI ver
|
|||||||
|
|
||||||
### D1 — Dokploy Compose na VPS, imagem imutável no GHCR
|
### D1 — Dokploy Compose na VPS, imagem imutável no GHCR
|
||||||
|
|
||||||
GitHub Actions (após CI verde em `main`) constrói **uma** imagem `ghcr.io/<owner>/<repo>:<git-sha>` (+ alias `:staging`), faz push privado e chama `POST /api/compose.deploy` (ou `compose.update` + `compose.deploy`) no Dokploy com `x-api-key`.
|
GitHub Actions (após CI verde em `main`) constrói **uma** imagem `ghcr.io/<owner>/<repo>:<git-sha>` (+ alias `:staging`), faz push privado e chama `POST /api/compose.deploy` no Dokploy com `x-api-key`.
|
||||||
|
|
||||||
Compose de staging referencia `${APP_IMAGE}` / `IMAGE_TAG` para `web`, `queue`, `scheduler` e job `migrate` one-shot (`php artisan migrate --force`). PostgreSQL é serviço Dokploy separado (não na imagem da app).
|
Compose compartilhado (`docker-compose.deploy.yml`) referencia `${APP_IMAGE}` / `IMAGE_TAG` para `web`, `queue`, `scheduler` e job `migrate` one-shot (`php artisan migrate --force`). Dokploy mantém **duas** stacks Compose (staging e production) com `IMAGE_TAG` distinto. PostgreSQL é serviço Dokploy separado por ambiente (não na imagem da app).
|
||||||
|
|
||||||
*Alternativas rejeitadas:* Railway (Pro para GHCR privado + desvio de plataforma); rebuild por serviço no Dokploy (quebra “mesma imagem” SPEC §14.3/§15.2); tag só `:latest` (rollback frágil).
|
Produção: `workflow_dispatch` com SHA + confirmação `PRODUCTION` move alias `:production` para o **mesmo digest** já publicado e dispara `compose.deploy` na stack de produção. Repo privado no GitHub Free não tem required reviewers de Environment; aprovação humana = disparo manual explícito (GitHub Pro opcional depois).
|
||||||
|
|
||||||
|
*Alternativas rejeitadas:* Railway (Pro para GHCR privado + desvio de plataforma); rebuild por serviço no Dokploy (quebra “mesma imagem” SPEC §14.3/§15.2); tag só `:latest` (rollback frágil); deploy automático direto em produção.
|
||||||
|
|
||||||
### D2 — Processos e health
|
### D2 — Processos e health
|
||||||
|
|
||||||
@@ -50,7 +54,11 @@ Healthcheck Docker/Dokploy e smoke pós-deploy usam `GET /up` (sem auth, sem sec
|
|||||||
|
|
||||||
### D3 — Secrets e providers
|
### D3 — Secrets e providers
|
||||||
|
|
||||||
Secrets em GitHub Actions + env Dokploy: `APP_KEY`, DB, `MAIL_MAILER=resend` / `RESEND_API_KEY`, `FILESYSTEM_DISK=r2` / `R2_*`, tokens Dokploy/GHCR. Nunca embeds em layer. Local continua `MAIL_MAILER=log` e disco `public`.
|
GitHub Actions guarda só orquestração: `DOKPLOY_URL`, API key, compose IDs, URLs públicas de smoke. Env Laravel (`APP_KEY`, DB, Resend, R2) vive **somente** no Dokploy, isolado por ambiente. Nunca embeds em layer. Local continua `MAIL_MAILER=log` e disco `public`.
|
||||||
|
|
||||||
|
### D3b — Backup e restore
|
||||||
|
|
||||||
|
PostgreSQL staging/produção: backup diário via Dokploy → destino S3-compatible, retenção mínima 14 dias (SPEC §16.3). Restore documentado e testado em staging antes da primeira promoção a produção.
|
||||||
|
|
||||||
### D4 — Compose local com app
|
### D4 — Compose local com app
|
||||||
|
|
||||||
@@ -74,26 +82,32 @@ Alinhar README, CI (`setup-php` 8.4) e `Dockerfile` `ARG PHP_VERSION=8.4`. Mante
|
|||||||
|
|
||||||
### D8 — Rollback
|
### D8 — Rollback
|
||||||
|
|
||||||
Rollback = apontar Compose staging para tag SHA anterior no GHCR e `compose.deploy`/`redeploy`. Falha de healthcheck impede promoção. Sem rebuild.
|
Rollback = mover alias do ambiente (`:staging` ou `:production`) para tag SHA anterior no GHCR e `compose.deploy`. Falha de healthcheck/smoke impede promoção. Sem rebuild.
|
||||||
|
|
||||||
|
### D9 — Trusted proxies atrás do Traefik
|
||||||
|
|
||||||
|
`bootstrap/app.php` confia em proxies (`trustProxies(at: '*')`) para honrar `X-Forwarded-*` do Traefik/Dokploy. Staging/produção usam `SESSION_SECURE_COOKIE=true` com HTTPS.
|
||||||
|
|
||||||
## Risks / Trade-offs
|
## Risks / Trade-offs
|
||||||
|
|
||||||
- **[GHCR privado + pull na VPS]** → configurar registry no Dokploy com PAT `read:packages`; documentar checklist.
|
- **[GHCR privado + pull na VPS]** → configurar registry no Dokploy com PAT `read:packages`; documentar checklist.
|
||||||
- **[Migrate one-shot falha]** → deploy não promove web; manter migrations backward-compatible.
|
- **[Migrate one-shot falha]** → web/queue/scheduler dependem de migrate exit 0; manter migrations backward-compatible.
|
||||||
- **[Cobertura 80% com Domain quase vazio]** → medir só namespaces existentes; baseline sobe conforme Fase 2 adiciona Domain.
|
- **[GitHub Free sem required reviewers]** → promoção humana via `workflow_dispatch` + input `PRODUCTION`; Pro opcional.
|
||||||
- **[npm audit ruido]** → `--omit=dev` + allowlist documentada se necessário; sem silenciar sem justificativa.
|
- **[Cobertura 80% com Domain quase vazio]** → medir só namespaces existentes; baseline sobe conforme Fase 2 adiciona Domain (pendente).
|
||||||
- **[E-mail verification em staging]** → seed/users de staging com `email_verified_at`; Resend para reset real quando configurado.
|
- **[npm audit ruido]** → `--omit=dev` + allowlist documentada se necessário (pendente).
|
||||||
- **[Compose local rebuild lento]** → documentar serve opcional; CI permanece fonte FrankenPHP.
|
- **[E-mail verification em staging]** → seed/users de staging com `email_verified_at`; Resend para reset real quando configurado (pendente).
|
||||||
|
- **[Compose local rebuild lento]** → documentar serve opcional; CI permanece fonte FrankenPHP (pendente).
|
||||||
|
|
||||||
## Migration Plan
|
## Migration Plan
|
||||||
|
|
||||||
1. Implementar auth/coverage/npm/strict_types/docs/Compose local; CI verde.
|
1. Fatia deploy: Compose deploy, workflows, smoke, trusted proxies, docs Dokploy/backup/rollback; CI verde.
|
||||||
2. Criar projeto Dokploy + Postgres + Compose app; registrar GHCR.
|
2. Criar projeto Dokploy + Postgres (staging + produção) + Compose apps; registrar GHCR.
|
||||||
3. Adicionar workflow deploy; primeiro push de imagem SHA; smoke `/up` + home + login.
|
3. Primeiro push de imagem SHA → staging; smoke `/up` + home + login; testar rollback e backup/restore.
|
||||||
4. Atualizar SPEC §18 Fase 0 apenas com itens comprovados; evidência no PR.
|
4. Promoção manual para produção após domínio/TLS/`APP_URL` confirmados.
|
||||||
5. Rollback: redeploy tag SHA anterior.
|
5. Fatias restantes da change (auth/coverage/npm/Compose local/PHP docs) em PRs seguintes.
|
||||||
|
6. Atualizar SPEC §18 Fase 0 apenas com itens comprovados; evidência no PR.
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
- Domínio público exato do staging (DNS) — preencher na implementação com valor do operador.
|
- Domínio público exato do staging/produção (DNS) — preencher na implementação com valor do operador.
|
||||||
- Se Dokploy Compose API exigir `compose.saveEnvironment` para `IMAGE_TAG` a cada deploy: confirmar payload na primeira fatia de integração.
|
- Se Dokploy Compose API exigir `compose.update` env para `IMAGE_TAG` a cada deploy: preferir aliases `:staging`/`:production` estáveis no Compose Dokploy para evitar rewrite de env.
|
||||||
|
|||||||
@@ -6,17 +6,20 @@ Fases 0 e 1 estão implementadas e mescladas, mas o critério de saída da Fase
|
|||||||
|
|
||||||
- Implantar staging na VPS própria via Dokploy (Docker Compose): imagem única por SHA no GHCR, serviços `web`/`queue`/`scheduler`/migrate, PostgreSQL gerenciado, healthcheck `/up`, smoke pós-deploy e rollback por tag SHA anterior.
|
- Implantar staging na VPS própria via Dokploy (Docker Compose): imagem única por SHA no GHCR, serviços `web`/`queue`/`scheduler`/migrate, PostgreSQL gerenciado, healthcheck `/up`, smoke pós-deploy e rollback por tag SHA anterior.
|
||||||
- Adicionar workflow GitHub Actions de deploy em `main` após CI verde (build → push GHCR → acionar API Dokploy).
|
- Adicionar workflow GitHub Actions de deploy em `main` após CI verde (build → push GHCR → acionar API Dokploy).
|
||||||
- Estender `docker-compose.yml` local com serviço de aplicação FrankenPHP (além do PostgreSQL).
|
- Adicionar promoção manual de produção: mesma digest SHA já publicada, alias `:production`, sem rebuild (`workflow_dispatch` + confirmação explícita).
|
||||||
- Fixar PHP **8.4** como versão canônica em Docker, CI e documentação.
|
- Documentar backup PostgreSQL diário (retenção ≥14d), restore e runbook operacional Dokploy/GHCR.
|
||||||
- Incluir `npm audit` e cobertura mínima de 80% para `Domain` e `Application` nos gates de qualidade (SPEC §12.6, §13.7, §13.9).
|
- Estender `docker-compose.yml` local com serviço de aplicação FrankenPHP (além do PostgreSQL) — **ainda pendente** nesta fatia de deploy.
|
||||||
- Exigir e-mail verificado no painel Filament e entregar reset de senha seguro (SPEC §12.1; ADM-01).
|
- Fixar PHP **8.4** como versão canônica em Docker, CI e documentação — **ainda pendente**.
|
||||||
- Corrigir `declare(strict_types=1);` em PHP próprio que ainda falte e cobrir regressões.
|
- Incluir `npm audit` e cobertura mínima de 80% para `Domain` e `Application` nos gates de qualidade (SPEC §12.6, §13.7, §13.9) — **ainda pendente**.
|
||||||
|
- Exigir e-mail verificado no painel Filament e entregar reset de senha seguro (SPEC §12.1; ADM-01) — **ainda pendente**.
|
||||||
|
- Corrigir `declare(strict_types=1);` em PHP próprio que ainda falte e cobrir regressões — **ainda pendente**.
|
||||||
|
|
||||||
## Non-Goals
|
## Non-Goals
|
||||||
|
|
||||||
Conforme [SPEC.md §4.2](../../SPEC.md):
|
Conforme [SPEC.md §4.2](../../SPEC.md):
|
||||||
|
|
||||||
- Produção com promoção humana, portal do cliente, multi-tenancy, Redis, FrankenPHP worker mode.
|
- Deploy automático direto em produção (produção exige promoção humana da mesma imagem).
|
||||||
|
- Portal do cliente, multi-tenancy, Redis, FrankenPHP worker mode.
|
||||||
- Fase 2 (WEB-05 briefing, CRM, E2E-01/E2E-02) — change futura `build-leads-crm` após esta fechar.
|
- Fase 2 (WEB-05 briefing, CRM, E2E-01/E2E-02) — change futura `build-leads-crm` após esta fechar.
|
||||||
- Provisionamento genérico de VPS/Dokploy para clientes finais.
|
- Provisionamento genérico de VPS/Dokploy para clientes finais.
|
||||||
- Templates de e-mail de lead, auditoria completa (ADM-02), documentos privados.
|
- Templates de e-mail de lead, auditoria completa (ADM-02), documentos privados.
|
||||||
@@ -25,19 +28,20 @@ Conforme [SPEC.md §4.2](../../SPEC.md):
|
|||||||
|
|
||||||
### New Capabilities
|
### New Capabilities
|
||||||
|
|
||||||
- `staging-deployment`: deploy automático de staging na VPS via Dokploy com imagem imutável por SHA, processos web/queue/scheduler, migração, healthcheck, smoke e rollback (SPEC §14.3, §15.2, §18 Fase 0).
|
- `staging-deployment`: deploy automático de staging na VPS via Dokploy com imagem imutável por SHA, processos web/queue/scheduler, migração, healthcheck, smoke e rollback; promoção manual da mesma digest para produção (SPEC §14.3, §15.2, §16.3, §18 Fase 0).
|
||||||
|
|
||||||
### Modified Capabilities
|
### Modified Capabilities
|
||||||
|
|
||||||
- `container-runtime`: Compose local com app FrankenPHP; PHP 8.4 canônico; alinhamento da mesma imagem a processos de staging.
|
- `container-runtime`: Compose local com app FrankenPHP; PHP 8.4 canônico; alinhamento da mesma imagem a processos de staging/produção.
|
||||||
- `quality-gates`: `npm audit` no gate; cobertura mínima 80% para Domain/Application; job de deploy staging após CI.
|
- `quality-gates`: `npm audit` no gate; cobertura mínima 80% para Domain/Application; job de deploy staging após CI.
|
||||||
- `health-check`: healthcheck e smoke pós-deploy de staging usam `/up` sem autenticação.
|
- `health-check`: healthcheck e smoke pós-deploy de staging usam `/up` sem autenticação.
|
||||||
- `internal-authentication`: e-mail verificado obrigatório para acesso ao painel; reset de senha seguro disponível (SPEC §12.1, ADM-01).
|
- `internal-authentication`: e-mail verificado obrigatório para acesso ao painel; reset de senha seguro disponível (SPEC §12.1, ADM-01).
|
||||||
|
|
||||||
## Impact
|
## Impact
|
||||||
|
|
||||||
- **Cria**: `docker-compose` de staging (ou extensão), workflow `.github/workflows/deploy-staging.yml`, docs operacionais de Dokploy/GHCR, testes de auth verification/reset e cobertura.
|
- **Cria (fatia deploy)**: `docker-compose.deploy.yml`, workflows `deploy-staging.yml` / `promote-production.yml`, scripts smoke/Dokploy, docs operacionais Dokploy/GHCR/backup/rollback.
|
||||||
- **Altera**: `docker-compose.yml`, `Dockerfile`/docs PHP, `composer.json`/`package.json` scripts, `.github/workflows/ci.yml`, `User`/`AdminPanelProvider`, README, `.env.example`.
|
- **Altera (fatia deploy)**: `bootstrap/app.php` (trusted proxies), `.env.example`, README.
|
||||||
- **Infra (manual)**: projeto Dokploy na VPS, registry GHCR, secrets (`DOKPLOY_*`, `GHCR_*`, DB, `APP_KEY`, Resend/R2).
|
- **Ainda pendente nesta change**: Compose local FrankenPHP, PHP 8.4 docs, npm audit/coverage, auth verification/reset, strict_types.
|
||||||
|
- **Infra (manual)**: projeto Dokploy na VPS (duas stacks), registry GHCR, secrets (`DOKPLOY_*`, DB, `APP_KEY`, Resend/R2), backup S3.
|
||||||
- **Depende de**: specs já arquivadas (`container-runtime`, `quality-gates`, `health-check`, `internal-authentication`, `transactional-email`, `object-storage`).
|
- **Depende de**: specs já arquivadas (`container-runtime`, `quality-gates`, `health-check`, `internal-authentication`, `transactional-email`, `object-storage`).
|
||||||
- **Risco**: secrets e registry privados; mitigações: tokens com escopo mínimo, imagem por SHA, healthcheck antes de promover, rollback por tag anterior.
|
- **Risco**: secrets e registry privados; mitigações: tokens com escopo mínimo, imagem por SHA, healthcheck antes de promover, rollback por tag anterior.
|
||||||
|
|||||||
@@ -55,3 +55,23 @@ Rollback SHALL redeploy a previously published SHA-tagged image without rebuildi
|
|||||||
- **WHEN** the operator points staging Compose at a previous SHA tag and redeploys
|
- **WHEN** the operator points staging Compose at a previous SHA tag and redeploys
|
||||||
- **THEN** web, queue, and scheduler MUST run that previous image
|
- **THEN** web, queue, and scheduler MUST run that previous image
|
||||||
- **AND** no source rebuild MUST be required
|
- **AND** no source rebuild MUST be required
|
||||||
|
|
||||||
|
### Requirement: Production promotion reuses the same immutable digest
|
||||||
|
|
||||||
|
Production SHALL be promoted from an already-published SHA-tagged image without rebuilding from source. Promotion MUST require explicit human action (SPEC §14.3).
|
||||||
|
|
||||||
|
#### Scenario: Operator promotes a staging-approved SHA to production
|
||||||
|
|
||||||
|
- **WHEN** the operator confirms promotion of commit SHA `abc123`
|
||||||
|
- **THEN** production web, queue, and scheduler MUST run the same digest previously published as `ghcr.io/<owner>/<repo>:abc123`
|
||||||
|
- **AND** MUST NOT rebuild from source for that promotion
|
||||||
|
|
||||||
|
### Requirement: Database backups exist before production cutover
|
||||||
|
|
||||||
|
Staging and production PostgreSQL services SHALL have automated daily backups with retention of at least 14 days, and a documented restore procedure MUST be verified on staging before the first production promotion (SPEC §16.3).
|
||||||
|
|
||||||
|
#### Scenario: Staging restore is proven before production promotion
|
||||||
|
|
||||||
|
- **WHEN** the operator prepares the first production promotion
|
||||||
|
- **THEN** a restore from a staging backup MUST have been documented and successfully tested
|
||||||
|
- **AND** production MUST have daily backup configured with retention of at least 14 days
|
||||||
|
|||||||
@@ -19,23 +19,25 @@
|
|||||||
- [ ] 3.3 Add/adjust unit tests if current Domain/Application coverage is below threshold
|
- [ ] 3.3 Add/adjust unit tests if current Domain/Application coverage is below threshold
|
||||||
- [ ] 3.4 Verify CI `static` and `unit` fail appropriately on intentional audit/coverage breakage in a branch experiment or equivalent proof
|
- [ ] 3.4 Verify CI `static` and `unit` fail appropriately on intentional audit/coverage breakage in a branch experiment or equivalent proof
|
||||||
|
|
||||||
## 4. Staging Compose and Dokploy prep
|
## 4. Staging/production Compose and Dokploy prep
|
||||||
|
|
||||||
- [ ] 4.1 Add versioned staging Compose template (web, queue, scheduler, migrate one-shot) parameterized by `APP_IMAGE`/`IMAGE_TAG`
|
- [x] 4.1 Add versioned Compose template (`docker-compose.deploy.yml`: web, queue, scheduler, migrate one-shot) parameterized by `APP_IMAGE`/`IMAGE_TAG` for staging and production stacks
|
||||||
- [ ] 4.2 Document Dokploy project setup: GHCR registry credentials, Postgres service, Compose import, required env vars (APP_KEY, DB, Resend, R2)
|
- [x] 4.2 Document Dokploy project setup: GHCR registry credentials, Postgres per environment, Compose import, required env vars (APP_KEY, DB, Resend, R2), trusted proxies/session cookies
|
||||||
- [ ] 4.3 Document rollback procedure: redeploy previous SHA tag without rebuild
|
- [x] 4.3 Document rollback procedure: move environment alias to previous SHA and redeploy without rebuild
|
||||||
|
- [x] 4.4 Document PostgreSQL daily backup (≥14d retention), restore procedure, and test restore on staging before first production promotion
|
||||||
|
|
||||||
## 5. Deploy workflow and smoke
|
## 5. Deploy workflow and smoke
|
||||||
|
|
||||||
- [ ] 5.1 Create `.github/workflows/deploy-staging.yml` gated on successful CI on `main`: build image, push `ghcr.io/...:<sha>` + `:staging`, trigger Dokploy `compose.deploy`
|
- [x] 5.1 Create `.github/workflows/deploy-staging.yml` gated on successful CI on `main`: build image, push `ghcr.io/...:<sha>` + `:staging`, trigger Dokploy `compose.deploy`
|
||||||
- [ ] 5.2 Wire migrate-before-serve (Compose migrate service or Dokploy deploy command) and healthcheck on `/up`
|
- [x] 5.2 Create `.github/workflows/promote-production.yml` (`workflow_dispatch` + confirmation): retag same digest as `:production`, deploy production stack, smoke
|
||||||
- [ ] 5.3 Add post-deploy smoke script/job for `/up`, `/`, `/admin/login` returning 200
|
- [x] 5.3 Wire migrate-before-serve (Compose migrate service) and healthcheck on `/up`
|
||||||
- [ ] 5.4 Store secrets only in GitHub/Dokploy; ensure no secrets in image layers (reuse container CI check)
|
- [x] 5.4 Add post-deploy smoke script/job for `/up`, `/`, `/admin/login` returning 200
|
||||||
|
- [x] 5.5 Store orchestration secrets only in GitHub; Laravel/DB/R2/Resend only in Dokploy; ensure no secrets in image layers
|
||||||
|
|
||||||
## 6. Phase 0 exit evidence
|
## 6. Phase 0 exit evidence
|
||||||
|
|
||||||
- [ ] 6.1 Perform first successful staging deploy of a `main` SHA and capture evidence (workflow URL, smoke output)
|
- [ ] 6.1 Perform first successful staging deploy of a `main` SHA and capture evidence (workflow URL, smoke output)
|
||||||
- [ ] 6.2 Verify rollback to previous SHA works once
|
- [ ] 6.2 Verify rollback to previous SHA works once on staging
|
||||||
- [ ] 6.3 Update `SPEC.md` §18 Fase 0 checkboxes only for items with evidence; note remaining deferred items if any
|
- [ ] 6.3 Update `SPEC.md` §18 Fase 0 checkboxes only for items with evidence; note remaining deferred items if any
|
||||||
- [ ] 6.4 Run full `composer quality` and confirm all five CI jobs + staging deploy path green
|
- [ ] 6.4 Run full `composer quality` and confirm all five CI jobs + staging deploy path green
|
||||||
- [ ] 6.5 Report in SPEC §24 format and mark this change ready to archive after merge
|
- [ ] 6.5 Report in SPEC §24 format; archive this change only after remaining parity tasks (1–3) also complete
|
||||||
|
|||||||
82
scripts/deploy/dokploy-deploy.sh
Executable file
82
scripts/deploy/dokploy-deploy.sh
Executable file
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Trigger a Dokploy Compose deploy and wait until it finishes.
|
||||||
|
# Required env:
|
||||||
|
# DOKPLOY_URL e.g. https://panel.example.com
|
||||||
|
# DOKPLOY_API_KEY x-api-key value
|
||||||
|
# DOKPLOY_COMPOSE_ID target compose id
|
||||||
|
# Optional env:
|
||||||
|
# DEPLOY_TITLE deployment title (default: GitHub deploy)
|
||||||
|
# DEPLOY_TIMEOUT_SEC total wait seconds (default: 900)
|
||||||
|
# DEPLOY_POLL_SEC poll interval (default: 10)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${DOKPLOY_URL:?DOKPLOY_URL is required}"
|
||||||
|
: "${DOKPLOY_API_KEY:?DOKPLOY_API_KEY is required}"
|
||||||
|
: "${DOKPLOY_COMPOSE_ID:?DOKPLOY_COMPOSE_ID is required}"
|
||||||
|
|
||||||
|
DOKPLOY_URL="${DOKPLOY_URL%/}"
|
||||||
|
DEPLOY_TITLE="${DEPLOY_TITLE:-GitHub deploy}"
|
||||||
|
DEPLOY_TIMEOUT_SEC="${DEPLOY_TIMEOUT_SEC:-900}"
|
||||||
|
DEPLOY_POLL_SEC="${DEPLOY_POLL_SEC:-10}"
|
||||||
|
|
||||||
|
api() {
|
||||||
|
local method="$1"
|
||||||
|
local path="$2"
|
||||||
|
shift 2
|
||||||
|
curl -fsS -X "$method" \
|
||||||
|
-H "accept: application/json" \
|
||||||
|
-H "content-type: application/json" \
|
||||||
|
-H "x-api-key: ${DOKPLOY_API_KEY}" \
|
||||||
|
"${DOKPLOY_URL}/api${path}" \
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Triggering Dokploy compose.deploy for ${DOKPLOY_COMPOSE_ID}..."
|
||||||
|
api POST "/compose.deploy" \
|
||||||
|
-d "$(jq -n \
|
||||||
|
--arg composeId "$DOKPLOY_COMPOSE_ID" \
|
||||||
|
--arg title "$DEPLOY_TITLE" \
|
||||||
|
'{composeId: $composeId, title: $title}')" >/dev/null
|
||||||
|
|
||||||
|
echo "Waiting for deployment to finish (timeout ${DEPLOY_TIMEOUT_SEC}s)..."
|
||||||
|
deadline=$((SECONDS + DEPLOY_TIMEOUT_SEC))
|
||||||
|
last_status=""
|
||||||
|
|
||||||
|
while (( SECONDS < deadline )); do
|
||||||
|
payload="$(api GET "/deployment.allByCompose?composeId=${DOKPLOY_COMPOSE_ID}")"
|
||||||
|
latest="$(echo "$payload" | jq -c 'if type=="array" then .[0] else . end')"
|
||||||
|
|
||||||
|
if [[ -z "$latest" || "$latest" == "null" ]]; then
|
||||||
|
echo "No deployment records yet; retrying..."
|
||||||
|
sleep "$DEPLOY_POLL_SEC"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
status="$(echo "$latest" | jq -r '.status // .deploymentStatus // empty')"
|
||||||
|
created_at="$(echo "$latest" | jq -r '.createdAt // .created_at // empty')"
|
||||||
|
title="$(echo "$latest" | jq -r '.title // .titleLog // empty')"
|
||||||
|
|
||||||
|
if [[ "$status" != "$last_status" ]]; then
|
||||||
|
echo "Deployment status=${status:-unknown} title=${title:-n/a} createdAt=${created_at:-n/a}"
|
||||||
|
last_status="$status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${status,,}" in
|
||||||
|
done|success|successful|finished)
|
||||||
|
echo "Dokploy deployment succeeded."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
error|failed|failure)
|
||||||
|
echo "Dokploy deployment failed." >&2
|
||||||
|
echo "$latest" | jq . >&2 || true
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
sleep "$DEPLOY_POLL_SEC"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Timed out waiting for Dokploy deployment after ${DEPLOY_TIMEOUT_SEC}s." >&2
|
||||||
|
api GET "/deployment.allByCompose?composeId=${DOKPLOY_COMPOSE_ID}" | jq . >&2 || true
|
||||||
|
exit 1
|
||||||
56
scripts/deploy/smoke.sh
Executable file
56
scripts/deploy/smoke.sh
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Post-deploy HTTP smoke checks for Amare.
|
||||||
|
# Required env:
|
||||||
|
# SMOKE_BASE_URL e.g. https://staging.example.com
|
||||||
|
# Optional env:
|
||||||
|
# SMOKE_RETRIES attempts per path (default: 30)
|
||||||
|
# SMOKE_SLEEP_SEC sleep between attempts (default: 5)
|
||||||
|
# SMOKE_PATHS space-separated paths (default: /up / /admin/login)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${SMOKE_BASE_URL:?SMOKE_BASE_URL is required}"
|
||||||
|
|
||||||
|
SMOKE_BASE_URL="${SMOKE_BASE_URL%/}"
|
||||||
|
SMOKE_RETRIES="${SMOKE_RETRIES:-30}"
|
||||||
|
SMOKE_SLEEP_SEC="${SMOKE_SLEEP_SEC:-5}"
|
||||||
|
SMOKE_PATHS="${SMOKE_PATHS:-/up / /admin/login}"
|
||||||
|
|
||||||
|
check_path() {
|
||||||
|
local path="$1"
|
||||||
|
local url="${SMOKE_BASE_URL}${path}"
|
||||||
|
local attempt
|
||||||
|
local code
|
||||||
|
|
||||||
|
for ((attempt = 1; attempt <= SMOKE_RETRIES; attempt++)); do
|
||||||
|
code="$(curl -sS -o /tmp/amare-smoke-body -w '%{http_code}' --max-time 20 "$url" 2>/dev/null || true)"
|
||||||
|
if [[ "$code" == "200" ]]; then
|
||||||
|
echo "OK ${path} (HTTP ${code}) attempt=${attempt}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "WAIT ${path} (HTTP ${code:-000}) attempt=${attempt}/${SMOKE_RETRIES}"
|
||||||
|
sleep "$SMOKE_SLEEP_SEC"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "FAIL ${path} after ${SMOKE_RETRIES} attempts" >&2
|
||||||
|
if [[ -f /tmp/amare-smoke-body ]]; then
|
||||||
|
head -c 500 /tmp/amare-smoke-body >&2 || true
|
||||||
|
echo >&2
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Smoke against ${SMOKE_BASE_URL}"
|
||||||
|
failed=0
|
||||||
|
for path in $SMOKE_PATHS; do
|
||||||
|
if ! check_path "$path"; then
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if (( failed != 0 )); then
|
||||||
|
echo "Smoke checks failed." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Smoke checks passed."
|
||||||
22
tests/Feature/TrustedProxyTest.php
Normal file
22
tests/Feature/TrustedProxyTest.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class TrustedProxyTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_forwarded_proto_https_is_recognized_behind_proxy(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/up', [
|
||||||
|
'HTTP_X_FORWARDED_PROTO' => 'https',
|
||||||
|
'HTTP_X_FORWARDED_FOR' => '203.0.113.10',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$this->assertTrue(request()->secure());
|
||||||
|
$this->assertSame('203.0.113.10', request()->ip());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user