{
  "manifest_version": "1.0.0",
  "template": {
    "id": "5219ca7e-ac25-4c01-beef-49a49312fca6",
    "slug": "opengym-v1-self-hosted-workout-tracker",
    "name": "openGym v1 Self-Hosted Workout Tracker",
    "description": "Plan routines, log workouts and track body weight on your own server.",
    "url": "https://railway.com/deploy/opengym-v1-self-hosted-workout-tracker",
    "upstream": {
      "repo_url": "https://github.com/baranberkay96/opengym-railway"
    }
  },
  "status": "unvalidated",
  "validated_at": null,
  "success_rate_30d": null,
  "services": [
    {
      "name": "openGym",
      "source": {
        "repo": "https://github.com/baranberkay96/opengym-railway"
      },
      "needs_volume": false,
      "http": true
    },
    {
      "name": "openGym API",
      "source": {
        "repo": "https://github.com/baranberkay96/opengym-railway"
      },
      "needs_volume": true,
      "volume_mount_path": "/data",
      "http": false
    }
  ],
  "required_inputs": [
    {
      "key": "PORT",
      "service": "openGym",
      "description": "Port nginx listens on (Railway's domain and healthcheck) and, in openGym's web image, also the API port it proxies /api to. Keep 8080 on both services.",
      "secret": false,
      "strategy": "default",
      "default": "8080"
    },
    {
      "key": "BACKEND",
      "service": "openGym",
      "description": "Private host name of the openGym API service; /api is proxied to it.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "RESOLVER",
      "service": "openGym",
      "description": "DNS server nginx uses to look up BACKEND. auto reads Railway's from /etc/resolv.conf.",
      "secret": false,
      "strategy": "default",
      "default": "auto"
    },
    {
      "key": "NGINX_PORT",
      "service": "openGym",
      "description": "Port nginx listens on. Keep it equal to PORT.",
      "secret": false,
      "strategy": "default",
      "default": "8080"
    },
    {
      "key": "OPENGYM_VERSION",
      "service": "openGym",
      "description": "openGym release for both services (web and API images, used as a build argument). Change it, then redeploy both.",
      "secret": false,
      "strategy": "default",
      "default": "1.4.3"
    },
    {
      "key": "MEDIA_UPLOAD_MAX",
      "service": "openGym",
      "description": "Largest photo or video upload nginx passes to the API (nginx size). Keep it above the API's MEDIA_VIDEO_MAX_MB (40).",
      "secret": false,
      "strategy": "default",
      "default": "48m"
    },
    {
      "key": "NGINX_WORKER_PROCESSES",
      "service": "openGym",
      "description": "nginx worker processes (1-64 or auto). auto would start one per host CPU, dozens on Railway.",
      "secret": false,
      "strategy": "default",
      "default": "2"
    },
    {
      "key": "OPENGYM_REAL_IP_HEADER",
      "service": "openGym",
      "description": "Header Railway's edge puts the visitor address in, so per-address sign-in limits count real visitors. off = every visitor counts as Railway's edge.",
      "secret": false,
      "strategy": "default",
      "default": "X-Real-IP"
    },
    {
      "key": "PORT",
      "service": "openGym API",
      "description": "Port the API listens on (private network and healthcheck). The openGym web service proxies to it; keep 8080 on both services.",
      "secret": false,
      "strategy": "default",
      "default": "8080"
    },
    {
      "key": "RP_ID",
      "service": "openGym API",
      "description": "Host name passkeys are bound to: the domain people open openGym on. Set it and ORIGIN to your custom domain if you add one.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "ORIGIN",
      "service": "openGym API",
      "description": "Full https address openGym is served from. Checked on every sign-in and sync; keep it in step with RP_ID.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "RP_NAME",
      "service": "openGym API",
      "description": "Name shown in the passkey prompt.",
      "secret": false,
      "strategy": "default",
      "default": "openGym"
    },
    {
      "key": "AUDIT_IP",
      "service": "openGym API",
      "description": "Visitor addresses in the admin activity log: off, net (network only) or full.",
      "secret": false,
      "strategy": "default",
      "default": "off"
    },
    {
      "key": "DATA_DIR",
      "service": "openGym API",
      "description": "Where openGym keeps everything (db.json, per-profile state, uploads, session key, push keys). Must stay on the /data volume.",
      "secret": false,
      "strategy": "default",
      "default": "/data"
    },
    {
      "key": "ALLOW_GUEST",
      "service": "openGym API",
      "description": "0 hides 'Continue without account' (browser-only guest mode). 1 shows it.",
      "secret": false,
      "strategy": "default",
      "default": "0"
    },
    {
      "key": "INVITE_ONLY",
      "service": "openGym API",
      "description": "1 = new profiles need an invite code from the admin dashboard (sign-up closed). 0 opens sign-up to anyone with the URL.",
      "secret": false,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "TRUST_PROXY",
      "service": "openGym API",
      "description": "Read the visitor address from the headers the openGym web service overwrites, for per-address sign-in limits. Keep 1.",
      "secret": false,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "SESSION_DAYS",
      "service": "openGym API",
      "description": "How long a sign-in lasts, in days (openGym's default). Applies to new sign-ins only.",
      "secret": false,
      "strategy": "default",
      "default": "90"
    },
    {
      "key": "COACH_DISABLED",
      "service": "openGym API",
      "description": "1 keeps the optional AI Coach off for good. With 0 it stays off until an admin connects a provider.",
      "secret": false,
      "strategy": "default",
      "default": "0"
    },
    {
      "key": "MEDIA_QUOTA_MB",
      "service": "openGym API",
      "description": "Upload space per profile for exercise and workout photos and videos, in MB (0 = no cap).",
      "secret": false,
      "strategy": "default",
      "default": "200"
    },
    {
      "key": "PASSWORD_LOGIN",
      "service": "openGym API",
      "description": "1 = name or e-mail plus password sign-in next to passkeys. The owner needs it until they add a passkey.",
      "secret": true,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "OPENGYM_VERSION",
      "service": "openGym API",
      "description": "Follows OPENGYM_VERSION on the openGym service, so the API and web images are always the same release.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "MEDIA_MIN_FREE_MB",
      "service": "openGym API",
      "description": "Uploads are refused when the volume has less free space than this, so workout saves never hit a full disk.",
      "secret": false,
      "strategy": "default",
      "default": "200"
    },
    {
      "key": "OPENGYM_OWNER_NAME",
      "service": "openGym API",
      "description": "Profile name of the owner (admin) created on the first boot; you sign in with it. openGym cannot rename profiles, so pick your own name before deploying.",
      "secret": false,
      "strategy": "default",
      "default": "Owner"
    },
    {
      "key": "OPENGYM_OWNER_PASSWORD",
      "service": "openGym API",
      "description": "Owner password, generated at deploy. Read on the first boot only; change it later in Settings, Account.",
      "secret": true,
      "strategy": "generate",
      "generate": "strong_password"
    }
  ],
  "deploy": {
    "mcp": {
      "server": "railway",
      "tool": "deploy_template",
      "args": {
        "template_code": "opengym-v1-self-hosted-workout-tracker"
      }
    },
    "cli": "railway deploy --template opengym-v1-self-hosted-workout-tracker",
    "api": {
      "method": "POST",
      "path": "/graphql/v2",
      "body": {
        "query": "mutation templateDeploy($input: TemplateDeployV2Input!) { templateDeployV2(input: $input) { projectId workflowId } }",
        "variables": {
          "input": {
            "templateId": "5219ca7e-ac25-4c01-beef-49a49312fca6",
            "serializedConfig": {
              "buckets": {},
              "services": {
                "aca2c0ec-ca9d-5cf2-8f5f-e551de71fb85": {
                  "icon": "https://cdn.jsdelivr.net/gh/DuarteSantos8/openGym@v1.4.3/frontend/public/icon-512.png",
                  "name": "openGym",
                  "build": {
                    "builder": "DOCKERFILE"
                  },
                  "deploy": {
                    "healthcheckPath": "/api/health",
                    "restartPolicyType": "ON_FAILURE",
                    "healthcheckTimeout": 900,
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "repo": "baranberkay96/opengym-railway",
                    "branch": "main",
                    "rootDirectory": "/services/web"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": true,
                      "description": "Port nginx listens on (Railway's domain and healthcheck) and, in openGym's web image, also the API port it proxies /api to. Keep 8080 on both services.",
                      "defaultValue": "8080"
                    },
                    "BACKEND": {
                      "isOptional": true,
                      "description": "Private host name of the openGym API service; /api is proxied to it.",
                      "defaultValue": "${{openGym API.RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "RESOLVER": {
                      "isOptional": true,
                      "description": "DNS server nginx uses to look up BACKEND. auto reads Railway's from /etc/resolv.conf.",
                      "defaultValue": "auto"
                    },
                    "NGINX_PORT": {
                      "isOptional": true,
                      "description": "Port nginx listens on. Keep it equal to PORT.",
                      "defaultValue": "8080"
                    },
                    "OPENGYM_VERSION": {
                      "isOptional": true,
                      "description": "openGym release for both services (web and API images, used as a build argument). Change it, then redeploy both.",
                      "defaultValue": "1.4.3"
                    },
                    "MEDIA_UPLOAD_MAX": {
                      "isOptional": true,
                      "description": "Largest photo or video upload nginx passes to the API (nginx size). Keep it above the API's MEDIA_VIDEO_MAX_MB (40).",
                      "defaultValue": "48m"
                    },
                    "NGINX_WORKER_PROCESSES": {
                      "isOptional": true,
                      "description": "nginx worker processes (1-64 or auto). auto would start one per host CPU, dozens on Railway.",
                      "defaultValue": "2"
                    },
                    "OPENGYM_REAL_IP_HEADER": {
                      "isOptional": true,
                      "description": "Header Railway's edge puts the visitor address in, so per-address sign-in limits count real visitors. off = every visitor counts as Railway's edge.",
                      "defaultValue": "X-Real-IP"
                    }
                  },
                  "networking": {
                    "tcpProxies": {},
                    "serviceDomains": {
                      "<hasDomain>:8080": {
                        "port": 8080
                      }
                    }
                  }
                },
                "f1b98dac-cba7-58e3-8ad8-2093d2aa961e": {
                  "icon": "https://cdn.jsdelivr.net/gh/DuarteSantos8/openGym@v1.4.3/frontend/public/icon-512.png",
                  "name": "openGym API",
                  "build": {
                    "builder": "DOCKERFILE"
                  },
                  "deploy": {
                    "healthcheckPath": "/api/health",
                    "restartPolicyType": "ON_FAILURE",
                    "healthcheckTimeout": 300,
                    "restartPolicyMaxRetries": 10
                  },
                  "source": {
                    "repo": "baranberkay96/opengym-railway",
                    "branch": "main",
                    "rootDirectory": "/services/api"
                  },
                  "variables": {
                    "PORT": {
                      "isOptional": true,
                      "description": "Port the API listens on (private network and healthcheck). The openGym web service proxies to it; keep 8080 on both services.",
                      "defaultValue": "8080"
                    },
                    "RP_ID": {
                      "isOptional": true,
                      "description": "Host name passkeys are bound to: the domain people open openGym on. Set it and ORIGIN to your custom domain if you add one.",
                      "defaultValue": "${{openGym.RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "ORIGIN": {
                      "isOptional": true,
                      "description": "Full https address openGym is served from. Checked on every sign-in and sync; keep it in step with RP_ID.",
                      "defaultValue": "https://${{openGym.RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "RP_NAME": {
                      "isOptional": true,
                      "description": "Name shown in the passkey prompt.",
                      "defaultValue": "openGym"
                    },
                    "AUDIT_IP": {
                      "isOptional": true,
                      "description": "Visitor addresses in the admin activity log: off, net (network only) or full.",
                      "defaultValue": "off"
                    },
                    "DATA_DIR": {
                      "isOptional": true,
                      "description": "Where openGym keeps everything (db.json, per-profile state, uploads, session key, push keys). Must stay on the /data volume.",
                      "defaultValue": "/data"
                    },
                    "ALLOW_GUEST": {
                      "isOptional": true,
                      "description": "0 hides 'Continue without account' (browser-only guest mode). 1 shows it.",
                      "defaultValue": "0"
                    },
                    "INVITE_ONLY": {
                      "isOptional": true,
                      "description": "1 = new profiles need an invite code from the admin dashboard (sign-up closed). 0 opens sign-up to anyone with the URL.",
                      "defaultValue": "1"
                    },
                    "TRUST_PROXY": {
                      "isOptional": true,
                      "description": "Read the visitor address from the headers the openGym web service overwrites, for per-address sign-in limits. Keep 1.",
                      "defaultValue": "1"
                    },
                    "DEFAULT_LANG": {
                      "isOptional": true,
                      "description": "Optional default language for the sign-in screen and new profiles (en, de, es, fr, pt-BR, ...).",
                      "defaultValue": ""
                    },
                    "SESSION_DAYS": {
                      "isOptional": true,
                      "description": "How long a sign-in lasts, in days (openGym's default). Applies to new sign-ins only.",
                      "defaultValue": "90"
                    },
                    "VAPID_SUBJECT": {
                      "isOptional": true,
                      "description": "Optional contact for push services, e.g. mailto:you@example.com. Empty = ORIGIN.",
                      "defaultValue": ""
                    },
                    "COACH_DISABLED": {
                      "isOptional": true,
                      "description": "1 keeps the optional AI Coach off for good. With 0 it stays off until an admin connects a provider.",
                      "defaultValue": "0"
                    },
                    "MEDIA_QUOTA_MB": {
                      "isOptional": true,
                      "description": "Upload space per profile for exercise and workout photos and videos, in MB (0 = no cap).",
                      "defaultValue": "200"
                    },
                    "PASSWORD_LOGIN": {
                      "isOptional": true,
                      "description": "1 = name or e-mail plus password sign-in next to passkeys. The owner needs it until they add a passkey.",
                      "defaultValue": "1"
                    },
                    "OPENGYM_VERSION": {
                      "isOptional": true,
                      "description": "Follows OPENGYM_VERSION on the openGym service, so the API and web images are always the same release.",
                      "defaultValue": "${{openGym.OPENGYM_VERSION}}"
                    },
                    "MEDIA_MIN_FREE_MB": {
                      "isOptional": true,
                      "description": "Uploads are refused when the volume has less free space than this, so workout saves never hit a full disk.",
                      "defaultValue": "200"
                    },
                    "OPENGYM_OWNER_NAME": {
                      "isOptional": true,
                      "description": "Profile name of the owner (admin) created on the first boot; you sign in with it. openGym cannot rename profiles, so pick your own name before deploying.",
                      "defaultValue": "Owner"
                    },
                    "OPENGYM_OWNER_EMAIL": {
                      "isOptional": true,
                      "description": "Optional e-mail the owner can sign in with instead of the name. openGym never sends mail to it.",
                      "defaultValue": ""
                    },
                    "OPENGYM_OWNER_PASSWORD": {
                      "isOptional": true,
                      "description": "Owner password, generated at deploy. Read on the first boot only; change it later in Settings, Account.",
                      "defaultValue": "{{OPENGYM_OWNER_PASSWORD}}"
                    }
                  },
                  "networking": {
                    "tcpProxies": {},
                    "serviceDomains": {}
                  },
                  "volumeMounts": {
                    "61088062-fec6-5a87-a03b-b23bf5faac00": {
                      "mountPath": "/data"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "post_deploy": {
    "healthcheck": {
      "service": "openGym",
      "method": "GET",
      "path": "/api/health",
      "expect_status": 200
    }
  },
  "resources": {
    "expected_services": 2,
    "needs_volume": true
  },
  "generated_at": "2026-10-30T16:14:57.468Z",
  "generator_version": "0.1.0"
}
