Skip to content

AndreaWeb architecture and operations overview

  • Document date: 2026-08-30
  • Last live verification: 2026-08-30, America/Bogota
  • Production domain: andreazambrano.co
  • Production host: OVH VPS vps114094
  • Repository: AndreaWeb monorepo; authoritative working copy in WSL at /home/justin/repos/AndreaWeb

This is the architectural overview and operator's map for AndreaWeb. It describes the deployed system, repository tooling, data ownership, network and security boundaries, current capacity, ordinary maintenance, and planned expansion. Values marked current were measured or read from the VPS and repository on the verification date. Values marked planned are not yet deployed.

Never put passwords, API keys, invitation tokens, customer data, or the contents of /opt/andreaweb/shared/production.env in this document, Git, CI logs, or issue trackers.

1. System purpose and application boundaries

AndreaWeb presents related but independently evolvable sites under one domain:

  • Portfolio: Andrea Carolina Zambrano Garcés's architecture portfolio, with Spanish as the default language and English routes.
  • Cementos store: a Medusa DTC storefront for handmade cement objects, with products, variants, COP pricing, images, availability, and a cart.
  • Commerce administration: Medusa Admin for products, variants, prices, media, inventory, users, and roles.
  • Inquiry checkout: the cart currently becomes a WhatsApp, email, or copied inquiry rather than a processor-confirmed payment.
  • Future publishing: an optional architecture/astronomy journal or blog, expected to be portfolio-owned rather than coupled to commerce.

The portfolio and storefront share branding and navigation intent, but neither should require the other to deploy. The portfolio must not depend on Medusa.

2. Production topology

flowchart TB
    Visitor[Browser or Medusa Admin user]
    DNS[Namecheap DNS]
    TLS[System Nginx<br/>TLS and reverse proxy<br/>ports 80 and 443]

    subgraph VPS[OVH VPS - Ubuntu 26.04]
      direction TB
      TLS

      subgraph Edge[Docker edge network]
        Portfolio[Portfolio container<br/>Vinext and React<br/>127.0.0.1:3000]
        Storefront[Storefront container<br/>Next.js Medusa DTC<br/>127.0.0.1:3100]
        Medusa[Medusa container<br/>API + Admin + worker<br/>127.0.0.1:9000]
        Garage[Garage web endpoint<br/>127.0.0.1:3902]
      end

      subgraph Commerce[Docker internal commerce network]
        Postgres[(PostgreSQL 17<br/>business source of truth)]
        Redis[(Redis 7.4<br/>cache, BullMQ, locks)]
        GarageData[(Garage S3 API<br/>product media)]
      end

      Storefront --> Medusa
      Medusa --> Postgres
      Medusa --> Redis
      Medusa --> GarageData
      Garage --- GarageData
    end

    Visitor --> DNS --> TLS
    TLS -->|apex and www, /portfolio| Portfolio
    TLS -->|apex and www, /cementos| Storefront
    TLS -->|api subdomain| Medusa
    TLS -->|admin subdomain| Medusa
    TLS -->|media subdomain| Garage

System Nginx is installed directly on Ubuntu. It is deliberately not part of the Compose project. This lets it retain ports 80/443, Let's Encrypt integration, and unrelated future sites independently from AndreaWeb application releases.

3. Domains, routes, and ports

DNS

Namecheap currently delegates every public application hostname to the same OVH VPS at 144.217.92.155:

Record Host Value Reason
A @ 144.217.92.155 apex portfolio and store
CNAME www andreazambrano.co alternate website hostname
A api 144.217.92.155 Medusa API
A admin 144.217.92.155 Medusa Admin
A media 144.217.92.155 Garage-backed media

DNS only selects the server. Nginx then selects the application by hostname and path. A future domain change therefore affects DNS, the certificate, Nginx server_name values, application origins/CORS, canonical URLs, sitemaps, email and provider callback URLs; it should not require rewriting the applications.

HTTP routing

