Updated Nov 5, 20267 min read

How to Move a Go API from Railway to AWS

Moving a Go service with the Railway Postgres and Redis templates, variable references and a cron service into your own AWS account.

A Go API on Railway usually looks the same: one service built from the repository, a Postgres template and a Redis template on the project canvas, and variables such as DATABASE_URL=${{Postgres.DATABASE_URL}} that wire them together. Scheduled work runs as a separate cron service that starts, does its job and exits. It is a fast way to ship, and it starts to pinch when a customer needs the data in your own AWS account, when you want backups and failover you do not operate yourself, or when the rest of the company already runs on AWS.

This guide moves a small Go product catalog off Railway. The app reads products from Postgres, caches them in Redis and calls an email API with a key stored in a Railway variable. It covers the mapping, the Encore (a tool that works alongside your framework) version of the service, the manual AWS path, the data move and the DNS switch.

Railway to AWS mapping at a glance

RailwayEncore on AWSManual AWS
ServiceEncore service, run on FargateECS service, task definition, load balancer
Postgres templatesqldb.NewDatabase, provisioned as RDSRDS instance, subnet group, security groups
Redis templatecache.NewCluster, provisioned as ElastiCacheElastiCache cluster and security groups
Variable referencesGenerated connections and secretsSecrets Manager plus task definition entries
Cron servicecron.NewJob calling an endpointEventBridge Scheduler and an ECS task
VolumeObject storage or the databaseEFS or S3

Each option is covered below, starting with Encore.

Declaring the Railway canvas in Go code with Encore

Encore combines a tool that works alongside your framework for Go and TypeScript with a platform that deploys into your own AWS or GCP account. Each box on the Railway canvas becomes a declaration in the service that uses it, and the variable references disappear because the connections come from the declarations:

// Service catalog was the Railway web service. It stores products in // Postgres and caches reads in Redis. package catalog import ( "context" "errors" "time" "encore.dev/beta/errs" "encore.dev/storage/cache" "encore.dev/storage/sqldb" ) // Replaces the Railway Postgres template: RDS on AWS, local Postgres under encore run. var db = sqldb.NewDatabase("catalog", sqldb.DatabaseConfig{ Migrations: "./migrations", }) // Replaces the Railway Redis template: ElastiCache on AWS, local Redis under encore run. var cluster = cache.NewCluster("catalog-cache", cache.ClusterConfig{ EvictionPolicy: cache.AllKeysLRU, }) type Product struct { ID int64 `json:"id"` Name string `json:"name"` PriceCents int `json:"priceCents"` SKU *string `json:"sku"` Cached bool `json:"cached"` } // Cached products expire after five minutes. var products = cache.NewStructKeyspace[int64, Product](cluster, cache.KeyspaceConfig{ KeyPattern: "product/:key", DefaultExpiry: cache.ExpireIn(5 * time.Minute), }) type CreateParams struct { Name string `json:"name"` PriceCents int `json:"priceCents"` SKU *string `json:"sku"` } //encore:api public method=POST path=/products func Create(ctx context.Context, p *CreateParams) (*Product, error) { prod := &Product{Name: p.Name, PriceCents: p.PriceCents, SKU: p.SKU} err := db.QueryRow(ctx, ` INSERT INTO products (name, price_cents, sku) VALUES ($1, $2, $3) RETURNING id`, p.Name, p.PriceCents, p.SKU).Scan(&prod.ID) return prod, err } //encore:api public method=GET path=/products/:id func Get(ctx context.Context, id int64) (*Product, error) { // Cache-aside: serve from Redis when we can. if p, err := products.Get(ctx, id); err == nil { p.Cached = true return &p, nil } else if !errors.Is(err, cache.Miss) { return nil, err } var p Product err := db.QueryRow(ctx, ` SELECT id, name, price_cents, sku FROM products WHERE id = $1`, id, ).Scan(&p.ID, &p.Name, &p.PriceCents, &p.SKU) if errors.Is(err, sqldb.ErrNoRows) { return nil, &errs.Error{Code: errs.NotFound, Message: "product not found"} } else if err != nil { return nil, err } if err := products.Set(ctx, id, p); err != nil { return nil, err } return &p, nil } type UpdatePriceParams struct { PriceCents int `json:"priceCents"` } //encore:api public method=PATCH path=/products/:id func UpdatePrice(ctx context.Context, id int64, p *UpdatePriceParams) error { if _, err := db.Exec(ctx, `UPDATE products SET price_cents = $1 WHERE id = $2`, p.PriceCents, id); err != nil { return err } // Drop the cached copy so the next read sees the new price. _, err := products.Delete(ctx, id) return err }

The two migrations in catalog/migrations recreate the table that already exists on Railway, which is what lets the data be restored into it later:

-- 1_create_products.up.sql CREATE TABLE products ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, price_cents INT NOT NULL ); -- 2_add_sku.up.sql ALTER TABLE products ADD COLUMN sku TEXT; CREATE UNIQUE INDEX products_sku_idx ON products (sku);

