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:
Bas van Rossem
2026-02-19 16:30:17 +01:00
parent 510820b77a
commit 9ef1199324
4 changed files with 96 additions and 2 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
node_modules
.git
data
*.pdf
*.sqlite
*.sqlite-journal
*.sqlite-wal
server/dist
web/dist

43
Dockerfile Normal file
View 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
View 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

View File

@@ -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);