Skip to content

Local WSL operations

Run project commands inside Ubuntu WSL from:

/home/justin/repos/AndreaWeb

Docker Desktop supplies the Linux Docker Engine through WSL integration. Node, npm, Git, and the worktree remain Linux-native. AndreaWeb does not modify shell, SSH, GitHub CLI, or Azure CLI configuration.

Local topology

Service Local address Persistent state
Landing and architecture through Nginx http://localhost:8080/ none
Medusa DTC storefront through Nginx http://localhost:8080/guanza Medusa/PostgreSQL
Direct Medusa DTC storefront http://localhost:3100/guanza Medusa/PostgreSQL
Medusa API and Admin http://localhost:9000 PostgreSQL
Medusa Admin development assets http://localhost:5173 none
PostgreSQL private Docker network only postgres_data volume
Redis private Docker network only redis_data volume
Garage S3 API private Docker network only Garage volumes
Garage public media http://*.web.garage.localhost:3902 garage_meta and garage_data volumes

The local Nginx container rehearses the reverse-proxy boundary. Production uses the VPS's existing system Nginx instead of this container.

Prerequisites

  • Docker Desktop with WSL integration for Ubuntu-26.04;
  • Docker Compose v2;
  • Node.js 22.13 or newer through the WSL fnm installation;
  • OpenSSL and curl in WSL;
  • the repository checked out on the WSL filesystem.

Verify the boundary before troubleshooting the application:

docker version
docker compose version
node --version
npm --version
git status --short --branch

node and npm should resolve under /home/justin, not /mnt/c. The dev-env wrappers for ssh, gh, and az intentionally delegate to their authenticated Windows counterparts; private keys and credential stores are not copied into WSL.

First complete startup

Install dependencies and create the ignored, mode-0600 local environment:

npm ci
npm --prefix apps/storefront ci
npm run local:env

The environment generator creates random local database, application, and Garage credentials. It never prints their values. Running it again preserves existing values and adds any newly required fields.

Build the images, prepare the database, and start the complete stack:

docker compose --profile commerce build medusa
docker compose --profile commerce run --rm medusa npm run db:migrate
npm run storefront:env
docker compose --profile commerce up -d --build
npm run media:import:starter
npm run admin:invite:local -- andrea@andreazambrano.co
npm run commerce:verify
npm run health:local

The migration command is intentionally explicit. It applies schema migrations and one-time seed migrations, including the Colombia/COP foundation and the four starter products. storefront:env reads the seed-created publishable key from the local database into ignored environment files without printing it. The media import uploads the existing product images through Medusa's S3 provider and safely skips products that already have images.

Open:

  • http://localhost:8080/
  • http://localhost:8080/arquitectura
  • http://localhost:8080/guanza (the real Medusa DTC storefront)
  • http://localhost:8080/en/arquitectura
  • http://localhost:8080/en/guanza
  • http://localhost:3100/guanza (direct storefront access)
  • http://localhost:9000/app
  • http://localhost:9000/health

garage-init exiting with status zero is expected. It is a one-time helper that ensures the media bucket has public website delivery enabled.

Manual cart inquiries

The Medusa cart offers three non-payment actions: a WhatsApp handoff, a pre-populated email to the public store alias, and an explicit copy-to-clipboard button. Opening either external application does not clear the cart, create an order, reserve inventory, or claim that the customer sent the draft.

Configure the destinations in the ignored root .env file:

# International digits only; keep blank to use recipient-free WhatsApp sharing.
STORE_INQUIRY_WHATSAPP_NUMBER=
STORE_INQUIRY_EMAIL=cementos@andreazambrano.co

Run npm run local:env to add missing keys without replacing existing values, then run npm run storefront:env and recreate the storefront container after a change. The native Next.js development environment receives the same values in its ignored, mode-0600 .env.local file.

The WhatsApp number is server-only. The cart submits a same-origin POST to the storefront BFF, which reloads the Medusa cart and returns an uncacheable 303 redirect containing the localized draft. The number is absent from the HTML, JavaScript bundle, repository, and JSON responses, although a determined visitor can still inspect the final redirect. Use a dedicated WhatsApp Business number before treating the destination as a public customer-service channel.

The form targets a new tab so the populated cart stays open. The BFF rejects cross-site submissions, empty carts, and direct GET requests; Nginx forwards the original host and port so the same-origin check works through the local :8080 rehearsal proxy.