Public address Nginx upstream Purpose
https://andreazambrano.co/portfolio 127.0.0.1:3000 Spanish portfolio
https://andreazambrano.co/en/portfolio 127.0.0.1:3000 English portfolio
https://andreazambrano.co/cementos 127.0.0.1:3100 Store entry; storefront resolves the country route
https://andreazambrano.co/cementos/co 127.0.0.1:3100 Colombia/COP storefront
https://www.andreazambrano.co/... same apex upstreams Alternate hostname; currently remains www after HTTPS redirect
https://api.andreazambrano.co 127.0.0.1:9000 Medusa Store/Admin APIs and health endpoint
https://admin.andreazambrano.co/app 127.0.0.1:9000 Medusa Admin SPA served by Medusa
https://media.andreazambrano.co 127.0.0.1:3902 Public Garage product media

Only Nginx is publicly exposed. Application ports bind to 127.0.0.1; PostgreSQL, Redis, and the Garage S3 API have no host port. Garage's public web endpoint uses the internal host alias andreaweb-media.web.garage.localhost at the proxy layer.

Current request-size limits are 10 MB for apex/portfolio/storefront requests and 25 MB for API/Admin requests. Media responses receive a one-year immutable cache header. Store, API, Admin, media, and robots responses remain blocked from indexing with robots.txt and X-Robots-Tag during private review.

The Let's Encrypt certificate covers the apex, www, api, admin, and media. Certbot owns renewal and the TLS directives it added to the live Nginx site.

Nginx configuration ownership

The live site is /etc/nginx/sites-available/andreaweb, enabled through Ubuntu's sites-enabled convention. The repository example is deploy/nginx-vps.conf.example; it is a review/reference input, not a file that may blindly overwrite Certbot-modified production configuration. Nginx forwards the original host/protocol and client-forwarding headers, preserves streaming behavior where required, and routes only to loopback application ports.

Certificate material is owned by Certbot under /etc/letsencrypt/live/andreazambrano.co/ and must never be copied into Git or the Docker images. The live certificate and renewal configuration cover five names. Any new subdomain must have DNS, an Nginx server block, firewall-safe routing, and certificate coverage reviewed together.

4. Containers and images

An image is an immutable application/runtime template. A container is a running instance of an image plus runtime configuration, network membership, and volume mounts. Replacing a stateless application container does not replace its database or object-storage volumes.

Compose service Current image Image disk snapshot Function Network Persistence Current memory snapshot
web andreaweb-web:latest 1.26 GB Portfolio server edge none 114 MiB
storefront andreaweb-storefront:latest 693 MB Next.js DTC storefront and inquiry BFF endpoints edge, commerce none 97.1 MiB
medusa andreaweb-medusa:latest 1.34 GB Medusa API, Admin bundle, workflows, events, shared worker edge, commerce PostgreSQL/Redis/Garage externally 290.4 MiB
postgres postgres:17-alpine 297 MB Commerce relational database commerce only andreaweb_postgres_data 61.1 MiB
redis redis:7.4-alpine 39.1 MB Cache, BullMQ queues, workflow coordination, locks commerce only andreaweb_redis_data 12.7 MiB
garage dxflrs/garage:v2.3.0 71.3 MB S3-compatible media storage and public media server edge, commerce andreaweb_garage_data, andreaweb_garage_meta 21.0 MiB
garage-init dxflrs/garage:v2.3.0 shared One-shot bucket website initialization commerce only reads Garage metadata not normally running

The six running containers used approximately 596 MiB in the measured idle snapshot. This is not a peak or capacity guarantee. Container CPU was near zero except for a momentary Garage sample; request load, image processing, migrations, builds, and Node garbage collection can increase both CPU and memory.

Image rows show Docker's per-image virtual size; shared layers mean they must not be summed to estimate disk use. docker system df reported 3.322 GB for the six active images at the same snapshot.

All long-running services use restart: unless-stopped and health checks. The portfolio filesystem is read-only with bounded temporary filesystems. Portfolio, storefront, and Medusa drop Linux capabilities and enable no-new-privileges.

Image construction

  • All application images use multi-stage node:22-alpine Dockerfiles and run as the unprivileged node user.
  • The storefront runtime installs production dependencies and copies the compiled Next.js output.
  • Medusa compiles its Admin bundle into the backend image; there is no separate Admin container.
  • The Medusa image includes versioned starter product assets under /seed-assets for the explicit starter-media import command.
  • The portfolio image currently copies more of the build tree than ideal and is approximately 1.26 GB; image-size optimization remains worthwhile.

