From e52bd2042eaeb6b8dc6dc9294cdc17438da2d6f9 Mon Sep 17 00:00:00 2001 From: Rostislav Dugin Date: Sun, 8 Jun 2025 13:58:13 +0300 Subject: [PATCH] FEATURE (docker): Build project for sending to Docker hub --- .env.example | 4 - .github/workflows/docker.yml | 36 +++++++ Dockerfile | 100 ++++++++++++++++++ backend/.env.development.example | 13 +++ .../{.env.example => .env.production.example} | 0 backend/.gitignore | 3 +- backend/Dockerfile | 44 -------- backend/cmd/main.go | 70 ++++++++---- backend/ui/readme.md | 2 + docker-compose.yml | 23 ++-- frontend/.env.development.example | 1 + frontend/.env.production.example | 1 + frontend/Dockerfile | 14 --- frontend/src/constants.ts | 9 +- 14 files changed, 217 insertions(+), 103 deletions(-) delete mode 100644 .env.example create mode 100644 .github/workflows/docker.yml create mode 100644 Dockerfile create mode 100644 backend/.env.development.example rename backend/{.env.example => .env.production.example} (100%) delete mode 100644 backend/Dockerfile create mode 100644 backend/ui/readme.md create mode 100644 frontend/.env.development.example create mode 100644 frontend/.env.production.example delete mode 100644 frontend/Dockerfile diff --git a/.env.example b/.env.example deleted file mode 100644 index f829030..0000000 --- a/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -# docker-compose.yml -DB_NAME=postgresus -DB_USERNAME=postgres -DB_PASSWORD=Q1234567 \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..00c987f --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,36 @@ +name: Build & push Docker image + +on: + push: + branches: [main] + workflow_dispatch: {} + +jobs: + docker: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up QEMU (enables multi-arch emulation) + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + platforms: linux/amd64,linux/arm64 # both chip families + tags: | + rostislavdugin/postgresus:latest + rostislavdugin/postgresus:${{ github.sha }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d5b024f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,100 @@ +# ========= BUILD FRONTEND ========= +FROM --platform=$BUILDPLATFORM node:24-alpine AS frontend-build + +WORKDIR /frontend + +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci +COPY frontend/ ./ + +# Copy .env file (with fallback to .env.production.example) +RUN if [ ! -f .env ]; then \ + if [ -f .env.production.example ]; then \ + cp .env.production.example .env; \ + fi; \ + fi + +RUN npm run build + +# ========= BUILD BACKEND ========= +FROM --platform=$BUILDPLATFORM golang:1.23.3 AS backend-build + +# Install Go public tools needed in runtime +RUN curl -fsSL https://raw.githubusercontent.com/pressly/goose/master/install.sh | sh +RUN go install github.com/swaggo/swag/cmd/swag@latest + +# Set working directory +WORKDIR /app + +# Install Go dependencies +COPY backend/go.mod backend/go.sum ./ +RUN go mod download + +# Create required directories for embedding +RUN mkdir -p /app/ui/build + +# Copy frontend build output for embedding +COPY --from=frontend-build /frontend/dist /app/ui/build + +# Generate Swagger documentation +COPY backend/ ./ +RUN swag init -d . -g cmd/main.go -o swagger + +# Compile the backend +ARG TARGETOS +ARG TARGETARCH +ARG TARGETVARIANT +RUN CGO_ENABLED=0 \ + GOOS=$TARGETOS \ + GOARCH=$TARGETARCH \ + go build -o /app/main ./cmd/main.go + + +# ========= RUNTIME ========= +FROM --platform=$TARGETPLATFORM debian:bookworm-slim + +# Install PostgreSQL client tools (versions 13-17) +RUN apt-get update && apt-get install -y --no-install-recommends \ + wget ca-certificates gnupg lsb-release && \ + wget -qO- https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \ + echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \ + > /etc/apt/sources.list.d/pgdg.list && \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + postgresql-client-13 postgresql-client-14 postgresql-client-15 \ + postgresql-client-16 postgresql-client-17 && \ + rm -rf /var/lib/apt/lists/* + +# Create symlinks for PostgreSQL client tools +RUN for v in 13 14 15 16 17; do \ + mkdir -p /usr/pgsql-$v/bin && \ + for b in pg_dump psql pg_restore createdb dropdb; do \ + ln -sf /usr/bin/$b /usr/pgsql-$v/bin/$b; \ + done; \ + done + +WORKDIR /app + +# Copy Goose from build stage +COPY --from=backend-build /usr/local/bin/goose /usr/local/bin/goose + +# Copy app binary +COPY --from=backend-build /app/main . + +# Copy migrations directory +COPY backend/migrations ./migrations + +# Copy UI files +COPY --from=backend-build /app/ui/build ./ui/build + +# Copy .env file (with fallback to .env.production.example) +COPY backend/.env* /app/ +RUN if [ ! -f /app/.env ]; then \ + if [ -f /app/.env.production.example ]; then \ + cp /app/.env.production.example /app/.env; \ + fi; \ + fi + +EXPOSE 4005 + +CMD ["./main"] diff --git a/backend/.env.development.example b/backend/.env.development.example new file mode 100644 index 0000000..65a6547 --- /dev/null +++ b/backend/.env.development.example @@ -0,0 +1,13 @@ +# docker-compose.yml +DEV_DB_NAME=postgresus +DEV_DB_USERNAME=postgres +DEV_DB_PASSWORD=Q1234567 +#app +ENV_MODE=development +# db +DATABASE_DSN=host=dev-db user=postgres password=Q1234567 dbname=postgresus port=5437 sslmode=disable +DATABASE_URL=postgres://postgres:Q1234567@dev-db:5437/postgresus?sslmode=disable +# migrations +GOOSE_DRIVER=postgres +GOOSE_DBSTRING=postgres://postgres:Q1234567@dev-db:5437/postgresus?sslmode=disable +GOOSE_MIGRATION_DIR=./migrations \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.production.example similarity index 100% rename from backend/.env.example rename to backend/.env.production.example diff --git a/backend/.gitignore b/backend/.gitignore index c267f00..5d8760c 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -9,4 +9,5 @@ swagger/* swagger/docs.go swagger/swagger.json swagger/swagger.yaml -postgresus-backend.exe \ No newline at end of file +postgresus-backend.exe +ui/build/* \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile deleted file mode 100644 index 600ead7..0000000 --- a/backend/Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -FROM golang:1.23.3 - -EXPOSE 4005 - -WORKDIR /app - -# Install PostgreSQL APT repository and all versions 13-17 -RUN apt-get update && apt-get install -y wget ca-certificates gnupg lsb-release -RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - -RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list -RUN apt-get update - -# Install PostgreSQL client tools for versions 13-17 -RUN apt-get install -y \ - postgresql-client-13 \ - postgresql-client-14 \ - postgresql-client-15 \ - postgresql-client-16 \ - postgresql-client-17 - -# Create symlinks to match expected paths (/usr/pgsql-{VERSION}/bin) -RUN for version in 13 14 15 16 17; do \ - mkdir -p /usr/pgsql-${version}/bin; \ - ln -sf /usr/bin/pg_dump /usr/pgsql-${version}/bin/pg_dump; \ - ln -sf /usr/bin/psql /usr/pgsql-${version}/bin/psql; \ - ln -sf /usr/bin/pg_restore /usr/pgsql-${version}/bin/pg_restore; \ - ln -sf /usr/bin/createdb /usr/pgsql-${version}/bin/createdb; \ - ln -sf /usr/bin/dropdb /usr/pgsql-${version}/bin/dropdb; \ - done - -# Install global dependencies -RUN curl -fsSL https://raw.githubusercontent.com/pressly/goose/master/install.sh | sh -RUN goose -version -RUN go install github.com/swaggo/swag/cmd/swag@latest - -COPY go.mod go.sum ./ -RUN go mod download - -COPY . . - -RUN swag init -d . -g cmd/main.go -o swagger -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main ./cmd/main.go - -CMD ["./main"] \ No newline at end of file diff --git a/backend/cmd/main.go b/backend/cmd/main.go index c2f21a6..c9678ea 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -3,14 +3,14 @@ package main import ( "context" "log/slog" + "net/http" "os" "os/exec" "os/signal" + "path/filepath" "syscall" "time" - "net/http" - "postgresus-backend/internal/config" "postgresus-backend/internal/downdetect" "postgresus-backend/internal/features/backups" @@ -51,30 +51,11 @@ func main() { gin.SetMode(gin.ReleaseMode) ginApp := gin.Default() - // Setup CORS - ginApp.Use(cors.New(cors.Config{ - AllowOrigins: []string{"*"}, - AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}, - AllowHeaders: []string{ - "Origin", - "Content-Length", - "Content-Type", - "Authorization", - "Accept", - "Accept-Language", - "Accept-Encoding", - "Access-Control-Request-Method", - "Access-Control-Request-Headers", - "Access-Control-Allow-Methods", - "Access-Control-Allow-Headers", - "Access-Control-Allow-Origin", - }, - AllowCredentials: true, - })) - + enableCors(ginApp) setUpRoutes(ginApp) setUpDependencies() runBackgroundTasks(log) + mountFrontend(ginApp) startServerWithGracefulShutdown(log, ginApp) } @@ -171,6 +152,10 @@ func runWithPanicLogging(log *slog.Logger, serviceName string, fn func()) { // is generated into Go files. So if we changed files, we generate // new docs, but still need to restart the server to see them. func generateSwaggerDocs(log *slog.Logger) { + if config.GetEnv().EnvMode == env_utils.EnvModeProduction { + return + } + // Run swag from the current directory instead of parent // Use the current directory as the base for swag init // This ensures swag can find the files regardless of where the command is run from @@ -212,3 +197,42 @@ func runMigrations(log *slog.Logger) { log.Info("Database migrations completed successfully", "output", string(output)) } + +func enableCors(ginApp *gin.Engine) { + if config.GetEnv().EnvMode == env_utils.EnvModeDevelopment { + // Setup CORS + ginApp.Use(cors.New(cors.Config{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}, + AllowHeaders: []string{ + "Origin", + "Content-Length", + "Content-Type", + "Authorization", + "Accept", + "Accept-Language", + "Accept-Encoding", + "Access-Control-Request-Method", + "Access-Control-Request-Headers", + "Access-Control-Allow-Methods", + "Access-Control-Allow-Headers", + "Access-Control-Allow-Origin", + }, + AllowCredentials: true, + })) + } +} + +func mountFrontend(ginApp *gin.Engine) { + staticDir := "./ui/build" + ginApp.NoRoute(func(c *gin.Context) { + path := filepath.Join(staticDir, c.Request.URL.Path) + + if info, err := os.Stat(path); err == nil && !info.IsDir() { + c.File(path) + return + } + + c.File(filepath.Join(staticDir, "index.html")) + }) +} diff --git a/backend/ui/readme.md b/backend/ui/readme.md new file mode 100644 index 0000000..8974d9f --- /dev/null +++ b/backend/ui/readme.md @@ -0,0 +1,2 @@ +In production during the build process, the ui is built +and will be placed in this folder under /build \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 699c1db..01478b3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,30 +1,21 @@ version: "3" services: - postgresus-frontend: + postgresus: build: - context: ./frontend - dockerfile: Dockerfile - ports: - - "4006:4006" - - postgresus-backend: - build: - context: ./backend + context: . dockerfile: Dockerfile ports: - "4005:4005" - volumes: - - ./postgresus-data:/postgresus-data postgresus-db: - env_file: - - .env image: postgres:17 + # we use default values, but do not expose + # PostgreSQL ports so it is safe environment: - - POSTGRES_DB=${DB_NAME} - - POSTGRES_USER=${DB_USERNAME} - - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=postgresus + - POSTGRES_USER=postgresus + - POSTGRES_PASSWORD=postgresus volumes: - ./pgdata:/var/lib/postgresql/data container_name: postgresus-db diff --git a/frontend/.env.development.example b/frontend/.env.development.example new file mode 100644 index 0000000..b3ec4c9 --- /dev/null +++ b/frontend/.env.development.example @@ -0,0 +1 @@ +MODE=development \ No newline at end of file diff --git a/frontend/.env.production.example b/frontend/.env.production.example new file mode 100644 index 0000000..923c1a2 --- /dev/null +++ b/frontend/.env.production.example @@ -0,0 +1 @@ +MODE=production \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile deleted file mode 100644 index eda55d9..0000000 --- a/frontend/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM node:18-alpine - -EXPOSE 4006 - -WORKDIR /app - -COPY package*.json ./ -RUN npm ci && npm install -g serve - -COPY . . - -RUN npm run build - -CMD ["serve", "-s", "dist", "-l", "4006"] diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts index c7f7a22..7abd24c 100644 --- a/frontend/src/constants.ts +++ b/frontend/src/constants.ts @@ -1,5 +1,12 @@ export function getApplicationServer() { const origin = window.location.origin; const url = new URL(origin); - return `${url.protocol}//${url.hostname}:4005`; + + const isDevelopment = import.meta.env.MODE === 'development'; + + if (isDevelopment) { + return `${url.protocol}//${url.hostname}:4005`; + } else { + return `${url.protocol}//${url.hostname}:${url.port || (url.protocol === 'https:' ? '443' : '80')}`; + } }