Skip to content

Dev environment (Railway)

DEV has completed its migration from Hostinger/Dokploy to Railway. Cloudflare remains the DNS edge and Cloudflare R2 bucket uploads-dev remains the object store. Hostnames follow the hostname scheme: DEV is the zone dev.useast.kottia.com, with api., assets. and docs. under it. Staging and production remain planned for GCP us-central1 and are not provisioned; see Production landscape.

Cloudflare DNS (zone kottia.com)
dev.useast.kottia.com → svelte-web
api.dev.useast.kottia.com → api
assets.dev.useast.kottia.com → imgproxy
docs.dev.useast.kottia.com → docs (reserved; DEV only, Cloudflare Access first)
bull.dev.useast.kottia.com → Bull Board (optional; Basic auth required)
Railway kottia.com / development
GitHub: svelte-web · api · worker · docs
Images: imgproxy · postgres · valkey · meilisearch
Private DNS: <service>.railway.internal
Off-platform: Cloudflare R2 uploads-dev · Resend · Google · Knock · GetStream ·
Sentry · PostHog · Replicate · Gemini · Stripe · FacturAPI

PostgreSQL, Valkey, and Meilisearch have no public domains. They are reachable only through Railway private networking.

The four app services build from real-estate-core/real-estate-core-mono, branch development. Their Docker builds use the repository root as context and retain the existing Dockerfiles.