The live VPS still runs mutable latest application tags from the earlier release workflow. The repository's current deployment tooling builds commit-tagged images such as andreaweb-storefront:<12-character-commit> on the next release and records per-service commits under /opt/andreaweb/shared/deployments/.

5. Application and platform versions

Application Current stack Notes
Portfolio Vinext 1.0.0-beta.2, Vite 8, React 19, Tailwind 4 Current portfolio implementation; bilingual routes and dummy project content
Storefront Next.js 16.3.2, React 19.2.8, Medusa JS SDK 2.19.0, Tailwind 3 Derived from the official Medusa DTC Next.js starter; mounted at /cementos
Backend/Admin Medusa 2.19.0, built-in Admin dashboard, Node 22 Shared server/worker mode for the MVP
Database PostgreSQL 17 Authoritative commerce and identity data
Cache/queues Redis 7.4 + BullMQ through Medusa providers Cache, events, workflows, timers, cleanup, and locks
Media Garage 2.3.0 through Medusa S3 File Module Single-node, replication factor 1

Important pinned application packages include:

Area Package versions
Portfolio build Vite 8.0.13, TypeScript 5.9.3, ESLint 9.39.4, Wrangler 4.92.0
Storefront build TypeScript 5.9.3, ESLint 9.39.5, PostCSS 8.5.26, Prettier 2.8.8
Storefront UI/data Medusa icons/SDK/UI preset 2.19.0, Headless UI 2.2.9
Medusa extensions Redis caching/locking 2.19.0, S3 File Module 2.19.0, Admin dashboard 2.19.0
Test/runtime utilities Jest 29.7.0, tsx 4.22.1, PostgreSQL client 8.16.3

The root package requires Node >=22.13.0; the backend supports Node ^20.19.0 or >=22.12.0. Application Dockerfiles intentionally standardize production on Node 22 Alpine. The lockfile is npm lockfile format 3, and the backend pins npm 11.17.0 as its package manager.

Verified production host/tool versions

Component Version verified 2026-08-30
Ubuntu 26.04 LTS
Linux kernel 7.0.0-29-generic
Docker Engine 24.0.2
Docker Compose plugin 2.18.1
Nginx 1.28.3 (Ubuntu package)
Certbot 4.0.0
OpenSSH server/client 10.2p1 (Ubuntu package)
OpenSSL 3.5.5
Node inside application containers 22.23.2
npm inside application containers 11.17.0
PostgreSQL container 17.11
Redis container 7.4.11
Garage image 2.3.0
Git on VPS 2.53.0
Python on VPS 3.14.4

Patch versions will move over time. Treat the Compose image tags, package lockfile, and a fresh runtime query as authoritative rather than copying this snapshot into a deployment decision.

Vinext is not the foundation of the commerce storefront. It is used by the current portfolio. The commerce application is Next.js. The planned portfolio migration is to Astro, independently of commerce.

6. Data ownership and persistence

Data Authoritative owner Volume/path Rebuildability
Products, variants, prices, inventory PostgreSQL andreaweb_postgres_data not safely rebuildable after Admin edits
Users, invitations, roles, policies PostgreSQL andreaweb_postgres_data not safely rebuildable
Carts, customers, orders PostgreSQL andreaweb_postgres_data not safely rebuildable
Product images Garage andreaweb_garage_data and metadata volume not safely rebuildable after uploads
Cache entries Redis andreaweb_redis_data derived and rebuildable
BullMQ events/workflow jobs/locks Redis andreaweb_redis_data operational state; loss may lose in-flight work
Private production configuration operator-managed file /opt/andreaweb/shared/production.env secrets must be backed up securely, never committed
Application source releases Git archive releases /opt/andreaweb/releases/<commit-prefix> rebuildable from Git
Current release pointer symlink /opt/andreaweb/current rebuildable

