Welcome to API Academy

Module 01 · Foundations · ~25 min

Infrastructure.

By the end of this module, you’ll have a real address on the internet that a trading bot can call home, somewhere it can run, restart, and keep its memory without your laptop staying open.

Before any bot code, before any control panel, you need a service running on the internet with a URL you can hit from your phone. This module ships a hello-world service to Railway, sets the env vars and the persistent volume the rest of the curriculum will use, and ends with you opening the URL on your phone. Twenty-five minutes; nothing yet trades, but everything from here on assumes this floor.

Quick answer

How do you deploy a trading bot to Railway?

Five commands from the Railway CLI: npm i -g @railway/cli (or brew install railway), railway login, railway init, railway up to build and deploy, then railway domain to mint a public URL you can open on your phone. The service itself is one file with two endpoints: / returns JSON and /health returns 200 for Railway’s health check; bind to 0.0.0.0, read PORT from the environment, and let a one-line Procfile tell Railway how to start it. Set secrets like LIMITLESS_API_KEY with railway variables --set (never in the repo), and mount a 1 GB volume at /app/data, addressed via ACADEMY_DATA_DIR, so SQLite files and NDJSON logs survive every redeploy. Nothing trades yet, but every later module assumes this floor: a URL, production env vars, persistent storage, and tailable logs.

Platform commands and pricing are illustrative.

Section 01

Why deploy first.

The classic order, build the bot on your laptop, then figure out hosting at the end, sounds reasonable and ages badly. By the time the bot works locally, the platform decisions, the env-var habits, and the persistent-storage path are all things you’re trying to retrofit around code that already assumes localhost. The result is a panicky last-mile sprint with real money on the table.

Deploying empty is the cheapest way to learn the platform. A hello-world that returns a JSON object teaches the same Procfile, the same env vars, the same volume mount, and the same domain wiring as a full bot, and if you break it, you’ve broken five lines of HTTP server code, not your trading loop. By the end of this module you have infrastructure; in Module 02 you put a control panel on it that surfaces positions, P&L, and a kill switch; in Modules 03–18 every section ends with “wire this into your panel.”

What “the floor” means in this curriculum

  • A URL. Reachable from your phone, your laptop, and a coach if you have one. Not localhost.
  • Production env vars. Set by CLI or UI, never in the repo. The deployed process reads them on boot.
  • Persistent storage. A path that survives redeploys, for SQLite, for the NDJSON log, for any state the bot needs.
  • Tailable logs. A single command tails the running process. You can read stdout from anywhere with internet.

Section 02

Pick your platform.

The curriculum picks Railway as the default because it gets you to a production URL with persistent storage in about ten minutes, with a free trial that’s long enough to finish the first three modules. The patterns, Procfile, env vars, volume mount, domain, carry over to Fly, Render, or a $5 VPS. Pick whichever matches how you want to learn; the rest of this module uses Railway commands.

Railway

Curriculum default

Why: CLI-first deploy, generous free trial, built-in persistent volumes, env-var UI + CLI, log streaming, custom domains in one command, nixpacks autodetects Python, Node, and Go.

Watch: trial ends; budget around $5–$10/mo for the panel + bot combined past that.

Fly.io

Why: region-aware deploy, slightly cheaper at idle, similar Procfile-style pattern via fly.toml.

Watch: persistent volumes are per-region, so multi-region is more involved than Railway.

Render

Why: friendly UI, good free tier for static + small services, native cron.

Watch: free tier sleeps; not ideal for a bot that needs to wake on a schedule.

$5 VPS

Why: Hetzner / DO / Vultr give you a flat $5–$6/mo bill and a real Linux box. Best long-term economics.

Watch: you’re the platform, systemd, certbot, ufw, log rotation. Worth it once you’ve done the managed path first.

Section 03

The hello-world service.

One file, two endpoints. / returns a JSON object so you have something to look at on your phone; /health returns 200 so Railway’s health-check passes. All three runtimes share the same Procfile pattern. Pick the tab that matches what you installed in Setup.

// app.ts, minimal hello-world for Railway
import { Hono } from 'hono';
import { serve } from '@hono/node-server';

const app = new Hono();

