# 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: ```bash # 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:** ```bash # 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):** ```yaml 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):** ```yaml 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) ```yaml 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: ```yaml # 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**: ```bash ./machine/setup.sh myservice prod nas-server ``` 2. **Deploy Service**: ```bash # 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**: ```bash docker compose ps docker compose logs -f ``` ### Service Management ```bash # 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:** ```bash # 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