Multi-App Project Example
Deploy multiple applications from a single repository (monorepo).
Basic Multi-App Setup
Project Structure
my-project/
├── cmd/
│ ├── api/
│ │ └── main.go
│ ├── worker/
│ │ └── main.go
│ └── admin/
│ └── main.go
├── services/
│ └── ml/
│ ├── server.py
│ └── requirements.txt
├── go.mod
├── go.sum
└── gokku.yml
gokku.yml
yaml
apps:
api:
path: ./cmd/api
binary_name: api
worker:
path: ./cmd/worker
binary_name: worker
admin:
path: ./cmd/admin
binary_name: admin
ml-service:
lang: python
path: ./services/ml
entrypoint: server.py
Setup All Apps
bash
# On local machine
git remote add api-prod ubuntu@server:api
git remote add worker-prod ubuntu@server:worker
git remote add admin-prod ubuntu@server:admin
git remote add ml-prod ubuntu@server:ml-service
Deploy
bash
# Deploy all to production
git push api-prod main
git push worker-prod main
git push admin-prod main
git push ml-prod main
Microservices Architecture
API + Worker + Admin
yaml
apps:
api:
path: ./cmd/api
worker:
path: ./cmd/worker
admin:
path: ./cmd/admin
Service Communication
go
// cmd/api/main.go
package main
import (
"net/http"
"os"
)
func main() {
workerURL := os.Getenv("WORKER_URL")
http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) {
// Call worker service
resp, _ := http.Post(workerURL+"/job", "application/json", r.Body)
// ...
})
http.ListenAndServe(":"+os.Getenv("PORT"), nil)
}
go
// cmd/worker/main.go
package main
import (
"net/http"
"os"
)
func main() {
http.HandleFunc("/job", func(w http.ResponseWriter, r *http.Request) {
// Process job
w.Write([]byte("Job processed"))
})
http.ListenAndServe(":"+os.Getenv("PORT"), nil)
}
Different Technologies
Mix Go, Python, Node.js in one project:
yaml
apps:
# Go API
api:
path: ./cmd/api
# Python ML Service
ml:
lang: python
path: ./services/ml
entrypoint: server.py
# Node.js Frontend
frontend:
lang: nodejs
path: ./frontend
entrypoint: server.js
Shared Dependencies
Go Modules (Shared)
my-project/
├── cmd/
│ ├── api/
│ └── worker/
├── internal/
│ ├── models/
│ └── db/
├── go.mod
└── gokku.yml
Both API and Worker share internal/
packages.
Python (Shared)
my-project/
├── services/
│ ├── ml/
│ │ └── server.py
│ └── worker/
│ └── worker.py
├── shared/
│ └── utils.py
└── gokku.yml
python
# services/ml/server.py
import sys
sys.path.append('../../')
from shared import utils
Different Versions
Each app can use different tool versions:
yaml
apps:
api-v1:
path: ./cmd/api-v1
go_version: "1.24"
api-v2:
path: ./cmd/api-v2
go_version: "1.25"
Or with .tool-versions
:
cmd/api-v1/.tool-versions:
golang 1.24.0
cmd/api-v2/.tool-versions:
golang 1.25.0
Background Workers
Cron-like Worker
go
// cmd/cron-worker/main.go
package main
import (
"log"
"time"
)
func main() {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for range ticker.C {
log.Println("Running scheduled task...")
// Do work
}
}
yaml
apps:
cron-worker:
path: ./cmd/cron-worker
Queue Worker (Celery)
python
# services/worker/worker.py
from celery import Celery
app = Celery('tasks', broker='redis://localhost:6379')
@app.task
def process_video(video_id):
# Process
return f"Done: {video_id}"
if __name__ == '__main__':
app.worker_main()
yaml
apps:
celery-worker:
lang: python
path: ./services/worker
entrypoint: worker.py
Staging + Production
Each app with both environments:
yaml
apps:
api:
path: ./cmd/api
worker:
path: ./cmd/worker
Deploy
bash
# Production
git remote add api-prod ubuntu@server:api
git remote add worker-prod ubuntu@server:worker
# Staging
git remote add api-staging ubuntu@server:api
git remote add worker-staging ubuntu@server:worker
# Deploy to staging
git push api-staging staging
git push worker-staging staging
# Deploy to production
git push api-prod main
git push worker-prod main
Database Migrations
Separate Migration App
yaml
apps:
api:
path: ./cmd/api
migrate:
path: ./cmd/migrate
go
// cmd/migrate/main.go
package main
import (
"database/sql"
"log"
"os"
_ "github.com/lib/pq"
)
func main() {
db, _ := sql.Open("postgres", os.Getenv("DATABASE_URL"))
// Run migrations
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(100)
)
`)
if err != nil {
log.Fatal(err)
}
log.Println("Migrations complete")
}
Deploy migrations before API:
bash
git push migrate-prod main
git push api-prod main
Load Balancer Setup
With multiple instances:
yaml
apps:
api-1:
path: ./cmd/api
api-2:
path: ./cmd/api
Use nginx for load balancing:
nginx
upstream api_backend {
server localhost:8080;
server localhost:8081;
}
server {
listen 80;
location / {
proxy_pass http://api_backend;
}
}
Monitoring All Apps
Health Check Aggregator
go
// cmd/health-checker/main.go
package main
import (
"net/http"
"encoding/json"
)
func main() {
http.HandleFunc("/health/all", func(w http.ResponseWriter, r *http.Request) {
services := map[string]string{
"api": checkHealth("http://localhost:8080/health"),
"worker": checkHealth("http://localhost:8081/health"),
"ml": checkHealth("http://localhost:8082/health"),
}
json.NewEncoder(w).Encode(services)
})
http.ListenAndServe(":9000", nil)
}
func checkHealth(url string) string {
resp, err := http.Get(url)
if err != nil || resp.StatusCode != 200 {
return "unhealthy"
}
return "healthy"
}
Complete Example
Full monorepo example: github.com/thadeu/gokku-examples/monorepo
Best Practices
- Shared Code: Keep shared code in
internal/
orpkg/
- Independent Deploys: Each app deploys independently
- Environment Parity: Keep staging and production configs similar
- Service Discovery: Use environment variables for service URLs
- Database Migrations: Deploy migrations before apps
- Health Checks: Each service should have
/health
endpoint
Next Steps
- Configuration - Advanced multi-app config
- Environments - Manage multiple environments
- Docker Support - Advanced Docker configuration