From 6f3c773dfd4c31335e41ee45326bb686ae872694 Mon Sep 17 00:00:00 2001 From: dnviti Date: Sun, 14 Dec 2025 22:48:41 +0100 Subject: [PATCH] feat: Add Docker containerization, production serving, and Gitea CI/CD workflow. --- .dockerignore | 8 ++++++++ .gitea/workflows/build.yml | 40 ++++++++++++++++++++++++++++++++++++++ Dockerfile | 27 +++++++++++++++++++++++++ src/package.json | 8 ++++---- src/server/index.ts | 20 +++++++++++++++++++ 5 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitea/workflows/build.yml create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..62b2fc1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +node_modules +src/node_modules +src/dist +src/client/dist +.env +.DS_Store +coverage diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..aa53544 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,40 @@ +name: Build and Deploy + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Gitea Container Registry + if: github.event_name == 'push' + uses: docker/login-action@v3 + with: + registry: ${{ vars.PACKAGES_REGISTRY }} + username: ${{ secrets.USERNAME }} + password: ${{ secrets.TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ vars.PACKAGES_REGISTRY }}/${{ gitea.repository }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name == 'push' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0e560e9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# Use Node.js LTS (Alpine for smaller size) +FROM node:20-alpine + +# Set working directory +WORKDIR /app + +# Copy package definition first to cache dependencies +COPY src/package.json src/package-lock.json ./ + +# Install dependencies +# Using npm install instead of ci to ensure updated package.json is respected +RUN npm install + +# Copy the rest of the source code +COPY src/ ./ + +# Build the frontend (Production build) +RUN npm run build + +# Remove development dependencies to keep image small +RUN npm prune --production + +# Expose the application port +EXPOSE 3000 + +# Start the application +CMD ["npm", "start"] diff --git a/src/package.json b/src/package.json index 53ff179..d883514 100644 --- a/src/package.json +++ b/src/package.json @@ -8,7 +8,7 @@ "server": "tsx watch server/index.ts", "client": "vite", "build": "tsc && vite build", - "start": "node server/dist/index.js" + "start": "NODE_ENV=production tsx server/index.ts" }, "dependencies": { "express": "^4.21.2", @@ -16,7 +16,8 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "socket.io": "^4.8.1", - "socket.io-client": "^4.8.1" + "socket.io-client": "^4.8.1", + "tsx": "^4.19.2" }, "devDependencies": { "@types/express": "^4.17.21", @@ -28,8 +29,7 @@ "concurrently": "^9.1.0", "postcss": "^8.4.49", "tailwindcss": "^3.4.16", - "tsx": "^4.19.2", "typescript": "^5.7.2", "vite": "^6.0.3" } -} +} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 06b7bf1..d9a71ba 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -36,6 +36,13 @@ app.get('/api/health', (_req: Request, res: Response) => { res.json({ status: 'ok', message: 'Server is running' }); }); +// Serve Frontend in Production +if (process.env.NODE_ENV === 'production') { + const distPath = path.resolve(process.cwd(), 'dist'); + app.use(express.static(distPath)); + +} + app.post('/api/cards/cache', async (req: Request, res: Response) => { try { const { cards } = req.body; @@ -201,6 +208,19 @@ io.on('connection', (socket) => { }); }); + +// Handle Client-Side Routing (Catch-All) - Must be last +if (process.env.NODE_ENV === 'production') { + app.get('*', (_req: Request, res: Response) => { + // Check if request is for API + if (_req.path.startsWith('/api') || _req.path.startsWith('/socket.io')) { + return res.status(404).json({ error: 'Not found' }); + } + const distPath = path.resolve(process.cwd(), 'dist'); + res.sendFile(path.join(distPath, 'index.html')); + }); +} + import os from 'os'; httpServer.listen(Number(PORT), '0.0.0.0', () => {