Skip to content

Commit eb77a08

Browse files
alanopsclaude
andcommitted
Optimize Dockerfile for Railway memory limits
- Switch to node:18-slim to reduce memory usage - Use single-stage build instead of multi-stage - Move scenario image building to runtime instead of build time - Install Docker CLI via curl instead of apk (more memory efficient) - Add background scenario building on server startup - This should resolve the Railway build memory issues 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent aeab20c commit eb77a08

3 files changed

Lines changed: 92 additions & 46 deletions

File tree

Dockerfile

Lines changed: 24 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,49 @@
1-
# Multi-stage build for full-stack Railway deployment
2-
FROM node:18-alpine AS frontend-builder
1+
# Simplified build for Railway - no Docker build during image creation
2+
FROM node:18-slim
3+
4+
# Install only essential packages
5+
RUN apt-get update && apt-get install -y \
6+
curl \
7+
&& rm -rf /var/lib/apt/lists/*
8+
9+
# Install Docker CLI (smaller alternative)
10+
RUN curl -fsSL https://get.docker.com | sh
311

412
WORKDIR /app
513

6-
# Copy frontend package files
14+
# Copy package files and install dependencies
715
COPY package*.json ./
16+
RUN npm ci --only=production
817

9-
# Install frontend dependencies
10-
RUN npm ci
18+
COPY backend/package*.json ./backend/
19+
RUN cd backend && npm ci --only=production
1120

12-
# Copy frontend source
21+
# Copy and build frontend
1322
COPY src/ ./src/
1423
COPY public/ ./public/
1524
COPY next.config.js ./
1625
COPY tailwind.config.js ./
1726
COPY postcss.config.js ./
1827
COPY tsconfig.json ./
1928

20-
# Build frontend (static export)
2129
RUN npm run build
2230

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
31+
# Copy and build backend
32+
COPY backend/ ./backend/
33+
RUN cd backend && npm run build
3434

35-
# Copy backend source
36-
COPY backend/ ./
35+
# Copy scenario definitions (build images at runtime, not build time)
3736
COPY scenarios/ ./scenarios/
3837
COPY docker/ ./docker/
3938
COPY Makefile ./
4039

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 ./
40+
# Setup file structure for serving
41+
RUN mkdir -p public && cp -r out/* public/
5942

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 ./
43+
WORKDIR /app/backend
6444

6545
# Expose port
6646
EXPOSE $PORT
6747

68-
# Start the backend server (which will also serve frontend)
69-
CMD ["node", "index.js"]
48+
# Start backend server (builds scenario images on first use)
49+
CMD ["node", "dist/index.js"]

backend/index.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,32 @@ async function ensureDockerNetwork() {
235235

236236
const PORT = process.env.PORT || 3001
237237

238+
// Build scenario images on startup
239+
async function buildScenarioImages() {
240+
console.log('Building scenario images...')
241+
try {
242+
const buildProcess = spawn('make', ['scenario-build'], { cwd: '..' })
243+
244+
await new Promise((resolve, reject) => {
245+
buildProcess.on('exit', (code) => {
246+
if (code === 0) {
247+
console.log('Scenario images built successfully')
248+
resolve(null)
249+
} else {
250+
console.log('Scenario image build failed, will build on demand')
251+
resolve(null) // Don't fail startup if images can't be built
252+
}
253+
})
254+
buildProcess.on('error', (error) => {
255+
console.log('Scenario image build error, will build on demand:', error.message)
256+
resolve(null) // Don't fail startup
257+
})
258+
})
259+
} catch (error) {
260+
console.log('Scenario image build error, will build on demand:', error)
261+
}
262+
}
263+
238264
// Initialize server with Docker checks
239265
async function initializeServer() {
240266
console.log('Checking Docker daemon...')
@@ -250,6 +276,11 @@ async function initializeServer() {
250276

251277
await ensureDockerNetwork()
252278

279+
// Build scenario images in background
280+
buildScenarioImages().catch(error => {
281+
console.log('Background scenario build failed:', error.message)
282+
})
283+
253284
server.listen(PORT, () => {
254285
console.log(`DevOps Dojo server running on port ${PORT}`)
255286
})

src/server/index.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ app.use(express.json())
2020
// Serve static frontend files
2121
app.use(express.static(path.join(__dirname, '../public')))
2222

23-
// Catch-all handler for frontend routes
24-
app.get('*', (req, res) => {
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+
}
2529
res.sendFile(path.join(__dirname, '../public/index.html'))
2630
})
2731

@@ -231,6 +235,32 @@ async function ensureDockerNetwork() {
231235

232236
const PORT = process.env.PORT || 3001
233237

238+
// Build scenario images on startup
239+
async function buildScenarioImages() {
240+
console.log('Building scenario images...')
241+
try {
242+
const buildProcess = spawn('make', ['scenario-build'], { cwd: '..' })
243+
244+
await new Promise((resolve, reject) => {
245+
buildProcess.on('exit', (code) => {
246+
if (code === 0) {
247+
console.log('Scenario images built successfully')
248+
resolve(null)
249+
} else {
250+
console.log('Scenario image build failed, will build on demand')
251+
resolve(null) // Don't fail startup if images can't be built
252+
}
253+
})
254+
buildProcess.on('error', (error) => {
255+
console.log('Scenario image build error, will build on demand:', error.message)
256+
resolve(null) // Don't fail startup
257+
})
258+
})
259+
} catch (error) {
260+
console.log('Scenario image build error, will build on demand:', error)
261+
}
262+
}
263+
234264
// Initialize server with Docker checks
235265
async function initializeServer() {
236266
console.log('Checking Docker daemon...')
@@ -246,6 +276,11 @@ async function initializeServer() {
246276

247277
await ensureDockerNetwork()
248278

279+
// Build scenario images in background
280+
buildScenarioImages().catch(error => {
281+
console.log('Background scenario build failed:', error.message)
282+
})
283+
249284
server.listen(PORT, () => {
250285
console.log(`DevOps Dojo server running on port ${PORT}`)
251286
})

0 commit comments

Comments
 (0)