Examples
Real-world Ring manifests for common use cases. Every example uses the formats Ring's deserializer actually accepts: structured volume objects, map-style labels, secretRef for sensitive environment values.
Simple web apps
Static site with Nginx
# static-website.yaml deployments: static-site: name: static-site namespace: web runtime: docker image: "nginx:1.21-alpine" replicas: 2 volumes: - type: bind source: /var/www/html destination: /usr/share/nginx/html driver: local permission: ro labels: app: static-website "traefik.enable": "true" "traefik.http.routers.static.rule": "Host(`www.example.com`)"
Node.js API
# nodejs-app.yaml deployments: nodejs-api: name: nodejs-api namespace: backend runtime: docker image: "node:18-alpine" replicas: 3 environment: NODE_ENV: "production" PORT: "3000" DATABASE_URL: secretRef: "database-url" JWT_SECRET: secretRef: "jwt-secret" volumes: - type: bind source: /opt/nodejs-api destination: /usr/src/app driver: local permission: ro command: ["npm", "start"] labels: app: nodejs-api tier: backend
Databases
PostgreSQL with persistence
# postgres.yaml deployments: postgres-db: name: postgres-db namespace: database runtime: docker image: "postgres:13" replicas: 1 environment: POSTGRES_DB: "myapp" POSTGRES_USER: "appuser" POSTGRES_PASSWORD: secretRef: "postgres-password" PGDATA: "/var/lib/postgresql/data/pgdata" volumes: - type: bind source: /var/lib/ring/postgres/data destination: /var/lib/postgresql/data driver: local permission: rw - type: bind source: /var/lib/ring/postgres/backup destination: /backup driver: local permission: rw labels: app: postgres type: database backup: enabled
Create the secret before applying:
ring secret create postgres-password -n database -v "secure-pg-password"
Redis cache
# redis.yaml deployments: redis-cache: name: redis-cache namespace: cache runtime: docker image: "redis:7-alpine" replicas: 1 environment: REDIS_PASSWORD: secretRef: "redis-password" volumes: - type: bind source: /var/lib/ring/redis destination: /data driver: local permission: rw command: ["redis-server", "--requirepass", "$REDIS_PASSWORD", "--appendonly", "yes"] labels: app: redis type: cache
The
command:interpolation$REDIS_PASSWORDis resolved at apply time from the shell environment ofring apply, not from the deployment's environment. To inject a Ring-managed secret into the command, runring applyin a shell whereREDIS_PASSWORDis set, or use--env-file.
Configs and named volumes
Ring supports three volume types: bind (host path), volume (named Docker volume), and config (mount a file from a ring config entry).
Named volume
deployments: app: name: app namespace: default runtime: docker image: "myapp:latest" replicas: 1 volumes: - type: volume source: app-data # named Docker volume destination: /data driver: local # or "nfs" permission: rw
Config-mounted file
First create the config:
ring config create nginx-config -n web -f ./custom.conf
Then mount it:
deployments: nginx: name: nginx namespace: web runtime: docker image: "nginx:1.21" replicas: 1 volumes: - type: config source: nginx-config # config name in the same namespace destination: /etc/nginx/conf.d/custom.conf driver: local permission: ro
WordPress + MySQL
# wordpress.yaml deployments: wordpress: name: wordpress namespace: cms runtime: docker image: "wordpress:latest" replicas: 2 environment: WORDPRESS_DB_HOST: "mysql.database:3306" WORDPRESS_DB_NAME: "wordpress" WORDPRESS_DB_USER: "wp_user" WORDPRESS_DB_PASSWORD: secretRef: "wp-db-password" volumes: - type: bind source: /var/lib/ring/wordpress/wp-content destination: /var/www/html/wp-content driver: local permission: rw labels: app: wordpress type: cms mysql: name: mysql namespace: database runtime: docker image: "mysql:8.0" replicas: 1 environment: MYSQL_ROOT_PASSWORD: secretRef: "mysql-root-password" MYSQL_DATABASE: "wordpress" MYSQL_USER: "wp_user" MYSQL_PASSWORD: secretRef: "wp-db-password" volumes: - type: bind source: /var/lib/ring/mysql destination: /var/lib/mysql driver: local permission: rw labels: app: mysql type: database
Cross-namespace networking is not automatic in Ring —
mysql.databaseresolves only if both deployments share a namespace, or if MySQL is reachable through external routing. For the example above, put both deployments in the same namespace.
Microservices
# microservices.yaml deployments: frontend: name: frontend namespace: microservices runtime: docker image: "nginx:alpine" replicas: 2 volumes: - type: bind source: /opt/frontend/dist destination: /usr/share/nginx/html driver: local permission: ro - type: bind source: /opt/frontend/nginx.conf destination: /etc/nginx/nginx.conf driver: local permission: ro labels: app: frontend tier: presentation api-gateway: name: api-gateway namespace: microservices runtime: docker image: "nginx:alpine" replicas: 2 volumes: - type: bind source: /opt/gateway/nginx.conf destination: /etc/nginx/nginx.conf driver: local permission: ro labels: app: api-gateway tier: gateway user-service: name: user-service namespace: microservices runtime: docker image: "mycompany/user-service:v1.2.0" replicas: 3 environment: DATABASE_URL: secretRef: "user-db-url" JWT_SECRET: secretRef: "jwt-secret" SERVICE_PORT: "8001" labels: app: user-service tier: backend service: users order-service: name: order-service namespace: microservices runtime: docker image: "mycompany/order-service:v1.1.5" replicas: 2 environment: DATABASE_URL: secretRef: "order-db-url" REDIS_URL: "redis://redis-cache:6379" SERVICE_PORT: "8002" labels: app: order-service tier: backend service: orders
Private image registry
# enterprise-app.yaml deployments: internal-app: name: internal-app namespace: enterprise runtime: docker image: "registry.company.com/internal/myapp:v2.1.0" replicas: 5 config: server: "registry.company.com" username: "registry-user" password: "$REGISTRY_PASSWORD" image_pull_policy: "Always" environment: APP_ENV: "production" LOG_LEVEL: "info" DB_HOST: "db.company.internal" DB_NAME: "production_db" DB_USER: "app_user" DB_PASSWORD: secretRef: "app-db-password" labels: app: internal-app environment: production team: platform
$REGISTRY_PASSWORD is interpolated by ring apply from your shell environment (or from a file passed with --env-file).
Multiple environments in one file
# multi-env.yaml namespaces: development: name: development staging: name: staging production: name: production deployments: dev-api: name: api namespace: development runtime: docker image: "myapp:dev" replicas: 1 environment: NODE_ENV: "development" LOG_LEVEL: "debug" config: image_pull_policy: "Always" labels: app: api environment: development staging-api: name: api namespace: staging runtime: docker image: "myapp:staging" replicas: 2 environment: NODE_ENV: "staging" LOG_LEVEL: "info" DATABASE_URL: secretRef: "staging-database-url" config: image_pull_policy: "IfNotPresent" labels: app: api environment: staging prod-api: name: api namespace: production runtime: docker image: "myapp:v1.5.2" replicas: 5 environment: NODE_ENV: "production" LOG_LEVEL: "warn" DATABASE_URL: secretRef: "production-database-url" config: image_pull_policy: "IfNotPresent" labels: app: api environment: production criticality: high
Workers vs jobs
Worker (default)
Long-running services with replica management.
deployments: web-server: name: web-server namespace: default runtime: docker kind: worker # default; can be omitted image: "nginx:latest" replicas: 3
Job
One-shot task. Always one instance, no respawn after exit.
deployments: migration: name: migration namespace: default runtime: docker kind: job image: "myapp:latest" replicas: 1 command: ["npm", "run", "migrate"]
Mixed workers and a scheduler
# workers.yaml deployments: web-api: name: web-api namespace: workers runtime: docker image: "myapp:latest" replicas: 3 environment: ROLE: "web" PORT: "8000" REDIS_URL: "redis://redis-cache:6379" DATABASE_URL: secretRef: "database-url" labels: app: myapp component: web background-worker: name: background-worker namespace: workers runtime: docker image: "myapp:latest" replicas: 2 environment: ROLE: "worker" REDIS_URL: "redis://redis-cache:6379" DATABASE_URL: secretRef: "database-url" WORKER_CONCURRENCY: "4" command: ["python", "worker.py"] labels: app: myapp component: worker scheduler: name: scheduler namespace: workers runtime: docker image: "myapp:latest" replicas: 1 environment: ROLE: "scheduler" REDIS_URL: "redis://redis-cache:6379" DATABASE_URL: secretRef: "database-url" command: ["python", "scheduler.py"] labels: app: myapp component: scheduler
Monitoring stack
# monitoring.yaml deployments: prometheus: name: prometheus namespace: monitoring runtime: docker image: "prom/prometheus:latest" replicas: 1 volumes: - type: bind source: /opt/monitoring/prometheus.yml destination: /etc/prometheus/prometheus.yml driver: local permission: ro grafana: name: grafana namespace: monitoring runtime: docker image: "grafana/grafana:latest" replicas: 1 environment: GF_SECURITY_ADMIN_PASSWORD: secretRef: "grafana-admin-password"
Ring itself exposes a health endpoint:
curl http://localhost:3030/healthz # {"state":"UP"}
Patterns
Labels for service discovery
Labels are a key/value map. They flow into Docker container labels, so any tool that reads container labels (Traefik, Prometheus relabel, custom scripts) can use them.
deployments: app: labels: app: myapp component: frontend version: v1.2.3 environment: production team: web monitoring: prometheus backup: enabled criticality: high "traefik.enable": "true" "traefik.http.routers.app.rule": "Host(`app.example.com`)"
Quote keys that contain dots — YAML treats them as strings only when quoted.
Secrets
Sensitive values should be created as Ring secrets and referenced by name. They are stored AES-256-GCM-encrypted and decrypted only at deployment time.
deployments: secure-app: namespace: production environment: NODE_ENV: "production" LOG_LEVEL: "info" PORT: "3000" DATABASE_PASSWORD: secretRef: "database-password" JWT_SECRET: secretRef: "jwt-secret" API_KEY: secretRef: "external-api-key"
Create them once:
ring secret create database-password -n production -v "$DATABASE_PASSWORD" ring secret create jwt-secret -n production -v "$JWT_SECRET" ring secret create external-api-key -n production -v "$EXTERNAL_API_KEY"
The server must be started with RING_SECRET_KEY set; without it, all secret operations return 500 Internal Server Error.
Health checks for rolling updates
Adding a health check is what unlocks the zero-downtime rolling update path. See managing deployments for the full lifecycle.
deployments: app: name: app namespace: production runtime: docker image: "myapp:v1.2.3" replicas: 3 health_checks: - type: http url: "http://localhost:8080/health" interval: "10s" timeout: "5s" threshold: 3 on_failure: restart
Health-check duration suffixes: ms and s. Larger suffixes (m, h) are not parsed.