From 2027bdb906fe3713c421bd2c2e4f595afe34a1ba Mon Sep 17 00:00:00 2001 From: Tushar Khatri Date: Wed, 18 Feb 2026 23:26:42 +0530 Subject: [PATCH] fix: replace removed bitnami/kafka with apache/kafka, production-harden deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch Kafka image from bitnami/kafka:3.7 (removed from Docker Hub) to apache/kafka:latest (official KRaft image) - Convert Bitnami KAFKA_CFG_* env vars to native KAFKA_* format - Rewrite Dockerfile as 3-stage build: Node (dashboard) → Maven (JAR) → JRE runtime — serves both API and dashboard on :8080 - Add .dockerignore to exclude .git, node_modules, ai-docs from builds - Use eclipse-temurin:21-jre instead of jdk (saves ~300MB) - Run app as non-root user (eventara) - Add resource limits to prod compose (postgres 1G, kafka 1G, redis 512M, eventara 1G) - Add healthcheck start_period for slower VPS environments - Remove SPRING_JPA_HIBERNATE_DDL_AUTO override that conflicted with Flyway in dev compose - Rewrite DEPLOYMENT.md with Coolify, DigitalOcean, and VPS guides - Add EVENTARA_PORT and JAVA_OPTS to .env.example --- .dockerignore | 35 +++++++++++++++ .env.example | 10 ++++- DEPLOYMENT.md | 92 ++++++++++++++++++++++++++++------------ Dockerfile | 35 +++++++++------ README.md | 2 +- docker-compose.prod.yaml | 63 ++++++++++++++++++--------- docker-compose.yaml | 48 ++++++++++----------- 7 files changed, 198 insertions(+), 87 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..49d58fe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,35 @@ +# === Build artifacts === +target/ +!target/*.jar + +# === Dashboard build deps (handled inside Docker) === +eventara-dashboard/node_modules/ +eventara-dashboard/dist/ + +# === IDE / editor === +.idea/ +*.iml +*.iws +.vscode/ +*.swp +*.swo +*~ + +# === OS junk === +.DS_Store +Thumbs.db + +# === Git === +.git/ +.gitignore + +# === Docs & non-essential === +ai-docs/ +*.md +LICENSE + +# === Docker (don't send compose files into the build) === +docker-compose*.yaml +.env +.env.example +Dockerfile diff --git a/.env.example b/.env.example index a3efdf3..519f592 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,7 @@ POSTGRES_PASSWORD=mysecretpassword POSTGRES_DB=eventara # Required for Kafka KRaft mode. Do not change after first boot unless you wipe the kafka volume. -KAFKA_KRAFT_CLUSTER_ID=NvDmnaWzQgiH8qbnraqxcg +CLUSTER_ID=NvDmnaWzQgiH8qbnraqxcg # Only needed if you want to connect to Kafka from outside Docker. # @@ -14,5 +14,11 @@ KAFKA_KRAFT_CLUSTER_ID=NvDmnaWzQgiH8qbnraqxcg # # KAFKA_EXTERNAL_HOST=localhost -# Feature flags +# Host port for the application +# EVENTARA_PORT=8080 + +# JVM memory (adjust for your VPS RAM) +# JAVA_OPTS=-Xms256m -Xmx512m + +# Feature flags (Please keep this as it is: Do not change no matter what) EVENTARA_METRICS_DISTRIBUTED_ENABLED=true diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 1cb4e2a..b5b240f 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,37 +1,57 @@ -# Deployment (Docker image) +# Deployment -This guide is the fastest way to run Eventara on a single VM using Docker. +Deploy Eventara on any VPS with Docker. ## Prerequisites -- Docker Engine + Docker Compose plugin installed -- A VM with ~2 CPU / 4 GB RAM is usually fine for a quick demo +- Docker Engine + Docker Compose plugin +- ~2 CPU / 4 GB RAM (minimum) -## Quick deploy (recommended) - -1. Clone the repo and create your env file: +## Quick deploy ```bash git clone https://github.com/tusharkhatriofficial/eventara.git cd eventara - cp .env.example .env -``` +# Edit .env — at minimum change POSTGRES_PASSWORD -2. Start the stack: - -```bash docker compose --env-file .env -f docker-compose.prod.yaml up -d --build ``` -3. Open: +The build takes 2-5 minutes. It compiles the React dashboard and Spring Boot app into a **single container** — both API and dashboard are served on port `8080`. + +| What | URL | +|---|---| +| Dashboard + API | `http://:8080` | +| Swagger | `http://:8080/swagger-ui.html` | -- API: `http://:8080` -- Swagger: `http://:8080/swagger-ui.html` +## Coolify -Note: If you have a frontend bundle in `src/main/resources/static`, Spring Boot will serve it at `/` (same `:8080`). If you want the Vite dev dashboard instead, use `docker-compose.yaml`. +1. **Add a new resource** → Docker Compose +2. Paste the contents of `docker-compose.prod.yaml` (or point it to the repo) +3. Add the environment variables from `.env.example` in the **Environment Variables** tab +4. Set the exposed port to `8080` +5. Deploy -Spring profile note: the compose files don’t set `SPRING_PROFILES_ACTIVE`, so the app runs with Spring’s default profile. If you need a specific profile, add `SPRING_PROFILES_ACTIVE` to the `eventara` service in `docker-compose.prod.yaml` (or `springboot` in `docker-compose.yaml`). +Coolify will handle the build and TLS for you. + +## DigitalOcean / Generic Ubuntu VPS + +```bash +# SSH into your droplet +ssh root@ + +# Install Docker (if not already installed) +curl -fsSL https://get.docker.com | sh + +# Clone and deploy +git clone https://github.com/tusharkhatriofficial/eventara.git +cd eventara +cp .env.example .env +nano .env # change POSTGRES_PASSWORD at minimum + +docker compose --env-file .env -f docker-compose.prod.yaml up -d --build +``` ## Common operations @@ -46,17 +66,37 @@ docker compose -f docker-compose.prod.yaml down docker compose -f docker-compose.prod.yaml down -v ``` -## Kafka without Zookeeper (KRaft) +## Resource limits + +The prod compose file sets memory limits: + +| Service | Memory Limit | +|---|---| +| Postgres | 1 GB | +| Kafka | 1 GB | +| Redis | 512 MB | +| Eventara (API + Dashboard) | 1 GB | + +Total: ~3.5 GB. A 4 GB VPS works for demos and light use. + +## Environment variables -`docker-compose.yaml` and `docker-compose.prod.yaml` run Kafka in **KRaft mode**, so there is no Zookeeper container. +| Variable | Default | Description | +|---|---|---| +| `POSTGRES_USER` | `postgres` | Database user | +| `POSTGRES_PASSWORD` | `mysecretpassword` | **Change this** | +| `POSTGRES_DB` | `eventara` | Database name | +| `CLUSTER_ID` | `NvDmnaWzQgiH8qbnraqxcg` | Kafka KRaft cluster ID — don't change after first boot | +| `EVENTARA_PORT` | `8080` | Host port for the app | +| `JAVA_OPTS` | `-Xms256m -Xmx512m` | JVM memory settings | -Important: `KAFKA_KRAFT_CLUSTER_ID` must stay stable for a given Kafka data volume. If you change it, wipe the Kafka volume (`docker compose down -v`) before starting again. +## Kafka (KRaft mode) -## Suggested changes for a real production setup +No Zookeeper needed. `CLUSTER_ID` must stay stable for a given Kafka data volume. If you change it, wipe the volume first (`docker compose down -v`). -These aren’t required for a “get it up in a few hours” deployment, but they’re the usual next steps: +## Production hardening checklist -1. **Use a real image registry** (GHCR/Docker Hub) and deploy by pulling an image instead of building on the server. -2. **Don’t publish Postgres/Redis ports** to the public internet (keep them on the Docker network only). -3. Put Eventara behind a reverse proxy (Nginx/Caddy) for TLS and (later) auth. -4. Replace the default credentials in `.env` and store secrets in your VM’s secret manager. +- [ ] Change default database password in `.env` +- [ ] Put behind a reverse proxy (Nginx/Caddy) for TLS +- [ ] Don't expose Postgres/Redis ports to the internet (prod compose already doesn't) +- [ ] Use a managed database if scaling beyond a single node diff --git a/Dockerfile b/Dockerfile index 6b338d9..a3049f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,31 @@ -# ===== Stage 1: Build the app ===== -FROM maven:3.9.9-eclipse-temurin-21 AS build +# ===== Stage 1: Build the dashboard ===== +FROM node:20-alpine AS dashboard-build +WORKDIR /app/eventara-dashboard +COPY eventara-dashboard/package*.json ./ +RUN npm ci --production=false +COPY eventara-dashboard/ . +# Vite builds to ../src/main/resources/static (relative to eventara-dashboard) +RUN npm run build + +# ===== Stage 2: Build the Spring Boot app ===== +FROM maven:3.9.9-eclipse-temurin-21 AS backend-build WORKDIR /app COPY pom.xml . COPY src ./src +# Copy dashboard build output into Spring Boot static resources +COPY --from=dashboard-build /app/src/main/resources/static ./src/main/resources/static/ RUN mvn clean package -DskipTests -# ===== Stage 2: Run the app ===== -FROM eclipse-temurin:21-jdk +# ===== Stage 3: Run the app ===== +FROM eclipse-temurin:21-jre WORKDIR /app -COPY --from=build /app/target/*.jar app.jar + +# Run as non-root +RUN groupadd -r eventara && useradd -r -g eventara eventara + +COPY --from=backend-build /app/target/*.jar app.jar +RUN chown eventara:eventara app.jar + +USER eventara EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"] - -# Simplified Dockerfile using local JAR -# FROM eclipse-temurin:21-jdk -# WORKDIR /app -# COPY target/*.jar app.jar -# EXPOSE 8080 -# ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/README.md b/README.md index 28c75bf..aa027aa 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ Core stack in one Docker Compose file. Full stack in 30 seconds. | Layer | Technology | |---|---| | Runtime | Java 21, Spring Boot 3.5.7 | -| Messaging | Apache Kafka 3.7 (KRaft mode; no Zookeeper) | +| Messaging | Apache Kafka (KRaft mode; no Zookeeper) | | Time-series | TimescaleDB (PostgreSQL 14) | | Cache / Metrics | Redis 7 | | Rule Engine | Java handler pattern (Drools for DRL validation) | diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml index f3e7669..2ae6b76 100644 --- a/docker-compose.prod.yaml +++ b/docker-compose.prod.yaml @@ -15,6 +15,11 @@ services: interval: 10s timeout: 5s retries: 5 + start_period: 30s + deploy: + resources: + limits: + memory: 1g redis: image: redis:7-alpine @@ -29,42 +34,51 @@ services: interval: 10s timeout: 5s retries: 5 + start_period: 10s + deploy: + resources: + limits: + memory: 512m kafka: - image: bitnami/kafka:3.7 + image: apache/kafka:latest restart: unless-stopped environment: - ALLOW_PLAINTEXT_LISTENER: "yes" - KAFKA_KRAFT_CLUSTER_ID: ${KAFKA_KRAFT_CLUSTER_ID:-NvDmnaWzQgiH8qbnraqxcg} + CLUSTER_ID: ${CLUSTER_ID:-NvDmnaWzQgiH8qbnraqxcg} - KAFKA_CFG_NODE_ID: 1 - KAFKA_CFG_PROCESS_ROLES: controller,broker - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1@kafka:9094 - KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_NODE_ID: 1 + KAFKA_PROCESS_ROLES: controller,broker + KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9094 + KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER # Internal-only listener for production (no host port published) - KAFKA_CFG_LISTENERS: INTERNAL://:9092,CONTROLLER://:9094 - KAFKA_CFG_ADVERTISED_LISTENERS: INTERNAL://kafka:9092 - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT - KAFKA_CFG_INTER_BROKER_LISTENER_NAME: INTERNAL + KAFKA_LISTENERS: INTERNAL://:9092,CONTROLLER://:9094 + KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "true" - KAFKA_CFG_NUM_PARTITIONS: 3 - KAFKA_CFG_DEFAULT_REPLICATION_FACTOR: 1 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" + KAFKA_NUM_PARTITIONS: 3 + KAFKA_DEFAULT_REPLICATION_FACTOR: 1 - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR: 1 - KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 - KAFKA_CFG_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 volumes: - - kafka_data:/bitnami/kafka + - kafka_data:/opt/kafka/kafka-logs networks: - eventara-network healthcheck: - test: ["CMD", "kafka-topics.sh", "--bootstrap-server", "kafka:9092", "--list"] + test: ["CMD", "/opt/kafka/bin/kafka-topics.sh", "--bootstrap-server", "kafka:9092", "--list"] interval: 10s timeout: 10s retries: 5 + start_period: 30s + deploy: + resources: + limits: + memory: 1g eventara: image: eventara:latest @@ -80,7 +94,7 @@ services: redis: condition: service_healthy ports: - - "8080:8080" + - "${EVENTARA_PORT:-8080}:8080" environment: SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-eventara} SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-postgres} @@ -92,8 +106,15 @@ services: SPRING_DATA_REDIS_PORT: 6379 EVENTARA_METRICS_DISTRIBUTED_ENABLED: "${EVENTARA_METRICS_DISTRIBUTED_ENABLED:-true}" + + # JVM memory — adjust based on your VPS RAM + JAVA_OPTS: "${JAVA_OPTS:--Xms256m -Xmx512m}" networks: - eventara-network + deploy: + resources: + limits: + memory: 1g volumes: postgres_data: diff --git a/docker-compose.yaml b/docker-compose.yaml index 910e680..0a389a3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -40,48 +40,47 @@ services: # Kafka Broker (KRaft mode - no Zookeeper) kafka: - image: bitnami/kafka:3.7 + image: apache/kafka:latest container_name: eventara-kafka restart: unless-stopped ports: - "9093:9093" environment: - ALLOW_PLAINTEXT_LISTENER: "yes" - KAFKA_KRAFT_CLUSTER_ID: ${KAFKA_KRAFT_CLUSTER_ID:-NvDmnaWzQgiH8qbnraqxcg} + CLUSTER_ID: ${CLUSTER_ID:-NvDmnaWzQgiH8qbnraqxcg} - KAFKA_CFG_NODE_ID: 1 - KAFKA_CFG_PROCESS_ROLES: controller,broker - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1@kafka:9094 - KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_NODE_ID: 1 + KAFKA_PROCESS_ROLES: controller,broker + KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9094 + KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER # Listeners for internal (Docker) and external (localhost) connections - KAFKA_CFG_LISTENERS: INTERNAL://:9092,EXTERNAL://:9093,CONTROLLER://:9094 - KAFKA_CFG_ADVERTISED_LISTENERS: INTERNAL://kafka:9092,EXTERNAL://${KAFKA_EXTERNAL_HOST:-localhost}:9093 - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT - KAFKA_CFG_INTER_BROKER_LISTENER_NAME: INTERNAL + KAFKA_LISTENERS: INTERNAL://:9092,EXTERNAL://:9093,CONTROLLER://:9094 + KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:9092,EXTERNAL://${KAFKA_EXTERNAL_HOST:-localhost}:9093 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT + KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL # Topic defaults - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "true" - KAFKA_CFG_DELETE_TOPIC_ENABLE: "true" - KAFKA_CFG_NUM_PARTITIONS: 3 - KAFKA_CFG_DEFAULT_REPLICATION_FACTOR: 1 + KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true" + KAFKA_DELETE_TOPIC_ENABLE: "true" + KAFKA_NUM_PARTITIONS: 3 + KAFKA_DEFAULT_REPLICATION_FACTOR: 1 # Performance settings - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR: 1 - KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 - KAFKA_CFG_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 # Log settings - KAFKA_CFG_LOG_RETENTION_HOURS: 168 - KAFKA_CFG_LOG_SEGMENT_BYTES: 1073741824 - KAFKA_CFG_LOG_RETENTION_CHECK_INTERVAL_MS: 300000 + KAFKA_LOG_RETENTION_HOURS: 168 + KAFKA_LOG_SEGMENT_BYTES: 1073741824 + KAFKA_LOG_RETENTION_CHECK_INTERVAL_MS: 300000 volumes: - - kafka_data:/bitnami/kafka + - kafka_data:/opt/kafka/kafka-logs networks: - eventara-network healthcheck: - test: ["CMD", "kafka-topics.sh", "--bootstrap-server", "kafka:9092", "--list"] + test: ["CMD", "/opt/kafka/bin/kafka-topics.sh", "--bootstrap-server", "kafka:9092", "--list"] interval: 10s timeout: 10s retries: 5 @@ -123,7 +122,6 @@ services: SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-eventara} SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER:-postgres} SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD:-mysecretpassword} - SPRING_JPA_HIBERNATE_DDL_AUTO: update # Kafka config SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:9092