The Compose project name is andreaweb. Ordinary builds, targeted deployments, container recreation, and docker compose down preserve named volumes. Never use down --volumes against persistent production unless deliberately destroying all production state from a verified backup.

At verification, active Docker volumes totaled about 98.5 MB. The complete /opt/andreaweb release/configuration tree used about 55 MB. These are small because the catalog is currently small; growth alerts and off-VPS backups are still required before treating the site as durable production.

7. PostgreSQL, Redis, BullMQ, and consistency

PostgreSQL is the source of truth. Redis does not contain a second authoritative copy of products, inventory, carts, or orders. Redis may contain derived cache entries and temporary operational payloads that refer to database entities.

sequenceDiagram
    actor Customer
    participant SF as Storefront
    participant M as Medusa workflow
    participant PG as PostgreSQL
    participant R as Redis / BullMQ
    participant S as Event subscriber

    Customer->>SF: Add item or submit operation
    SF->>M: API request
    M->>R: Acquire coordination lock if required
    M->>PG: Transactionally read/write business entities
    PG-->>M: Committed authoritative state
    M->>R: Enqueue event, retry, timeout, or follow-up
    M-->>SF: API result
    R-->>S: Worker delivers queued event
    S-->>R: Acknowledge or retry

Redis responsibilities

  1. Caching: Medusa checks its cache abstraction; the Redis provider supplies cached derived results. On a miss, Medusa queries PostgreSQL and may populate Redis. Correct invalidation and contextual keys prevent stale prices or markets.
  2. Event bus: Medusa's official Redis event-bus provider uses BullMQ Queue and Worker objects. Its custom prefix produces keys such as RedisEventBusService:events-queue:*.
  3. Workflow engine: BullMQ queues named medusa-workflows, medusa-workflows-jobs, and workflows-cleaner handle retries, timeouts, scheduled work, and retention cleanup.
  4. Distributed locking: Redis locks coordinate operations across requests or future multiple Medusa processes.

BullMQ is used because it is Medusa's supported Redis implementation and Redis is already required for the other providers. RabbitMQ would add a second broker while Redis/BullMQ would still be needed for the workflow engine unless custom providers replaced the standard architecture.

Redis runs with AOF enabled and an RDB save rule of one change in 60 seconds. The Append-Only File records Redis writes for replay after restart, primarily protecting queue/workflow state rather than merely disposable cache entries. Redis currently uses noeviction with no Redis-level maxmemory; monitoring and a deliberate container/memory policy are planned.

Queue delivery and retries do not remove the need for idempotent handlers. Redis and PostgreSQL are different transactional systems; future critical integrations should consider reconciliation and a transactional outbox where appropriate.

8. Product media

Medusa's S3 File Module sends uploads to Garage's private S3 endpoint at port 3900. The storefront receives public URLs under media.andreazambrano.co; Nginx proxies those reads to Garage's website endpoint on loopback port 3902.

Garage is currently a single-node, replication-factor-one service using SQLite for metadata. This is suitable for the MVP but is not high availability. Losing the VPS or both Garage volumes loses Admin-uploaded media unless an off-VPS backup exists.

Starter media import is explicit and idempotent for products that already have images; it is not part of every deployment.

9. Cart and inquiry flow

The storefront has a real Medusa-backed cart with variants, managed inventory, COP prices, and availability. It does not currently perform Stripe checkout.

The inquiry flow formats the cart and offers:

  • a server-generated WhatsApp destination URL;
  • an email draft to cementos@andreazambrano.co;
  • a clipboard copy.

The WhatsApp number is server-only in production.env, so it is not compiled into the public JavaScript bundle. It can still be obtained by invoking the endpoint and is therefore privacy reduction, not access control. Browser back navigation retains the populated client cart after the 303 inquiry redirect.

The payment evolution, including manual Nequi/Bre-B reconciliation, hosted support links, Wompi/Mercado Pago evaluation, Stripe eligibility, future Medusa payment providers, webhooks, refunds, and thank-you semantics, is maintained in Future commerce, payments, and communications. Provider-confirmed success—not a browser redirect alone—must control any future payment-success claim.

10. Admin identity and RBAC

