From 4243b5e76f48df92aff11b8b402f3e835f523eec Mon Sep 17 00:00:00 2001 From: Charles Madjeri Date: Thu, 5 Dec 2024 12:28:23 +0100 Subject: [PATCH] feat(docker): add fullstack docker setup Signed-off-by: Charles Madjeri --- .env.example | 9 +++- .gitignore | 2 + Makefile | 2 +- README.md | 36 +++++--------- client_mobile/.env.mobile.example | 14 ------ client_mobile/Dockerfile | 23 ++++++--- client_web/Dockerfile | 65 ++++++++++++++++--------- client_web/nginx.conf | 7 ++- client_web/vite.config.ts | 5 -- docker-compose.yml | 79 ++++++++++++++++++++++++++++--- server/.env.example | 7 --- server/.env.server.example | 7 +++ server/.gitignore | 2 + server/Dockerfile | 31 ++++++++---- server/init.sql | 6 +++ server/main.go | 11 ++++- 16 files changed, 209 insertions(+), 97 deletions(-) delete mode 100644 server/.env.example create mode 100644 server/.env.server.example create mode 100644 server/.gitignore create mode 100644 server/init.sql diff --git a/.env.example b/.env.example index b86b39a..2f46d8c 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,8 @@ -VITE_PORT=8081 \ No newline at end of file +VITE_PORT=8081 + +# Server database environment variables +DB_NAME=area +DB_PASSWORD=change-me +DB_HOST=mariadb +DB_PORT=3306 +DB_USER=root \ No newline at end of file diff --git a/.gitignore b/.gitignore index e4c9141..93ed00d 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,5 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +ssl/ diff --git a/Makefile b/Makefile index 35c5e34..cbefb78 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ TARGET_MAX_CHAR_NUM=20 .PHONY: start build stop restart reset logs clean help -PROJECT_IMAGES = area-client-web area-client-mobile +PROJECT_IMAGES = area-client-web area-client-mobile area-server mariadb rabbitmq ## Show help help: diff --git a/README.md b/README.md index fc47963..2560090 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ With AREA, you can create automated workflows that integrate various services an ## Table of Contents - [Getting Started](#getting-started) - - [Prerequisites](#prerequisites) - - [Installation & Usage](#installation--usage) + - [Prerequisites](#prerequisites) + - [Installation & Usage](#installation--usage) - [Documentation](#documentation) - - [Requirements](#requirements) - - [Usage](#usage) + - [Requirements](#requirements) + - [Usage](#usage) - [Tests](#tests) - [License](#license) - [Contributors](#contributors) @@ -19,9 +19,8 @@ With AREA, you can create automated workflows that integrate various services an ### Prerequisites -- vite js -- go - docker +- make ### Installation & Usage @@ -29,40 +28,30 @@ With AREA, you can create automated workflows that integrate various services an Click to expand 1. Clone the repo + ```sh git clone git@github.com:ASM-Studios/AREA.git ``` 2. Create .env files + - Run the following command to create private env files + ```sh cp .env.example .env +cp server/.env.server.example server/.env.server cp client_web/.env.local.example .env.local cp client_mobile/.env.mobile.example .env.mobile ``` + - Fill the .env, .env.web and .env.mobile files -3. Install NPM packages -```sh -cd AREA/client-web -npm install -``` +4. Run the project -3. Install Go packages ```sh -cd AREA/server - +make start ``` -4. Run the project -```sh -cd AREA/client-web -npm run start -``` -```sh -cd AREA/server -go run ./... -``` ### Documentation @@ -87,6 +76,7 @@ The documentation is automatically built and deployed to GitHub Pages when a pus You can consult the documentation online at [AREA Documentation](https://asm-studios.github.io/AREA/). You can build the documentation locally by running the following command: + ```sh cd AREA/docs make docs diff --git a/client_mobile/.env.mobile.example b/client_mobile/.env.mobile.example index 965d1d9..e538184 100644 --- a/client_mobile/.env.mobile.example +++ b/client_mobile/.env.mobile.example @@ -1,17 +1,3 @@ -VITE_PORT=8081 -VITE_ENDPOINT=http://localhost:8080 - -VITE_GOOGLE_CLIENT_ID= -VITE_GOOGLE_CLIENT_SECRET= - -VITE_MICROSOFT_CLIENT_ID= - -VITE_LINKEDIN_CLIENT_ID= -VITE_LINKEDIN_CLIENT_SECRET= - -VITE_SPOTIFY_CLIENT_ID= -VITE_SPOTIFY_CLIENT_SECRET= - # Server URLs API_URL=http://localhost:8080 WEB_CLIENT_URL=http://localhost:8081 diff --git a/client_mobile/Dockerfile b/client_mobile/Dockerfile index 778bcff..0e479c7 100644 --- a/client_mobile/Dockerfile +++ b/client_mobile/Dockerfile @@ -1,7 +1,20 @@ -FROM ghcr.io/cirruslabs/flutter:stable +FROM ghcr.io/cirruslabs/flutter:3.24.5 AS builder WORKDIR /app +# Fix git permissions and Flutter SDK issues +RUN git config --system --add safe.directory /sdks/flutter && \ + git config --system --add safe.directory /app && \ + chmod -R 777 /sdks/flutter + +# Copy pubspec files first to leverage cache +COPY pubspec.* ./ +RUN flutter pub get + +# Copy the rest of the source code +COPY . . + +# Build arguments ARG VITE_PORT ARG VITE_ENDPOINT ARG VITE_GOOGLE_CLIENT_ID @@ -17,10 +30,8 @@ ARG MOBILE_CLIENT_URL ARG GITHUB_CLIENT_ID ARG GITHUB_CLIENT_SECRET -COPY . . - -RUN flutter pub get +# Build APK RUN flutter build apk --release -RUN mv build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/client.apk -RUN chmod -R 755 build/app/outputs/flutter-apk/ \ No newline at end of file +# Rename the APK +RUN mv build/app/outputs/flutter-apk/app-release.apk build/app/outputs/flutter-apk/client.apk \ No newline at end of file diff --git a/client_web/Dockerfile b/client_web/Dockerfile index 2d06b21..281d36a 100644 --- a/client_web/Dockerfile +++ b/client_web/Dockerfile @@ -1,25 +1,31 @@ ###----------------------- Certificate generation stage -----------------------### -FROM alpine:3.19 AS cert-builder +FROM alpine:3.20.3 AS cert-builder -# Install mkcert dependencies -RUN apk add --no-cache \ - curl \ - nss \ - nss-tools +# Install OpenSSL +RUN apk add --no-cache openssl -# Install mkcert -RUN curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64" \ - && chmod +x mkcert-v*-linux-amd64 \ - && mv mkcert-v*-linux-amd64 /usr/local/bin/mkcert \ - && mkcert -install \ - && mkcert localhost +# Generate self-signed certificate +RUN mkdir -p /etc/nginx/ssl && \ + openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout /etc/nginx/ssl/private.key \ + -out /etc/nginx/ssl/certificate.crt \ + -subj "/C=FR/ST=Paris/L=Paris/O=Area/OU=IT/CN=localhost" \ + -addext "subjectAltName = DNS:localhost" && \ + chmod 644 /etc/nginx/ssl/certificate.crt /etc/nginx/ssl/private.key ###----------------------- Build stage for Node.js application -----------------------### -FROM node:latest AS builder +FROM node:20-alpine AS builder WORKDIR /app +# Copy package files first to leverage cache +COPY package*.json ./ +RUN npm ci + +# Copy source files and build arguments +COPY . . + ARG VITE_PORT ARG VITE_ENDPOINT ARG VITE_GOOGLE_CLIENT_ID @@ -35,20 +41,35 @@ ARG MOBILE_CLIENT_URL ARG GITHUB_CLIENT_ID ARG GITHUB_CLIENT_SECRET -COPY ./package*.json ./ -RUN npm install -COPY . . - RUN npm run build ###----------------------- Production stage -----------------------### -FROM nginx:alpine AS production +FROM nginx:1.25.3-alpine + +# Create non-root user and set up directories +RUN adduser -D nginxuser && \ + mkdir -p /usr/share/nginx/html/mobile_builds && \ + mkdir -p /etc/nginx/ssl && \ + chown -R nginxuser:nginxuser /usr/share/nginx/html && \ + chown -R nginxuser:nginxuser /etc/nginx/ssl && \ + chown -R nginxuser:nginxuser /var/cache/nginx && \ + chown -R nginxuser:nginxuser /var/log/nginx && \ + touch /var/run/nginx.pid && \ + chown -R nginxuser:nginxuser /var/run/nginx.pid + +# Copy SSL certificates with proper permissions +COPY --from=cert-builder --chown=nginxuser:nginxuser /etc/nginx/ssl /etc/nginx/ssl + +# Copy built files and configuration +COPY --from=builder --chown=nginxuser:nginxuser /app/dist /usr/share/nginx/html +COPY --chown=nginxuser:nginxuser ./nginx.conf /etc/nginx/conf.d/default.conf + +USER nginxuser -COPY --from=builder /app/dist /usr/share/nginx/html -COPY ./nginx.conf /etc/nginx/conf.d/default.conf -RUN mkdir -p /usr/share/nginx/html/mobile_builds +EXPOSE 8081 -EXPOSE ${VITE_PORT} +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8081/health || exit 1 CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/client_web/nginx.conf b/client_web/nginx.conf index 036ee9a..32e8d6e 100644 --- a/client_web/nginx.conf +++ b/client_web/nginx.conf @@ -1,7 +1,12 @@ server { - listen 8081; + listen 8081 ssl; server_name localhost; + ssl_certificate /etc/nginx/ssl/certificate.crt; + ssl_certificate_key /etc/nginx/ssl/private.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + location / { root /usr/share/nginx/html; index index.html index.htm; diff --git a/client_web/vite.config.ts b/client_web/vite.config.ts index 1371208..42b73cd 100644 --- a/client_web/vite.config.ts +++ b/client_web/vite.config.ts @@ -1,6 +1,5 @@ import { defineConfig, loadEnv } from 'vite' import react from '@vitejs/plugin-react' -import fs from 'fs' import path from 'path' export default defineConfig(({ mode }) => { @@ -10,10 +9,6 @@ export default defineConfig(({ mode }) => { plugins: [react()], server: { port: parseInt(env.VITE_PORT) || 8081, - https: { - key: fs.readFileSync(path.resolve(__dirname, 'localhost-key.pem')), - cert: fs.readFileSync(path.resolve(__dirname, 'localhost.pem')), - }, }, resolve: { alias: { diff --git a/docker-compose.yml b/docker-compose.yml index f8dde58..f76cb60 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,67 @@ services: + rabbitmq: + image: rabbitmq:4.0.4-management-alpine + ports: + - "8082:15672" + - "5000:5673" + networks: + - area_network + volumes: + - ./rabbit-mq/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro + healthcheck: + test: [ "CMD", "rabbitmqctl", "status" ] + interval: 5s + timeout: 15s + retries: 5 + restart: unless-stopped + + mariadb: + image: mariadb:11.4.4 + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} + MYSQL_DATABASE: ${DB_NAME} + MYSQL_ROOT_HOST: "%" + volumes: + - mariadb_data:/var/lib/mysql + - ./server/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + networks: + - area_network + healthcheck: + test: [ "CMD", "mariadb-admin", "ping", "-h", "localhost", "-u", "root", "-p${DB_PASSWORD}" ] + interval: 10s + timeout: 5s + retries: 5 + env_file: + - .env + restart: unless-stopped + + area-server: + build: + context: ./server + dockerfile: Dockerfile + ports: + - "8080:8080" + volumes: + - ./ssl:/app/ssl:ro + environment: + DB_HOST: ${DB_HOST} + DB_PORT: ${DB_PORT} + DB_USER: ${DB_USER} + DB_NAME: ${DB_NAME} + DB_PASSWORD: ${DB_PASSWORD} + depends_on: + mariadb: + condition: service_healthy + rabbitmq: + condition: service_healthy + networks: + - area_network + env_file: + - server/.env.server + restart: unless-stopped + area-client-mobile: build: context: ./client_mobile @@ -19,9 +82,9 @@ services: - GITHUB_CLIENT_ID - GITHUB_CLIENT_SECRET volumes: - - area-client-data:/app/build/app/outputs/flutter-apk + - area_client_data:/app/build/app/outputs/flutter-apk networks: - - area-network + - area_network env_file: - ./client_mobile/.env.mobile @@ -47,17 +110,21 @@ services: ports: - "${VITE_PORT}:${VITE_PORT}" volumes: - - area-client-data:/usr/share/nginx/html/mobile_builds + - area_client_data:/usr/share/nginx/html/mobile_builds:ro depends_on: - area-client-mobile + - area-server networks: - - area-network + - area_network env_file: - ./client_web/.env.local + restart: unless-stopped volumes: - area-client-data: + area_client_data: + mariadb_data: networks: - area-network: + area_network: + driver: bridge diff --git a/server/.env.example b/server/.env.example deleted file mode 100644 index 510adf3..0000000 --- a/server/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -SECRET_KEY= -DB_HOST= -DB_PORT= -DB_NAME= -DB_USER= -DB_PASSWORD= -RMQ_URL= diff --git a/server/.env.server.example b/server/.env.server.example new file mode 100644 index 0000000..d544f4a --- /dev/null +++ b/server/.env.server.example @@ -0,0 +1,7 @@ +SECRET_KEY="change-this-secret-key" +DB_HOST=localhost +DB_PORT=3306 +DB_NAME=area +DB_USER=root +DB_PASSWORD=change-me +RMQ_URL="amqp://root:root@localhost:5672/" \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..796fa7f --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,2 @@ +.env +.env.server diff --git a/server/Dockerfile b/server/Dockerfile index cc75960..e1f66cf 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,22 +1,35 @@ -FROM golang:1.23.3-alpine as builder +FROM golang:1.23.4-alpine3.20 AS builder + +RUN apk add --no-cache gcc musl-dev + WORKDIR /app -COPY . . -RUN go mod tidy +COPY go.mod go.sum ./ RUN go mod download -RUN go get -u github.com/swaggo/swag -RUN go get -u github.com/gin-gonic/gin -RUN go build -o main . +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . -FROM debian:bullseye-slim +FROM alpine:3.20.3 + +RUN apk add --no-cache ca-certificates && \ + adduser -D appuser WORKDIR /app + COPY --from=builder /app/main . -COPY --from=builder /app/.env . COPY --from=builder /app/config.json . +COPY .env.server .env + +RUN chown -R appuser:appuser /app && \ + chmod +x /app/main -RUN chmod +x /app/main +USER appuser EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 + CMD ["./main"] diff --git a/server/init.sql b/server/init.sql new file mode 100644 index 0000000..ea08740 --- /dev/null +++ b/server/init.sql @@ -0,0 +1,6 @@ +CREATE DATABASE IF NOT EXISTS area; +USE area; + +-- Grant all privileges to root user from any host +GRANT ALL PRIVILEGES ON area.* TO 'root'@'%' IDENTIFIED BY 'root'; +FLUSH PRIVILEGES; \ No newline at end of file diff --git a/server/main.go b/server/main.go index bfb0bef..cd03209 100644 --- a/server/main.go +++ b/server/main.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/gin-gonic/gin" "log" + "net/http" "strconv" ) @@ -36,7 +37,13 @@ func main() { router := routers.SetupRouter() port := strconv.Itoa(config.AppConfig.Port) log.Printf("Starting %s on port %s in %s mode", config.AppConfig.AppName, port, config.AppConfig.GinMode) - if err := router.Run(fmt.Sprintf(":%s", port)); err != nil { - log.Fatalf("Failed to start server: %v", err) + + server := &http.Server{ + Addr: fmt.Sprintf(":%s", port), + Handler: router, + } + + if err := server.ListenAndServe(); err != nil { + log.Printf("Server error: %v", err) } }