Stay Sidekick is composed of five Docker services that communicate exclusively over an internal bridge network calledDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/sdurutr436/stay-sidekick/llms.txt
Use this file to discover all available pages before exploring further.
app-net. Only one service — nginx — exposes a port to the host (port 80), acting as the single entry point for all HTTP traffic. The other four services (frontend, web, backend, postgres) are invisible to the outside world and are reachable only by name within app-net. This design keeps the attack surface minimal and makes it straightforward to replicate the same topology in production.
In local development, all traffic is served over plain HTTP on port 80. TLS is not handled by the nginx container — it is terminated at Railway’s edge in production. See the Railway Production section below for details.
Service Overview
| Service | Image / Build | Host Port | Internal Port | Role |
|---|---|---|---|---|
nginx | ./nginx (nginx:alpine) | 80 | 80 | Reverse proxy — routes /api/*, /menu/*, /* and applies security headers |
frontend | ./frontend | — | 80 | Angular 21 SPA served by an internal nginx instance |
web | ./web (build context: project root) | — | 80 | 11ty static site served by an internal nginx instance |
backend | ./backend | — | 5000 | Flask REST API served by Gunicorn |
postgres | postgres:16-alpine | — | 5432 | PostgreSQL 16 with a persistent postgres_data volume and automatic schema seed |
Docker Network Topology
All five services share theapp-net bridge network. The diagram below shows how traffic moves from an HTTP client through to the database:
Request Flow
Understanding how a request travels through the stack makes it easier to debug issues and reason about latency or failures at each layer.- Client → nginx (port 80): The browser or API client sends an HTTP request to
localhost:80. nginx receives it as the sole externally exposed service. - nginx → upstream service: nginx inspects the URL path prefix and proxies the request to the appropriate upstream:
/api/*→ Flask backend onbackend:5000/menu/*→ Angular SPA container onfrontend:80/*→ 11ty static site container onweb:80
- Backend → PostgreSQL: For authenticated API requests, the Flask backend queries PostgreSQL on
postgres:5432over the internalapp-netnetwork using theDATABASE_URLconnection string. - Response path: PostgreSQL returns results to Flask, which serialises them to JSON and returns the response upstream through Gunicorn → nginx → client.
/menu/* and /*) never touch the backend or database; nginx proxies them directly to the pre-built static asset containers.
Route Prefixes
nginx uses three route prefix rules to distribute traffic across the stack:| Prefix | Upstream | Description |
|---|---|---|
/api/* | backend:5000 (Flask + Gunicorn) | All REST API calls — authentication, data, integrations |
/menu/* | frontend:80 (Angular SPA) | The private operational panel — requires a valid JWT |
/* | web:80 (11ty static) | Public landing page, product pages, and legal content |
The Angular SPA handles its own client-side routing under
/menu/. Any path under that prefix that is not a static asset is served the index.html shell, and Angular’s router takes over from there.Nginx Security Headers
All HTTP responses — regardless of which upstream served them — pass back through the main nginx container, which injects a set of security headers before returning the response to the client. These headers are defined in bothnginx.conf (local) and nginx.railway.conf (production) and apply to every route:
| Header | Purpose |
|---|---|
Strict-Transport-Security (HSTS) | Instructs browsers to use HTTPS only after first contact |
Content-Security-Policy (CSP) | Restricts resource origins to prevent XSS and data injection |
X-Frame-Options: SAMEORIGIN | Prevents the app from being embedded in third-party iframes |
X-Content-Type-Options: nosniff | Stops browsers from MIME-sniffing responses |
Referrer-Policy: strict-origin-when-cross-origin | Controls how much referrer information is included in requests |
Railway Production
In production, Stay Sidekick runs on Railway with the same five-service topology. The key differences from the local Docker Compose setup are:- Internal hostnames: Services communicate using Railway’s
.railway.internalprivate network. For example, the backend reaches PostgreSQL via theDATABASE_URLinjected by the Railway Postgres plugin (${{ Postgres.DATABASE_URL }}), and nginx proxies tofrontend.railway.internal,web.railway.internal, andbackend.railway.internal. - nginx is the only public service: Only the
nginxservice has a Railway public domain assigned. All other services are reachable only within the private network — identical behaviour to the localapp-netbridge. - TLS terminated at Railway’s edge: Railway handles certificate provisioning, renewal, and HTTPS redirection automatically. The nginx container itself listens only on port 80 and does not manage any TLS certificates. The
RAILWAY=truebuild argument switches nginx to usenginx.railway.conf, which resolves internal hostnames with a low DNS TTL to avoid stale IPs after redeployments. - CI/CD gate: Each service is configured with Wait for CI in Railway so that a failing GitHub Actions pipeline blocks a deployment automatically.
Cloudflare is not used as a CDN or TLS proxy in this project. The only Cloudflare integration is Turnstile — the widget used for anti-spam protection on the public contact and company request forms.