The first production invitation bootstraps Andrea as role_super_admin. With the installed Medusa Admin RBAC UI she can create roles, attach policies/permissions, invite users, and assign users to roles. Reasonable future roles include:

  • catalog manager;
  • inventory manager;
  • order/inquiry manager;
  • administrator;
  • super-admin.

The production invitation helper always creates a super-admin invitation and must not be used for restricted staff. UI invitation delivery also requires a configured notification/email provider; that provider is not yet configured. Until it is, restricted invitation delivery needs a deliberately designed operator workflow.

Invitation files contain bearer tokens, are created mode 0600, and must be sent securely and deleted after acceptance. A second recovery super-admin is optional later; Andrea may be the only super-admin for the MVP.

11. VPS capacity and current consumption

Host capacity, verified 2026-08-30

Resource Current value
vCPU 1
RAM 3.7 GiB
Swap none
Root filesystem 39 GB
Root used/free 14 GB used, 26 GB available, 36% used
Host memory snapshot 1.4 GiB used, 2.4 GiB available
Application image storage 3.322 GB across six active images
Active persistent volumes 98.5 MB
Docker build cache 0 after cleanup
Journal storage 257 MB after retention cleanup

The host has only one vCPU and no swap. Concurrent image builds, migrations, backup compression, and production traffic can therefore contend. Build cache and a small number of rollback images may be retained under a disk budget, but unused customer projects, anonymous volumes, and unbounded build cache must not accumulate again.

Current hard limits

The Compose file does not currently impose per-container memory or CPU limits; Docker reports the host's full 3.726 GiB as each container's limit. The application therefore relies on host capacity, Linux scheduling, and service behavior. This is a known hardening gap, not an assertion that every service may safely consume the entire host.

Before adding limits, measure peak Admin uploads, catalog operations, migrations, builds, and storefront load. A limit that is too low can create avoidable OOM kills; no limit allows one process to starve the VPS. Container restart/OOM counts and host available memory must be monitored. Adding a modest swap file may improve survival during brief spikes but is not a substitute for memory sizing.

Journald limits

The live drop-in /etc/systemd/journald.conf.d/andreaweb-retention.conf contains:

[Journal]
SystemMaxUse=756M
SystemKeepFree=2G
MaxRetentionSec=21day

The size is a ceiling. Journald may retain less after whole archived files are vacuumed. Docker uses the json-file logging driver, so application/container logs are primarily under Docker rather than systemd-journald. Docker log rotation is not yet explicitly configured in Compose and should be added with bounded file size and count.

12. Security and exposure

  • UFW allows OpenSSH, Nginx Full, and the separately configured SSH port 1022.
  • Only system Nginx listens publicly for web traffic.
  • The commerce Docker network is internal; the edge network connects proxy-facing services without exposing their container ports publicly.
  • Production secrets are mode 0600 outside release directories.
  • Medusa, storefront, and portfolio telemetry are disabled where configured.
  • CORS explicitly identifies storefront and Admin origins.
  • Current Nginx security headers are suitable for private preview but require a reviewed CSP and longer HSTS policy before public launch.
  • Admin should later receive a VPN, IP allowlist, or additional authentication layer if Andrea's access pattern permits it.
  • Current noindex behavior must be intentionally removed only when public launch and privacy/consent requirements are ready.

13. Release layout and deployment model

/opt/andreaweb/
├── current -> releases/<commit-prefix>
├── releases/
│   └── <commit-prefix>/
└── shared/
    ├── production.env                 # private, mode 0600
    ├── deployments/                   # per-service deployed commit markers
    └── production-admin-invite.txt    # temporary; delete after acceptance

The repository deployment planner compares the candidate commit with the separately recorded portfolio, storefront, and Medusa commits. It recommends no deployment, a targeted application release, or a conservative all-apps release. Migration paths add an explicit --migrate flag.

