Skip to content

AndreaWeb architecture and operations overview

  • Document date: 2026-09-28
  • Last VPS verification: 2026-09-28, America/Bogota
  • Frontend routes/release verified: 2026-09-27; see the redesign cutover
  • Last GitLab Pages verification: 2026-08-31, America/Bogota
  • Production domain: andreazambrano.co
  • Production hosts: OVH VPS vps114094 and GitLab Pages
  • 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.

The September frontend route/branding updates below supersede the August public paths. The host, container, disk, and logging snapshot below was re-measured on September 28, 2026 after the Phase 5 runtime-resource and Phase 6–7 production verification and cleanup work. Other host/package versions retain their stated verification dates.

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.
  • Guanza store: a Medusa DTC storefront with Orígenes and Piezas Intervenidas, 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.
  • Documentation: a Material for MkDocs site at docs.andreazambrano.co, built and hosted independently by GitLab Pages from an exported AndreaWeb documentation snapshot.
  • Journal: a bilingual Astro site at journal.andreazambrano.co with Markdown content, Pagefind search, topics, tags, RSS, and a browser editor provided by Sveltia CMS at /admin/.

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[Public visitor]
    Author[Journal author]
    DNS[Namecheap DNS]
    VPSProxy[System Nginx<br/>VPS TLS and reverse proxy<br/>ports 80 and 443]
    GitLabPages[GitLab Pages<br/>static hosting and TLS]
    Docs[Material for MkDocs<br/>documentation site]
    Journal[Astro static journal<br/>Pagefind, RSS, sitemap]
    Sveltia[Sveltia CMS SPA<br/>journal /admin/]
    GitLabAPI[GitLab OAuth and API]
    JournalRepo[(Journal Git repository<br/>Markdown, media, and site source)]
    JournalCI[GitLab CI<br/>Astro build]

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

      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
    DNS -->|apex, www, api, admin, media| VPSProxy
    DNS -->|docs and journal CNAMEs| GitLabPages
    VPSProxy -->|apex and www, landing / arquitectura| Portfolio
    VPSProxy -->|apex and www, guanza / en/guanza| Storefront
    VPSProxy -->|api subdomain| Medusa
    VPSProxy -->|admin subdomain| Medusa
    VPSProxy -->|media subdomain| Garage
    GitLabPages --> Docs
    GitLabPages --> Journal
    GitLabPages --> Sveltia
    Author -->|loads /admin/| Sveltia
    Sveltia -->|OAuth PKCE and content commits| GitLabAPI
    GitLabAPI --> JournalRepo --> JournalCI --> Journal

System Nginx is installed directly on Ubuntu and is deliberately not part of the Compose project. It owns only the VPS-hosted properties. GitLab Pages separately serves the docs and journal static artifacts and manages TLS for their custom subdomains; requests for those properties never pass through the VPS or its Nginx.

3. Domains, routes, and ports

DNS

Namecheap delegates the application hostnames to one of two hosting boundaries:

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
CNAME docs andreazambrano.gitlab.io GitLab Pages documentation site
CNAME journal andreazambrano.gitlab.io GitLab Pages journal and editor shell

GitLab Pages ownership-verification TXT records also exist for the docs and journal hostnames. Their generated values should be copied from GitLab's Pages domain screen and must not be guessed or reused for another hostname.

DNS only selects the hosting boundary. Nginx selects among VPS applications, while GitLab Pages maps the docs and journal hostnames to their Pages projects. A domain change therefore affects the matching DNS record, certificate owner, canonical URLs, sitemaps, and any OAuth callback URLs. Nginx and VPS certificates change only for a VPS-hosted name.

HTTP routing

