Moving from Railway

miosa import railway reads a Railway project through Railway’s public API and writes a plan. The plan lists the MIOSA project, one Deploy target per Railway environment, and in each target the services, databases, variable names and shared variables it would create. It also lists everything it cannot carry across. Nothing is created until you review the plan and apply it, and an apply starts nothing unless you ask.

This page is the reference for what carries over, what does not, and what to do about the secrets.

Before you start

  • A Railway token: an account, workspace or project token from Railway’s account settings. --project takes the project’s id, or its name when the login can list it; if Railway refuses, the import prints Railway’s message and asks for the id. MIOSA reads it, never stores it, and never writes it to the plan. If you are logged in with the Railway CLI, --via railway-cli reads through that session instead, so no token passes through MIOSA.
  • The MIOSA CLI, @miosa/cli 2.13.4 or newer:
npm install -g @miosa/cli
miosa --version
  • GitHub connected to MIOSA, when your services build from GitHub repositories:
miosa connections add apps github
  • Assume every secret in the Railway project is compromised. Railway disclosed on 2026-10-21 that its secrets were exposed, so the import copies names, not values, unless you explicitly ask otherwise.

The steps

export RAILWAY_TOKEN=...                                          # or --token-file, or a prompt
miosa import railway --project acme-api --environment all         # review only; nothing is created
miosa import railway --project acme-api --environment all --out plan.json
miosa import railway --apply --plan plan.json --region us-east    # creates everything, stopped
miosa service vars set acme-api api STRIPE_KEY=<new value> --deploy-target production
miosa service deploy acme-api api --deploy-target production

--environment takes one environment, several (production,staging), or all. The default is production.

The review prints what will be created in each Deploy target, where each setting came from, which variables need a value, and a “Finish by hand” list with the next step for each item. Every field that cannot be carried appears there; nothing is dropped silently.

What apply does

  • It creates the project, then one Deploy target per Railway environment, reusing a target that already exists.
  • It creates every service in the Deploy target of its environment, so production and development keep separate services, variables and settings.
  • It creates every service stopped. Nothing builds or runs until you deploy it with miosa service deploy, or apply with --deploy.
  • --deploy starts every service that has a source, after the GitHub copies are ready. It refuses while any variable has no value, so a service never starts with an empty secret.
  • It does not attach custom domains unless you pass --attach-domains. Attaching a live domain during the import would move its traffic before the MIOSA services run. Without the flag, the review lists each domain as skipped and the summary prints the command that attaches it later.
  • It links a GitHub repository, private or public, through your GitHub connection. MIOSA copies the repository into Forge and builds from the copy, so no credential is stored on the service.
  • It checks everything before creating anything. If GitHub is not connected, it stops with Connect GitHub first: miosa connections add apps github. If your connection cannot read a repository, it names the repository and stops. If your organization cannot build services from a repository yet, it says so and stops. In each case nothing is created.
  • Running apply again creates only what is missing: every project, Deploy target and service is looked up first.
  • It gives every service a name and slug MIOSA accepts. A package name such as @acme/web becomes acme-web, and references to it follow. A slug longer than 40 characters is shortened with a short hash of the name, the same on every run.

What carries over

RailwayMIOSA
ProjectProject
EnvironmentA Deploy target of the same name, with its own services and variables
ServiceWeb service, Worker or Schedule, in its environment’s Deploy target
Repository, branch, root directoryThe service source; a GitHub repository is linked through your GitHub connection
Dockerfile pathsource.dockerfile, relative to the root directory
Build commandsource.build_command
Start commandstart_command
Pre-deploy command (for example alembic upgrade head, drizzle-kit migrate)pre_deploy_command
Health check path and timeouthealthcheck_path and healthcheck_timeout_s
Restart policy and retriesrestart_policy and max_retries
Replicasreplicas (1 to 50)
Cron scheduleA Schedule service
railway.json, railway.tomlRead from the repository through Railway; the file wins over the service settings, as on Railway
VariablesNames, build-visible as on Railway; ${{Service.VAR}} references, rewritten where MIOSA’s names differ
Shared variablesShared variables of the same Deploy target (names)
Postgres, Redis, MySQL template images and pluginsA database service with a managed database
Domain target portThe service port, which the health check and the public address use
Custom domainsListed; attached with --attach-domains, and served from MIOSA once their DNS points here

Each setting is taken from the first place that has it: the repository’s config file, then the service settings, then what the latest deployment ran. The plan records which one, for every setting.

References

MIOSA uses the same ${{Service.NAME}} reference syntax as Railway. The import rewrites the names that differ:

