diff --git a/.github/workflows/typescript-ci.yml b/.github/workflows/typescript-ci.yml index ccf2190..024549c 100644 --- a/.github/workflows/typescript-ci.yml +++ b/.github/workflows/typescript-ci.yml @@ -180,3 +180,46 @@ jobs: else echo "⚠️ No Schemathesis report found" fi + + docker-build-test: + runs-on: ubuntu-latest + needs: build-and-test + permissions: + contents: read + defaults: + run: + working-directory: src/typescript + + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: | + # Copy the OpenAPI spec to the expected location for Docker build + mkdir -p docs/api + cp ../../docs/api/openapi.yaml ./docs/api/ + docker build -t lamp-control-api-ts:latest . + + - name: Test Docker container + run: | + # Start container in background + docker run --rm -d -p 8080:8080 --name lamp-api-test lamp-control-api-ts:latest + + # Wait for container to be ready + timeout 30 bash -c 'until docker exec lamp-api-test curl -f http://127.0.0.1:8080/health > /dev/null 2>&1; do sleep 2; done' + + # Test health endpoint + docker exec lamp-api-test curl -s http://127.0.0.1:8080/health | grep '"status":"ok"' + + # Test API endpoint + docker exec lamp-api-test curl -s http://127.0.0.1:8080/v1/lamps | grep '"data":\[\]' + + # Stop container + docker stop lamp-api-test + + echo "✅ Docker container tests passed!" + + - name: Check Docker image size + run: | + echo "## Docker Image Information" >> $GITHUB_STEP_SUMMARY + docker images lamp-control-api-ts:latest --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" >> $GITHUB_STEP_SUMMARY diff --git a/src/typescript/.dockerignore b/src/typescript/.dockerignore new file mode 100644 index 0000000..fa1b7eb --- /dev/null +++ b/src/typescript/.dockerignore @@ -0,0 +1,35 @@ +# Ignore build artifacts +dist/ +coverage/ + +# Ignore development files +.git +.gitignore +*.md +README.md + +# Ignore test files +tests/ +*.test.ts +*.test.js + +# Ignore configuration files not needed in production +.eslintrc.json +.prettierrc +jest.config.js + +# Ignore documentation +# docs/ + +# Ignore CI/CD files +.github/ + +# Ignore development artifacts +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local \ No newline at end of file diff --git a/src/typescript/Dockerfile b/src/typescript/Dockerfile new file mode 100644 index 0000000..387cf48 --- /dev/null +++ b/src/typescript/Dockerfile @@ -0,0 +1,44 @@ +# Multi-stage Docker configuration for TypeScript implementation +# Stage 1: Build environment +FROM node:22 AS build-env + +WORKDIR /app + +# Copy package files for dependency installation +COPY package*.json ./ + +# Install all dependencies using npm ci for reproducible builds +RUN npm ci + +# Copy all source files +COPY src ./src +COPY tsconfig.json ./ + +# Create the directory structure and copy OpenAPI spec from context root +RUN mkdir -p /docs/api +COPY docs/api/openapi.yaml /docs/api/openapi.yaml + +# Build the TypeScript application +RUN npm run build + +# Remove development dependencies to reduce final image size +RUN npm prune --omit=dev + +# Stage 2: Production runtime +FROM gcr.io/distroless/nodejs22-debian12 + +WORKDIR /app + +# Copy built application and production dependencies from build stage +COPY --from=build-env /app/dist ./dist +COPY --from=build-env /app/node_modules ./node_modules +COPY --from=build-env /app/package*.json ./ + +# Copy the OpenAPI spec from build stage to the expected location +COPY --from=build-env /docs/api/openapi.yaml /docs/api/openapi.yaml + +# Expose the application port +EXPOSE 8080 + +# Set the command to run the application +CMD ["dist/src/index.js"] \ No newline at end of file diff --git a/src/typescript/README.md b/src/typescript/README.md index fe8e01b..4dc7c6f 100644 --- a/src/typescript/README.md +++ b/src/typescript/README.md @@ -112,6 +112,26 @@ npm run start:native This eliminates the build step entirely while maintaining production performance. +### Docker Deployment + +Build and run with Docker using the multi-stage Dockerfile: + +```bash +# Build the Docker image +docker build -t lamp-control-api-ts . + +# Run the container +docker run --rm -p 8080:8080 lamp-control-api-ts +``` + +The Docker configuration uses: +- **Multi-stage build**: Optimized for production with separate build and runtime stages +- **Node.js 22**: Build stage for compilation and dependency installation +- **Distroless runtime**: Minimal, secure production runtime environment +- **Optimized layers**: Efficient Docker layer caching for faster builds + +The container exposes the API on port 8080 and includes all necessary dependencies and the OpenAPI specification. + ## API Documentation The API documentation is available at `http://localhost:8080/api-docs` when the server is running. diff --git a/src/typescript/package.json b/src/typescript/package.json index baff226..87140d5 100644 --- a/src/typescript/package.json +++ b/src/typescript/package.json @@ -13,7 +13,7 @@ "clean": "rm -rf dist", "dev": "tsx src/index.ts", "dev:native": "node --experimental-strip-types src/index.ts", - "start": "node dist/index.js", + "start": "node dist/src/index.js", "start:native": "node --experimental-strip-types src/index.ts", "test": "NODE_OPTIONS=--experimental-vm-modules jest", "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch", diff --git a/src/typescript/src/index.ts b/src/typescript/src/index.ts index c925e73..d8b7383 100644 --- a/src/typescript/src/index.ts +++ b/src/typescript/src/index.ts @@ -2,7 +2,7 @@ import { buildApp } from './infrastructure/app.ts'; import type { FastifyInstance } from 'fastify'; buildApp().then((server: FastifyInstance) => { - server.listen({ port: 8080 }, (err: Error | null, address: string) => { + server.listen({ port: 8080, host: '0.0.0.0' }, (err: Error | null, address: string) => { if (err) { server.log.error(err); process.exit(1); diff --git a/src/typescript/tests/api.test.ts b/src/typescript/tests/api.test.ts index 183f094..2a53a79 100644 --- a/src/typescript/tests/api.test.ts +++ b/src/typescript/tests/api.test.ts @@ -13,7 +13,9 @@ describe('Lamp API Endpoints', () => { }); afterAll(async () => { - await app.close(); + if (app) { + await app.close(); + } }); describe('GET /health', () => {