Local WSL operations¶
Run project commands inside Ubuntu WSL from:
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
fnminstallation; - OpenSSL and curl in WSL;
- the repository checked out on the WSL filesystem.
Verify the boundary before troubleshooting the application:
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:
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/portfoliohttp://localhost:8080/cementos(the real Medusa DTC storefront)http://localhost:8080/en/portfoliohttp://localhost:8080/en/cementoshttp://localhost:3100/cementos(direct development access; redirects to the Colombia storefront)http://localhost:3100/cementos/cohttp://localhost:9000/apphttp://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:
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:
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:
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:
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:
Stop and remove project containers and networks while retaining named volumes:
Start again with:
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:
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:
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¶
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: