AndreaWeb architecture and operations overview¶
- Document date: 2026-08-30
- Last live verification: 2026-08-30, America/Bogota
- Production domain:
andreazambrano.co - Production host: OVH VPS
vps114094 - Repository: AndreaWeb monorepo; authoritative working copy in WSL at
/home/justin/repos/AndreaWeb
This is the architectural overview and operator's map for AndreaWeb. It describes the deployed system, repository tooling, data ownership, network and security boundaries, current capacity, ordinary maintenance, and planned expansion. Values marked current were measured or read from the VPS and repository on the verification date. Values marked planned are not yet deployed.
Never put passwords, API keys, invitation tokens, customer data, or the contents
of /opt/andreaweb/shared/production.env in this document, Git, CI logs, or issue
trackers.
1. System purpose and application boundaries¶
AndreaWeb presents related but independently evolvable sites under one domain:
- Portfolio: Andrea Carolina Zambrano Garcés's architecture portfolio, with Spanish as the default language and English routes.
- Cementos store: a Medusa DTC storefront for handmade cement objects, with products, variants, COP pricing, images, availability, and a cart.
- Commerce administration: Medusa Admin for products, variants, prices, media, inventory, users, and roles.
- Inquiry checkout: the cart currently becomes a WhatsApp, email, or copied inquiry rather than a processor-confirmed payment.
- Future publishing: an optional architecture/astronomy journal or blog, expected to be portfolio-owned rather than coupled to commerce.
The portfolio and storefront share branding and navigation intent, but neither should require the other to deploy. The portfolio must not depend on Medusa.
2. Production topology¶
flowchart TB
Visitor[Browser or Medusa Admin user]
DNS[Namecheap DNS]
TLS[System Nginx<br/>TLS and reverse proxy<br/>ports 80 and 443]
subgraph VPS[OVH VPS - Ubuntu 26.04]
direction TB
TLS
subgraph Edge[Docker edge network]
Portfolio[Portfolio container<br/>Vinext and React<br/>127.0.0.1:3000]
Storefront[Storefront container<br/>Next.js Medusa DTC<br/>127.0.0.1:3100]
Medusa[Medusa container<br/>API + Admin + worker<br/>127.0.0.1:9000]
Garage[Garage web endpoint<br/>127.0.0.1:3902]
end
subgraph Commerce[Docker internal commerce network]
Postgres[(PostgreSQL 17<br/>business source of truth)]
Redis[(Redis 7.4<br/>cache, BullMQ, locks)]
GarageData[(Garage S3 API<br/>product media)]
end
Storefront --> Medusa
Medusa --> Postgres
Medusa --> Redis
Medusa --> GarageData
Garage --- GarageData
end
Visitor --> DNS --> TLS
TLS -->|apex and www, /portfolio| Portfolio
TLS -->|apex and www, /cementos| Storefront
TLS -->|api subdomain| Medusa
TLS -->|admin subdomain| Medusa
TLS -->|media subdomain| Garage
System Nginx is installed directly on Ubuntu. It is deliberately not part of the Compose project. This lets it retain ports 80/443, Let's Encrypt integration, and unrelated future sites independently from AndreaWeb application releases.
3. Domains, routes, and ports¶
DNS¶
Namecheap currently delegates every public application hostname to the same OVH
VPS at 144.217.92.155:
| Record | Host | Value | Reason |
|---|---|---|---|
| A | @ |
144.217.92.155 |
apex portfolio and store |
| CNAME | www |
andreazambrano.co |
alternate website hostname |
| A | api |
144.217.92.155 |
Medusa API |
| A | admin |
144.217.92.155 |
Medusa Admin |
| A | media |
144.217.92.155 |
Garage-backed media |
DNS only selects the server. Nginx then selects the application by hostname and
path. A future domain change therefore affects DNS, the certificate, Nginx
server_name values, application origins/CORS, canonical URLs, sitemaps, email and
provider callback URLs; it should not require rewriting the applications.
HTTP routing¶
| Public address | Nginx upstream | Purpose |
|---|---|---|
https://andreazambrano.co/portfolio |
127.0.0.1:3000 |
Spanish portfolio |
https://andreazambrano.co/en/portfolio |
127.0.0.1:3000 |
English portfolio |
https://andreazambrano.co/cementos |
127.0.0.1:3100 |
Store entry; storefront resolves the country route |
https://andreazambrano.co/cementos/co |
127.0.0.1:3100 |
Colombia/COP storefront |
https://www.andreazambrano.co/... |
same apex upstreams | Alternate hostname; currently remains www after HTTPS redirect |
https://api.andreazambrano.co |
127.0.0.1:9000 |
Medusa Store/Admin APIs and health endpoint |
https://admin.andreazambrano.co/app |
127.0.0.1:9000 |
Medusa Admin SPA served by Medusa |
https://media.andreazambrano.co |
127.0.0.1:3902 |
Public Garage product media |
Only Nginx is publicly exposed. Application ports bind to 127.0.0.1; PostgreSQL,
Redis, and the Garage S3 API have no host port. Garage's public web endpoint uses
the internal host alias andreaweb-media.web.garage.localhost at the proxy layer.
Current request-size limits are 10 MB for apex/portfolio/storefront requests and
25 MB for API/Admin requests. Media responses receive a one-year immutable cache
header. Store, API, Admin, media, and robots responses remain blocked from indexing
with robots.txt and X-Robots-Tag during private review.
The Let's Encrypt certificate covers the apex, www, api, admin, and media.
Certbot owns renewal and the TLS directives it added to the live Nginx site.
Nginx configuration ownership¶
The live site is /etc/nginx/sites-available/andreaweb, enabled through Ubuntu's
sites-enabled convention. The repository example is
deploy/nginx-vps.conf.example; it is a review/reference input, not a file that may
blindly overwrite Certbot-modified production configuration. Nginx forwards the
original host/protocol and client-forwarding headers, preserves streaming behavior
where required, and routes only to loopback application ports.
Certificate material is owned by Certbot under
/etc/letsencrypt/live/andreazambrano.co/ and must never be copied into Git or the
Docker images. The live certificate and renewal configuration cover five names. Any
new subdomain must have DNS, an Nginx server block, firewall-safe routing, and
certificate coverage reviewed together.
4. Containers and images¶
An image is an immutable application/runtime template. A container is a running instance of an image plus runtime configuration, network membership, and volume mounts. Replacing a stateless application container does not replace its database or object-storage volumes.
| Compose service | Current image | Image disk snapshot | Function | Network | Persistence | Current memory snapshot |
|---|---|---|---|---|---|---|
web |
andreaweb-web:latest |
1.26 GB | Portfolio server | edge |
none | 114 MiB |
storefront |
andreaweb-storefront:latest |
693 MB | Next.js DTC storefront and inquiry BFF endpoints | edge, commerce |
none | 97.1 MiB |
medusa |
andreaweb-medusa:latest |
1.34 GB | Medusa API, Admin bundle, workflows, events, shared worker | edge, commerce |
PostgreSQL/Redis/Garage externally | 290.4 MiB |
postgres |
postgres:17-alpine |
297 MB | Commerce relational database | commerce only |
andreaweb_postgres_data |
61.1 MiB |
redis |
redis:7.4-alpine |
39.1 MB | Cache, BullMQ queues, workflow coordination, locks | commerce only |
andreaweb_redis_data |
12.7 MiB |
garage |
dxflrs/garage:v2.3.0 |
71.3 MB | S3-compatible media storage and public media server | edge, commerce |
andreaweb_garage_data, andreaweb_garage_meta |
21.0 MiB |
garage-init |
dxflrs/garage:v2.3.0 |
shared | One-shot bucket website initialization | commerce only |
reads Garage metadata | not normally running |
The six running containers used approximately 596 MiB in the measured idle snapshot. This is not a peak or capacity guarantee. Container CPU was near zero except for a momentary Garage sample; request load, image processing, migrations, builds, and Node garbage collection can increase both CPU and memory.
Image rows show Docker's per-image virtual size; shared layers mean they must not be
summed to estimate disk use. docker system df reported 3.322 GB for the six
active images at the same snapshot.
All long-running services use restart: unless-stopped and health checks. The
portfolio filesystem is read-only with bounded temporary filesystems. Portfolio,
storefront, and Medusa drop Linux capabilities and enable no-new-privileges.
Image construction¶
- All application images use multi-stage
node:22-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.2, React 19.2.8, Medusa JS SDK 2.19.0, Tailwind 3 |
Derived from the official Medusa DTC Next.js starter; mounted at /cementos |
| Backend/Admin | Medusa 2.19.0, built-in Admin dashboard, Node 22 |
Shared server/worker mode for the MVP |
| Database | PostgreSQL 17 | Authoritative commerce and identity data |
| Cache/queues | Redis 7.4 + BullMQ through Medusa providers | Cache, events, workflows, timers, cleanup, and locks |
| Media | Garage 2.3.0 through Medusa S3 File Module | Single-node, replication factor 1 |
Important pinned application packages include:
| Area | Package versions |
|---|---|
| Portfolio build | Vite 8.0.13, TypeScript 5.9.3, ESLint 9.39.4, Wrangler 4.92.0 |
| Storefront build | TypeScript 5.9.3, ESLint 9.39.5, PostCSS 8.5.26, Prettier 2.8.8 |
| Storefront UI/data | Medusa icons/SDK/UI preset 2.19.0, Headless UI 2.2.9 |
| Medusa extensions | Redis caching/locking 2.19.0, S3 File Module 2.19.0, Admin dashboard 2.19.0 |
| Test/runtime utilities | Jest 29.7.0, tsx 4.22.1, PostgreSQL client 8.16.3 |
The root package requires Node >=22.13.0; the backend supports Node ^20.19.0
or >=22.12.0. Application Dockerfiles intentionally standardize production on
Node 22 Alpine. The lockfile is npm lockfile format 3, and the backend pins npm
11.17.0 as its package manager.
Verified production host/tool versions¶
| Component | Version verified 2026-08-30 |
|---|---|
| Ubuntu | 26.04 LTS |
| Linux kernel | 7.0.0-29-generic |
| Docker Engine | 24.0.2 |
| Docker Compose plugin | 2.18.1 |
| Nginx | 1.28.3 (Ubuntu package) |
| Certbot | 4.0.0 |
| OpenSSH server/client | 10.2p1 (Ubuntu package) |
| OpenSSL | 3.5.5 |
| Node inside application containers | 22.23.2 |
| npm inside application containers | 11.17.0 |
| PostgreSQL container | 17.11 |
| Redis container | 7.4.11 |
| Garage image | 2.3.0 |
| Git on VPS | 2.53.0 |
| Python on VPS | 3.14.4 |
Patch versions will move over time. Treat the Compose image tags, package lockfile, and a fresh runtime query as authoritative rather than copying this snapshot into a deployment decision.
Vinext is not the foundation of the commerce storefront. It is used by the current portfolio. The commerce application is Next.js. The planned portfolio migration is to Astro, independently of commerce.
6. Data ownership and persistence¶
| Data | Authoritative owner | Volume/path | Rebuildability |
|---|---|---|---|
| Products, variants, prices, inventory | PostgreSQL | andreaweb_postgres_data |
not safely rebuildable after Admin edits |
| Users, invitations, roles, policies | PostgreSQL | andreaweb_postgres_data |
not safely rebuildable |
| Carts, customers, orders | PostgreSQL | andreaweb_postgres_data |
not safely rebuildable |
| Product images | Garage | andreaweb_garage_data and metadata volume |
not safely rebuildable after uploads |
| Cache entries | Redis | andreaweb_redis_data |
derived and rebuildable |
| BullMQ events/workflow jobs/locks | Redis | andreaweb_redis_data |
operational state; loss may lose in-flight work |
| Private production configuration | operator-managed file | /opt/andreaweb/shared/production.env |
secrets must be backed up securely, never committed |
| Application source releases | Git archive releases | /opt/andreaweb/releases/<commit-prefix> |
rebuildable from Git |
| Current release pointer | symlink | /opt/andreaweb/current |
rebuildable |
The Compose project name is andreaweb. Ordinary builds, targeted deployments,
container recreation, and docker compose down preserve named volumes. Never use
down --volumes against persistent production unless deliberately destroying all
production state from a verified backup.
At verification, active Docker volumes totaled about 98.5 MB. The complete
/opt/andreaweb release/configuration tree used about 55 MB. These are small
because the catalog is currently small; growth alerts and off-VPS backups are still
required before treating the site as durable production.
7. PostgreSQL, Redis, BullMQ, and consistency¶
PostgreSQL is the source of truth. Redis does not contain a second authoritative copy of products, inventory, carts, or orders. Redis may contain derived cache entries and temporary operational payloads that refer to database entities.
sequenceDiagram
actor Customer
participant SF as Storefront
participant M as Medusa workflow
participant PG as PostgreSQL
participant R as Redis / BullMQ
participant S as Event subscriber
Customer->>SF: Add item or submit operation
SF->>M: API request
M->>R: Acquire coordination lock if required
M->>PG: Transactionally read/write business entities
PG-->>M: Committed authoritative state
M->>R: Enqueue event, retry, timeout, or follow-up
M-->>SF: API result
R-->>S: Worker delivers queued event
S-->>R: Acknowledge or retry
Redis responsibilities¶
- 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-08-30¶
| Resource | Current value |
|---|---|
| vCPU | 1 |
| RAM | 3.7 GiB |
| Swap | none |
| Root filesystem | 39 GB |
| Root used/free | 14 GB used, 26 GB available, 36% used |
| Host memory snapshot | 1.4 GiB used, 2.4 GiB available |
| Application image storage | 3.322 GB across six active images |
| Active persistent volumes | 98.5 MB |
| Docker build cache | 0 after cleanup |
| Journal storage | 257 MB after retention cleanup |
The host has only one vCPU and no swap. Concurrent image builds, migrations, backup compression, and production traffic can therefore contend. Build cache and a small number of rollback images may be retained under a disk budget, but unused customer projects, anonymous volumes, and unbounded build cache must not accumulate again.
Current hard limits¶
The Compose file does not currently impose per-container memory or CPU limits; Docker reports the host's full 3.726 GiB as each container's limit. The application therefore relies on host capacity, Linux scheduling, and service behavior. This is a known hardening gap, not an assertion that every service may safely consume the entire host.
Before adding limits, measure peak Admin uploads, catalog operations, migrations, builds, and storefront load. A limit that is too low can create avoidable OOM kills; no limit allows one process to starve the VPS. Container restart/OOM counts and host available memory must be monitored. Adding a modest swap file may improve survival during brief spikes but is not a substitute for memory sizing.
Journald limits¶
The live drop-in /etc/systemd/journald.conf.d/andreaweb-retention.conf contains:
The size is a ceiling. Journald may retain less after whole archived files are
vacuumed. Docker uses the json-file logging driver, so application/container logs
are primarily under Docker rather than systemd-journald. Docker log rotation is not
yet explicitly configured in Compose and should be added with bounded file size and
count.
12. Security and exposure¶
- UFW allows OpenSSH, Nginx Full, and the separately configured SSH port 1022.
- Only system Nginx listens publicly for web traffic.
- The
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.
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/portfolio
curl --fail http://127.0.0.1:3100/cementos/co
curl --fail http://127.0.0.1:9000/health
External checks:
curl --fail https://andreazambrano.co/portfolio
curl --fail https://andreazambrano.co/cementos/co
curl --fail https://api.andreazambrano.co/health
For an emergency same-image restart on the VPS, use Compose directly. restart
does not rebuild an image, run migrations, or reload changed environment values:
docker compose \
--env-file /opt/andreaweb/shared/production.env \
-f compose.prod.yaml restart storefront
For a changed server-side environment value, use operate-ovh.sh recreate from
the workstation instead; container recreation is what applies the new environment.
Logs¶
Application logs on the VPS:
cd /opt/andreaweb/current
docker compose \
--env-file /opt/andreaweb/shared/production.env \
-f compose.prod.yaml logs --tail=200 web storefront medusa
docker compose \
--env-file /opt/andreaweb/shared/production.env \
-f compose.prod.yaml logs --since=30m --follow medusa
docker compose \
--env-file /opt/andreaweb/shared/production.env \
-f compose.prod.yaml logs --tail=200 postgres redis garage
Host and proxy logs:
sudo journalctl -u docker --since "1 hour ago"
sudo journalctl -u nginx --since "1 hour ago"
sudo journalctl -p warning..alert --since today
sudo tail -n 200 /var/log/nginx/access.log
sudo tail -n 200 /var/log/nginx/error.log
Do not paste complete production logs into public systems without checking for IP addresses, email addresses, URLs, tokens, or customer content.
Resource and disk inspection¶
docker stats --no-stream
docker system df -v
docker ps -a
docker volume ls
free -h
df -h /
du -sh /opt/andreaweb /var/log/journal
sudo journalctl --disk-usage
Service-level, read-only diagnostics:
docker inspect --format '{{.Name}} oom={{.State.OOMKilled}} restarts={{.RestartCount}}' \
andreaweb-web-1 andreaweb-storefront-1 andreaweb-medusa-1
docker compose \
--env-file /opt/andreaweb/shared/production.env \
-f compose.prod.yaml exec redis redis-cli INFO memory
docker compose \
--env-file /opt/andreaweb/shared/production.env \
-f compose.prod.yaml exec redis redis-cli INFO persistence
docker compose \
--env-file /opt/andreaweb/shared/production.env \
-f compose.prod.yaml exec postgres sh -lc \
'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c \
"select pg_size_pretty(pg_database_size(current_database()));"'
Use Redis SCAN, not blocking KEYS, if queue-key inspection is ever necessary on
a nontrivial dataset. Prefer Medusa/BullMQ-aware metrics and supported application
tools to manually mutating queue keys. Database reports and cleanup scripts must be
versioned, read-only/dry-run by default, and follow the safety rules in the
observability document.
Docker pruning has no dry-run and can erase rollback images, build acceleration, stopped diagnostics, and unused volume data. Use an explicit retention policy and inspect targets before pruning. Never automate production volume deletion.
Nginx and certificates¶
sudo nginx -t
sudo systemctl reload nginx
sudo certbot certificates
sudo certbot renew --dry-run
systemctl status certbot.timer
Always edit the AndreaWeb site additively and run nginx -t before reload. Do not
replace unrelated site files. The live Nginx file still begins with a stale
"Disposable preview" comment even though it now routes the persistent stack; the
comment should be corrected during the next reviewed Nginx maintenance.
15. Local development¶
Local work runs from WSL and Docker Desktop. The local Compose topology exercises the same classes of PostgreSQL, Redis, Garage, Medusa, storefront, and portfolio services. Local data and invitations are independent of VPS production state.
Local Compose additionally runs nginx:1.27-alpine on host port 8080 to rehearse
path routing without changing Windows/WSL system Nginx. This local proxy does not
exist in production, where Ubuntu's system Nginx owns ports 80/443. Medusa exposes
loopback port 9000 and its development Admin/Vite port 5173; the storefront exposes
loopback port 3100 and Garage web media exposes loopback port 3902.
Local named volumes backend_node_modules, storefront_node_modules, and
storefront_next_cache keep Linux container dependencies/build cache separate from
the WSL bind-mounted source tree. They prevent a host install from replacing native
container dependencies and make repeated local starts faster. They are disposable
development state, unlike the PostgreSQL/Garage production data. Production images
contain their installed dependencies and do not mount these dependency volumes.
Common commands are documented in docs/local-operations.md. The root scripts run
linting, tests, builds, environment synchronization, local health checks, and media
persistence verification. Production changes must come from committed releases,
not copied local build artifacts or Windows-specific scripts.
Developer environment inventory¶
The supported interactive development shell is zsh in Ubuntu WSL. Justin's
~/.zshrc loads the shared dev-environment setup, fnm, Windows interoperability
wrappers, and the SSH agent/key bridge. The ovh SSH config alias selects the VPS
and the accepted justin-omen ED25519 key; deploy scripts use that alias rather than
embedding a host, username, password, or private-key path.
| Interactive WSL tool | Version verified 2026-08-30 | Source/role |
|---|---|---|
| zsh | system /usr/bin/zsh |
supported project shell |
| fnm | 1.39.0 |
selects Node per interactive shell |
| Node | 24.19.0 |
developer CLI; production remains Node 22 |
| npm / npx | 11.17.0 |
workspace install/scripts and package executables |
| Python | 3.14.4 |
supporting local tooling; not an app runtime |
| Git | 2.53.0 |
source/release identity |
| GitHub CLI | 2.97.0 |
authenticated as masojus; Git transport uses SSH |
| SSH bridge | Windows OpenSSH 9.5p2 |
WSL function uses the shared Windows agent/key |
| Docker Desktop engine | 29.7.2 |
local Linux containers |
| Docker Compose | 5.3.1 |
local topology |
| Azure CLI | 2.89.1 |
general dev environment; not required by AndreaWeb |
An interactive zsh session must be used when relying on fnm; a minimal
non-interactive shell may not source the same initialization. Verify before operating:
type fnm node npm npx gh ssh
fnm --version
node --version
npm --version
npx --version
gh auth status
ssh-add -l
The developer Node version may be newer than production because the repository's declared engine range permits it. Container builds remain the production compatibility authority. npm and npx are paired entry points from the same npm installation; npx runs package-provided executables and is not a separate runtime.
16. Backup, recovery, and rollback¶
Current named volumes survive container replacement, but persistence is not backup. Before public launch the system still requires:
- scheduled PostgreSQL logical/physical backup with retention;
- Garage data and metadata backup;
- encrypted off-VPS copies;
- production environment secret recovery procedure;
- automated backup-result reporting;
- regular restore into a disposable environment;
- disk-capacity and backup-age alerts.
Application rollback can target a prior commit-tagged image/release. Database schema rollback is separate and must never be inferred from application rollback. Before a migration, confirm backup and compatibility with the known-good application version.
The desired image/cache retention policy is to keep a small number of known-good commit images plus a bounded recent build cache. This policy is planned but not yet automated.
17. Observability and CI/CD direction¶
The immediate operational baseline is health checks, Compose status/logs, bounded system journal, host/Docker resource inspection, deployment commit markers, external uptime, certificate monitoring, backup verification, and disk alerts.
OpenTelemetry-compatible service/resource naming should be designed from the start. The small VPS should not receive a full Grafana/Loki/Tempo stack without measured capacity. A resource-capped collector and an external or carefully sized backend are future options.
The planned CI/CD pipeline will add pull-request EOL/policy checks, lint and type checks, tests, production builds, Compose/container validation, disposable migration rehearsal, dependency/image scanning, immutable artifacts, protected production approval, deployment events, and smoke tests. Current known portfolio lint warnings must not grow; the target is zero warnings after focused cleanup.
See docs/ci-cd-plan.md and docs/observability-operations-and-growth.md.
18. Future expansion¶
Portfolio and blog¶
The planned portfolio framework is Astro with typed content collections, static generation, Spanish-default/English routes, responsive galleries, and Markdown/MDX projects. An architecture or astronomy journal can use Astro content collections for articles, tags, archives, RSS, search, and navigation. It should remain independently deployable and can later adopt a Git-backed editor or headless CMS if Andrea finds Markdown authoring unsuitable.
An entirely separate blog sub-application remains possible behind Nginx without changing the store decision. Hugo is a reasonable alternative for a fully separate Go-based Markdown site but is not the default.
Commerce¶
Likely additions include color variants with independent inventory, Medusa price lists and sale presentation, manual inquiry/order conversion, inventory reservations, business WhatsApp, transactional email, support/contact pages, a real payment provider appropriate for Colombia, tip/support links, and provider-confirmed thank-you pages. Detailed payment and communications design remains in Future commerce, payments, and communications.
Growth and privacy¶
Analytics, advertising, customer outreach, loyalty, and a client database require purpose limitation, retention rules, access controls, consent handling, and deletion workflows. A cookie banner is required when nonessential storage begins; a CMP may be appropriate if advertising or EEA requirements justify it. AdSense belongs only on future editorial content, not the portfolio or checkout journey by default.
19. Known gaps and next architectural actions¶
- Add PostgreSQL and Garage off-VPS backup/restore automation and test it.
- Add explicit Docker
json-filelog rotation. - Measure production peaks and add reviewed container memory/resource controls.
- Consider a small swap file after evaluating latency and OOM behavior.
- Deploy the commit-tagged targeted release tooling and implement image/cache retention by count, age, or disk budget.
- 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.
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 — blog, 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
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.