Portfolio-only preview

The landing and architecture application can run without the commerce profile:

docker compose up --build -d

That starts only the portfolio application and disposable local Nginx proxy. The /guanza route intentionally returns an upstream error in this mode instead of falling back to the retired hard-coded catalog; start the commerce profile whenever the storefront is needed.

Local application preview

Normal Compose serves the landing and architecture pages from the portfolio app and /guanza plus /en/guanza from the Medusa storefront. The first redesign's route-spike overlay is retired. From the WSL repository:

npm run local:env
npm run storefront:env
docker compose --profile commerce up -d --build

The local storefront builds before starting so its production-like asset paths work behind the split-app proxy, without the previously observed development HMR/hydration failure. This source-mounted container needs a rebuild/restart after code edits; it is not hot-reloading. For direct native Next development, run npm run storefront:dev after stopping the container occupying port 3100; its .env.local deliberately leaves the asset prefix empty for direct access. If the persistent storefront_node_modules development volume is missing new packages after a lockfile change, update only that volume and restart the storefront; do not delete the database or media volumes:

docker compose --profile commerce exec -T -u root storefront npm ci
docker compose --profile commerce exec -T -u root storefront \
  chown -R node:node /storefront/node_modules
docker compose --profile commerce restart storefront

For an empty local catalog after the foundation seed, prepare the two collections and assign the existing starter products. To review Piezas Intervenidas as well, run the existing opt-in sample import scripts. Do not run these commands against the VPS or a production database:

docker compose --profile commerce exec -T medusa \
  npx medusa exec ./src/scripts/setup-local-collection-fixtures.ts
docker compose --profile commerce exec -T medusa \
  npx medusa exec ./src/scripts/seed-sample-piezas.ts
docker compose --profile commerce exec -T medusa \
  npx medusa exec ./src/scripts/import-sample-piezas-media.ts

Open http://localhost:8080/, /arquitectura, /guanza, and their /en counterparts. Set ARCHITECTURE_WHATSAPP_NUMBER and STORE_INQUIRY_WHATSAPP_NUMBER in the ignored local .env only if testing the contact redirects; both are server-side runtime settings. Recreate the affected frontend container to pick up an environment change. Inspect a same-origin POST's Location header without following it or sending a message. Account creation is intentionally absent from the new header pending the account readiness audit.

The optional media adapter is disabled by default; see ngrok sharing to enable it for a remote laptop review. Local Nginx preserves the forwarded HTTPS scheme and the original host/port for contact origin validation. Neither feature changes production configuration.

Production-image rehearsal

Use the reusable release-review overlay to build isolated frontend images against local Medusa and serve them on loopback port 8081. It does not replace the normal frontends or modify production data. First synchronize the local environment and ensure the local commerce services are running:

npm run storefront:env
docker compose -f compose.yaml -f compose.release-review.yaml \
  --profile commerce --profile release-review up -d --build \
  release-web release-storefront release-nginx
node scripts/check-release.mjs

The rehearsal has no media URL rewrite; its local Garage URLs are reachable on this computer, not on another laptop. For remote review use port 8080 with the explicit media settings below. Stop only the rehearsal containers when finished:

docker compose -f compose.yaml -f compose.release-review.yaml \
  --profile release-review stop release-nginx release-storefront release-web

node scripts/check-release.mjs --local checks the normal port 8080 entry point. --production selects read-only checks of the fixed public HTTPS site. The optional check-app-layout.mjs uses separately installed Playwright tooling for the local viewport matrix; --release-review selects port 8081. Its product fixtures must exist locally. Do not seed production to make browser tests pass.

Create a local Medusa administrator

Create an invitation for the intended local administrator:

npm run admin:invite:local -- andrea@andreazambrano.co

The command uses Medusa's supported user-invitation flow instead of placing a password on a command line. It writes the local acceptance URL to the ignored mode-0600 file work/local-admin-invite.txt. Open that URL, let Andrea choose her development password, confirm that she can sign in at http://localhost:9000/app, and then delete the invitation file. Rerunning the command before acceptance replaces the earlier pending invitation and invalidates its URL. When Medusa RBAC is enabled, the workflow explicitly assigns the built-in super-admin role; otherwise Medusa's standard superuser semantics apply.

