Skip to content

OVH deployment and first persistent cutover

This runbook replaces the disposable August preview with the Medusa-backed storefront and establishes the first persistent application state. System Nginx and Let's Encrypt remain installed directly on Ubuntu; the applications, PostgreSQL, Redis, and Garage run as containers.

Production topology

system Nginx :80/:443
  -> 127.0.0.1:3000  portfolio
  -> 127.0.0.1:3100  Medusa DTC storefront under /cementos
  -> 127.0.0.1:9000  Medusa API and built-in Admin
  -> 127.0.0.1:3902  Garage product media

private Docker network
  -> PostgreSQL 17
  -> Redis 7.4
  -> Garage S3 API

The Compose project is named andreaweb. Its PostgreSQL, Redis, and Garage named volumes survive image rebuilds, container replacement, release-directory changes, and ordinary docker compose down commands.

One-time replacement of the disposable preview

The August preview used the separate Compose project name andreaweb-preview. Destroy it only immediately before deploying the prepared replacement release:

ssh ovh
cd /opt/andreaweb/current
docker compose --env-file /opt/andreaweb/shared/preview.env \
  -f compose.prod.yaml down --volumes --remove-orphans
rm -f /opt/andreaweb/shared/preview-admin-invite.txt
exit

That deliberately deletes the preview database, accounts, invitations, inventory, carts, Redis data, and uploaded media. It does not alter Nginx, the Let's Encrypt certificate, DNS, or committed release directories. The old preview.env may be retained temporarily for audit or removed after the new deployment is healthy; it is never reused by the new Compose project.

Do not repeat this reset after the first persistent deployment.

Plan and deploy a committed release

From /home/justin/repos/AndreaWeb in WSL:

./scripts/deploy-ovh.sh plan

The plan is read-only. It compares the candidate commit with the separately recorded portfolio, storefront, and Medusa commits on the VPS. Documentation, tests, and deployment-tool-only changes produce no runtime action. Application changes produce an exact command such as:

./scripts/deploy-ovh.sh apply \
  --commit 0123456789abcdef0123456789abcdef01234567 \
  --target storefront

Do not shorten or substitute the reviewed commit. apply refuses a dirty worktree, uploads that committed tree as an immutable release, builds only the selected application image, replaces only that stateless container, waits for health, and records the deployed commit. Supported targets are portfolio, storefront, medusa, and all-apps. A Medusa plan adds --migrate only when the changed paths include a migration. System Nginx is not rebuilt or reloaded.

On a genuinely new environment, explicitly bootstrap all services:

commit=$(git rev-parse HEAD)
./scripts/deploy-ovh.sh apply --commit "$commit" --target bootstrap

The first bootstrap creates a private mode-0600 environment at:

/opt/andreaweb/shared/production.env

Bootstrap builds the three application images, starts the stateful services, runs database migrations, reads the publishable key into the private environment, and checks all loopback services. It is deliberately non-destructive: it never removes a volume, resets application data, or overwrites an existing private environment. initialize-production.sh remains as a compatibility alias for bootstrap-production.sh.

Normal releases do not bootstrap Garage, seed products, or import media. Starter data is explicit maintenance:

ssh ovh
cd /opt/andreaweb/current
./scripts/maintain-production-catalog.sh \
  /opt/andreaweb/shared/production.env seed-starter-catalog
./scripts/maintain-production-catalog.sh \
  /opt/andreaweb/shared/production.env import-starter-media

The catalog command creates only missing starter handles and their inventory; it does not replace Admin-created products. The media command uploads bundled starter images only for starter products without images. Neither is a generic database restore or a required part of deployment.

The generated WhatsApp destination is blank. Before the cutover, set the approved international-digits-only value in production.env; it remains a server-only storefront setting. The public inquiry address defaults to cementos@andreazambrano.co. After changing a server-only storefront value, recreate the existing container without publishing or rebuilding a release:

./scripts/operate-ovh.sh recreate storefront

This reuses the exact image already running. It does not run migrations or touch PostgreSQL, Redis, Garage, or their volumes. Public NEXT_PUBLIC_* values are compiled into the storefront image and therefore require a storefront release instead of a recreate.

Use ./scripts/operate-ovh.sh status for a read-only remote Compose status. The recreate command accepts only portfolio, storefront, or medusa; stateful-service recreation is intentionally not exposed by this convenience tool.

Deployment command selection

Change Command selected
Storefront source or compiled public setting plan, then targeted apply --target storefront
Portfolio source plan, then targeted apply --target portfolio
Medusa source without migrations plan, then targeted apply --target medusa
Medusa migration plan, then targeted Medusa apply with --migrate
Shared production/Compose change conservative all-apps apply
Server-only environment value edit the private VPS environment, then operate-ovh.sh recreate SERVICE
Documentation, tests, or deployment tools only no runtime deployment
New or repaired full environment explicit apply --target bootstrap

The /opt/andreaweb/current symlink identifies the newest uploaded source release. Runtime truth is stored per application under /opt/andreaweb/shared/deployments/*.commit, because targeted deployments can leave the three applications on different commits.

Update Nginx additively

The committed example routes /cementos to port 3100 while preserving the portfolio on port 3000. Because Certbot has already augmented the live file, review the difference instead of overwriting it blindly:

ssh ovh
sudo cp -a /etc/nginx/sites-available/andreaweb \
  /etc/nginx/sites-available/andreaweb.before-storefront
sudo diff -u /etc/nginx/sites-available/andreaweb \
  /opt/andreaweb/current/deploy/nginx-vps.conf.example || true

Add the two /cementos location blocks from the committed example to the apex and www server block, then validate before reloading:

sudo nginx -t
sudo systemctl reload nginx

Do not replace unrelated Nginx sites. The existing certificate already covers the apex, www, api, admin, and media; this route change does not require a new certificate.

Create the first administrator

After Medusa is healthy, create the first invitation without needing an existing user:

cd /opt/andreaweb/current
./scripts/create-production-admin-invite.sh \
  /opt/andreaweb/shared/production.env \
  andrea@andreazambrano.co \
  /opt/andreaweb/shared/production-admin-invite.txt

Copy only the HTTPS acceptance URL from the mode-0600 file to Andrea. Delete the file after acceptance. Running the command again before acceptance replaces the pending token; after acceptance it safely reports that the user exists.

Verify the release

On the VPS:

cd /opt/andreaweb/current
docker compose --env-file /opt/andreaweb/shared/production.env \
  -f compose.prod.yaml ps
docker compose --env-file /opt/andreaweb/shared/production.env \
  -f compose.prod.yaml logs --tail=100 web storefront medusa
curl --fail http://127.0.0.1:3000/portfolio
curl --fail http://127.0.0.1:3100/cementos/co
curl --fail http://127.0.0.1:9000/health

Then verify externally:

  • https://andreazambrano.co/portfolio
  • https://andreazambrano.co/cementos
  • https://api.andreazambrano.co/health
  • https://admin.andreazambrano.co/app
  • one product image under https://media.andreazambrano.co

Keep the current X-Robots-Tag and robots.txt blocking in place while access is limited to review. In Admin, create an unknown product with multiple images, a COP price, managed inventory, and a quantity at Inventario principal; then confirm it appears in the storefront after refresh.

Persistence rule after cutover

Do not use down --volumes for normal operations. Rebuilds and deployments must preserve named volumes. Before the data is treated as durable production state, complete automated backups, restore testing, monitoring, disk-capacity alerts, and a migration-aware rollback procedure from the roadmap.