{"result":{"depiction_url":null,"edges":{"refers":{"objects":[{"created":"2026-11-05T18:43:22Z","object_id":{"id":1902,"is_a":["text","documentation","reference","module"],"name":"doc_module_mod_video","title":"mod_video","uri":"https:\/\/zotonic.com\/id\/1902"},"seq":1000000},{"created":"2026-11-05T18:43:22Z","object_id":{"id":3142,"is_a":["text","documentation","developerguide"],"name":"doc_guide_file_storage","title":"How Zotonic stores and collects media files","uri":"https:\/\/zotonic.com\/id\/3142"},"seq":1000000}],"predicate":{"id":2409,"is_a":["meta","predicate"],"name":"refers","title":{"_type":"trans","tr":{"en":"Refers"}},"uri":"https:\/\/zotonic.com\/id\/refers"}},"subject":{"objects":[{"created":"2026-11-05T18:43:22Z","object_id":{"id":2556,"is_a":["categorization","keyword","keyword_information_type"],"name":"zotonic_topic_explanation","title":"Explanation","uri":"https:\/\/zotonic.com\/id\/2556"},"seq":1},{"created":"2026-11-05T18:43:22Z","object_id":{"id":2564,"is_a":["categorization","keyword","keyword_audience"],"name":"zotonic_topic_backend_developer","title":"Backend developer","uri":"https:\/\/zotonic.com\/id\/2564"},"seq":2},{"created":"2026-11-05T18:43:22Z","object_id":{"id":2566,"is_a":["categorization","keyword","keyword_audience"],"name":"zotonic_topic_operator","title":"Operator","uri":"https:\/\/zotonic.com\/id\/2566"},"seq":3},{"created":"2026-11-05T18:43:22Z","object_id":{"id":2576,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_media_management","title":"Media management","uri":"https:\/\/zotonic.com\/id\/2576"},"seq":4},{"created":"2026-11-05T18:43:22Z","object_id":{"id":2581,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_file_storage","title":"File storage","uri":"https:\/\/zotonic.com\/id\/2581"},"seq":5},{"created":"2026-11-05T18:43:22Z","object_id":{"id":2599,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_api_and_integration","title":"API and integration","uri":"https:\/\/zotonic.com\/id\/2599"},"seq":6}],"predicate":{"id":308,"is_a":["meta","predicate"],"name":"subject","title":{"_type":"trans","tr":{"en":"Keyword"}},"uri":"http:\/\/purl.org\/dc\/elements\/1.1\/subject"}}},"id":3143,"is_a":["text","documentation","developerguide"],"links":[{"rel":"self","target":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/3143"},{"rel":"hub","target":"https:\/\/zotonic.com\/.zotonic\/websub"}],"medium":null,"medium_url":null,"name":"doc_guide_mediarunner","page_url":{"en":"https:\/\/zotonic.com\/docs\/3143\/running-media-processing-on-a-separate-host","x-default":"https:\/\/zotonic.com\/docs\/3143\/running-media-processing-on-a-separate-host"},"preview_url":null,"resource":{"version":15,"pivot_location_lat":null,"title":"Running media processing on a separate host","is_authoritative":true,"body":"<h1>Running media processing on a separate host<\/h1>\n<p>Image resizing, PDF previews and video conversion can use considerable CPU, memory\nand temporary disk space. The <strong>mediarunner<\/strong> is a dedicated Zotonic site that\nperforms this work for other Zotonic installations. Moving processing to a runner\nlets you size and maintain the processing host separately from the websites.<\/p>\n<p>The website still owns its media records, access rules and stored files. The\nrunner receives the inputs needed for a job and returns its outputs. Its cache\nhelps avoid repeated transfers and rendering; it is not the website&#39;s permanent\nmedia store. See <a href=\"\/id\/doc_guide_file_storage\">how Zotonic stores files<\/a> for that part.<\/p>\n<h2>The parts of the system<\/h2>\n<p><img src=\"https:\/\/github.com\/zotonic\/zotonic\/blob\/master\/doc\/guides\/images\/mediarunner-architecture.png\" alt=\"The website owns media records and storage. It exchanges authenticated jobs and files with a runner, whose isolated jobs use private copies of cached inputs.\" \/><\/p>\n<p>A runner has a PostgreSQL database for its queue and cache registry, persistent\ndisk for cached inputs and results, and private working directories for commands.\nGeneral jobs and full ffmpeg video conversions have separate worker pools, so a\nlong video conversion need not hold up image previews or video thumbnails.<\/p>\n<p>On the website, Zotonic&#39;s media pipeline chooses the appropriate processing\nprofile automatically. Custom Erlang code uses <code>z_exec:run\/4<\/code> with a profile, a\nsite context, and explicit <code>read<\/code> and <code>write<\/code> paths. Plain <code>z_exec:run\/1,2<\/code> commands\nremain local. See <a href=\"https:\/\/github.com\/zotonic\/zotonic\/blob\/master\/apps\/zotonic_core\/src\/support\/z_exec.erl\">the execution API<\/a> for the profile interface.<\/p>\n<h2>Set up a runner<\/h2>\n<ol><li>Install the <a href=\"https:\/\/github.com\/zotonic\/mediarunner\">mediarunner site<\/a> under\n<code>apps_user\/mediarunner<\/code> in a compatible Zotonic checkout. Use a dedicated host\nor container, with persistent disk and OS or container resource limits.<\/li><li>Install the sandbox helper and the required processing tools. Typical installs\nneed ImageMagick, ffmpeg, ffprobe and Ghostscript for PDF processing. Install the\nfonts and ImageMagick policy that your previews require, too.<\/li><li>Set the runner hostname, database, TLS and administrator credentials in private\nsite configuration, then enable the site. The supplied site configuration is\ndisabled. Budget disk for both the persistent cache and concurrent job copies.<\/li><li>Check <code>z_exec:sandbox_status\/0<\/code> on the runner and the isolation indicator on its\ndashboard. Confirm the actual restrictions on the deployment platform before\naccepting jobs.<\/li><li>In the runner dashboard, choose <strong>Add website \/ consumer<\/strong>. Store the generated\nOAuth2 bearer key securely; it is shown only once. Use a separate consumer for\neach client installation that should have its own cache access and statistics.<\/li><\/ol>\n<p>The runner&#39;s <a href=\"https:\/\/github.com\/zotonic\/mediarunner\/blob\/master\/docs\/docker.md\">deployment documentation<\/a>\nand <a href=\"https:\/\/github.com\/zotonic\/mediarunner\/blob\/master\/docs\/reference.md\">configuration reference<\/a>\ncover installation commands, capacity limits and container setup.<\/p>\n<h2>Connect the website<\/h2>\n<p>Add these entries to the <code>zotonic<\/code> application section of the <strong>client&#39;s system\nconfiguration<\/strong>, <code>zotonic.config<\/code>:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">{media_runner_hostname, &lt;&lt;&quot;media.example.com&quot;&gt;&gt;},\n{media_runner_oauth2_key, &lt;&lt;&quot;YOUR-OAUTH2-BEARER-TOKEN&quot;&gt;&gt;},\n{media_runner_local_fallback, false}\n<\/code><\/pre>\n<p>The hostname can include a port. HTTPS is required, certificates are checked, and\nredirects are refused. Keep the <code>file<\/code> utility installed on the client for local\nMIME detection. Without a configured runner, processing stays local.<\/p>\n<p>The runner must also be able to reach the website&#39;s canonical HTTPS callback URL.\nBehind a load balancer, route callbacks to the <strong>Zotonic node that submitted the\njob<\/strong>: the waiting callback registration belongs to that node. Check the site&#39;s\ncanonical hostname and proxy configuration, rather than relying on the hostname\nof an incoming browser request.<\/p>\n<p>Allow streaming PUTs and GETs through the runner&#39;s reverse proxy. Avoid buffering\nlarge file transfers, and set request-size and timeout limits appropriate for the\nlargest media you accept. The control API and byte transfers use different routes:<\/p>\n<table class=\"table\"><thead><tr><th>Traffic<\/th><th>Route on the runner<\/th><\/tr><\/thead><tbody><tr><td>Job submission, status, capabilities and receipt<\/td><td><code>\/api\/model\/mediarunner_job\/…<\/code><\/td><\/tr><tr><td>Input uploads<\/td><td><code>\/media-runner\/jobs\/files\/:hash<\/code><\/td><\/tr><tr><td>Result downloads<\/td><td><code>\/media-runner\/jobs\/results\/:hash<\/code><\/td><\/tr><\/tbody><\/table>\n<p>Start with a test image and resized preview, then a small video. Check queue\ncompletion, callback delivery and successful downloads. For several processing\nhosts, use the client runner-pool configuration described in the runner&#39;s\n<a href=\"https:\/\/github.com\/zotonic\/mediarunner\/blob\/master\/docs\/pool.md\">pool guide<\/a>.<\/p>\n<h2>What crosses the network<\/h2>\n<p><img src=\"https:\/\/github.com\/zotonic\/zotonic\/blob\/master\/doc\/guides\/images\/mediarunner-job.png\" alt=\"Sequence diagram: the website submits a job to the runner, uploads missing inputs, and waits for its processing worker. The runner sends a callback or answers status polling; the website downloads, verifies and acknowledges the results.\" \/><\/p>\n<p>In the sequence diagram, time flows downward; labelled conditional exchanges occur\nonly when inputs or callbacks are missing. The worker represents the runner’s\nprocessing service, which stages inputs and stores outputs around the sandboxed\ncommand.<\/p>\n<p>The job describes the command, profile, input hashes, output files and expiry.\nSmall control messages use JSON. File bytes are streamed separately, rather than\nembedded in JSON. If the runner already has an input for that consumer, the client\ncan reuse it. Concurrent clients reserve a missing hash so only one uploads it.<\/p>\n<p>The runner persists accepted jobs before processing. A completed job produces a\nmanifest containing output sizes, SHA-256 hashes and authenticated download URLs.\nThe callback announces that manifest; status polling can recover a lost callback.\nThe client accepts result URLs only from the selected runner endpoint and verifies\nsizes and hashes before publishing the downloaded outputs.<\/p>\n<p><strong>Callback delivery and file receipt are different events.<\/strong> The runner retains a\ncompleted manifest and its protected outputs until the client acknowledges receipt\nor result retention expires. The default retention is one day after completion.\nStatus recovery remains available during that window even after execution expiry.\nThe client&#39;s receipt acknowledgement is bounded and best effort; losing it leaves\ncleanup to the retention policy.<\/p>\n<h2>Isolation and trust<\/h2>\n<p>On supported systems, commands run with profile-specific filesystem and resource\nrestrictions and without network access. They can use their staged inputs,\noutputs, scratch space and required tool\/runtime files. They cannot access the\nshared cache directly. Inputs are copied into private job directories, so a command\nthat overwrites an input cannot alter another job&#39;s cached original.<\/p>\n<p>Isolation depends on the host. Unsupported operating systems, including Linux\nwithout the required Landlock support, can continue <strong>without sandbox isolation<\/strong>\nand display an alarm. Missing helpers or other sandbox setup failures on supported\nsystems block processing. Check the runner dashboard and <code>z_exec:sandbox_status\/0<\/code>;\na running service alone does not prove that isolation is active.<\/p>\n<p>Bearer keys authorize command execution within the selected profiles. Issue them\nonly to trusted clients, use separate consumer accounts, and rotate keys when\naccess changes. Native sandboxing complements host isolation and resource limits;\nlimits on individual processes do not provide a single budget for every delegate\na command might start. See the runner&#39;s configuration reference for the exact\nplatform behavior and permissions.<\/p>\n<h2>Timeouts, retries and maintenance<\/h2>\n<p>Three different waits matter:<\/p>\n<ul><li>A browser waits up to five seconds for a preview lookup. Pending or failed work\nreturns HTTP 503 with <code>Retry-After<\/code> and <code>Cache-Control: no-store<\/code>; shared generation\ncan continue for a later request. Failed previews do not become permanent\nplaceholders under the successful preview pathname.<\/li><li>A command has its profile&#39;s execution limit.<\/li><li>The client has a total operation budget, covering packing, transfers, queueing,\nexecution and result installation, including failover. The global\n<code>media_runner_wait_timeout<\/code> sets this budget in seconds. Its default is the command\ntimeout plus 300 seconds. Keep client and runner clocks synchronized for wire expiry.<\/li><\/ul>\n<p>Local fallback is optional and uses the remaining operation budget. It applies to\navailability failures, not authentication or integrity failures. Enabling it means\nprovisioning compatible tools and an appropriate local sandbox policy on the\nwebsite host. Keep ImageMagick versions, fonts and rendering policies consistent.<\/p>\n<p>Temporary video-processing failures retain the conversion task and its source.\nAutomatic retries use backoff and pause after 24 hours of recorded failures;\noperator-action errors pause immediately. Deleted or replaced media release retained\ninputs during reconciliation. See <a href=\"\/id\/doc_module_mod_video\">video conversion<\/a> for retry\nstate and administrative recovery.<\/p>\n<p>Watch queue age, failed or paused work, disk usage, callback failures and isolation\nstatus. The runner cache evicts eligible entries; private job directories need disk\nspace as well. Change <code>mediarunner_cache_version<\/code> after changing rendering dependencies\nthat are not otherwise reflected in the cache key. The website&#39;s CAFS renderer version\nis a separate setting, explained in the <a href=\"\/id\/doc_guide_file_storage\">file-storage guide<\/a>.<\/p>\n<p>Runner capacity and cache settings belong in the <strong>runner&#39;s site configuration<\/strong>;\nclient connection settings belong in <strong>system configuration<\/strong>. For detailed control\noperations see <a href=\"https:\/\/github.com\/zotonic\/mediarunner\/blob\/master\/src\/models\/m_mediarunner_job.erl\">the job model<\/a>, and for consumer credentials\nsee <a href=\"https:\/\/github.com\/zotonic\/mediarunner\/blob\/master\/src\/models\/m_mediarunner_consumer.erl\">the consumer model<\/a>. Upgrade compatible runner and\nclient versions together; deploy result-retention support before depending on status\nrecovery. Pending client calls do not survive a client-node restart. There is currently\nno remote cancellation API: abandoned remote attempts remain bounded by their expiry.<\/p>","slug":"running-media-processing-on-a-separate-host","is_protected":false,"visible_for":0,"tz":"UTC","language":["en"],"doc_source_hash":"4411e492aa73f36250ac1082b460a9612e9e54d8bd06f37a6f10b6fe87414e7a","is_featured":false,"content_group_id":{"id":2551,"is_a":["meta","content_group"],"name":"content_group_imported_docs","title":"Imported documentation","uri":"https:\/\/zotonic.com\/id\/content_group_imported_docs"},"category_id":{"id":317,"is_a":["meta","category"],"name":"developerguide","title":"Developer guide","uri":"https:\/\/zotonic.com\/id\/developerguide"},"doc_source_path":"doc\/guides\/mediarunner.md","publication_start":"2026-11-05T18:43:22Z","github_url":"https:\/\/github.com\/zotonic\/zotonic\/blob\/master\/doc\/guides\/mediarunner.md","pivot_location_lng":null,"doc_source_kind":"guide","name":"doc_guide_mediarunner","is_unfindable":false,"is_published":true,"pivot_geocode":null,"created":"2026-11-05T18:43:22Z","uri":null,"doc_status":"current","is_dependent":false,"publication_end":"9999-06-01T00:00:00Z","modifier_id":{"id":1,"is_a":["person"],"name":"administrator","title":"Site Administrator","uri":"https:\/\/zotonic.com\/id\/1"},"privacy":0,"doc_source_commit":"becf161621dcc4de964bbf083a88aff6d3ab21ae\n","creator_id":{"id":1,"is_a":["person"],"name":"administrator","title":"Site Administrator","uri":"https:\/\/zotonic.com\/id\/1"},"modified":"2026-11-05T18:43:22Z","title_slug":"running-media-processing-on-a-separate-host"},"uri":"https:\/\/zotonic.com\/id\/3143","uri_template":"https:\/\/zotonic.com\/id\/:id","websub":{"hub":"https:\/\/zotonic.com\/.zotonic\/websub","topic":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/3143"}},"status":"ok"}