3
CannaManage 09 Deployment
Patrick Plate edited this page 2026-06-19 16:43:56 +02:00

09 — Deployment Guide

Project: CannaManage — B2B SaaS for German Cannabis Social Clubs Version: 14.0 (Sprint 14) Date: 2026-06-19 Target environment: TrueNAS Docker — Docker Compose (Production) | Gitea Actions (CI/CD)


1. Infrastructure Overview

graph TB
    Dev["👨‍💻 Dev Workstation\n(macOS)"]
    Gitea["🏠 Gitea\n(TrueNAS :30008)"]
    Runner["⚙️ Gitea Actions Runner\n(TrueNAS Docker)"]

    Dev -->|"git push"| Gitea
    Gitea -->|"triggers CI"| Runner
    Runner -->|"build + test + deploy"| Prod

    subgraph Prod ["🖧 TrueNAS — Production (Docker Compose)"]
        Nginx["🔒 Nginx\nreverse proxy + TLS\n:443 → :8080/:3000"]
        Backend["☕ cannamanage-app\nSpring Boot 4.0.6\n:8080"]
        Frontend["⚛️ cannamanage-frontend\nNext.js 15\n:3000"]
        DB["🐘 PostgreSQL 16\n:5432\n(persistent volume)"]
        
        Nginx --> Backend
        Nginx --> Frontend
        Backend --> DB
    end

    Internet["🌍 Internet\nhttps://cannamanage.plate-software.de"] -->|":443"| Nginx

Environment Summary

Environment Host Purpose
Development macOS (local) Feature development, unit tests, docker-compose.yml
CI/CD TrueNAS Gitea Actions Runner Automated build, test (PostgreSQL service container), deploy
Production TrueNAS Docker Compose Live application at cannamanage.plate-software.de

Note: The project previously planned Hetzner VPS hosting but migrated to TrueNAS Docker for cost savings and network locality. The production URL is forwarded to the TrueNAS instance via port forwarding.


2. Prerequisites

TrueNAS Docker Host

Resource Value
Platform TrueNAS Scale (Docker)
Docker Engine 24+
Docker Compose v2 (compose plugin)
RAM allocated 4 GB+ for all containers
Storage Persistent volumes on ZFS pools

DNS / Networking

Record Value
Domain cannamanage.plate-software.de
TLS Let's Encrypt via Nginx (certbot)
Port forwarding Router :443 → TrueNAS :443

Required Software (on TrueNAS)

  • Docker Engine 24+
  • Docker Compose v2
  • Gitea (self-hosted, port 30008)
  • Gitea Actions Runner (registered)
  • Certbot / Nginx for TLS

3. Docker Compose — Production

File: docker-compose.truenas.yml

networks:
  cannamanage_net:
    driver: bridge

volumes:
  pgdata:
    driver: local

services:
  db:
    image: postgres:16-alpine
    container_name: cannamanage-db
    environment:
      POSTGRES_DB: cannamanage
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks:
      - cannamanage_net
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

  backend:
    build:
      context: .
      dockerfile: Dockerfile.backend
    container_name: cannamanage-app
    environment:
      SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/cannamanage
      SPRING_DATASOURCE_USERNAME: ${DB_USER}
      SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD}
      SPRING_PROFILES_ACTIVE: production
      JWT_SECRET: ${JWT_SECRET}
      STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
      STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
      MAIL_HOST: ${MAIL_HOST}
      MAIL_USERNAME: ${MAIL_USERNAME}
      MAIL_PASSWORD: ${MAIL_PASSWORD}
    ports:
      - "8080:8080"
    depends_on:
      db:
        condition: service_healthy
    networks:
      - cannamanage_net
    restart: unless-stopped

  frontend:
    build:
      context: ./cannamanage-frontend
      dockerfile: Dockerfile
    container_name: cannamanage-frontend
    environment:
      NEXTAUTH_URL: https://cannamanage.plate-software.de
      NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
      NEXT_PUBLIC_API_URL: https://cannamanage.plate-software.de/api
    ports:
      - "3000:3000"
    depends_on:
      - backend
    networks:
      - cannamanage_net
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    container_name: cannamanage-nginx
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - ./deploy/nginx/cannamanage.conf:/etc/nginx/conf.d/default.conf:ro
      - /etc/letsencrypt:/etc/letsencrypt:ro
    depends_on:
      - backend
      - frontend
    networks:
      - cannamanage_net
    restart: unless-stopped

4. Nginx Configuration

File: deploy/nginx/cannamanage.conf