Railway referenceBecomes
${{Redis.REDISHOST}}, REDISPORT, REDISPASSWORD${{Redis.REDIS_HOST}}, REDIS_PORT, REDIS_PASSWORD
${{Redis.REDISUSER}}default
${{Redis.REDIS_URL}}unchanged
${{Postgres.POSTGRES_USER}}, POSTGRES_PASSWORD, POSTGRES_DB${{Postgres.PGUSER}}, PGPASSWORD, PGDATABASE
${{Postgres.DATABASE_URL}}, PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASEunchanged
${{api.RAILWAY_PRIVATE_DOMAIN}}${{api.MIOSA_PRIVATE_DOMAIN}} (api.miosa.internal)
RAILWAY_SERVICE_NAME, RAILWAY_SERVICE_ID, RAILWAY_ENVIRONMENT_NAME, RAILWAY_ENVIRONMENT_ID, RAILWAY_PROJECT_ID, RAILWAY_PROJECT_NAME, RAILWAY_REGIONthe MIOSA_ name

A service is unreachable by its private domain on MIOSA today, because private networking between services is off. The review flags every variable that reaches another service that way; point it at an address MIOSA can reach (for SMTP, your mail provider).

A reference with no MIOSA equivalent, such as RAILWAY_PUBLIC_DOMAIN, REDIS_PUBLIC_URL or DATABASE_PUBLIC_URL, is listed in the review. That variable is created without a value, and you set it after the import.

Railway’s own variables (RAILWAY_*) are not imported; MIOSA injects its own MIOSA_* names.

Build-visible variables

Railway exposes every service variable to the build, so a framework inlines NEXT_PUBLIC_*, VITE_* and PUBLIC_* values when it builds. The import keeps that: every imported variable and shared variable is created build-visible, and the review says how many. MIOSA_* and RAILPACK_* names stay runtime only, because MIOSA reserves them. A variable created without a value becomes build-visible when you set it with --build; the summary prints that command.

On MIOSA a build-visible variable reaches the build as a BuildKit secret, so it is never stored in an image layer. The exception is a Dockerfile that declares an ARG of the same name: that variable is passed as a build argument, as on Railway, and a RUN step that uses it is recorded in the image history.

To create every variable runtime only, pass --runtime-only-vars when you write the plan. You can change one later:

miosa service vars set <project> <service> NEXT_PUBLIC_API_URL=https://api.example.com --build
miosa service vars set <project> <service> STRIPE_KEY=<new value> --no-build

What does not carry over

RailwayWhat to do
Variable valuesSet new values on MIOSA; see below
Region outside the US (for example asia-southeast1, europe-west4)MIOSA runs in us-east, us-west and us-mia; the review lists every such service and apply needs --region
Builder (Nixpacks)MIOSA builds the Dockerfile if the repository has one, else detects the stack with Railpack; check the first build
Watch patternsMIOSA builds on every push; the review lists them
Sleep on idleNot carried; set the sleep setting after the import
Database data and volume contentsNot copied; the review lists each volume and its size, and you move the data
Service volumesListed with the miosa volume create command and the mount path to attach
*.up.railway.app domainsRailway-owned DNS; MIOSA assigns <label>.<org>.miosa.app
A service uploaded with the Railway CLI (no repository or image)Created without a source; upload the code with miosa service deploy <project> <service> --dir <path>
MongoDB pluginsCreate a MongoDB database with miosa db create --engine mongodb

Rotate the secrets

Apply refuses to run while a variable has no value, unless you pass --allow-missing-values, which creates those variables empty. Services stay stopped either way, so nothing starts with a missing or leaked secret.

  1. Assume every secret in the Railway project is compromised.
  2. Do not copy values during the import; set new ones on MIOSA.
  3. Rotate at the provider first (database password, Stripe, SMTP, API keys), then paste the new value into MIOSA.
  4. Deploy after the new values are set.
  5. Delete the old Railway project once the MIOSA one serves traffic.
miosa service vars set <project> <service> STRIPE_KEY=<new value> --deploy-target production
miosa project vars set <project> APP_NAME=<value> --deploy-target production
miosa service deploy <project> <service> --deploy-target production

--copy-variables exists for a test project where nothing is sensitive. It prints the same warning, and it is never the default.

If you only have a config file

--from-file reads a railway.json or railway.toml locally, with no token and no network call:

miosa import railway --from-file ./railway.toml --service-name api

The plan has no source, because a config file has none; add one to the plan before you apply.

Undo

Every id the apply creates is recorded as it is created.

miosa import railway --cleanup --state .miosa/import-railway-acme-api.json --yes

--cleanup prints exactly what it will delete first, and deletes only those ids. An interrupted apply continues with --resume.

Railway terms, MIOSA names

Railway saysMIOSA calls it
ProjectProject
EnvironmentDeploy target
ServiceService (Web service, Worker, Private service, Schedule)
Plugin, database templateDatabase service
DeployRelease
VariablesVariables
Shared variablesShared variables
VolumeVolume
DomainPublic address or custom domain
Was this page helpful?