Skip to content

Commit aeab20c

Browse files
alanopsclaude
andcommitted
Convert to full-stack Railway deployment
- Create unified Dockerfile that builds both frontend and backend - Update backend to serve static frontend files - Configure frontend to connect to same domain WebSocket - Update Railway configuration for single-service deployment - Add comprehensive deployment documentation - Optimize with .dockerignore for faster builds This eliminates the need for separate Netlify + Railway deployment. Now everything runs on Railway as a single service. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a48ebde commit aeab20c

File tree

8 files changed

+244
-7
lines changed

8 files changed

+244
-7
lines changed

.dockerignore

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Development files
2+
node_modules
3+
npm-debug.log*
4+
yarn-debug.log*
5+
yarn-error.log*
6+
7+
# Next.js build output
8+
.next
9+
out
10+
11+
# Environment files
12+
.env*
13+
!.env.example
14+
15+
# Git
16+
.git
17+
.gitignore
18+
19+
# Documentation
20+
*.md
21+
!README.md
22+
23+
# Test files
24+
tests/
25+
coverage/
26+
.nyc_output
27+
28+
# IDE files
29+
.vscode/
30+
.idea/
31+
*.swp
32+
*.swo
33+
34+
# OS files
35+
.DS_Store
36+
Thumbs.db
37+
38+
# Build artifacts
39+
dist/
40+
build/
41+
42+
# Backend-only files in root (will be copied from backend/ dir)
43+
backend-only.tar.gz
44+
45+
# Netlify specific
46+
netlify/
47+
netlify.toml
48+
.netlify/

DEPLOY-FULLSTACK-RAILWAY.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Full-Stack Railway Deployment
2+
3+
Deploy the entire DevOps Learn platform to Railway as a single service.
4+
5+
## Architecture
6+
7+
- **Single Railway Service**: Backend serves both WebSocket API and static frontend
8+
- **Frontend**: Next.js static export served by Express
9+
- **Backend**: Node.js WebSocket server with Docker scenario support
10+
- **Scenarios**: Kubernetes containers with kind clusters
11+
12+
## Deployment Steps
13+
14+
### 1. Deploy to Railway
15+
16+
1. **Create Railway Account**: Go to [railway.app](https://railway.app)
17+
2. **New Project****Deploy from GitHub repo**
18+
3. **Select this repository** (root directory)
19+
4. **Set Environment Variables**:
20+
```
21+
NODE_ENV=production
22+
PORT=3000
23+
```
24+
5. **Deploy**: Railway will use the root Dockerfile automatically
25+
26+
### 2. How It Works
27+
28+
1. **Build Process**:
29+
- Frontend builds to static files (`out/`)
30+
- Backend TypeScript compiles to JavaScript
31+
- Docker scenario images are built
32+
- Everything is packaged in a single container
33+
34+
2. **Runtime**:
35+
- Express server serves static frontend files
36+
- Same server handles WebSocket connections for scenarios
37+
- Frontend connects to backend on same domain (no CORS issues)
38+
39+
### 3. Benefits
40+
41+
**Simplified Deployment**: One service, one URL
42+
**No CORS Issues**: Frontend and backend on same domain
43+
**Cost Effective**: Single Railway service ($5/month free credit)
44+
**Docker Support**: Full scenario functionality
45+
**WebSocket Support**: Real-time terminal connections
46+
47+
### 4. Local Development
48+
49+
```bash
50+
# Frontend development
51+
npm run dev
52+
53+
# Backend development
54+
cd src/server
55+
npm run dev
56+
57+
# Full stack test
58+
npm run build
59+
npm run railway:start
60+
```
61+
62+
### 5. Environment Variables
63+
64+
**Required**:
65+
- `PORT` - Server port (set by Railway automatically)
66+
67+
**Optional**:
68+
- `NODE_ENV=production`
69+
- `FRONTEND_URL` - For CORS (not needed in full-stack mode)
70+
71+
## File Structure
72+
73+
```
74+
/
75+
├── Dockerfile # Full-stack Railway build
76+
├── railway.toml # Railway configuration
77+
├── src/
78+
│ ├── components/ # React components
79+
│ ├── pages/ # Next.js pages
80+
│ └── server/ # Backend WebSocket server
81+
├── scenarios/ # Docker scenario definitions
82+
├── docker/ # Docker base images
83+
└── public/ # Static assets
84+
```
85+
86+
## Cost Estimate
87+
88+
- **Railway**: $5/month free credit
89+
- **Total**: $0/month for starter projects
90+
91+
Perfect for learning and prototyping! 🚀

Dockerfile

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Multi-stage build for full-stack Railway deployment
2+
FROM node:18-alpine AS frontend-builder
3+
4+
WORKDIR /app
5+
6+
# Copy frontend package files
7+
COPY package*.json ./
8+
9+
# Install frontend dependencies
10+
RUN npm ci
11+
12+
# Copy frontend source
13+
COPY src/ ./src/
14+
COPY public/ ./public/
15+
COPY next.config.js ./
16+
COPY tailwind.config.js ./
17+
COPY postcss.config.js ./
18+
COPY tsconfig.json ./
19+
20+
# Build frontend (static export)
21+
RUN npm run build
22+
23+
# Backend stage
24+
FROM node:18-alpine AS backend-builder
25+
26+
# Install Docker CLI for scenario management
27+
RUN apk add --no-cache docker-cli make
28+
29+
WORKDIR /app
30+
31+
# Copy backend files
32+
COPY backend/package*.json ./
33+
RUN npm ci --only=production
34+
35+
# Copy backend source
36+
COPY backend/ ./
37+
COPY scenarios/ ./scenarios/
38+
COPY docker/ ./docker/
39+
COPY Makefile ./
40+
41+
# Build TypeScript
42+
RUN npm run build
43+
44+
# Production stage
45+
FROM node:18-alpine AS production
46+
47+
# Install Docker CLI for runtime
48+
RUN apk add --no-cache docker-cli
49+
50+
WORKDIR /app
51+
52+
# Copy built frontend to be served by backend
53+
COPY --from=frontend-builder /app/out ./public
54+
55+
# Copy backend
56+
COPY --from=backend-builder /app/dist ./
57+
COPY --from=backend-builder /app/node_modules ./node_modules/
58+
COPY --from=backend-builder /app/package.json ./
59+
60+
# Copy scenarios and docker setup
61+
COPY --from=backend-builder /app/scenarios ./scenarios/
62+
COPY --from=backend-builder /app/docker ./docker/
63+
COPY --from=backend-builder /app/Makefile ./
64+
65+
# Expose port
66+
EXPOSE $PORT
67+
68+
# Start the backend server (which will also serve frontend)
69+
CMD ["node", "index.js"]

backend/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createServer } from 'http'
33
import { Server } from 'socket.io'
44
import { spawn, ChildProcess } from 'child_process'
55
import cors from 'cors'
6+
import path from 'path'
67