flowchart LR
    Commit[Clean reviewed Git commit] --> Plan[deploy-ovh.sh plan]
    Plan --> Review{Review exact target<br/>commit and migrations}
    Review --> Apply[deploy-ovh.sh apply]
    Apply --> Archive[Upload immutable Git archive]
    Archive --> Build[Build only selected image]
    Build --> Migrate{Migration planned?}
    Migrate -->|yes| DB[Run Medusa migrations]
    Migrate -->|no| Replace[Replace selected container]
    DB --> Replace
    Replace --> Health[Wait for health and smoke test]
    Health --> Marker[Record per-service commit]
    Marker --> Current[Advance current source symlink]

System Nginx, Certbot, DNS, the firewall, persistent volumes, and unrelated VPS services are outside ordinary application deployment.

14. Operator commands

Unless stated otherwise, repository commands run in WSL from:

cd /home/justin/repos/AndreaWeb

Plan and apply releases

./scripts/deploy-ovh.sh plan

Run the exact command printed by the plan, for example:

./scripts/deploy-ovh.sh apply \
  --commit <full-reviewed-commit> \
  --target storefront

Supported targets are portfolio, storefront, medusa, all-apps, and the explicit full-environment bootstrap. Only Medusa/all-apps/bootstrap accept --migrate.

Status and environment-only recreation

./scripts/operate-ovh.sh status
./scripts/operate-ovh.sh recreate storefront
./scripts/operate-ovh.sh recreate portfolio
./scripts/operate-ovh.sh recreate medusa

Recreation reuses the currently running image. It is appropriate for server-only environment values. A NEXT_PUBLIC_* or other compile-time setting requires a new image/release.

Non-destructive bootstrap

For a genuinely new or repaired environment:

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

Bootstrap creates an environment only when absent, starts stateful services, initializes Garage, migrates Medusa, synchronizes the publishable key, builds apps, and verifies health. It never removes volumes or overwrites an existing environment.

Catalog maintenance

On the VPS:

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 first command creates only missing versioned starter handles. The second adds starter media only where starter products do not already have images. Neither is a substitute for backup/restore and neither runs in ordinary targeted deployment.

Create Andrea's production Admin invitation

On the VPS:

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

cat /opt/andreaweb/shared/production-admin-invite.txt

Delete the file after acceptance:

rm /opt/andreaweb/shared/production-admin-invite.txt

Rerunning before acceptance replaces the pending token. An existing Admin user is preserved. The local invitation command affects only the local development database and is not used for Andrea's production account.

Compose status and health from the VPS

cd /opt/andreaweb/current

docker compose \
  --env-file /opt/andreaweb/shared/production.env \
  -f compose.prod.yaml ps

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

External checks:

curl --fail https://andreazambrano.co/portfolio
curl --fail https://andreazambrano.co/cementos/co
curl --fail https://api.andreazambrano.co/health

For an emergency same-image restart on the VPS, use Compose directly. restart does not rebuild an image, run migrations, or reload changed environment values:

docker compose \
  --env-file /opt/andreaweb/shared/production.env \
  -f compose.prod.yaml restart storefront

For a changed server-side environment value, use operate-ovh.sh recreate from the workstation instead; container recreation is what applies the new environment.

Logs

Application logs on the VPS:

cd /opt/andreaweb/current

docker compose \
  --env-file /opt/andreaweb/shared/production.env \
  -f compose.prod.yaml logs --tail=200 web storefront medusa

docker compose \
  --env-file /opt/andreaweb/shared/production.env \
  -f compose.prod.yaml logs --since=30m --follow medusa

docker compose \
  --env-file /opt/andreaweb/shared/production.env \
  -f compose.prod.yaml logs --tail=200 postgres redis garage

Host and proxy logs:

sudo journalctl -u docker --since "1 hour ago"
sudo journalctl -u nginx --since "1 hour ago"
sudo journalctl -p warning..alert --since today
sudo tail -n 200 /var/log/nginx/access.log
sudo tail -n 200 /var/log/nginx/error.log

Do not paste complete production logs into public systems without checking for IP addresses, email addresses, URLs, tokens, or customer content.

Resource and disk inspection

docker stats --no-stream
docker system df -v
docker ps -a
docker volume ls
free -h
df -h /
du -sh /opt/andreaweb /var/log/journal
sudo journalctl --disk-usage

Service-level, read-only diagnostics:

