Skip to content

Search docs

Search the Skiffly documentation

Migrating from Railway

The concepts map one to one, most API documents run unchanged and the CLI commands have the same names. What to change, in order.

Concept mapping#

RailwaySkifflyNotes
Workspace / teamWorkspaceroles owner, admin, member; no access groups
Project → Environment → ServicesamePR environments are pr-<n>, enabled per project
Service source: repo, imagesameprivate registries not supported yet
Railpack / Nixpacks / DockerfileRailpack / DockerfileNixpacks projects build with Railpack or a Dockerfile
${{Postgres.DATABASE_URL}}, ${{shared.VAR}}same syntax
RAILWAY_PUBLIC_DOMAIN, RAILWAY_PRIVATE_DOMAIN, …SKIFFLY_PUBLIC_DOMAIN, SKIFFLY_PRIVATE_DOMAIN, …see built-ins
Private networking service.railway.internal<service-slug> (http://api:8080)IPv4, same environment only
Generated domain *.up.railway.app*.skiffly.cloud
Custom domain: CNAME + verificationCNAME to edge.skiffly.cloud + TXT _skiffly.<host>
TCP proxyTCP proxy (edge.skiffly.cloud:2xxxx)skiffly proxy, skiffly connect
VolumesVolumesone replica per volume service; daily backups
Cron scheduleCron schedule
App SleepingApp Sleeping~10 min idle
Health check pathHealth check pathup to 5 min startup
Replicas, regionsReplicasEU only today
Staged changes → Deploynonechanges apply immediately; redeploy when needed
Usage billing, invoiceUsage billing, prepaid balancePayPal or crypto; 3-day grace then pause
railway CLIskiffly CLIsame command names where the feature exists
Railway MCP serverSkiffly MCP serversame tool names where the feature exists
railway/iac (defineRailway)@skiffly/config (defineSkiffly)search-and-replace
TemplatesTemplates136 in the catalog, templateDeployV2
Observability: logs, metricsLogs (5 000 lines buffer), metrics (7 days)no log drains yet
Webhooks, functions, buckets, sandboxesnot available

Import with the CLI: skiffly import --from-railway#

The CLI (or the import_from_railway MCP tool) reads your Railway project through the Railway Public API and recreates it on Skiffly in one go, so steps 2, 4, 5 and most of 6 below happen automatically. You need a Railway account token (a workspace token works too); it is only used to read and is not stored.

skiffly login
skiffly import --from-railway --dry-run          # pick the Railway project, preview the plan
skiffly import --from-railway                    # apply: creates a new Skiffly project (or --project <id|name>)

Flags: --railway-token <t> (or RAILWAY_TOKEN), --railway-project <id> (the id from the Railway dashboard URL or railway status), --project <id|name|new> and --workspace <w> for the Skiffly target, --environment <name> to import one environment only, --dry-run, --yes, --json.

What is imported, per environment (PR environments are skipped):

RailwaySkiffly
EnvironmentsEnvironments with the same name (production maps onto the project's production)
Services from a GitHub repository — branch, root directory, Dockerfile path, build commandserviceCreate with the same source and builder (RAILPACK, or DOCKERFILE when a Dockerfile path is set); Nixpacks/Heroku/Paketo builds fall back to Railpack
Services from a Docker imageImage service; the port is guessed from PORT or the image (Postgres 5432, Redis 6379, …)
Start command, health check, cron schedule, replicas, app sleepingserviceInstanceUpdate per environment
Variables (per service and shared), unrenderedCopied with ${{Service.VAR}} and ${{shared.VAR}} references kept; RAILWAY_PUBLIC_DOMAIN, RAILWAY_PRIVATE_DOMAIN, RAILWAY_ENVIRONMENT, … inside references become SKIFFLY_*; the RAILWAY_* variables themselves are not copied
VolumesCreated empty at the same mount path (size rounded up to whole GB)
Custom domainsAttached to the service; the CNAME and _skiffly TXT records are printed
TCP proxiesRecreated on edge.skiffly.cloud with a new port

Not imported: the contents of volumes and databases, Railway-generated *.up.railway.app domains, pre-deploy commands, deployment history, private registry credentials, railway.json/railway.toml (the values on the service instance are used instead). Services that already exist by name in the target project are left untouched. Nothing is deployed — services are created with deployments off so you can review variables first.

The command ends with a table of what was created and a Do this by hand list, typically:

  • copy database/volume data (pg_dump on Railway → skiffly connect <service> / skiffly ssh to restore, see step 3);
  • create the DNS records for each custom domain (skiffly domain status <domain> shows them), then remove the domain from Railway;
  • update clients of TCP proxies to the new host:port (skiffly proxy list) and variables that referenced RAILWAY_TCP_PROXY_*;
  • set any variable that referenced a Railway-only built-in; move pre-deploy commands into the start command;
  • skiffly link <projectId>, then skiffly up / skiffly redeploy -s <service> per service, keeping Railway running until Skiffly is healthy.

From an AI agent: the import_from_railway tool of the Skiffly MCP server takes the same parameters (railwayToken, railwayProjectId, projectId or createProjectName, environment, dryRun) and returns the same report.

Step by step#

  1. Sign in at app.skiffly.dev with GitHub, install the GitHub App for private repositories, top up the balance.

  2. Export variables from Railway: railway variables --kv > vars.env per service. Remove RAILWAY_* entries; replace references to Railway-provided variables with the SKIFFLY_* equivalents.

  3. Databases first. Deploy the template (skiffly deploy --template postgres), then move the data through a TCP proxy:

    # Railway side
    pg_dump "$RAILWAY_DATABASE_URL" -Fc -f db.dump
    # Skiffly side
    skiffly connect postgres --print          # prints the URL; creates the proxy on first use
    pg_restore -d "<url from --print>" --no-owner --clean --if-exists db.dump
    skiffly proxy delete <port>               # close the public port when done

    Redis and Mongo: redis-cli --rdb/mongodump and the matching restore over skiffly connect.

  4. Services. skiffly init in each repository, then skiffly up. Set the variables:

    skiffly variables set --skip-deploys $(grep -v '^RAILWAY_' vars.env | xargs)
    skiffly variables set 'DATABASE_URL=${{Postgres.DATABASE_URL}}'
    skiffly redeploy

    Or describe everything once in .skiffly/skiffly.ts and skiffly config apply; a Railway railway.ts ports with defineRailwaydefineSkiffly.

  5. Settings that Railway kept in railway.json/railway.toml (build command, start command, health check, restart policy, cron, watch patterns) go on the service: update-service in MCP, serviceInstanceUpdate in GraphQL, or the config file. Skiffly does not read railway.json.

  6. Domains. Generate a skiffly.cloud domain to test, then add the custom domain and switch the CNAME when the certificate is issued. Keep the Railway service running until DNS has propagated.

  7. API clients. Change the endpoint to https://api.skiffly.dev/graphql and the token. Documents using projects, deployments(input:), variables, serviceInstanceUpdate, serviceInstanceDeployV2, customDomainCreate, templateDeployV2 and the other compatible fields work as they are; buildLogs becomes deploymentLogs filtered by stream.

  8. Automation. GitHub Actions that ran railway up run npx skiffly up (or skiffly config apply --yes) with SKIFFLY_TOKEN.

Things to check before cutting over#

  • The app listens on $PORT (Skiffly sets it; default 8080 if you set none).
  • Nothing relies on *.railway.internal hostnames or IPv6 private networking.
  • Outbound SMTP on port 25 is blocked; use 587 with authentication or an email API.
  • Plan limits: Hobby allows 8 GiB / 8 vCPU per service, Pro 32 GiB / 32 vCPU. Free is one service.
  • Preview environments: enable PR previews per project; forks are ignored.