app.get('/', (c) => {
  return c.json({
    service: 'api-academy-deploy',
    module: 'Module 01, Infrastructure',
    msg: 'You are reading this from a server you deployed.',
    ts: new Date().toISOString(),
  });
});

app.get('/health', (c) => c.text('ok'));

const port = Number(process.env.PORT) || 8080;
serve({ fetch: app.fetch, port });
console.log(`listening on :${port}`);

Procfile

One line. Railway reads Procfile to know how to start the service. The same Procfile pattern works on Heroku, Fly, and a VPS via honcho.

# Procfile (TypeScript)
web: node --env-file=.env app.ts

# Procfile (Python)
web: uvicorn app:app --host 0.0.0.0 --port $PORT

# Procfile (Go), build first, then run the binary
# release: go build -o api-academy-deploy ./...
web: ./api-academy-deploy

requirements / package files

Pinned versions. Railway’s nixpacks builder reads these to install dependencies. Don’t skip the pin, floating versions are how supply-chain compromises slip in.

# requirements.txt
fastapi==0.115.0
uvicorn[standard]==0.30.6

# package.json (TS), npm i hono @hono/node-server
{ "dependencies": {
    "hono": "4.6.5",
    "@hono/node-server": "1.13.2"
  } }

# go.mod (Go), stdlib only, no third-party deps
module api-academy-deploy
go 1.22

Section 04

Deploy it.

Railway’s CLI is the fastest path to a URL. Five commands, in order. The first one is interactive (a browser tab opens), everything after that runs unattended.

If railway login hangs

In a sandboxed terminal, the browser tab won’t open automatically. Use railway login --browserless, it prints a URL and a code to paste.

From your project root

# 1. Install + log in
npm i -g @railway/cli      # or: brew install railway
railway login              # opens a browser tab

# 2. Create or link a project
railway init               # name it "api-academy-deploy"

# 3. Deploy
railway up                 # uploads, builds, runs

# 4. Set env vars (one per line, repeat as needed)
railway variables --set "LIMITLESS_API_KEY=lmt_…"
railway variables --set "PANEL_TOKEN=$(openssl rand -hex 32)"

# 5. Generate a public domain
railway domain             # prints something like api-academy-deploy.up.railway.app

After step 5, hit the URL in your browser. You should see the JSON from /. If you don’t, the next section’s log-tailing command will tell you why.

Section 05

Persistent storage.

Every redeploy gives you a fresh container with a fresh filesystem. Anything written to disk during one deploy is gone after the next push. The fix is a volume, a chunk of storage that survives redeploys, mounted at a known path. Module 02 puts the panel’s seed data here; Module 13 (PnL Analysis) puts the SQLite-backed order history here; Module 18 (Production Bot) puts the NDJSON event log here. Wire it once, in Module 01.

Mount a volume at /app/data

# From the Railway dashboard, Service → Volumes → New
#   Mount path: /app/data
#   Size:        1 GB (more than enough for the curriculum)
# Or via CLI (newer versions):
railway volume add --mount-path /app/data --size 1

# Tell your code where to write. Set this once and use it everywhere.
railway variables --set "ACADEMY_DATA_DIR=/app/data"

# Verify the volume survives a redeploy:
railway run "echo first-deploy > /app/data/touch.txt"
railway up                              # redeploy
railway run "cat /app/data/touch.txt"   # should print: first-deploy

Reading the path in code

# Python
import os
DATA_DIR = os.environ.get("ACADEMY_DATA_DIR", "./data")
LOG_PATH = os.path.join(DATA_DIR, "bot.log.ndjson")

# TypeScript
const DATA_DIR =
  process.env.ACADEMY_DATA_DIR ?? './data';
const LOG_PATH =
  `${DATA_DIR}/bot.log.ndjson`;

// Go
dataDir := os.Getenv("ACADEMY_DATA_DIR")
if dataDir == "" { dataDir = "./data" }
logPath := filepath.Join(dataDir, "bot.log.ndjson")

The fallback to ./data means the same code runs locally without the env var set. Module 02 follows this pattern for the panel’s seed data.

Don’t put these on the volume

  • Secrets. Env vars are encrypted; volumes are not. Keys go in env, never on disk.
  • Anything you can rebuild from source. Build artefacts, vendored deps, the volume is for state, not code.
  • Long-term audit logs. 1 GB fills fast under verbose logging. Rotate or stream off-host.