Public address Nginx upstream Purpose
https://andreazambrano.co/ and /en 127.0.0.1:3000 Bilingual personal landing
https://andreazambrano.co/arquitectura and /en/arquitectura 127.0.0.1:3000 Bilingual architecture portfolio
https://andreazambrano.co/guanza and /en/guanza 127.0.0.1:3100 Bilingual collection entrance
/guanza/origenes, /guanza/piezas, /guanza/productos/<handle>, /guanza/carrito and their /en counterparts 127.0.0.1:3100 Same Colombia/COP market and cart in both languages
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
https://docs.andreazambrano.co GitLab Pages Material for MkDocs documentation snapshot and search
https://journal.andreazambrano.co/ GitLab Pages Astro journal landing page
https://journal.andreazambrano.co/journal/ GitLab Pages Spanish journal, topics, tags, search, RSS, and articles
https://journal.andreazambrano.co/en/journal/ GitLab Pages English journal and articles
https://journal.andreazambrano.co/admin/ GitLab Pages Sveltia CMS browser application; GitLab sign-in required to edit

For the VPS boundary, 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. The docs and journal are public static GitLab Pages sites and have no route into those private services.

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 VPS Let's Encrypt certificate covers the apex, www, api, admin, and media; Certbot owns its renewal and Nginx directives. GitLab Pages independently obtains and renews Let's Encrypt certificates for docs and journal. Separate certificates for different hostnames do not conflict.

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.

VPS 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 VPS certificate and renewal configuration cover five names. Any new subdomain must have DNS, routing, and certificate ownership reviewed together; an Nginx server block and VPS certificate change are needed only when the new property is actually hosted on the VPS.

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 Earlier image disk snapshot Function Network Persistence Earlier 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 table's memory values are earlier idle snapshots and have been superseded. At final Phase 7 verification, after about one hour with all six long-running services healthy, measured usage was 145.3 MiB (web), 126.3 MiB (storefront), 271.9 MiB (medusa), 29.97 MiB (postgres), 6.449 MiB (redis), and 9.023 MiB (garage). Each service was below its configured limit. The sample is still not a representative peak workload or a capacity guarantee.

Image rows show the earlier Docker per-image virtual sizes; shared layers mean they must not be summed to estimate disk use. At that earlier snapshot, docker system df reported 3.322 GB for the six active images. Phase 6 later removed only a redundant unused web image and duplicate storefront tag; retained image reclaimable space is intentional as described below.

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.6, React 19.2.8, Medusa JS SDK 2.19.0, Tailwind 3 Empty basePath; public /guanza and /en/guanza routes; /guanza asset prefix
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
Documentation Material for MkDocs >=9.6,<10, Python 3.13 build image Static GitLab Pages snapshot with search and Mermaid rendering
Journal Astro ^7.2.9, Pagefind ^1.4.0, Sveltia CMS 0.201.2 Static GitLab Pages site; Sveltia is the browser editor, not a runtime server

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
Documentation source AndreaWeb repository docs/, roadmap.md, and GitLab Pages export templates authoritative and rebuildable from Git
Published documentation snapshot separate GitLab Docs project exported Markdown, MkDocs configuration, and Pages artifact derived; replace from a fresh AndreaWeb export
Journal articles and uploads Journal GitLab project src/content/posts/ and public/uploads/ authoritative Git-backed editorial content; back up the repository
Journal public site and search index GitLab Pages artifact Astro dist/ output generated in CI derived and rebuildable from the Journal repository

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 final verification, the same four named Docker volumes—created on 2026-08-25— totaled about 105.6 MB. The exact volume identities were preserved through the runtime reconfiguration. Volume size is small because the catalog is currently small; growth alerts and tested recovery remain future work.

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-09-28

Resource Current measured value
vCPU 1
RAM 3.7 GiB
Memory snapshot 3.7 GiB total; 1.1 GiB used; 2.6 GiB available at final verification
Swap 1 GiB active; approximately 7 MiB used; vm.swappiness=10 is persistent
Root filesystem Before cleanup: ~39 GB total, 26 GB used, 14 GB available (67%); final: 15 GB used, 24 GB available (39%); inodes 13% used
Docker build cache 2.016 GB before cleanup; verified idle/reclaimable, then pruned to 0
Docker images 6.228 GB total / 3.057 GB reclaimable before; final 5.126 GB total / 1.955 GB retained as reclaimable rollback
Active persistent volumes approximately 105.6 MB; same four names, created 2026-08-25
Release directories Four retained: current runtime config, deployed web/storefront source, backend rollback source, prior runtime config
System journal 595.2 MB before rotation/vacuum; 490.5 MB afterward

