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:
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:
The first bootstrap creates a private mode-0600 environment at:
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:
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:
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/portfoliohttps://andreazambrano.co/cementoshttps://api.andreazambrano.co/healthhttps://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.