Adding New Apps to ERP-Unlocked Monorepo
This guide provides comprehensive instructions for adding new applications to the ERP-Unlocked monorepo. We currently support four types of applications, each with specific setup requirements and deployment considerations.
Table of Contents
Section titled “Table of Contents”- Supported App Types
- Python FastAPI Applications
- Python Celery Worker Applications
- Node.js Server Applications
- Astro.js Applications
- GitHub Actions Configuration
- Deployment Considerations
- Internal Networking Setup
Supported App Types
Section titled “Supported App Types”| Type | Use Case | Example | Port | Access |
|---|---|---|---|---|
| Python FastAPI | REST APIs, microservices | PDF Processor API | 8000 | Internal/Public |
| Python Celery Worker | Background tasks, job processing | PDF Worker | - | Internal |
| Node.js Server | APIs, real-time services | WebSocket server | 3000+ | Internal/Public |
| Astro.js | Static sites, SSR apps | Marketing site | 4321 | Public |
Python FastAPI Applications
Section titled “Python FastAPI Applications”Directory Structure
Section titled “Directory Structure”my_fastapi_service/├── app/│ ├── __init__.py│ ├── main.py # FastAPI app entry point│ ├── models/│ ├── routes/│ ├── services/│ └── utils/├── tests/├── Dockerfile├── pyproject.toml├── uv.lock└── README.mdSetup Steps
Section titled “Setup Steps”-
Create the service directory:
Terminal window mkdir my_fastapi_servicecd my_fastapi_service -
Initialize Python project with uv:
Terminal window uv inituv add fastapi uvicorn pydanticuv add --dev pytest pytest-asyncio httpx -
Create the main FastAPI app:
app/main.py from fastapi import FastAPIfrom fastapi.middleware.cors import CORSMiddlewareimport osapp = FastAPI(title="My FastAPI Service",description="Description of your service",version="1.0.0")# CORS middlewareapp.add_middleware(CORSMiddleware,allow_origins=["*"], # Configure appropriatelyallow_credentials=True,allow_methods=["*"],allow_headers=["*"],)@app.get("/health")async def health_check():return {"status": "healthy", "service": "my-fastapi-service"}@app.get("/")async def root():return {"message": "My FastAPI Service"} -
Create Dockerfile:
# syntax=docker/dockerfile:1# ---- Builder Stage ----FROM python:3.10-slim AS builder# Install system dependenciesRUN apt-get update && apt-get install -y \build-essential \libpq-dev \&& rm -rf /var/lib/apt/lists/*# Install uvCOPY --from=ghcr.io/astral-sh/uv:0.7.12 /uv /uvx /usr/local/bin/# Copy dependency filesCOPY uv.lock pyproject.toml ./# Install dependencies using uvRUN uv sync --frozen --no-install-project# ---- Final Stage ----FROM python:3.10-slim AS final# Install system dependenciesRUN apt-get update && apt-get install -y \libpq-dev \curl \&& rm -rf /var/lib/apt/lists/*# Copy the virtual environment from the builder stageCOPY --from=builder /.venv /.venv# Ensure the virtual environment is usedENV PATH="/.venv/bin:$PATH"# Install pip and observability toolsRUN /.venv/bin/python -m ensurepip --upgrade && \/.venv/bin/python -m pip install --upgrade pip && \/.venv/bin/opentelemetry-bootstrap --action=install# Set working directoryWORKDIR /service# Copy application codeCOPY app/ app/# Create an __init__.py to make app a proper Python packageRUN echo "# Python package" > /service/app/__init__.py# Set environment variablesENV PYTHONPATH=/serviceENV PYTHONUNBUFFERED=1# Make app directory readableRUN chmod -R 755 /service/app# Health checkHEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \CMD curl -f http://localhost:8000/health || exit 1# Expose portEXPOSE 8000# Start the applicationCMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] -
Add tests:
tests/test_main.py import pytestfrom fastapi.testclient import TestClientfrom app.main import appclient = TestClient(app)def test_health_check():response = client.get("/health")assert response.status_code == 200assert response.json()["status"] == "healthy"def test_root():response = client.get("/")assert response.status_code == 200assert "message" in response.json()
Python Celery Worker Applications
Section titled “Python Celery Worker Applications”Directory Structure
Section titled “Directory Structure”my_worker_service/├── app/│ ├── __init__.py│ ├── celery_app.py # Celery app configuration│ ├── tasks/│ │ ├── __init__.py│ │ └── example_tasks.py│ ├── models/│ ├── services/│ └── utils/├── tests/├── Dockerfile├── pyproject.toml├── uv.lock└── README.mdSetup Steps
Section titled “Setup Steps”-
Create the service directory:
Terminal window mkdir my_worker_servicecd my_worker_service -
Initialize Python project with uv:
Terminal window uv inituv add celery redisuv add --dev pytest pytest-asyncio -
Create the Celery app:
app/celery_app.py from celery import Celeryimport os# Configure Celerycelery_app = Celery("my_worker_service",broker=os.getenv("REDIS_URL", "redis://redis:6379/0"),backend=os.getenv("REDIS_URL", "redis://redis:6379/0"),include=["app.tasks.example_tasks"])# Configure Celery settingscelery_app.conf.update(task_serializer="json",accept_content=["json"],result_serializer="json",timezone="UTC",enable_utc=True,task_track_started=True,task_time_limit=30 * 60, # 30 minutestask_soft_time_limit=25 * 60, # 25 minutesworker_prefetch_multiplier=1,worker_max_tasks_per_child=1000,)if __name__ == "__main__":celery_app.start() -
Create example tasks:
app/tasks/example_tasks.py from app.celery_app import celery_appimport time@celery_app.taskdef example_task(message: str):"""Example background task"""time.sleep(2) # Simulate workreturn f"Processed: {message}"@celery_app.taskdef health_check_task():"""Health check task for monitoring"""return "Worker is healthy" -
Create Dockerfile:
# syntax=docker/dockerfile:1# ---- Builder Stage ----FROM python:3.10-slim AS builder# Install system dependenciesRUN apt-get update && apt-get install -y \build-essential \libpq-dev \&& rm -rf /var/lib/apt/lists/*# Install uvCOPY --from=ghcr.io/astral-sh/uv:0.7.12 /uv /uvx /usr/local/bin/# Copy dependency filesCOPY uv.lock pyproject.toml ./# Install dependencies using uvRUN uv sync --frozen --no-install-project# ---- Final Stage ----FROM python:3.10-slim AS final# Install system dependenciesRUN apt-get update && apt-get install -y \libpq-dev \curl \&& rm -rf /var/lib/apt/lists/*# Copy the virtual environment from the builder stageCOPY --from=builder /.venv /.venv# Ensure the virtual environment is usedENV PATH="/.venv/bin:$PATH"# Install pip and observability toolsRUN /.venv/bin/python -m ensurepip --upgrade && \/.venv/bin/python -m pip install --upgrade pip && \/.venv/bin/opentelemetry-bootstrap --action=install# Set working directoryWORKDIR /service# Copy application codeCOPY app/ app/# Create an __init__.py to make app a proper Python packageRUN echo "# Python package" > /service/app/__init__.py# Set environment variablesENV PYTHONPATH=/serviceENV PYTHONUNBUFFERED=1# Make app directory readableRUN chmod -R 755 /service/app# Health check using Celery inspectHEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \CMD celery --workdir=/service -A app.celery_app inspect ping -d celery@$HOSTNAME || exit 1# Start the Celery workerCMD ["celery", "--workdir=/service", "-A", "app.celery_app", "worker", "--loglevel=info"]
Node.js Server Applications
Section titled “Node.js Server Applications”Directory Structure
Section titled “Directory Structure”my_node_service/├── src/│ ├── index.ts # Main entry point│ ├── app.ts # Express app setup│ ├── routes/│ ├── middleware/│ ├── services/│ └── utils/├── tests/├── Dockerfile├── package.json├── tsconfig.json├── .env.example└── README.mdSetup Steps
Section titled “Setup Steps”-
Create the service directory:
Terminal window mkdir my_node_servicecd my_node_service -
Initialize Node.js project:
Terminal window npm init -ynpm install express cors helmet morgan dotenvnpm install -D typescript @types/node @types/express @types/cors ts-node nodemon jest @types/jest supertest @types/supertest -
Create TypeScript configuration:
{"compilerOptions": {"target": "ES2020","module": "commonjs","lib": ["ES2020"],"outDir": "./dist","rootDir": "./src","strict": true,"esModuleInterop": true,"skipLibCheck": true,"forceConsistentCasingInFileNames": true,"resolveJsonModule": true,"declaration": true,"declarationMap": true,"sourceMap": true},"include": ["src/**/*"],"exclude": ["node_modules", "dist", "tests"]} -
Create the main application:
src/app.ts import express from 'express';import cors from 'cors';import helmet from 'helmet';import morgan from 'morgan';const app = express();// Middlewareapp.use(helmet());app.use(cors());app.use(morgan('combined'));app.use(express.json());app.use(express.urlencoded({ extended: true }));// Health check endpointapp.get('/health', (req, res) => {res.json({ status: 'healthy', service: 'my-node-service' });});// Root endpointapp.get('/', (req, res) => {res.json({ message: 'My Node.js Service' });});export default app;src/index.ts import app from './app';import dotenv from 'dotenv';dotenv.config();const PORT = process.env.PORT || 3000;app.listen(PORT, () => {console.log(`Server running on port ${PORT}`);}); -
Create Dockerfile:
# syntax=docker/dockerfile:1# ---- Dependencies Stage ----FROM node:18-alpine AS dependencies# Install pnpmRUN npm install -g pnpmWORKDIR /app# Copy package filesCOPY package.json pnpm-lock.yaml* ./# Install dependenciesRUN pnpm install --frozen-lockfile# ---- Build Stage ----FROM dependencies AS build# Copy source codeCOPY . .# Build the applicationRUN pnpm run build# ---- Production Stage ----FROM node:18-alpine AS production# Install pnpmRUN npm install -g pnpm# Create app directoryWORKDIR /app# Copy package filesCOPY package.json pnpm-lock.yaml* ./# Install production dependencies onlyRUN pnpm install --frozen-lockfile --prod# Copy built applicationCOPY --from=build /app/dist ./dist# Create non-root userRUN addgroup -g 1001 -S nodejsRUN adduser -S nodejs -u 1001RUN chown -R nodejs:nodejs /appUSER nodejs# Health checkHEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1# Expose portEXPOSE 3000# Start the applicationCMD ["node", "dist/index.js"] -
Update package.json scripts:
{"scripts": {"start": "node dist/index.js","dev": "nodemon src/index.ts","build": "tsc","test": "jest","lint": "eslint src/**/*.ts","type-check": "tsc --noEmit"}}
Astro.js Applications
Section titled “Astro.js Applications”Directory Structure
Section titled “Directory Structure”my_astro_app/├── src/│ ├── pages/│ ├── components/│ ├── layouts/│ └── content/├── public/├── dist/├── Dockerfile├── astro.config.mjs├── package.json├── tsconfig.json└── README.mdSetup Steps
Section titled “Setup Steps”-
Create the Astro application:
Terminal window npm create astro@latest my_astro_appcd my_astro_app -
Configure Astro for production:
astro.config.mjs import { defineConfig } from 'astro/config';export default defineConfig({output: 'static', // or 'server' for SSRbuild: {assets: 'assets',},server: {port: 4321,host: true,},}); -
Create Dockerfile:
# syntax=docker/dockerfile:1# ---- Dependencies Stage ----FROM node:18-alpine AS dependencies# Install pnpmRUN npm install -g pnpmWORKDIR /app# Copy package filesCOPY package.json pnpm-lock.yaml* ./# Install dependenciesRUN pnpm install --frozen-lockfile# ---- Build Stage ----FROM dependencies AS build# Copy source codeCOPY . .# Build the applicationRUN pnpm run build# ---- Production Stage (Static) ----FROM nginx:alpine AS production-static# Copy built assetsCOPY --from=build /app/dist /usr/share/nginx/html# Copy nginx configurationCOPY nginx.conf /etc/nginx/nginx.conf# Health checkHEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \CMD wget --no-verbose --tries=1 --spider http://localhost:80 || exit 1EXPOSE 80CMD ["nginx", "-g", "daemon off;"]# ---- Production Stage (SSR) ----FROM node:18-alpine AS production-ssr# Install pnpmRUN npm install -g pnpmWORKDIR /app# Copy package filesCOPY package.json pnpm-lock.yaml* ./# Install production dependenciesRUN pnpm install --frozen-lockfile --prod# Copy built applicationCOPY --from=build /app/dist ./dist# Create non-root userRUN addgroup -g 1001 -S nodejsRUN adduser -S nodejs -u 1001RUN chown -R nodejs:nodejs /appUSER nodejs# Health checkHEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \CMD wget --no-verbose --tries=1 --spider http://localhost:4321 || exit 1EXPOSE 4321CMD ["node", "./dist/server/entry.mjs"] -
Create nginx configuration (for static builds):
nginx.conf events {worker_connections 1024;}http {include /etc/nginx/mime.types;default_type application/octet-stream;server {listen 80;server_name localhost;root /usr/share/nginx/html;index index.html;location / {try_files $uri $uri/ /index.html;}location /health {return 200 "healthy\n";add_header Content-Type text/plain;}}}
GitHub Actions Configuration
Section titled “GitHub Actions Configuration”Update CI/CD Pipeline
Section titled “Update CI/CD Pipeline”For each new service, update .github/workflows/ci-cd-pipeline.yml:
-
Add path filters:
filters: |my-new-service:- 'my_new_service/**'# existing filters... -
Add build job:
build-my-new-service:runs-on: ubuntu-latestneeds: detect-changesif: needs.detect-changes.outputs.my-new-service == 'true' || needs.detect-changes.outputs.force-build == 'true'permissions:contents: readpackages: writesteps:- name: Checkout codeuses: actions/checkout@v4- name: Set up Docker Buildxuses: docker/setup-buildx-action@v3- name: Login to GitHub Container Registryuses: docker/login-action@v3with:registry: ${{ env.REGISTRY }}username: ${{ github.actor }}password: ${{ secrets.GITHUB_TOKEN }}- name: Extract metadataid: metauses: docker/metadata-action@v5with:images: ${{ env.IMAGE_PREFIX }}/my-new-servicetags: |type=ref,event=branchtype=ref,event=prtype=sha,prefix={{branch}}-type=raw,value=latest,enable={{is_default_branch}}- name: Build and pushuses: docker/build-push-action@v5with:context: ./my_new_servicefile: ./my_new_service/Dockerfilepush: truetags: ${{ steps.meta.outputs.tags }}labels: ${{ steps.meta.outputs.labels }}cache-from: type=ghacache-to: type=gha,mode=max -
Update trigger-deployment job:
trigger-deployment:runs-on: ubuntu-latestneeds: [detect-changes, build-web, build-pdf-processor, build-my-new-service]# ... rest of the job
Update Branch Protection
Section titled “Update Branch Protection”Add validation for new service in .github/workflows/branch-protection.yml:
validate-my-new-service: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4
# Add appropriate validation steps based on service type # For Python services: - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.10'
- name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Install dependencies run: | cd my_new_service uv sync --frozen
- name: Run tests run: | cd my_new_service uv run pytest tests/
# For Node.js services: - name: Setup pnpm uses: pnpm/action-setup@v2 with: version: 8
- name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 18 cache: 'pnpm'
- name: Install dependencies run: | cd my_new_service pnpm install --frozen-lockfile
- name: Run tests run: | cd my_new_service pnpm run testDeployment Considerations
Section titled “Deployment Considerations”Coolify Service Creation
Section titled “Coolify Service Creation”-
Create new service in Coolify:
Terminal window # Via Coolify UI or APIcurl -X POST "https://coolify.yourdomain.com/api/v1/applications" \-H "Authorization: Bearer $COOLIFY_API_TOKEN" \-H "Content-Type: application/json" \-d '{"name": "erp-unlocked-my-new-service","description": "My new service","docker_image": "ghcr.io/your-org/erp-unlocked/my-new-service:latest","ports": "3000:3000","environment_variables": {"NODE_ENV": "production"}}' -
Configure health checks:
- Set health check endpoint (e.g.,
/health) - Configure appropriate timeout and interval
- Set up monitoring and alerting
- Set health check endpoint (e.g.,
-
Set up environment variables:
- Database connection strings
- API keys and secrets
- Service configuration
Service Communication
Section titled “Service Communication”Update existing services to communicate with new service:
// Example: Calling new service from existing serviceconst response = await fetch('http://erp-unlocked-my-new-service:3000/api/endpoint', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data),});Internal Networking Setup
Section titled “Internal Networking Setup”Network Configuration
Section titled “Network Configuration”-
Ensure services are on the same Docker network:
# In Coolify, services automatically join the 'coolify' networknetworks:- coolify -
Update service discovery:
Terminal window # Services can reach each other using container nameshttp://erp-unlocked-my-new-service:3000http://erp-unlocked-pdf-api:8000http://redis:6379 -
Configure load balancing (if needed):
# For public services, configure Traefik labelslabels:- 'traefik.enable=true'- 'traefik.http.routers.my-service.rule=Host(`api.yourdomain.com`)'- 'traefik.http.routers.my-service.tls=true'- 'traefik.http.routers.my-service.tls.certresolver=letsencrypt'
Best Practices Checklist
Section titled “Best Practices Checklist”For All Services
Section titled “For All Services”- Health check endpoint implemented
- Proper error handling and logging
- Environment variables for configuration
- Dockerfile follows multi-stage build pattern
- Tests cover core functionality
- CI/CD pipeline updated
- Documentation in README.md
- Security considerations addressed
Python Services
Section titled “Python Services”- uv for dependency management
- Virtual environment in Docker
- OpenTelemetry for observability
- Pydantic for data validation
- Async/await where appropriate
Node.js Services
Section titled “Node.js Services”- TypeScript for type safety
- pnpm for package management
- ESLint and Prettier configured
- Security middleware (helmet, cors)
- Non-root user in Docker
Astro.js Services
Section titled “Astro.js Services”- Static vs SSR decision made
- Build optimization configured
- SEO considerations addressed
- Performance optimizations applied
Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”-
Service can’t communicate with others:
- Check Docker network configuration
- Verify service names and ports
- Test connectivity from within containers
-
Build failures:
- Review Dockerfile syntax
- Check dependency installations
- Verify build context
-
Deployment failures:
- Check Coolify logs
- Verify image availability in GHCR
- Review environment variables
Debug Commands
Section titled “Debug Commands”# Check service connectivitydocker exec -it erp-unlocked-web curl http://erp-unlocked-my-new-service:3000/health
# View service logsdocker logs erp-unlocked-my-new-service
# Inspect networkdocker network inspect coolify
# Check running containersdocker psConclusion
Section titled “Conclusion”Following this guide ensures consistent, secure, and maintainable service additions to the ERP-Unlocked monorepo. Each service type has specific requirements, but they all follow the same deployment pipeline and internal networking patterns.
Remember to:
- Update CI/CD workflows
- Configure Coolify deployment
- Set up proper monitoring
- Update documentation
- Test service communication
For questions or additional service types, refer to the existing implementations and update this guide accordingly.