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
vps114094and 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.cowith 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-alpineDockerfiles and run as the unprivilegednodeuser. - 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-assetsfor 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¶
- 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.
- Event bus: Medusa's official Redis event-bus provider uses BullMQ
QueueandWorkerobjects. Its custom prefix produces keys such asRedisEventBusService:events-queue:*. - Workflow engine: BullMQ queues named
medusa-workflows,medusa-workflows-jobs, andworkflows-cleanerhandle retries, timeouts, scheduled work, and retention cleanup. - 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:
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
docsandjournal. - The
commerceDocker network is internal; theedgenetwork connects proxy-facing services without exposing their container ports publicly. - Production secrets are mode
0600outside 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
noindexbehavior 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:
Plan and apply releases¶
Run the exact command printed by the plan, for example:
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:
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:
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:
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¶
- 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.
- Measure representative production peaks and revisit container memory/resource limits against that workload.
- Define and automate a bounded image/release/cache retention cadence while preserving verified recovery and rollback targets.
- Configure transactional email before relying on Admin UI invitation delivery.
- Restrict Admin exposure before broad public launch.
- Remove preview-only Nginx comments and deliberately decide canonical
wwwbehavior. - Add external uptime, certificate, disk, memory, OOM, backup-age, and queue-backlog monitoring.
- Implement the staged CI/CD plan and branch protection.
- Resolve existing portfolio image lint warnings and optimize the portfolio image.
- Remove
noindexonly through an explicit public-launch checklist. - Complete Andrea's Journal authoring acceptance test, then grant her only the project role required to publish through Sveltia CMS.
- Define Journal repository backup and uploaded-image optimization/retention practices before it holds irreplaceable editorial media.
- Implement the planned cross-property theme mapping only after the shared tokens and per-property overrides are approved.
20. Related documentation¶
- Documentation index — entry point and document ownership
- Roadmap — phased product and infrastructure roadmap
- OVH deployment — deployment and persistent cutover runbook
- Local operations — local WSL/Docker operations
- CI/CD plan — quality and delivery plan
- Observability, operations, and growth — telemetry, reporting, maintenance, client relationships, and growth
- Future commerce and communications — payments, messaging, email, and commerce evolution
- Publishing, analytics, and advertising — current Astro/Sveltia journal, analytics, consent, and ads
- SEO and public indexing — crawlability, bilingual search, structured data, and ongoing search operations
- ADR 0001 — foundation decision
- ADR 0002 — Garage/media decision
- ADR 0003 — resource and retention policy
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.