ServiceDockerfile / imageDomainWatch paths
svelte-webapps/svelte-web/Dockerfiledev.useast.kottia.comapps/svelte-web/**, packages/**, package.json, bun.lock
apiapps/api/Dockerfileapi.dev.useast.kottia.comapps/api/**, packages/**, package.json, bun.lock
workerapps/worker/Dockerfilenone by defaultapps/worker/**, packages/**, package.json, bun.lock
docsapps/docs/Dockerfilenone yet — docs.dev.useast.kottia.com reserved, Access-gatedapps/docs/**, packages/**, package.json, bun.lock
imgproxyghcr.io/imgproxy/imgproxy:v3.30.1assets.dev.useast.kottia.com—
postgrespostgis/postgis:16-3.5private—
valkeyvalkey/valkey:8private—
meilisearchgetmeili/meilisearch:v1.9.0private—

Persistent volumes mount at:

  • PostgreSQL: /var/lib/postgresql/data
  • Valkey: /data
  • Meilisearch: /meili_data

Railway deprecated the migration’s earlier per-service railway.json Config as Code. The current supported repository configuration is project-wide .railway/railway.ts, using the railway TypeScript SDK. Do not document or add railway.json as current configuration.

From the repository root:

Terminal window
railway config pull
railway config plan
# Review every proposed service, domain, variable, and volume change.
railway config apply

Apply only an explicitly reviewed plan. Values represented by preserve() remain managed in Railway rather than stored in source.

Use Railway service references for non-secret dependencies; do not duplicate resolved credentials across services. Consumers resolve the data services through:

postgres.railway.internal
valkey.railway.internal
meilisearch.railway.internal

The Valkey service enables requirepass, owns the credentialed VALKEY_URL, and exports that URL to web, API, and worker through ${{valkey.VALKEY_URL}}. Generate VALKEY_PASSWORD with openssl rand -hex 32; the value must remain URL-safe and shell-safe because it is embedded in the URL and passed through VALKEY_EXTRA_FLAGS.

DEV’s required shared settings include:

Terminal window
QUEUE_SUFFIX=-dev
COOKIE_DOMAIN=.dev.useast.kottia.com
ORIGIN=https://dev.useast.kottia.com

The API uses:

Terminal window
CORS_ORIGINS=https://dev.useast.kottia.com
BETTER_AUTH_URL_API=https://api.dev.useast.kottia.com

TRUSTED_PROXY_CIDRS=100.64.0.0/10 is currently configured as Railway’s internal proxy range. That is not yet a permanent assumption: verify the socket peer seen by web and API and prove that forged forwarding headers sent directly to each Railway-provided domain cannot alter client-IP resolution.

The existing Cloudflare R2 wiring remains:

Terminal window
AWS_S3_BUCKET_NAME=uploads-dev
AWS_DEFAULT_REGION=auto
IMGPROXY_URL=https://assets.dev.useast.kottia.com

Keep the existing R2 endpoint and credentials plus the matching IMGPROXY_KEY / IMGPROXY_SALT. The worker also reads the existing search-seeds/*.ndjson.gz artifacts from R2.

assets.dev.useast is a DNS prerequisite, not decoration: it must be a Cloudflare CNAME (zone kottia.com) to the *.up.railway.app target Railway shows for the domain on the imgproxy service, with the TXT ownership record Railway lists next to it. The DEV/STG hosts are two or three labels below the apex, which Cloudflare’s free Universal SSL does not cover — keep them DNS-only (Railway’s Let’s Encrypt cert terminates TLS) unless the zone has Advanced Certificate Manager, in which case proxying requires SSL/TLS mode Full. The record was never recreated when imgproxy moved off Dokploy, and every image on DEV was broken for weeks (PRO-18). When it is missing or unverified, both /api/images/* proxies answer 502 image_service_unavailable and log [images] imgproxy upstream unreachable, while avatars and team logos — loaded from signed imgproxy URLs directly in the browser — just fail to render. Confirm with dig +short assets.dev.useast.kottia.com and curl https://assets.dev.useast.kottia.com/health.

Set PUBLIC_API_URL=https://api.dev.useast.kottia.com on svelte-web. Railway passes service variables into Docker builds for matching declared ARGs, so the value reaches the existing Dockerfile as a build argument. It is baked into the browser bundle; rebuild svelte-web after changing it. A runtime-only update cannot repair an already-built bundle.

Integration secrets were manually copied from Dokploy to the corresponding Railway services and remain out of source. PII_ENCRYPTION_KEY must be copied verbatim: it decrypts persisted fiscal PII and cannot be re-issued. An escrowed copy must exist outside both platforms, and the owner must verify that escrow before the Hostinger VPS is cancelled. No escrow location is asserted here.

Railway PostgreSQL is fresh: run migrations only, with no Dokploy data restore. Open a shell in the deployed worker or API container:

Terminal window
cd /app/packages/database && bun run scripts/migrate.ts

Kysely tracks applied migrations, so the command is idempotent.

Meilisearch is also fresh. The worker restores its location and service-area indexes at startup from the existing R2 search-seeds/*.ndjson.gz artifacts. Do not delete that prefix during DEV cleanup.

Railway’s GitHub integration auto-deploys development. Each app watches its app path, packages/**, and the root package.json and bun.lock; GitHub Actions remain checks only.

The docs service is DEV-only and its hostname docs.dev.useast.kottia.com is reserved, not yet declared. Before exposing it, configure an enforceable Cloudflare Access/SSO policy (which requires the record to be proxied — see the TLS note above). An anonymous request must return 401/403 or a 30x whose Location points to the configured Cloudflare Access login host; reject any 200 response containing the docs body or any response carrying X-Railway-Edge, because either proves the request reached Railway without Access. Only then add the domain to Railway and .railway/railway.ts.

When removing docs exposure, delete or park the Cloudflare docs.dev.useast DNS record first and verify the hostname no longer resolves. Only then delete the Railway custom domain; this prevents a dangling DNS record from becoming a subdomain-takeover surface.

  • Web loads and hydrates at dev.useast.kottia.com.
  • Email OTP and Google OAuth work.
  • Browser tRPC returns JSON from api.dev.useast.kottia.com/api/trpc/*.
  • Cross-subdomain cookies work and API CORS accepts only the web origin.
  • Direct Railway-domain requests cannot spoof the resolved client IP.
  • Search data restores from R2 seed artifacts.
  • Uploads land in uploads-dev and render through assets.dev.useast.kottia.com.
  • assets.dev.useast.kottia.com resolves and its /health returns 200.
  • GET /api/images/... for a real photo returns 200, not 502.
  • Worker jobs use QUEUE_SUFFIX=-dev.
  • Pushes rebuild only services selected by watch paths.
  • PostgreSQL, Valkey, and Meilisearch have mounted volumes and no public domains.
  • The docs service has no Railway custom domain until docs.dev.useast.kottia.com is verified behind Cloudflare Access.
  • No production or future staging service carries a docs hostname.

Bull Board is sensitive: it grants queue control and exposes lead PII. Keep it internal by default; if bull.dev.useast.kottia.com is enabled, BULL_BOARD_USER and BULL_BOARD_PASSWORD are mandatory.

Dokploy applications stay stopped while the Hostinger VPS remains available for about one week. Do not run both platforms as simultaneous active DEV writers.

Cancel Hostinger and remove the Dokploy GitHub App only after Railway has passed verification, remained stable through the observation window, and the owner has confirmed the PII_ENCRYPTION_KEY escrow outside both platforms.