docker inspect --format '{{.Name}} oom={{.State.OOMKilled}} restarts={{.RestartCount}}' \
  andreaweb-web-1 andreaweb-storefront-1 andreaweb-medusa-1

docker compose \
  --env-file /opt/andreaweb/shared/production.env \
  -f compose.prod.yaml exec redis redis-cli INFO memory

docker compose \
  --env-file /opt/andreaweb/shared/production.env \
  -f compose.prod.yaml exec redis redis-cli INFO persistence

docker compose \
  --env-file /opt/andreaweb/shared/production.env \
  -f compose.prod.yaml exec postgres sh -lc \
  'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c \
  "select pg_size_pretty(pg_database_size(current_database()));"'

Use Redis SCAN, not blocking KEYS, if queue-key inspection is ever necessary on a nontrivial dataset. Prefer Medusa/BullMQ-aware metrics and supported application tools to manually mutating queue keys. Database reports and cleanup scripts must be versioned, read-only/dry-run by default, and follow the safety rules in the observability document.

Docker pruning has no dry-run and can erase rollback images, build acceleration, stopped diagnostics, and unused volume data. Use an explicit retention policy and inspect targets before pruning. Never automate production volume deletion.

Nginx and certificates

sudo nginx -t
sudo systemctl reload nginx
sudo certbot certificates
sudo certbot renew --dry-run
systemctl status certbot.timer

Always edit the AndreaWeb site additively and run nginx -t before reload. Do not replace unrelated site files. The live Nginx file still begins with a stale "Disposable preview" comment even though it now routes the persistent stack; the comment should be corrected during the next reviewed Nginx maintenance.

15. Local development

Local work runs from WSL and Docker Desktop. The local Compose topology exercises the same classes of PostgreSQL, Redis, Garage, Medusa, storefront, and portfolio services. Local data and invitations are independent of VPS production state.

Local Compose additionally runs nginx:1.27-alpine on host port 8080 to rehearse path routing without changing Windows/WSL system Nginx. This local proxy does not exist in production, where Ubuntu's system Nginx owns ports 80/443. Medusa exposes loopback port 9000 and its development Admin/Vite port 5173; the storefront exposes loopback port 3100 and Garage web media exposes loopback port 3902.

Local named volumes backend_node_modules, storefront_node_modules, and storefront_next_cache keep Linux container dependencies/build cache separate from the WSL bind-mounted source tree. They prevent a host install from replacing native container dependencies and make repeated local starts faster. They are disposable development state, unlike the PostgreSQL/Garage production data. Production images contain their installed dependencies and do not mount these dependency volumes.

Common commands are documented in docs/local-operations.md. The root scripts run linting, tests, builds, environment synchronization, local health checks, and media persistence verification. Production changes must come from committed releases, not copied local build artifacts or Windows-specific scripts.

Developer environment inventory

The supported interactive development shell is zsh in Ubuntu WSL. Justin's ~/.zshrc loads the shared dev-environment setup, fnm, Windows interoperability wrappers, and the SSH agent/key bridge. The ovh SSH config alias selects the VPS and the accepted justin-omen ED25519 key; deploy scripts use that alias rather than embedding a host, username, password, or private-key path.

Interactive WSL tool Version verified 2026-08-30 Source/role
zsh system /usr/bin/zsh supported project shell
fnm 1.39.0 selects Node per interactive shell
Node 24.19.0 developer CLI; production remains Node 22
npm / npx 11.17.0 workspace install/scripts and package executables
Python 3.14.4 supporting local tooling; not an app runtime
Git 2.53.0 source/release identity
GitHub CLI 2.97.0 authenticated as masojus; Git transport uses SSH
SSH bridge Windows OpenSSH 9.5p2 WSL function uses the shared Windows agent/key
Docker Desktop engine 29.7.2 local Linux containers
Docker Compose 5.3.1 local topology
Azure CLI 2.89.1 general dev environment; not required by AndreaWeb

An interactive zsh session must be used when relying on fnm; a minimal non-interactive shell may not source the same initialization. Verify before operating:

type fnm node npm npx gh ssh
fnm --version
node --version
npm --version
npx --version
gh auth status
ssh-add -l