The email key that lived in a Railway variable becomes a secret, a field on var secrets struct { ResendAPIKey string } in the service that sends mail, set with encore secret set --type prod ResendAPIKey. Values that are not secret can be constants in the code. The caching docs cover the keyspace types, and the cache is not a general Redis connection, so check that your code only uses operations the keyspaces provide.

encore run starts Postgres and Redis locally and applies both migrations, replacing a laptop pointed at a shared Railway database. A Railway cron service becomes a cron.NewJob that calls an endpoint, which runs in the deployed environments; locally you call the endpoint by hand.

After connecting your AWS account and pushing, Encore provisions RDS, ElastiCache, the secrets in Secrets Manager and the compute in your own account, and gives the service its own IAM role. Migrations run before the new code goes live on each deploy. The Railway migration guide lists how every Railway concept maps.

The Infrastructure page in the Encore dashboard with a database's configuration panel open, showing fields for vCPUs, memory, storage, availability type, max connections and backups.
Each provisioned resource can be reviewed and its configuration edited per environment in the Encore dashboard. The screenshot shows a GCP Cloud SQL database.

For a team that writes code with AI agents, the canvas turns into code an agent can read and change in a pull request, and the agent can run the result with encore run before anyone deploys it.

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

Manual path: ECS on Fargate, RDS and ElastiCache

Without Encore, the Go binary stays the same and reads DATABASE_URL, REDIS_URL and the API key from environment variables. A non-root distroless image replaces Railway's automatic build:

FROM golang:1.25 AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -o /catalog ./cmd/catalog FROM gcr.io/distroless/static-debian12:nonroot COPY --from=build /catalog /catalog EXPOSE 8080 ENTRYPOINT ["/catalog"]

Railway sets PORT for you, and on ECS you choose the container port yourself, so read it from configuration with a default. Then come the AWS resources: an RDS instance and an ElastiCache cluster in private subnets, security groups that let only the service reach them, an ECR repository, an ECS service behind an Application Load Balancer, Secrets Manager entries referenced from the task definition and an execution role that can read them. The cron service becomes an EventBridge Scheduler schedule that runs an ECS task with its own IAM role. Most teams write this in Terraform so staging can be a second copy.

Good to know

ElastiCache with in-transit encryption needs TLS on the client side, so a redis:// URL that worked on Railway's private network becomes rediss://. RDS requires TLS by default too, so keep sslmode=require in the connection string.

Moving the data

Stop writes by scaling down the Railway web service and pausing the cron service, along with any other service that writes to the database, then dump the database through the public connection URL shown in the Postgres service's variables (DATABASE_PUBLIC_URL) with pg_dump -Fc --data-only --no-owner, which writes the custom format that pg_restore reads. With Encore the schema already exists from the migrations, so restore only the data:

~/catalog
$ pg_dump -Fc --data-only --no-owner "$DATABASE_PUBLIC_URL" -f catalog.dump$ pg_restore --data-only --no-owner \ --dbname "$(encore db conn-uri catalog --env=prod --write)" catalog.dump

--data-only also restores the current value of products_id_seq, so new products continue from the highest id. Leave Redis behind: the cache-aside Get fills the new ElastiCache cluster from Postgres on the first read of each product. Check row counts before switching traffic. For the manual path, restore into RDS from a host inside the VPC, such as CloudShell in a VPC environment, since the instance is not publicly reachable.

Switching DNS

Add the custom domain to the new environment, wait for its certificate, then point the DNS record away from Railway. Keep the Railway project for a few days so you can switch back if something was missed. Leave the Railway cron service paused so it does not run against the old data.

How to choose

Pick Encore when you want the Railway experience of pushing code and getting a running app, with the database, cache, secrets and scheduled jobs defined in the Go code and running in your own AWS account. Pick the manual ECS path when your team already runs Terraform for other AWS services and wants this app to use the same modules. See Encore vs Railway for a broader comparison.

Frequently asked questions

How do I migrate a Go app from Railway to AWS?

Map each Railway service to an AWS one: the web service to a container service on Fargate, the Postgres template to RDS, the Redis template to ElastiCache, sensitive variables to Secrets Manager and the cron service to a scheduled job. Copy the data with pg_dump and pg_restore during a short write freeze, then switch DNS. With Encore, the database, cache and secrets are declared in the Go code and provisioned in your AWS account on deploy.

What replaces Railway variable references like DATABASE_URL on AWS?

Store the connection string and API keys in Secrets Manager and reference them from the ECS task definition, which injects them as environment variables when the task starts. Each environment gets its own entries.

Do I need to copy Redis data when leaving Railway?

Usually not, if Redis is used as a cache. A cache-aside cache refills from Postgres on the first read of each key, so a new ElastiCache cluster can start empty. Redis used as a primary store or queue needs its own migration plan.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders