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
Portfolio through Nginx http://localhost:8080/portfolio none
Medusa DTC storefront through Nginx http://localhost:8080/cementos Medusa/PostgreSQL
Direct Medusa DTC development server http://localhost:3100/cementos 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/portfolio
  • http://localhost:8080/cementos (the real Medusa DTC storefront)
  • http://localhost:8080/en/portfolio
  • http://localhost:8080/en/cementos
  • http://localhost:3100/cementos (direct development access; redirects to the Colombia storefront)
  • http://localhost:3100/cementos/co
  • 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 design PoC can still run without the commerce profile:

docker compose up --build -d

That starts only the portfolio application and disposable local Nginx proxy. The /cementos 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.

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.

Share the current public PoC:

ngrok http http://localhost:8080

The assigned URL is public to anyone who has it. Stop the tunnel with Ctrl+C; the Docker stack keeps running.

The Medusa DTC storefront can be tunneled separately:

ngrok http http://localhost:3100

Its generated product-media and browser-side API URLs still point at local loopback services, so that tunnel is suitable for routing checks rather than a complete remote demo. The VPS deployment will use the public api and media hostnames and will not have that limitation.

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 are also local-only. The complete remote commerce experience should use the real HTTPS admin, api, and media hostnames on the VPS instead of a collection of temporary tunnels.

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

/cementos is compiled into Next.js through NEXT_PUBLIC_STOREFRONT_BASE_PATH; it is not an Nginx-only rewrite. After changing it, rerun npm run storefront:env with the intended values and rebuild the storefront image. The explicit root route exists so /cementos reliably redirects to the default Colombia route /cementos/co.

Storefront production dependency audit

The controlled Next.js 16.3.2 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