The developer Node version may be newer than production because the repository's declared engine range permits it. Container builds remain the production compatibility authority. npm and npx are paired entry points from the same npm installation; npx runs package-provided executables and is not a separate runtime.

16. Backup, recovery, and rollback

Current named volumes survive container replacement, but persistence is not backup. Before public launch the system still requires:

  • scheduled PostgreSQL logical/physical backup with retention;
  • Garage data and metadata backup;
  • encrypted off-VPS copies;
  • production environment secret recovery procedure;
  • automated backup-result reporting;
  • regular restore into a disposable environment;
  • disk-capacity and backup-age alerts.

Application rollback can target a prior commit-tagged image/release. Database schema rollback is separate and must never be inferred from application rollback. Before a migration, confirm backup and compatibility with the known-good application version.

The desired image/cache retention policy is to keep a small number of known-good commit images plus a bounded recent build cache. This policy is planned but not yet automated.

17. Observability and CI/CD direction

The immediate operational baseline is health checks, Compose status/logs, bounded system journal, host/Docker resource inspection, deployment commit markers, external uptime, certificate monitoring, backup verification, and disk alerts.

OpenTelemetry-compatible service/resource naming should be designed from the start. The small VPS should not receive a full Grafana/Loki/Tempo stack without measured capacity. A resource-capped collector and an external or carefully sized backend are future options.

The planned CI/CD pipeline will add pull-request EOL/policy checks, lint and type checks, tests, production builds, Compose/container validation, disposable migration rehearsal, dependency/image scanning, immutable artifacts, protected production approval, deployment events, and smoke tests. Current known portfolio lint warnings must not grow; the target is zero warnings after focused cleanup.

See docs/ci-cd-plan.md and docs/observability-operations-and-growth.md.

18. Future expansion

Portfolio and blog

The planned portfolio framework is Astro with typed content collections, static generation, Spanish-default/English routes, responsive galleries, and Markdown/MDX projects. An architecture or astronomy journal can use Astro content collections for articles, tags, archives, RSS, search, and navigation. It should remain independently deployable and can later adopt a Git-backed editor or headless CMS if Andrea finds Markdown authoring unsuitable.

An entirely separate blog sub-application remains possible behind Nginx without changing the store decision. Hugo is a reasonable alternative for a fully separate Go-based Markdown site but is not the default.

Commerce

Likely additions include color variants with independent inventory, Medusa price lists and sale presentation, manual inquiry/order conversion, inventory reservations, business WhatsApp, transactional email, support/contact pages, a real payment provider appropriate for Colombia, tip/support links, and provider-confirmed thank-you pages. Detailed payment and communications design remains in Future commerce, payments, and communications.

Growth and privacy

Analytics, advertising, customer outreach, loyalty, and a client database require purpose limitation, retention rules, access controls, consent handling, and deletion workflows. A cookie banner is required when nonessential storage begins; a CMP may be appropriate if advertising or EEA requirements justify it. AdSense belongs only on future editorial content, not the portfolio or checkout journey by default.

19. Known gaps and next architectural actions

  1. Add PostgreSQL and Garage off-VPS backup/restore automation and test it.
  2. Add explicit Docker json-file log rotation.
  3. Measure production peaks and add reviewed container memory/resource controls.
  4. Consider a small swap file after evaluating latency and OOM behavior.
  5. Deploy the commit-tagged targeted release tooling and implement image/cache retention by count, age, or disk budget.
  6. Configure transactional email before relying on Admin UI invitation delivery.
  7. Restrict Admin exposure before broad public launch.
  8. Remove preview-only Nginx comments and deliberately decide canonical www behavior.
  9. Add external uptime, certificate, disk, memory, OOM, backup-age, and queue-backlog monitoring.
  10. Implement the staged CI/CD plan and branch protection.
  11. Resolve existing portfolio image lint warnings and optimize the portfolio image.
  12. Remove noindex only through an explicit public-launch checklist.

Update this document whenever the production topology, domain routing, data owner, framework, release mechanism, resource policy, or recovery procedure materially changes. Refresh live measurements rather than treating the 2026-08-30 snapshot as a permanent capacity guarantee.