chore: add Dockerfile and docker-compose with static file serving
Multi-stage Docker build (web → server → production). Fastify serves React build in production with SPA fallback. Docker Compose with volume for SQLite persistence. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
9
.dockerignore
Normal file
9
.dockerignore
Normal file
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
.git
|
||||
data
|
||||
*.pdf
|
||||
*.sqlite
|
||||
*.sqlite-journal
|
||||
*.sqlite-wal
|
||||
server/dist
|
||||
web/dist
|
||||
43
Dockerfile
Normal file
43
Dockerfile
Normal file
@@ -0,0 +1,43 @@
|
||||
# ---- Stage 1: Build the React frontend ----
|
||||
FROM node:18-alpine AS web-build
|
||||
WORKDIR /app/web
|
||||
COPY web/package.json web/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY web/ ./
|
||||
RUN npm run build
|
||||
|
||||
# ---- Stage 2: Build the Node.js server ----
|
||||
FROM node:18-alpine AS server-build
|
||||
WORKDIR /app/server
|
||||
COPY server/package.json server/package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY server/ ./
|
||||
RUN npm run build
|
||||
|
||||
# ---- Stage 3: Production image ----
|
||||
FROM node:18-alpine AS production
|
||||
WORKDIR /app
|
||||
|
||||
# Copy server package files and install production deps only
|
||||
COPY server/package.json server/package-lock.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# Copy compiled server
|
||||
COPY --from=server-build /app/server/dist ./dist
|
||||
|
||||
# Copy server migration files (needed at runtime)
|
||||
COPY --from=server-build /app/server/src/db/migrations ./dist/db/migrations
|
||||
|
||||
# Copy frontend build output
|
||||
COPY --from=web-build /app/web/dist ./web-dist
|
||||
|
||||
# Create data directory for SQLite
|
||||
RUN mkdir -p /data
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
ENV DB_PATH=/data/spelljammer.sqlite
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
21
docker-compose.yml
Normal file
21
docker-compose.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
spelljammer:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: spelljammer-ships
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- spelljammer-data:/data
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- DB_PATH=/data/spelljammer.sqlite
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
spelljammer-data:
|
||||
driver: local
|
||||
@@ -1,7 +1,10 @@
|
||||
import { createServer } from 'node:http';
|
||||
import { join } from 'node:path';
|
||||
import { existsSync } from 'node:fs';
|
||||
import Fastify from 'fastify';
|
||||
import cors from '@fastify/cors';
|
||||
import { PORT } from './config.js';
|
||||
import fastifyStatic from '@fastify/static';
|
||||
import { PORT, NODE_ENV } from './config.js';
|
||||
import { runMigrations } from './db/migrate.js';
|
||||
import { healthRoutes } from './routes/health.js';
|
||||
import { shipRoutes } from './routes/ships.js';
|
||||
@@ -27,8 +30,26 @@ const start = async () => {
|
||||
await app.register(shipRoutes);
|
||||
await app.register(weaponRoutes);
|
||||
|
||||
// In production, serve the React build as static files
|
||||
const webDistPath = join(process.cwd(), 'web-dist');
|
||||
if (NODE_ENV === 'production' && existsSync(webDistPath)) {
|
||||
await app.register(fastifyStatic, {
|
||||
root: webDistPath,
|
||||
prefix: '/',
|
||||
wildcard: false,
|
||||
});
|
||||
|
||||
// SPA fallback: non-API GET requests return index.html
|
||||
app.setNotFoundHandler((request, reply) => {
|
||||
if (request.method === 'GET' && !request.url.startsWith('/api')) {
|
||||
return reply.sendFile('index.html');
|
||||
}
|
||||
reply.code(404).send({ error: 'Not found' });
|
||||
});
|
||||
}
|
||||
|
||||
await app.listen({ port: PORT, host: '0.0.0.0' });
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
console.log(`Server running on port ${PORT} (${NODE_ENV})`);
|
||||
} catch (err) {
|
||||
app.log.error(err);
|
||||
process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user