Skip to content

Deployment

FileUse CaseDatabase
docker-compose.ymlStandard deploymentSQLite (default) or PostgreSQL
docker-compose.full.ymlFull stack with monitoringPostgreSQL + Prometheus + Grafana + Loki
docker-compose.customer.ymlCustomer deployment packageSQLite + optional monitoring
docker-compose.dev.ymlDevelopmentSQLite
Terminal window
cd deployment
# Create .env file
cat > .env <<EOF
LICENSE_JWT_SECRET=$(openssl rand -base64 32)
LICENSE_LOGGING_LEVEL=info
TZ=Europe/Berlin
EOF
# Start
docker compose up -d
# Verify
docker compose ps
curl http://localhost:5656/health
Terminal window
cd deployment
cat > .env <<EOF
LICENSE_JWT_SECRET=$(openssl rand -base64 32)
LICENSE_DATABASE_DRIVER=postgres
LICENSE_DATABASE_PASSWORD=$(openssl rand -base64 24)
LICENSE_LOGGING_LEVEL=info
TZ=Europe/Berlin
EOF
# Start with PostgreSQL profile
docker compose --profile postgres up -d

Includes PostgreSQL, Prometheus, Grafana, Loki, Promtail, AlertManager, cAdvisor, and Node Exporter.

Terminal window
cd deployment
cat > .env.full <<EOF
LICENSE_JWT_SECRET=$(openssl rand -base64 32)
LICENSE_DATABASE_PASSWORD=$(openssl rand -base64 24)
GF_SECURITY_ADMIN_PASSWORD=changeme
EOF
docker compose -f docker-compose.full.yml --env-file .env.full up -d

There are three ways to build the Docker image:

Terminal window
# Using Taskfile
task docker:build VERSION=v1.0.0
# Using the release tool
./bin/release-tool docker build --version v1.0.0
# Directly with docker
docker build \
--build-arg VERSION=v1.0.0 \
--build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \
--build-arg BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ) \
-f deployment/Dockerfile \
-t license-server:v1.0.0 .

The production Dockerfile uses three stages:

  1. Stage 1 — bun:alpine: Installs frontend dependencies and builds the React app with Bun.
  2. Stage 2 — golang:alpine: Compiles the Go binary with CGO enabled (required for SQLite), embeds the built frontend assets, and compresses the binary with UPX.
  3. Stage 3 — alpine: Minimal production image running as a non-root user. Contains only the compressed binary and required runtime dependencies.

VolumeMount PointDescription
license-data/app/dataSQLite database, signing keys
license-backups/app/backupsDatabase backups
Certificates/app/certsTLS certificates (read-only)
Config/app/configs/config.yamlConfiguration file (read-only)

The container includes a built-in health check:

GET http://localhost:5656/health

Returns 200 OK when the server is ready to accept requests.


apiVersion: apps/v1
kind: Deployment
metadata:
name: license-server
labels:
app: license-server
spec:
replicas: 1
selector:
matchLabels:
app: license-server
template:
metadata:
labels:
app: license-server
spec:
containers:
- name: license-server
image: git.prd.embidio.de/hive/license-server:v1.0.0
ports:
- containerPort: 5656
name: http
- containerPort: 50090
name: grpc
env:
- name: LICENSE_JWT_SECRET
valueFrom:
secretKeyRef:
name: license-server-secrets
key: jwt-secret
- name: LICENSE_DATABASE_DRIVER
value: "postgres"
- name: LICENSE_DATABASE_HOST
value: "postgres-service"
- name: LICENSE_DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: license-server-secrets
key: db-password
volumeMounts:
- name: data
mountPath: /app/data
- name: backups
mountPath: /app/backups
livenessProbe:
httpGet:
path: /health
port: 5656
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 5656
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: data
persistentVolumeClaim:
claimName: license-server-data
- name: backups
persistentVolumeClaim:
claimName: license-server-backups
apiVersion: v1
kind: Service
metadata:
name: license-server
spec:
selector:
app: license-server
ports:
- name: http
port: 5656
targetPort: 5656
- name: grpc
port: 50090
targetPort: 50090
Terminal window
kubectl create secret generic license-server-secrets \
--from-literal=jwt-secret=$(openssl rand -base64 32) \
--from-literal=db-password=$(openssl rand -base64 24)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: license-server-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: license-server-backups
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: license-server
annotations:
cert-manager.io/cluster-issuer: letsencrypt
spec:
tls:
- hosts:
- license.example.com
secretName: license-server-tls
rules:
- host: license.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: license-server
port:
number: 5656

  • Linux (amd64)
  • SQLite or PostgreSQL
  • (Optional) systemd for service management
Terminal window
# 1. Extract the release package
tar xzf license-server-v1.0.0-linux-amd64.tar.gz
cd license-server
# 2. Create config
./license-server config init --output config.yaml
# 3. Edit config.yaml with your settings
# At minimum, set jwt.secret
# 4. Run migrations
./license-server migrate up
# 5. Create initial admin
./license-server seed
# 6. Start the server
./license-server serve

Create /etc/systemd/system/license-server.service:

[Unit]
Description=License Server
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=license-server
Group=license-server
WorkingDirectory=/opt/license-server
ExecStart=/opt/license-server/license-server serve
Restart=on-failure
RestartSec=5
# Environment
Environment=LICENSE_JWT_SECRET=your-secret-here
Environment=LICENSE_LOGGING_LEVEL=info
Environment=LICENSE_LOGGING_FORMAT=json
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/license-server/data /opt/license-server/backups
PrivateTmp=true
[Install]
WantedBy=multi-user.target

Enable and start the service:

Terminal window
# Create service user
sudo useradd -r -s /sbin/nologin license-server
# Set permissions
sudo chown -R license-server:license-server /opt/license-server
# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable license-server
sudo systemctl start license-server
# Check status
sudo systemctl status license-server
sudo journalctl -u license-server -f
/opt/license-server/
├── license-server # Binary
├── config.yaml # Configuration
├── server.license # Server license file
├── data/
│ ├── license.db # SQLite database
│ └── keys/ # Signing keys
│ └── server/
│ ├── signing.key
│ └── signing.pub
├── backups/ # Database backups
└── uploads/ # Avatar images, logos

EndpointPortDescription
Web UI:5656Admin dashboard
REST API:5656/apiHTTP API
OpenAPI Docs:5656/docsRedoc API documentation
Health Check:5656/healthLiveness/readiness
Metrics:5656/metricsPrometheus metrics
gRPC API:50090License validation (client library)

Terminal window
openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt \
-days 365 -nodes -subj '/CN=license.example.com'
Terminal window
LICENSE_SSL_ENABLED=true
LICENSE_SSL_CERT_FILE=/app/certs/server.crt
LICENSE_SSL_KEY_FILE=/app/certs/server.key
LICENSE_SSL_CA_FILE=/app/certs/ca.crt # For mTLS

Mount the certificates directory as a read-only volume:

volumes:
- ./certs:/app/certs:ro