The invitation is local database state and survives ordinary container rebuilds. Creating the eventual production Andrea account remains a separate, deliberate deployment action with a new invitation and password.

Once signed in, follow the Medusa Admin product guide to create products, upload and assign images, configure variants and COP prices, and connect stock to Inventario principal.

Verify all Step 7 foundation records and their important links:

npm run commerce:verify

This fails unless the store defaults to COP, the Colombia region includes CO, the Cementos sales channel is linked to the primary stock location and publishable key, all four seed products are published with positive COP prices and managed inventory, and either a development Admin user or pending invitation exists. It does not expose the publishable token or Admin invitation token.

Routine validation and diagnostics

npm run health:local
npm run commerce:verify
npm run lint
npm test
npm run build
docker compose --profile commerce config --quiet

The health command checks all running containers, their configured Docker health checks, both storefront previews, Medusa API/Admin, and the Garage bucket. It is read-only.

Exercise an actual Medusa upload, public read, Garage restart, second read, and cleanup:

npm run media:verify

This creates only a temporary verification object and removes it on success or failure. The four catalog images remain untouched.

Useful diagnostics:

docker compose --profile commerce ps -a
docker compose logs --follow --tail=100 medusa
docker compose logs --follow --tail=100 storefront
docker compose logs --follow --tail=100 postgres redis garage
docker stats
docker system df

PostgreSQL, Redis, the Garage S3 API, and the Garage admin API are not published to the LAN or internet. The development Medusa and Garage website ports bind to 127.0.0.1 only.

Restarting and stopping safely

Restart replaceable processes while preserving data:

docker compose restart

Stop and remove project containers and networks while retaining named volumes:

docker compose --profile commerce down

Start again with:

docker compose --profile commerce up -d

The development backend and storefront mount named node_modules volumes over the dependency directories baked into their development images. On first use, Docker copies the image's installed packages into each empty volume. This keeps Linux dependencies off the source bind mount and makes routine container restarts fast. Docker does not refresh an existing volume merely because an image was rebuilt, however. After either application's lockfile changes, remove only that application's replaceable dependency/cache volumes and recreate it. For the storefront:

docker compose stop storefront
docker compose rm -f storefront
docker volume rm andreaweb_storefront_node_modules \
  andreaweb_storefront_next_cache
docker compose --profile commerce up -d --build storefront

Those two storefront volumes contain downloaded packages and generated Next.js output only; rebuilding recreates them. They are intentionally different from postgres_data, redis_data, garage_meta, and garage_data, which hold application state and must not be removed during routine work.

Production will not use dependency or framework-cache volumes. Its immutable, multi-stage images will contain the exact dependencies and compiled output from the reviewed lockfile. Only stateful services—PostgreSQL, persistent Redis when enabled, and Garage media/metadata—will use durable volumes on the VPS.

Do not add --volumes or -v to docker compose down during routine work. That explicitly deletes the PostgreSQL, Redis, and Garage volumes and therefore destroys local products, inventory, users, and uploaded media.

npm run local:env:rotate rotates the local database password, JWT secret, and cookie secret while preserving existing Garage credentials. Use it deliberately; containers must be recreated afterward so they receive the new values.

Sharing with ngrok

Authenticate the native WSL ngrok agent once; never commit or paste its token into repository files.

With the local application preview running, share the complete landing, architecture, and Guanza catalog through the single local Nginx entry point. For remote product images, explicitly enable these settings in the ignored root .env and recreate the storefront:

NEXT_PUBLIC_MEDIA_SOURCE_ORIGIN=http://andreaweb-media.web.garage.localhost:3902
NEXT_PUBLIC_MEDIA_PROXY_PATH=/guanza/media
docker compose --profile commerce up -d storefront

Medusa already has the files in Garage. The adapter rewrites only the matching local media origin into a same-host path, which local Nginx forwards to the public Garage bucket. It does not alter stored media URLs or upload anything. Keep these settings out of production. Remove or blank them and recreate the storefront when the remote review is over. Then start the tunnel:

ngrok http http://localhost:8080

Open the assigned HTTPS URL yourself and check a collection image and the architecture WhatsApp handoff before sending it to Andrea. The URL is public to anyone who has it; the tunnel, Docker stack, and computer must stay running while she reviews. Stop the tunnel with Ctrl+C; the Docker stack keeps running. Do not treat this temporary URL as the production release or as approval to change VPS routes.