78
const app = express()
89
const server = createServer(app)
@@ -16,6 +17,18 @@ const io = new Server(server, {
1617
app.use(cors())
1718
app.use(express.json())
1819

20+
// Serve static frontend files
21+
app.use(express.static(path.join(__dirname, '../public')))
22+
23+
// Catch-all handler for frontend routes (except WebSocket and API)
24+
app.get('*', (req, res, next) => {
25+
// Skip serving static files for socket.io and API routes
26+
if (req.path.startsWith('/socket.io') || req.path.startsWith('/api')) {
27+
return next()
28+
}
29+
res.sendFile(path.join(__dirname, '../public/index.html'))
30+
})
31+
1932
interface ScenarioSession {
2033
containerId: string
2134
process: ChildProcess

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
"description": "DevOps Learning Platform - Learn by Fixing Broken Things",
55
"scripts": {
66
"dev": "next dev",
7-
"build": "next build",
7+
"build": "next build && npm run build:server",
8+
"build:server": "cd src/server && npm install && npm run build",
89
"start": "next start",
10+
"start:server": "cd src/server && npm start",
911
"export": "next export",
1012
"lint": "next lint",
1113
"test": "jest",
12-
"typecheck": "tsc --noEmit"
14+
"typecheck": "tsc --noEmit",
15+
"railway:start": "cd src/server && node dist/index.js"
1316
},
1417
"dependencies": {
1518
"@radix-ui/react-dialog": "^1.0.5",

railway.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[build]
22
builder = "dockerfile"
3-
dockerfilePath = "backend/Dockerfile"
4-
3+
dockerfilePath = "Dockerfile"
4+
55
[deploy]
6-
startCommand = "npm start"
6+
startCommand = "node index.js"
77
healthcheckPath = "/health"

src/components/Terminal.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,12 @@ export default function TerminalComponent({ scenarioId }: TerminalComponentProps
5656
terminal.current.open(terminalRef.current)
5757
fitAddon.current.fit()
5858

59-
// Connect to backend WebSocket
60-
socket.current = io(process.env.NEXT_PUBLIC_WS_URL || 'http://localhost:3001', {
59+
// Connect to backend WebSocket (same domain in production, localhost for dev)
60+
const wsUrl = process.env.NEXT_PUBLIC_WS_URL ||
61+
(typeof window !== 'undefined' && window.location.origin) ||
62+
'http://localhost:3001'
63+
64+
socket.current = io(wsUrl, {
6165
query: { scenarioId }
6266
})
6367

src/server/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createServer } from 'http'
33
import { Server } from 'socket.io'
44
import { spawn, ChildProcess } from 'child_process'
55
import cors from 'cors'
6+
import path from 'path'
67

78
const app = express()
89
const server = createServer(app)
@@ -16,6 +17,14 @@ const io = new Server(server, {
1617
app.use(cors())
1718
app.use(express.json())
1819

20+
// Serve static frontend files
21+
app.use(express.static(path.join(__dirname, '../public')))
22+
23+
// Catch-all handler for frontend routes
24+
app.get('*', (req, res) => {
25+
res.sendFile(path.join(__dirname, '../public/index.html'))
26+
})
27+
1928
interface ScenarioSession {
2029
containerId: string
2130
process: ChildProcess

0 commit comments

Comments
 (0)