Section 06

Open it on your phone.

This is the part that matters. The hello-world is doing nothing impressive, it returns a JSON object, but it’s doing it from a server you deployed, behind a domain you bound, reachable from a device you didn’t configure. The “a thing I made is on the actual internet” moment is the entire point of moving infrastructure first.

Pull out your phone. Type the domain Railway printed. You should see the JSON. If you don’t, the log will tell you why, the next code block tails the running process so you can read stdout from anywhere.

Tail logs from anywhere

# Live tail, Ctrl+C to exit
railway logs

# Last 100 lines (no follow)
railway logs --tail 100
09:42 Live
{ "service": "api-academy-deploy", "module": "Module 01, Infrastructure", "msg": "You are reading this from a server you deployed.", "ts": "2026-05-01T14:42:08.211Z" }

Mockup, yours will show your timestamp.

Common questions

Bot infrastructure on Railway: what people ask

Each answer also ships invisibly as schema.org FAQ data for search engines and AI assistants. Tap a question to expand.

  1. Why deploy infrastructure before writing the trading bot?
    Because retrofitting hosting around code that assumes localhost ages badly: by the time the bot works locally, you’re bolting platform decisions, env-var habits, and storage paths onto a working trading loop with real money on the table. Deploying an empty hello-world teaches the same Procfile, env vars, volume mount, and domain wiring as a full bot, and if you break it, you’ve broken five lines of HTTP server code, not your strategy.
  2. What is a Railway volume and where should a bot mount it?
    A volume is storage that survives redeploys; without one, every deploy gives the container a fresh filesystem and wipes anything written to disk. The curriculum mounts 1 GB at /app/data (via railway volume add --mount-path /app/data --size 1) and sets ACADEMY_DATA_DIR=/app/data so code reads the path from env, falling back to ./data locally. Later modules put the panel’s seed data, the SQLite-backed order history, and the NDJSON event log there.
  3. Where do a trading bot’s API keys and secrets live?
    In environment variables set via railway variables --set, never in the repo, never in the image, never in a log. Env vars are encrypted; volumes are not, so keys never go on disk. Generate tokens like PANEL_TOKEN with openssl rand -hex 32 and confirm they show up in railway variables. The deployed process reads them on boot.
  4. Is Railway the only option for hosting a trading bot?
    No. Railway is the curriculum default because it reaches a production URL with persistent storage in about ten minutes, but the patterns (Procfile, env vars, volume mount, domain) carry over to Fly.io, Render, or a $5 VPS. Watch the costs: Railway’s trial ends, so budget around $5–$10/mo for the panel plus bot; Hetzner, DO, or Vultr give a flat $5–$6/mo Linux box once you’re ready to be the platform yourself.
  5. How do you read your bot’s logs on Railway?
    railway logs live-tails the running process from any machine with internet (Ctrl+C to exit); railway logs --tail 100 prints the last 100 lines without following. Tailable logs are part of the module’s definition of “the floor”: if the URL doesn’t show your JSON after a deploy, the log tells you why.

Section 07

Module checklist.

Every box checked means you have running infrastructure. Module 02 will deploy a control panel on top of it; if any of these fail, fix them now, everything downstream assumes the floor.

Module 01 complete

On the air.

Your bot has an address now. When it eventually places trades, it has somewhere to live that doesn’t disappear when you close your laptop, and a memory that survives every redeploy.

Concretely, you shipped a service to the internet, set production env vars without committing them, mounted a volume that survives redeploys, and hit your domain from a device you didn’t configure. The hello-world is a placeholder; the floor underneath it is the real artefact.

01

A URL beats localhost for every kind of feedback, debugging, sharing, intervening from your phone. Deploy first, build second.

02

Secrets live in env vars, set via the platform’s CLI or UI. Never in the repo, never in the image, never in a log.

03

Persistent state has one home: a volume mounted at /app/data, addressed via ACADEMY_DATA_DIR. Every later module assumes both.

Next up: Module 02 builds the Trader Control Panel on top of this infrastructure, a single-page surface for positions, P&L, and a kill switch, running on seed data so you see your first “trade” on the panel before the bot even exists. The visceral moment.

Complete the checklist above to unlock