Skip to main content

Documentation 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.

Stay Sidekick is composed of five Docker services that communicate exclusively over an internal bridge network called 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

ServiceImage / BuildHost PortInternal PortRole
nginx./nginx (nginx:alpine)8080Reverse proxy — routes /api/*, /menu/*, /* and applies security headers
frontend./frontend80Angular 21 SPA served by an internal nginx instance
web./web (build context: project root)8011ty static site served by an internal nginx instance
backend./backend5000Flask REST API served by Gunicorn
postgrespostgres:16-alpine5432PostgreSQL 16 with a persistent postgres_data volume and automatic schema seed

Docker Network Topology

All five services share the app-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.
  1. 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.
  2. nginx → upstream service: nginx inspects the URL path prefix and proxies the request to the appropriate upstream:
    • /api/* → Flask backend on backend:5000
    • /menu/* → Angular SPA container on frontend:80
    • /* → 11ty static site container on web:80
  3. Backend → PostgreSQL: For authenticated API requests, the Flask backend queries PostgreSQL on postgres:5432 over the internal app-net network using the DATABASE_URL connection string.
  4. Response path: PostgreSQL returns results to Flask, which serialises them to JSON and returns the response upstream through Gunicorn → nginx → client.
Static routes (/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:
PrefixUpstreamDescription
/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 both nginx.conf (local) and nginx.railway.conf (production) and apply to every route:
HeaderPurpose
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: SAMEORIGINPrevents the app from being embedded in third-party iframes
X-Content-Type-Options: nosniffStops browsers from MIME-sniffing responses
Referrer-Policy: strict-origin-when-cross-originControls how much referrer information is included in requests
You can verify that these headers are being applied correctly by running curl -I http://localhost/ after starting the stack. In production, replace http://localhost with your Railway public domain.

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.internal private network. For example, the backend reaches PostgreSQL via the DATABASE_URL injected by the Railway Postgres plugin (${{ Postgres.DATABASE_URL }}), and nginx proxies to frontend.railway.internal, web.railway.internal, and backend.railway.internal.
  • nginx is the only public service: Only the nginx service has a Railway public domain assigned. All other services are reachable only within the private network — identical behaviour to the local app-net bridge.
  • 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=true build argument switches nginx to use nginx.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.
Internet (HTTPS)

   [nginx]  ← only service with a public Railway domain
      │ .railway.internal private network
      ├── [frontend]  Angular SPA      :80
      ├── [web]       11ty static      :80
      └── [backend]   Flask API        :5000

                      [PostgreSQL]  ← Railway Database Plugin
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.

Build docs developers (and LLMs) love