The Medusa DTC storefront can be tunneled separately:

ngrok http http://localhost:3100

Do not use a direct 3100 tunnel for the complete site review. It bypasses the preview-only media route and split-app navigation; product-image URLs will still point at local-only services. The VPS deployment will use the public api and media hostnames instead of this temporary proxy.

The Admin can technically be tunneled from port 9000, but do not share it until an administrator exists and the exposure is deliberate. Local Garage media URLs remain local-only without the optional media rewrite. A production release uses the real HTTPS admin, api, and media hostnames on the VPS, not the temporary preview proxy.

An optional ngrok container can share the PoC without installing the native agent:

read -rsp "ngrok authtoken: " NGROK_AUTHTOKEN
echo
export NGROK_AUTHTOKEN

docker run --rm -it \
  --name andreaweb-ngrok \
  --network andreaweb_default \
  -e NGROK_AUTHTOKEN \
  ngrok/ngrok:latest \
  http nginx:80

unset NGROK_AUTHTOKEN

Common problems

A required environment variable is missing

npm run local:env
docker compose --profile commerce up -d --force-recreate

The first command upgrades an older ignored .env without replacing existing credentials.

The storefront key is missing or Store API requests return 401

Medusa creates the publishable storefront key during the database seed, so a clean installation cannot configure the DTC app before migrations have run. Use this order:

docker compose --profile commerce run --rm medusa npm run db:migrate
npm run storefront:env
docker compose --profile commerce up -d --build --force-recreate medusa storefront

The synchronization command reads the active publishable key from PostgreSQL, updates only ignored local environment files, and adds port 3100 to the local Medusa CORS lists. It never prints the key. Recreating Medusa is necessary when an already-running container still has the older CORS environment.

A native storefront build reports .next permission or missing-page errors

An older root-running storefront container could create root-owned files in the WSL worktree. A running dev compiler could also race a simultaneous native next build when both used apps/storefront/.next.

The current setup prevents both problems: the container runs as UID/GID 1000 (node) and mounts its own storefront_next_cache volume at /storefront/.next. Native WSL builds continue to use the worktree's .next directory, so the two compilers no longer share build output.

For a checkout that was started with the older configuration, repair ownership once before rebuilding:

docker compose stop storefront
sudo chown -R "$(id -u):$(id -g)" apps/storefront/.next
docker compose run --rm --no-deps -u root storefront \
  chown -R node:node /storefront/.next
docker compose --profile commerce up -d --build storefront

These commands change only disposable Next.js build-cache ownership. They do not touch PostgreSQL, Redis, Garage, product media, or other persistent data.

Changing the storefront base path

The supported basePath is empty. /guanza and /en/guanza are application-owned routes, not a Next basePath. Docker uses /guanza as the built asset prefix and Nginx rewrites those asset requests to Next's internal /_next path. Run npm run storefront:env to replace an old local /cementos setting, then recreate/rebuild the storefront. Changing Nginx alone cannot change a compiled basePath. Native direct development uses an empty asset prefix instead.

Storefront production dependency audit

The controlled Next.js 16.3.6 and React 19.2.8 upgrade is complete. Both npm --prefix apps/storefront audit and the production-only audit report zero known vulnerabilities. Continue upgrading deliberately and do not use npm audit fix --force to cross framework major versions.

Next.js regenerates apps/storefront/next-env.d.ts for the active development or production output directory. It is intentionally ignored, as recommended by current Next.js documentation, so running next dev, next build, or next typegen does not create a false source change.

The four initial catalog products currently have zero seeded inventory. Product and cart routes can be validated, but Add to cart correctly remains disabled until inventory is assigned through Medusa Admin or a deliberate test fixture. That stock state is catalog data, not a Next.js upgrade failure.

A service is unhealthy

npm run health:local
docker compose --profile commerce ps -a
docker compose logs --tail=150 SERVICE_NAME

Inspect the failing service before restarting or removing anything.

Nginx returns 502 during startup

docker compose logs --tail=100 web nginx
curl --retry 10 --retry-delay 1 --retry-connrefused \
  http://localhost:8080/portfolio

Confirm Docker is using the WSL checkout

docker inspect andreaweb-web-1 \
  --format '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}'

Expected output:

/home/justin/repos/AndreaWeb