homelab infrastructure services

Service Creation Guide#

This guide explains how to create services that work with tinsnip's NFS-backed persistence architecture.

Overview#

tinsnip provides standardized infrastructure for service deployment with:

  • NFS-backed persistence: Data survives machine rebuilds
  • XDG integration: Data accessible through standard Linux paths
  • UID isolation: Each service runs with dedicated user/permissions
  • Port allocation: Automatic port assignment based on service UID

Services can be created using two patterns depending on their origin and requirements.

The tinsnip Target Pattern#

tinsnip establishes a standard environment that services can leverage:

Infrastructure Provided#

NFS Mount Structure:

/mnt/tinsnip/                   # NFS mount point
├── data/                       # Persistent application data
├── config/                     # Service configuration files
├── state/                      # Service state (logs, databases, etc.)
└── service/                    # Docker compose location
    └── myservice/
        ├── docker-compose.yml
        └── setup.sh (optional)

Service Environment File (.env): Generated by tinsnip setup with deployment-specific paths:

# Tinsnip deployment - direct NFS mounts
XDG_DATA_HOME=/mnt/tinsnip/data
XDG_CONFIG_HOME=/mnt/tinsnip/config
XDG_STATE_HOME=/mnt/tinsnip/state

# Service metadata
TIN_SERVICE_NAME=myservice
TIN_SERVICE_ENVIRONMENT=prod
TIN_SERVICE_UID=11100
PRIMARY_PORT=11100
SECONDARY_PORT=11101

Environment Variable Mapping#

Environment Variable Value (set in .env) Container Path
TIN_SERVICE_UID 11100 Used for user
TIN_SERVICE_NAME myservice -
TIN_SERVICE_ENVIRONMENT prod -
TIN_NAMESPACE dynamicalsystem -
PRIMARY_PORT 11100 11100
SECONDARY_PORT 11101 11101
XDG_DATA_HOME /mnt/tinsnip/data /data
XDG_CONFIG_HOME /mnt/tinsnip/config /config
XDG_STATE_HOME /mnt/tinsnip/state /state

Path Resolution#

Host Path Container Path Description
${XDG_DATA_HOME}/${TIN_NAMESPACE}/${TIN_SERVICE_NAME} /data Application data
${XDG_CONFIG_HOME}/${TIN_NAMESPACE}/${TIN_SERVICE_NAME} /config Configuration files
${XDG_STATE_HOME}/${TIN_NAMESPACE}/${TIN_SERVICE_NAME} /state State/logs/cache

Example Resolution:

# For myservice-prod in dynamicalsystem namespace
${XDG_DATA_HOME}/${TIN_NAMESPACE}/${TIN_SERVICE_NAME}
(from .env)
/mnt/tinsnip/data/dynamicalsystem/myservice
(NFS mount)
nas-server:/volume1/dynamicalsystem/myservice/prod/data

Volume Requirements#

⚠️ CRITICAL: Services MUST use bind mounts to XDG-integrated NFS directories, not Docker named volumes.

✅ CORRECT (XDG + NFS-backed persistence):

volumes:
  - ${XDG_DATA_HOME}/${TIN_NAMESPACE}/${TIN_SERVICE_NAME}:/app/data
  - ${XDG_CONFIG_HOME}/${TIN_NAMESPACE}/${TIN_SERVICE_NAME}:/app/config  
  - ${XDG_STATE_HOME}/${TIN_NAMESPACE}/${TIN_SERVICE_NAME}:/app/state

❌ INCORRECT (Local storage - data lost on rebuild):

volumes:
  - myservice_data:/app/data    # Stored locally, lost on rebuild
volumes:
  myservice_data:               # Breaks continuous delivery

Why This Matters#

tinsnip's Value Proposition: Continuous delivery with persistent data that survives machine rebuilds.

  • With XDG Bind Mounts: Data stored on NFS → Survives machine rebuilds → True continuous delivery
  • With Named Volumes: Data stored locally → Lost on rebuild → Breaks continuous delivery

Pattern 1: Home-grown Services#

Use Case: Services built specifically for tinsnip that can follow conventions natively.

Design Principles#

  • Built to expect tinsnip's XDG + NFS directory structure
  • Uses environment variables for all configuration
  • Designed for the target UID and port scheme
  • No adaptation layer needed

Example: Custom Web Service (Gazette)#

services:
  gazette:
    image: myorg/gazette:latest
    ports:
      - "${PRIMARY_PORT}:3000"
    volumes:
      - ${XDG_DATA_HOME}/${TIN_NAMESPACE}/${TIN_SERVICE_NAME}:/app/documents
      - ${XDG_CONFIG_HOME}/${TIN_NAMESPACE}/${TIN_SERVICE_NAME}:/app/config
      - ${XDG_STATE_HOME}/${TIN_NAMESPACE}/${TIN_SERVICE_NAME}:/app/logs
    user: "${TIN_SERVICE_UID}:${TIN_SERVICE_UID}"
    environment:
      # Service-specific environment variables
      - GAZETTE_DOCUMENT_ROOT=/app/documents
      - GAZETTE_CONFIG_FILE=/app/config/gazette.yaml
      - GAZETTE_LOG_DIR=/app/logs
      - GAZETTE_PORT=3000
      - GAZETTE_BASE_URL=http://localhost:${PRIMARY_PORT}
      - GAZETTE_UID=${TIN_SERVICE_UID}
      - GAZETTE_NAMESPACE=${TIN_NAMESPACE}
    restart: unless-stopped
    networks:
      - tinsnip_network

