---
name: pimcore-good-enough-prod
description: >-
  Good-enough Pimcore production on a bare Ubuntu VPS — not a
  polished platform, just a workable Compose + Traefik/MariaDB host with
  hardening (SSH/fail2ban/UFW), deploy user pimcore + pimcore-ctl, shared/.env,
  GitHub Actions SSH deploy, product registration, one-time pimcore-install,
  and go-live checks. Use when standing up a new Pimcore project on a VPS or you
  need a pragmatic host stack and accept the sharp edges.
  Reference pack and host scripts live in this skill.
---

# Pimcore good-enough prod

A pragmatic greenfield stack — battle-tested enough to ship, not the fanciest
bootstrap. Everything for a new project lives **in this skill**:

| Path | Contents |
|------|----------|
| [reference-pack/](reference-pack/) | Anonymized Compose, `env.example`, `docker/`, Doctrine URL wiring, `github/workflows/deploy.yml`, deploy docs |
| [scripts/](scripts/) | Host bootstrap: harden, `pimcore` user, fill `.env`, CI key, Traefik/DB |
| [reference-pitfalls.md](reference-pitfalls.md) | Production pitfalls |

Do **not** copy host scripts into the application git repo — only `reference-pack/` app files.

Orchestrator (workstation → `root@SERVER`):

```bash
./.agents/skills/pimcore-good-enough-prod/scripts/bootstrap-new-project.sh \
  --host <SERVER_IPV4> \
  --public-host pimcore.example.com \
  --acme-email ops@example.com \
  --deploy-pub /path/to/laptop.pub \
  --compose-network example-pimcore
```

## Inputs to collect

| Input | Placeholder |
|-------|-------------|
| Pulumi / server name | `{project}-{env}` |
| Public hostname | `pimcore.example.com` |
| Server IPv4 | `<SERVER_IPV4>` |
| ACME email | `ops@example.com` |
| App git remote | GitHub `org/your-pimcore-app` |
| Laptop deploy pubkey | ed25519 `.pub` for user `pimcore` |

## End-to-end checklist

```
Progress:
- [ ] 1. Pulumi server + DNS A → host IP
- [ ] 2. App repo: copy reference-pack (compose, .docker, doctrine, deploy.yml)
- [ ] 3. Host bootstrap script (user, env, CI key, harden)
- [ ] 4. GitHub Environment production secrets
- [ ] 5. First Deploy (code sync; may fail at bundle:install)
- [ ] 6. Remove ACME placeholder when nginx is up
- [ ] 7. One-time pimcore-install on host
- [ ] 8. Align product triad (.env = install = GitHub)
- [ ] 9. Second Deploy green + HTTPS + /admin
```

### 1. Infrastructure

Provision an Ubuntu VPS with a public IPv4 (any cloud / Pulumi / Terraform / console). Enable whatever backup policy your org requires for production hosts.

```bash
# example if you use Pulumi locally
cd <your-infra-repo> && pulumi preview && pulumi up
```

Point DNS **A** of `PUBLIC_HOST` at the new IP before expecting LE certs.

### 2. Application repository

Pack paths are discovery-safe (no leading dots). When copying into the app repo, restore conventional names:

```bash
SKILL=.agents/skills/pimcore-good-enough-prod/reference-pack
cp "$SKILL/docker-compose.yml" "$SKILL/env.example" .
mv env.example .env.example
cp -r "$SKILL/docker" .docker
mkdir -p config/packages .github/workflows docs
cp "$SKILL/config/packages/doctrine.yaml" config/packages/
cp "$SKILL/github/workflows/deploy.yml" .github/workflows/
cp "$SKILL/docs/deploy.md" docs/
# Rename compose name/network from example-pimcore if desired
```

Must-haves (see pack + pitfalls):

- Bind-mount `../shared/.env` and shared `var/*` / `config/local` (host symlinks invisible in container)
- `DATABASE_URL` `serverVersion=mariadb-X.Y.Z` (not `10.11-MariaDB`)
- `GOTENBERG_BASE_URL=http://gotenberg:3000`
- `MAILER_DSN=smtp://mail:change-me@mailpit:1025` (Mailpit always on; UI `127.0.0.1:8025`)
- Deploy: `ubuntu-latest` → SSH as `pimcore` → `pimcore-ctl` (no self-hosted runner)
- Action pins: major tags (`@v7`, …)

Server layout:

| Path | Role |
|------|------|
| `/services/pimcore/latest` | App + compose (owned by `pimcore`) |
| `/services/pimcore/shared` | Durable data; `.env` is `root:pimcore` **640** |

### 3. Host bootstrap

```bash
./.agents/skills/pimcore-good-enough-prod/scripts/bootstrap-new-project.sh \
  --host <SERVER_IPV4> \
  --public-host pimcore.example.com \
  --acme-email ops@example.com \
  --deploy-pub /path/to/laptop.pub
```

Order: `harden-host` → `setup-pimcore-user` → seed+`fill-shared-env` → `add-ci-deploy-key` → optional Traefik/DB placeholder.

Details: [scripts/HARDENING.md](scripts/HARDENING.md).

### 4. GitHub Environment `production` secrets

| Secret | Source |
|--------|--------|
| `DEPLOY_SSH_KEY` | Private key matching pubkey in `pimcore` `authorized_keys` (prefer `/root/pimcore-ci-deploy`) |
| `DEPLOY_HOST` | `<SERVER_IPV4>` or hostname |
| `APP_SECRET` | `openssl rand -base64 48` (≥32 chars) |
| `MERCURE_JWT_KEY` | Same as server `MERCURE_PUBLISHER_JWT_KEY` |
| `PIMCORE_ENCRYPTION_SECRET` | Defuse key (`def…`) |
| `PIMCORE_INSTANCE_IDENTIFIER` | UUID |
| `PIMCORE_PRODUCT_KEY` | Product registration for that secret+instance |

Compose-only secrets stay on the server. Sync CI key from a workstation:

```bash
scp root@<SERVER_IPV4>:/root/pimcore-ci-deploy ./pimcore-ci-deploy.pem
gh secret set DEPLOY_SSH_KEY -R org/your-pimcore-app -e production < pimcore-ci-deploy.pem
shred -u pimcore-ci-deploy.pem
```

### 5–9. Deploy, install, go-live

See [reference-pack/docs/deploy.md](reference-pack/docs/deploy.md).

After nginx is up: `docker rm -f pimcore-acme-placeholder` (same Traefik router name → 404 otherwise).

One-time install (not CI):

```bash
ssh pimcore@<SERVER_IPV4>
sudo pimcore-ctl exec-php vendor/bin/pimcore-install
```

## Agent rules

- Prefer files under this skill; do not reinvent Compose/deploy/harden.
- `pimcore-ctl env-apply` must leave `.env` as `root:pimcore` 640.
- Never put host-bootstrap scripts into the application git repo.
- Do not auto-run `pimcore-install` in CI unless the user explicitly asks.
- After rotating `MYSQL_*` in `.env`, wipe or `ALTER USER` the MariaDB volume.
- Keep the reference pack anonymized (no real IPs, keys, personal pubkeys).
- When refreshing the pack from a live app, re-anonymize and drop secrets.
