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#
- Path Mapping: Map container's expected paths to tinsnip XDG structure
- Port Injection: Override container's ports with tinsnip allocation
- User Override: Force container to run as tinsnip service UID
- Config Adaptation: Transform tinsnip config to container's expected format
- 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#
-
Prepare Infrastructure:
./machine/setup.sh myservice prod nas-server -
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 -
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