Home-grown Service Benefits#

  • Clean, simple docker-compose.yml
  • No path translations or adaptations needed
  • Full leverage of tinsnip environment
  • Predictable behavior across deployments
  • Direct XDG compliance

Pattern 2: Third-party Adaptation#

Use Case: Existing external containers that need to be wrapped to work with tinsnip's conventions.

Adaptation Strategies#

  1. Path Mapping: Map container's expected paths to tinsnip XDG structure
  2. Port Injection: Override container's ports with tinsnip allocation
  3. User Override: Force container to run as tinsnip service UID
  4. Config Adaptation: Transform tinsnip config to container's expected format
  5. Environment Translation: Convert tinsnip variables to container's expectations

Example: LLDAP (Identity Service)#

LLDAP is an external container with its own conventions that we adapt:

# Third-party container adaptation
services:
  lldap:
    image: lldap/lldap:latest-alpine-rootless
    container_name: ${TIN_SERVICE_NAME:-lldap}-${TIN_SERVICE_ENVIRONMENT:-prod}
    ports:
      # Adapt: LLDAP's default ports → tinsnip port allocation
      - "${PRIMARY_PORT}:3890"      # LDAP protocol
      - "${SECONDARY_PORT}:17170"   # Web UI
    volumes:
      # Adapt: LLDAP expects /data → map to tinsnip XDG structure
      - ${XDG_DATA_HOME}/${TIN_NAMESPACE}/${TIN_SERVICE_NAME}:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    user: "${TIN_SERVICE_UID}:${TIN_SERVICE_UID}"
    environment:
      # Adapt: Translate tinsnip config to LLDAP's expected variables
      - LLDAP_JWT_SECRET=changeme-jwt-secret-32-chars-min
      - LLDAP_KEY_SEED=changeme-key-seed-32-chars-minimum
      - LLDAP_BASE_DN=dc=home,dc=local
      - LLDAP_LDAP_USER_DN=admin
      - LLDAP_LDAP_USER_PASS=changeme-admin-password
      - LLDAP_DATABASE_URL=sqlite:///data/users.db
    restart: unless-stopped
    networks:
      - tinsnip_network

networks:
  tinsnip_network:
    external: true

Service Deployment#

Deployment Process#

  1. Prepare Infrastructure:

    ./machine/setup.sh myservice prod nas-server
    
  2. Deploy Service:

    # Switch to service user
    sudo -u myservice-prod -i
    
    # Copy from service catalog or create locally
    cp -r ~/.local/opt/dynamicalsystem.service/myservice /mnt/docker/service/
    cd /mnt/docker/service/myservice
    
    # Run setup if present
    [[ -f setup.sh ]] && ./setup.sh
    
    # Deploy
    docker compose up -d
    
  3. Verify Deployment:

    docker compose ps
    docker compose logs -f
    

Service Management#

# Status check
docker compose ps

# View logs
docker compose logs -f [service-name]

# Restart service
docker compose restart

# Update service
docker compose pull
docker compose up -d

# Stop service
docker compose down

Data Access#

Direct NFS Mount Access:

# Access service data directly on NFS mounts
ls /mnt/tinsnip/data/dynamicalsystem/myservice     # Application data
ls /mnt/tinsnip/config/dynamicalsystem/myservice   # Configuration  
ls /mnt/tinsnip/state/dynamicalsystem/myservice    # State/logs

Note: With the new direct mount approach, XDG paths point directly to NFS mounts via the .env file, eliminating the need for symlinks.

Validation Checklist#

Before deploying, verify your service:

Volume Configuration#

  • No named volumes in volumes: section
  • All volumes use XDG environment variables
  • Volumes map to appropriate container paths
  • XDG paths resolve to NFS-backed directories

User and Permissions#

  • Service specifies user: "${TIN_SERVICE_UID}:${TIN_SERVICE_UID}"
  • Container processes run as non-root
  • File permissions work with tinsnip UID

Port Configuration#

  • Ports use environment variables (${PRIMARY_PORT}, etc.)
  • No hardcoded port numbers
  • Port allocation fits within UID range (UID to UID+9)

Network Configuration#

  • Service connects to tinsnip_network
  • Network is marked as external: true
  • Inter-service communication uses service names

Environment Variables#

  • Uses tinsnip-provided variables where appropriate
  • No hardcoded values that should be dynamic
  • Secrets loaded from files, not environment variables

XDG Integration#

  • Volumes reference XDG environment variables
  • Paths follow XDG Base Directory specification
  • Data accessible through both XDG and direct paths

Troubleshooting#

Data Not Persisting#

Problem: Data lost after docker compose down Solution: Check for named volumes, ensure XDG bind mounts

Permission Denied#

Problem: Container can't write to mounted directories Solution: Verify user: directive and NFS mount permissions

XDG Paths Not Working#

Problem: XDG symlinks broken or missing Solution: Re-run machine setup to recreate XDG symlinks

Port Conflicts#

Problem: Service won't start, port already in use Solution: Check environment variable usage, verify UID calculation

Config Not Loading#

Problem: Third-party service ignoring configuration Solution: Verify config file paths match container expectations