server {
    listen 80;
    server_name cannamanage.plate-software.de;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    server_name cannamanage.plate-software.de;

    ssl_certificate /etc/letsencrypt/live/cannamanage.plate-software.de/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cannamanage.plate-software.de/privkey.pem;

    # Backend API
    location /api/ {
        proxy_pass http://backend:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Swagger UI
    location /swagger-ui/ {
        proxy_pass http://backend:8080;
        proxy_set_header Host $host;
    }

    location /v3/api-docs {
        proxy_pass http://backend:8080;
        proxy_set_header Host $host;
    }

    # Frontend (everything else)
    location / {
        proxy_pass http://frontend:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

5. CI/CD — Gitea Actions

File: .gitea/workflows/ci.yml

The CI pipeline runs on every push to main:

name: CI/CD Pipeline

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: cannamanage_test
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U test"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Build & Test Backend
        run: |
          mvn clean verify -B \
            -Dspring.datasource.url=jdbc:postgresql://localhost:5432/cannamanage_test \
            -Dspring.datasource.username=test \
            -Dspring.datasource.password=test
        env:
          SPRING_PROFILES_ACTIVE: test

      - name: Frontend Tests
        working-directory: cannamanage-frontend
        run: |
          npm install -g pnpm
          pnpm install --frozen-lockfile
          pnpm test

Pipeline Stages

Stage What Fails on
Checkout Clone repo
Java Setup JDK 21 Temurin
Maven Build Compile + unit tests + integration tests Test failure, compilation error
JaCoCo Coverage report (80% gate) Coverage below threshold
Frontend pnpm install + Vitest Test failure
Deploy docker-compose up (on main merge) Build failure

6. Environment Variables

File: .env.production (on TrueNAS host, NOT committed to git)

Variable Purpose Example
DB_USER PostgreSQL username cannamanage
DB_PASSWORD PostgreSQL password (generated)
JWT_SECRET JWT signing key (256-bit) (generated)
NEXTAUTH_SECRET NextAuth session encryption (generated)
STRIPE_SECRET_KEY Stripe API secret sk_live_...
STRIPE_WEBHOOK_SECRET Stripe webhook signing whsec_...
MAIL_HOST SMTP server smtp.example.com
MAIL_USERNAME SMTP user noreply@cannamanage.de
MAIL_PASSWORD SMTP password (secret)

7. Backup Strategy

#!/bin/bash
# deploy/backup.sh — runs daily via cron
DATE=$(date +%Y-%m-%d_%H%M)
BACKUP_DIR="/mnt/pool/backups/cannamanage"

# PostgreSQL dump
docker exec cannamanage-db pg_dumpall -U ${DB_USER} | gzip > "${BACKUP_DIR}/db_${DATE}.sql.gz"

# Retain last 30 days
find "${BACKUP_DIR}" -name "db_*.sql.gz" -mtime +30 -delete
What Frequency Retention
PostgreSQL full dump Daily (cron) 30 days
ZFS snapshots Hourly (TrueNAS) 7 days
Git repository Every push (Gitea) Permanent

8. Deployment Commands

Initial Setup

# Clone on TrueNAS
git clone http://truenas.local:30008/pplate/cannamanage.git /opt/cannamanage
cd /opt/cannamanage

# Create .env.production
cp .env.example .env.production
# Edit with real values...

# Start all services
docker compose -f docker-compose.truenas.yml --env-file .env.production up -d

# Verify
docker compose -f docker-compose.truenas.yml ps
curl -k https://cannamanage.plate-software.de/api/v1/health

Redeploy After Push

cd /opt/cannamanage
git pull origin main
docker compose -f docker-compose.truenas.yml --env-file .env.production up -d --build

Logs

# All services
docker compose -f docker-compose.truenas.yml logs -f

# Backend only
docker logs -f cannamanage-app

# Database
docker logs -f cannamanage-db

9. Monitoring

Check Method Alert
Application health GET /api/v1/health (Spring Actuator) HTTP 200 expected
Database connectivity Docker healthcheck (pg_isready) Container restart
Disk usage ZFS pool monitoring (TrueNAS) >80% alert
TLS certificate Certbot auto-renewal (cron) 30-day warning
Container status docker compose ps Any "unhealthy" or "exited"

10. Troubleshooting

Issue Diagnosis Fix
502 Bad Gateway Backend not started docker compose logs backend — check for OOM or startup errors
Database connection refused DB container unhealthy docker compose restart db — check pgdata volume
TLS cert expired Certbot renewal failed certbot renew --nginx
Port 443 not reachable Router forwarding lost Re-add port forward rule on router
Out of disk ZFS pool full docker system prune -a + check backup retention
Frontend 500 NEXTAUTH_SECRET mismatch Verify .env.production matches container env