The host has only one vCPU. Concurrent image builds, migrations, backup compression, and production traffic can still contend. The 1 GiB disk-backed /swapfile is an emergency buffer, not additional application capacity. It is mode 0600, owned by root, active without a reboot, and persistent through the single /etc/fstab entry /swapfile none swap sw 0 0. Systemd generated the active swapfile.swap unit from that entry. The setting vm.swappiness=10 is persisted in /etc/sysctl.d/99-andreaweb-emergency-swap.conf.

Both exact VPS cutover backup directories were deleted only after verification; both verified off-server copies remain. The backup audit and retention procedure is described in the deployment runbook. The 2.016 GB build cache was pruned only after confirming it was idle and reclaimable. Four obsolete release directories were removed, leaving the runtime-configuration release e4147a72374c, the deployed web/storefront source release 0f56ca8156ef, the backend rollback source release aebdc81d8bda, and the previous configuration release c5c3dfaa8058. Redundant images were removed while current and intentional known-good rollback images were preserved. Docker image use was 5.126 GB total, with 1.955 GB reclaimable retained for rollback. The legacy wedding systemd unit, its /var/aspnetcore/wedding tree, six .NET 2.2 packages, and the unowned 1.1 GB /usr/share/dotnet tree were removed after ownership/dependency checks; four orphaned libraries were then automatically removed. No failed systemd units remained. A cloud-init duplicate was absent at final check; recheck after package upgrades. The root filesystem passno=0 entry was reviewed as part of the cloud image and left unchanged.

The operator's local SSH ovh alias now enables keepalives at 30 seconds with a count maximum of 6; this is a local client setting, not a server-wide SSH change.

Current container resource limits

Phase 5 applied per-container memory and PID limits. The six long-running services have a combined memory ceiling of 2,944 MiB:

Service Memory limit PID limit
web 384 MiB 128
storefront 512 MiB 128
medusa 1,024 MiB 256
postgres 512 MiB 128
redis 256 MiB 64
garage 256 MiB 128

garage-init is bounded at 128 MiB and 64 PIDs. Redis retains noeviction and maxmemory=0; no CPU caps or arbitrary Node heap flags were added. The observed peaks, all-six healthy state, OOMKilled=false, and restart count 0 after recreation are a short verification window, not proof of peak workload headroom. Continue watching memory, OOM state, restarts, and host availability.

Journald limits

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

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

The size is a ceiling. Journald may retain less after whole archived files are vacuumed. On 2026-09-28 it occupied 595.2 MB before rotation/vacuum and 490.5 MB afterward. systemd-journald is active. Docker uses the json-file logging driver, so application/container logs are primarily under Docker rather than systemd-journald. Compose now applies one shared bounded policy to all seven services, including the one-shot garage-init: max-size: "10m" and max-file: "3". Existing containers were recreated with their exact running images to activate the policy; the four named persistent volumes were verified unchanged. The one-shot garage-init definition was validated but not rerun.

Logrotate had been failing because /etc/logrotate.d/cloud-init duplicated the package's obsolete stanza byte-for-byte with /etc/logrotate.d/cloud-init-base. The obsolete duplicate was removed; the active base configuration and Nginx rotation/ownership/reopen behavior were retained. A normal logrotate run exited successfully, and logrotate.timer is enabled and active on its daily schedule. At final verification, logrotate.service had exit status 0, and Nginx, Docker, and journald were active. A scrubbed 24-hour application-log audit found 58 Medusa lines and no Garage lines; it showed no current log-flood evidence. This is a point-in-time check, not a substitute for ongoing log monitoring.

12. Security and exposure

  • UFW allows OpenSSH, Nginx Full, and the separately configured SSH port 1022.
  • Only system Nginx listens publicly for VPS-hosted web traffic; GitLab Pages is the separate public edge for docs and journal.
  • 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.

The docs and journal have separate release paths. A commit to their GitLab projects starts a GitLab CI pipeline, which publishes a static Pages artifact. The docs project receives a deliberate exported snapshot from AndreaWeb; the Journal project owns its Astro source, Sveltia configuration, Markdown, and uploads. Neither Pages pipeline deploys a container or changes the VPS release pointer.

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/
curl --fail http://127.0.0.1:3100/guanza
curl --fail http://127.0.0.1:9000/health

External checks:

curl --fail https://andreazambrano.co/
curl --fail https://andreazambrano.co/guanza
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.

The production Compose project uses a shared json-file logging policy for every service, with a 10 MiB maximum per file and three files per container. Docker applies this policy to newly created containers; changing the Compose file alone does not alter an existing container. Use the guarded runtime-config target in the deployment runbook to apply this Compose-only policy without building or pulling images, running migrations, updating application deployment markers, or rerunning garage-init. It verifies the current application image references, container health, logging options, and named-volume identity after recreation.

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.

For a read-only host inventory, use the repository audit script. It reports disk and inode use, memory and active swap, AndreaWeb container health/resource/log settings, Docker disk and volume inventory, logrotate/journald state, backup age, and release-pointer validity:

ssh ovh 'sudo bash -s' < scripts/audit-production-host.sh

The script does not send alerts or modify the host. No on-VPS backup is a warning, not a failure, because off-server-only custody is intentional after the verified cutover backups were removed from the VPS. Recheck cloud-init logrotate files after package upgrades.

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. Two verified off-server cutover backup copies are retained; their temporary VPS copies were deleted only after verification. This does not provide scheduled or current database/media recovery coverage. 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 reviewed one-time image/release cleanup retained the active runtime configuration, deployed application source, backend rollback source, previous configuration, and known-good rollback images. Build cache was reduced to zero after idle/reclaimable verification. A recurring image/cache retention job remains unimplemented; future cleanup must preserve verified recovery and rollback targets.

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. Publishing baseline and future expansion

Portfolio and journal

The planned portfolio framework is Astro with typed content collections, static generation, Spanish-default/English routes, responsive galleries, and Markdown/MDX projects.

The separate journal baseline is now deployed at journal.andreazambrano.co. Astro validates the content model and generates the static site; Pagefind adds the build-time search index; GitLab CI builds it; and GitLab Pages serves it. Sveltia CMS is a static single-page editor at /admin/. After GitLab OAuth PKCE sign-in it uses the GitLab API to commit Markdown and uploaded media to the Journal repository, which triggers another Pages build. There is no Sveltia server, journal database, or Astro process running after the build.

The remaining work is editorial and operational: test the complete author workflow with Andrea, give her project-scoped Developer access after her GitLab account exists, replace sample content, decide the translation cadence, and establish image rights, optimization, backup, and retention practices. A later Astro portfolio can reuse the content-model and authoring lessons without coupling its deployment to the journal.

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. Schedule PostgreSQL and Garage off-VPS backups, set retention, and rehearse a full Garage restore; the two verified cutover copies are not a recurring backup system.
  2. Measure representative production peaks and revisit container memory/resource limits against that workload.
  3. Define and automate a bounded image/release/cache retention cadence while preserving verified recovery and rollback targets.
  4. Configure transactional email before relying on Admin UI invitation delivery.
  5. Restrict Admin exposure before broad public launch.
  6. Remove preview-only Nginx comments and deliberately decide canonical www behavior.
  7. Add external uptime, certificate, disk, memory, OOM, backup-age, and queue-backlog monitoring.
  8. Implement the staged CI/CD plan and branch protection.
  9. Resolve existing portfolio image lint warnings and optimize the portfolio image.
  10. Remove noindex only through an explicit public-launch checklist.
  11. Complete Andrea's Journal authoring acceptance test, then grant her only the project role required to publish through Sveltia CMS.
  12. Define Journal repository backup and uploaded-image optimization/retention practices before it holds irreplaceable editorial media.
  13. Implement the planned cross-property theme mapping only after the shared tokens and per-property overrides are approved.

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.