@jaredwray/mockhttp 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,664 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <meta name="description" content="Run MockHTTP from the jaredwray/mockhttp image, with Docker Compose, or by building the image from this repository.">
8
+ <link rel="stylesheet" href="/css/variables.css">
9
+ <link rel="stylesheet" href="/css/styles.css">
10
+ <link rel="stylesheet" href="/css/highlight/styles/base16/docula.css">
11
+ <link rel="icon" href="/logo.svg">
12
+ <script>
13
+ (function(){
14
+ window.__doculaThemeKey = 'docula:theme:' + ('https://mockhttp.org').replace(/^https?:\/\//, '');
15
+ var mode = localStorage.getItem(window.__doculaThemeKey) || 'light';
16
+ var resolved = mode === 'system'
17
+ ? (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark')
18
+ : mode;
19
+ if (resolved === 'light') document.documentElement.setAttribute('data-theme', 'light');
20
+ try {
21
+ var authState = JSON.parse(localStorage.getItem('docula-auth-state'));
22
+ if (authState) {
23
+ window.__doculaAuth = authState;
24
+ if (authState.loggedIn) {
25
+ document.documentElement.classList.add('docula-auth-logged-in');
26
+ }
27
+ }
28
+ } catch (e) {
29
+ // Ignore localStorage parsing errors
30
+ }
31
+ if (!window.__doculaAuth) {
32
+ window.__doculaAuth = { loggedIn: false, displayName: null };
33
+ }
34
+ })();
35
+ </script>
36
+ <title>MockHTTP - Docker</title>
37
+ <script type="application/ld+json">
38
+ {"@context":"https://schema.org","@type":"TechArticle","headline":"Docker","description":"Run MockHTTP from the jaredwray/mockhttp image, with Docker Compose, or by building the image from this repository.","url":"https://mockhttp.org/docs/how-to-deploy/docker/","publisher":{"@type":"Organization","name":"MockHTTP"},"dateModified":"2026-10-20","keywords":[]}
39
+ </script>
40
+ </head>
41
+
42
+ <body>
43
+ <header class="header">
44
+ <div class="header-top">
45
+ <div class="header-top__content">
46
+ <button class="mobile-menu-toggle" id="mobile-menu-toggle" aria-label="Toggle navigation menu">
47
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
48
+ </button>
49
+ <a href="/" class="logo-link">
50
+ <img alt="MockHTTP" class="logo__img" src="/logo.svg">
51
+ </a>
52
+ <nav class="header-bottom__nav">
53
+ <a class="header-bottom__item" href="/docs/" id="nav-docs">
54
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
55
+ <span>Documentation</span>
56
+ </a>
57
+ <a class="header-bottom__item" href="/api" id="nav-api">
58
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>
59
+ <span>API Reference</span>
60
+ </a>
61
+ <a class="header-bottom__item" href="/changelog/" id="nav-changelog">
62
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 6a13 13 0 0 0 8.4-2.8A1 1 0 0 1 21 4v12a1 1 0 0 1-1.6.8A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"/><path d="M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14"/><path d="M8 6v8"/></svg>
63
+ <span>Changelog</span>
64
+ </a>
65
+ </nav>
66
+ <div class="header-actions">
67
+ <button class="search-button" id="search-button" type="button" aria-label="Search" aria-keyshortcuts="Meta+K Control+K">
68
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
69
+ <span class="search-button__text">Search</span>
70
+ <span class="search-button__shortcut">⌘K</span>
71
+ </button>
72
+ <button class="theme-button" id="theme-toggle" aria-label="Theme: system (following system)">
73
+ <svg class="theme-button__icon theme-button__icon--system" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>
74
+ <svg class="theme-button__icon theme-button__icon--sun theme-button__icon--hidden" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
75
+ <svg class="theme-button__icon theme-button__icon--moon theme-button__icon--hidden" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/></svg>
76
+ </button>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ </header>
81
+ <aside class="mobile-sidebar" id="mobile-sidebar">
82
+ <nav class="mobile-nav">
83
+ <a class="mobile-nav__item" href="/docs/">
84
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
85
+ <span>Documentation</span>
86
+ </a>
87
+ <a class="mobile-nav__item" href="/api">
88
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>
89
+ <span>API Reference</span>
90
+ </a>
91
+ <a class="mobile-nav__item" href="/changelog/">
92
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 6a13 13 0 0 0 8.4-2.8A1 1 0 0 1 21 4v12a1 1 0 0 1-1.6.8A13 13 0 0 0 11 14H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2z"/><path d="M6 14a12 12 0 0 0 2.4 7.2 2 2 0 0 0 3.2-2.4A8 8 0 0 1 10 14"/><path d="M8 6v8"/></svg>
93
+ <span>Changelog</span>
94
+ </a>
95
+ </nav>
96
+ <div class="mobile-sidebar__docs" id="mobile-sidebar-docs"></div>
97
+ </aside>
98
+ <div class="sidebar-backdrop" id="sidebar-backdrop"></div>
99
+ <div class="search-modal" id="search-modal" hidden>
100
+ <div class="search-modal__backdrop" data-search-close></div>
101
+ <div class="search-modal__panel" role="dialog" aria-modal="true" aria-label="Search">
102
+ <div class="search-modal__header">
103
+ <svg class="search-modal__icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
104
+ <input
105
+ type="search"
106
+ id="search-input"
107
+ class="search-modal__input"
108
+ placeholder="Search"
109
+ autocomplete="off"
110
+ autocorrect="off"
111
+ autocapitalize="off"
112
+ spellcheck="false"
113
+ aria-label="Search"
114
+ aria-controls="search-results"
115
+ aria-activedescendant=""
116
+ role="combobox"
117
+ aria-expanded="true"
118
+ />
119
+ <button class="search-modal__clear" id="search-clear" type="button" aria-label="Clear search" hidden>
120
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
121
+ </button>
122
+ </div>
123
+ <div class="search-modal__body">
124
+ <ul class="search-results" id="search-results" role="listbox" aria-label="Search results"></ul>
125
+ <div class="search-modal__empty" id="search-empty" hidden>
126
+ No results for "<span class="search-modal__empty-query" id="search-empty-query"></span>"
127
+ </div>
128
+ <div class="search-modal__initial" id="search-initial">
129
+ Type to search the documentation.
130
+ </div>
131
+ </div>
132
+ <div class="search-modal__footer">
133
+ <span class="search-modal__hint"><kbd class="search-modal__kbd">↑</kbd><kbd class="search-modal__kbd">↓</kbd> to navigate</span>
134
+ <span class="search-modal__hint"><kbd class="search-modal__kbd">↵</kbd> to select</span>
135
+ <span class="search-modal__hint"><kbd class="search-modal__kbd">esc</kbd> to close</span>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ <script>
140
+ window.__doculaSearch = { indexUrl: "/search-index.json" };
141
+ </script>
142
+ <script src="/js/search.js" defer></script>
143
+ <div class="page-dropdown" id="page-dropdown">
144
+ <button class="page-dropdown__toggle" id="page-dropdown-toggle">
145
+ <span class="page-dropdown__title">Docker</span>
146
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
147
+ </button>
148
+ <div class="page-dropdown__list" id="page-dropdown-list">
149
+ <section class="nav-sidebar__section">
150
+ <ul class="nav-sidebar__list">
151
+ <li>
152
+ <a href="/docs/" class="nav-sidebar__item">Getting Started</a>
153
+ </li>
154
+ </ul>
155
+ </section>
156
+ <section class="nav-sidebar__section">
157
+ <ul class="nav-sidebar__list">
158
+ <li>
159
+ <a href="/docs/https/" class="nav-sidebar__item">HTTPS and HTTP/2</a>
160
+ </li>
161
+ </ul>
162
+ </section>
163
+ <section class="nav-sidebar__section">
164
+ <ul class="nav-sidebar__list">
165
+ <li>
166
+ <a href="/docs/taps/" class="nav-sidebar__item">Taps</a>
167
+ </li>
168
+ </ul>
169
+ </section>
170
+ <section class="nav-sidebar__section">
171
+ <ul class="nav-sidebar__list">
172
+ <li>
173
+ <a href="/docs/bins/" class="nav-sidebar__item">Bins</a>
174
+ </li>
175
+ </ul>
176
+ </section>
177
+ <section class="nav-sidebar__section">
178
+ <ul class="nav-sidebar__list">
179
+ <li>
180
+ <a href="/docs/configuration/" class="nav-sidebar__item">Configuration</a>
181
+ </li>
182
+ </ul>
183
+ </section>
184
+ <section class="nav-sidebar__section">
185
+ <ul class="nav-sidebar__list">
186
+ <li>
187
+ <a href="/docs/library/" class="nav-sidebar__item">Library API</a>
188
+ </li>
189
+ </ul>
190
+ </section>
191
+ <section class="nav-sidebar__section">
192
+ <ul class="nav-sidebar__list">
193
+ <li>
194
+ <a href="/docs/hosted/" class="nav-sidebar__item">MockHttp.org Live</a>
195
+ </li>
196
+ </ul>
197
+ </section>
198
+ <section class="nav-sidebar__section nav-sidebar__section--collapsible">
199
+ <h2 class="nav-sidebar__title">
200
+ <button type="button" class="nav-sidebar__toggle" aria-expanded="true">
201
+ <span class="nav-sidebar__toggle-label">How to Deploy</span>
202
+ <svg class="nav-sidebar__chevron" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
203
+ </button>
204
+ </h2>
205
+ <ul class="nav-sidebar__list">
206
+ <li>
207
+ <a href="/docs/how-to-deploy/cloudflare/" class="nav-sidebar__item">Cloudflare</a>
208
+ </li>
209
+ <li>
210
+ <a href="/docs/how-to-deploy/docker/" class="nav-sidebar__item">Docker</a>
211
+ </li>
212
+ <li>
213
+ <a href="/docs/how-to-deploy/wasmer/" class="nav-sidebar__item">Wasmer</a>
214
+ </li>
215
+ </ul>
216
+ </section>
217
+ </div>
218
+ </div>
219
+ <div class="content">
220
+ <aside class="nav-sidebar" id="nav-sidebar">
221
+ <section class="nav-sidebar__section">
222
+ <ul class="nav-sidebar__list">
223
+ <li>
224
+ <a href="/docs/" class="nav-sidebar__item">Getting Started</a>
225
+ </li>
226
+ </ul>
227
+ </section>
228
+ <section class="nav-sidebar__section">
229
+ <ul class="nav-sidebar__list">
230
+ <li>
231
+ <a href="/docs/https/" class="nav-sidebar__item">HTTPS and HTTP/2</a>
232
+ </li>
233
+ </ul>
234
+ </section>
235
+ <section class="nav-sidebar__section">
236
+ <ul class="nav-sidebar__list">
237
+ <li>
238
+ <a href="/docs/taps/" class="nav-sidebar__item">Taps</a>
239
+ </li>
240
+ </ul>
241
+ </section>
242
+ <section class="nav-sidebar__section">
243
+ <ul class="nav-sidebar__list">
244
+ <li>
245
+ <a href="/docs/bins/" class="nav-sidebar__item">Bins</a>
246
+ </li>
247
+ </ul>
248
+ </section>
249
+ <section class="nav-sidebar__section">
250
+ <ul class="nav-sidebar__list">
251
+ <li>
252
+ <a href="/docs/configuration/" class="nav-sidebar__item">Configuration</a>
253
+ </li>
254
+ </ul>
255
+ </section>
256
+ <section class="nav-sidebar__section">
257
+ <ul class="nav-sidebar__list">
258
+ <li>
259
+ <a href="/docs/library/" class="nav-sidebar__item">Library API</a>
260
+ </li>
261
+ </ul>
262
+ </section>
263
+ <section class="nav-sidebar__section">
264
+ <ul class="nav-sidebar__list">
265
+ <li>
266
+ <a href="/docs/hosted/" class="nav-sidebar__item">MockHttp.org Live</a>
267
+ </li>
268
+ </ul>
269
+ </section>
270
+ <section class="nav-sidebar__section nav-sidebar__section--collapsible">
271
+ <h2 class="nav-sidebar__title">
272
+ <button type="button" class="nav-sidebar__toggle" aria-expanded="true">
273
+ <span class="nav-sidebar__toggle-label">How to Deploy</span>
274
+ <svg class="nav-sidebar__chevron" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
275
+ </button>
276
+ </h2>
277
+ <ul class="nav-sidebar__list">
278
+ <li>
279
+ <a href="/docs/how-to-deploy/cloudflare/" class="nav-sidebar__item">Cloudflare</a>
280
+ </li>
281
+ <li>
282
+ <a href="/docs/how-to-deploy/docker/" class="nav-sidebar__item">Docker</a>
283
+ </li>
284
+ <li>
285
+ <a href="/docs/how-to-deploy/wasmer/" class="nav-sidebar__item">Wasmer</a>
286
+ </li>
287
+ </ul>
288
+ </section>
289
+ </aside>
290
+ <article class="article">
291
+ <header class="article__header">
292
+ <h1 class="article__title">Docker</h1>
293
+ <p class="article__description">Run MockHTTP from the jaredwray/mockhttp image, with Docker Compose, or by building the image from this repository.</p>
294
+ </header>
295
+ <div class="article__body">
296
+ <main class="article__main">
297
+ <p>The published image is <a href="https://hub.docker.com/r/jaredwray/mockhttp"><code>jaredwray/mockhttp</code></a> on Docker Hub. It runs the Node.js server and the documentation site in one container. The <code>latest</code> tag is <code>linux/amd64</code> only. Apple Silicon runs it under emulation. On arm64 Linux, build the image from this repository instead.</p>
298
+ <h2 id="table-of-contents">Table of Contents</h2>
299
+ <ul>
300
+ <li><a href="#run-the-image">Run the image</a></li>
301
+ <li><a href="#docker-compose">Docker Compose</a></li>
302
+ <li><a href="#environment-variables">Environment variables</a></li>
303
+ <li><a href="#build-the-image">Build the image</a></li>
304
+ <li><a href="#publish-to-docker-hub">Publish to Docker Hub</a></li>
305
+ </ul>
306
+ <h2 id="run-the-image">Run the image</h2>
307
+ <pre><code class="hljs language-bash">docker run -d -p 3000:3000 jaredwray/mockhttp
308
+ </code></pre>
309
+ <p>The server listens on port 3000. Open <code>http://localhost:3000/get</code> for a mock response, <code>http://localhost:3000/docs</code> for these guides, and <code>http://localhost:3000/api</code> for the OpenAPI reference.</p>
310
+ <p><code>pnpm docker:run</code> is <code>docker run -p 3000:3000 jaredwray/mockhttp</code>. It stays in the foreground. Docker pulls the Hub image when it is not already local.</p>
311
+ <p>Rate limiting is on: 1000 requests per minute. The allow list is only <code>127.0.0.1</code> and <code>::1</code>. A published port delivers traffic from the Docker bridge address, not from loopback inside the container, so those requests count against the limit. None of the environment variables below turn it off. Change or disable it with the library <a href="/docs/configuration/"><code>rateLimit</code> option</a>. A test suite that sends more than 1000 requests a minute to the published port will get <code>429</code> responses.</p>
312
+ <h2 id="docker-compose">Docker Compose</h2>
313
+ <pre><code class="hljs language-yaml"><span class="hljs-attr">services:</span>
314
+ <span class="hljs-attr">mockhttp:</span>
315
+ <span class="hljs-attr">image:</span> <span class="hljs-string">jaredwray/mockhttp:latest</span>
316
+ <span class="hljs-attr">ports:</span>
317
+ <span class="hljs-bullet">-</span> <span class="hljs-string">"3000:3000"</span>
318
+ </code></pre>
319
+ <p>The Compose file in this repository publishes port 3001 and sets <code>PORT</code> so the process listens on that same port:</p>
320
+ <pre><code class="hljs language-yaml"><span class="hljs-attr">services:</span>
321
+ <span class="hljs-attr">mockhttp:</span>
322
+ <span class="hljs-attr">image:</span> <span class="hljs-string">jaredwray/mockhttp:latest</span>
323
+ <span class="hljs-attr">ports:</span>
324
+ <span class="hljs-bullet">-</span> <span class="hljs-string">"3001:3001"</span>
325
+ <span class="hljs-attr">environment:</span>
326
+ <span class="hljs-bullet">-</span> <span class="hljs-string">PORT=3001</span>
327
+ </code></pre>
328
+ <p><code>pnpm docker:compose:up</code> and <code>pnpm docker:compose:down</code> call the <code>docker-compose</code> binary. Current Docker installs often provide <code>docker compose</code> instead, which you can run against the same file.</p>
329
+ <h2 id="environment-variables">Environment variables</h2>
330
+ <p>The container starts <code>node dist/index.mjs</code>. That process reads these variables:</p>
331
+ <table>
332
+ <thead>
333
+ <tr>
334
+ <th>Variable</th>
335
+ <th>Default</th>
336
+ <th>Effect</th>
337
+ </tr>
338
+ </thead>
339
+ <tbody>
340
+ <tr>
341
+ <td><code>PORT</code></td>
342
+ <td><code>3000</code></td>
343
+ <td>Port the server listens on. Match it to the published container port.</td>
344
+ </tr>
345
+ <tr>
346
+ <td><code>HOST</code></td>
347
+ <td><code>0.0.0.0</code></td>
348
+ <td>Bind address.</td>
349
+ </tr>
350
+ <tr>
351
+ <td><code>LOGGING</code></td>
352
+ <td>enabled</td>
353
+ <td>Set to <code>false</code> to turn logging off.</td>
354
+ </tr>
355
+ <tr>
356
+ <td><code>HTTP2</code></td>
357
+ <td>off</td>
358
+ <td>Set to <code>true</code> to enable HTTP/2 cleartext (h2c). The image does not load a TLS certificate.</td>
359
+ </tr>
360
+ <tr>
361
+ <td><code>AUTO_DETECT_PORT</code></td>
362
+ <td><code>true</code></td>
363
+ <td>Set to <code>false</code> to keep <code>PORT</code> even when that port is already in use.</td>
364
+ </tr>
365
+ </tbody>
366
+ </table>
367
+ <p>HTTPS certificates are configured in the <a href="/docs/https/">library</a>, not through container environment variables. Put a TLS proxy in front of the container when you need HTTPS.</p>
368
+ <h2 id="build-the-image">Build the image</h2>
369
+ <p>The <a href="https://github.com/jaredwray/mockhttp/blob/main/Dockerfile"><code>Dockerfile</code></a> copies <code>node_modules</code>, <code>package.json</code>, <code>public/</code>, <code>dist/</code>, and <code>site/dist/</code> from the build context. Build the package and the docs site before <code>docker build</code>.</p>
370
+ <pre><code class="hljs language-bash">pnpm install
371
+ pnpm build
372
+ pnpm website:build
373
+ docker build -t jaredwray/mockhttp .
374
+ docker run -d -p 3000:3000 jaredwray/mockhttp
375
+ </code></pre>
376
+ <p><code>pnpm docker:build</code> is the <code>docker build</code> step. The image is based on Node.js 24 Alpine, runs as the <code>node</code> user, exposes port 3000, and starts <code>node dist/index.mjs</code>.</p>
377
+ <h2 id="publish-to-docker-hub">Publish to Docker Hub</h2>
378
+ <p><a href="https://github.com/jaredwray/mockhttp/blob/main/.github/workflows/docker-publish.yaml"><code>.github/workflows/docker-publish.yaml</code></a> runs on the <code>released</code> event (a published release, not a prerelease) and when the workflow is started by hand. It builds <code>jaredwray/mockhttp:&#x3C;version></code> and <code>jaredwray/mockhttp:latest</code> with <code>docker build</code> on <code>ubuntu-latest</code>, pushes both tags, and updates the Docker Hub description from <code>DOCKER.md</code>. That build is <code>linux/amd64</code>.</p>
379
+ <p>The same Node server can run on <a href="/docs/how-to-deploy/wasmer/">Wasmer Edge</a>. The public site at <a href="https://mockhttp.org">mockhttp.org</a> is the <a href="/docs/how-to-deploy/cloudflare/">Cloudflare Worker</a>, not this image.</p>
380
+ </main>
381
+ <aside class="content-aside">
382
+ <h2 class="content-aside__title">TABLE OF CONTENTS</h2>
383
+ <ul class="content-aside__list" id="toc-list">
384
+ </ul>
385
+ </aside>
386
+ </div>
387
+ <a class="article__edit-link" href="https://github.com/jaredwray/mockhttp/edit/main/site/docs/how-to-deploy/docker.md" target="_blank" rel="noopener noreferrer">
388
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
389
+ Edit this page
390
+ </a>
391
+ </article>
392
+ </div>
393
+ <footer>
394
+ <a href="/llms.txt" class="footer__link">llms.txt</a>
395
+ <a href="/feed.xml" class="footer__link" aria-label="RSS Feed">
396
+ <svg class="footer__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><circle cx="6.18" cy="17.82" r="2.18"/><path d="M4 4.44v2.83c7.03 0 12.73 5.7 12.73 12.73h2.83c0-8.59-6.97-15.56-15.56-15.56zm0 5.66v2.83c3.9 0 7.07 3.17 7.07 7.07h2.83c0-5.47-4.43-9.9-9.9-9.9z"/></svg>
397
+ </a>
398
+
399
+ <a href="https://docula.org/" target="_blank" rel="noopener noreferrer" class="footer__link" aria-label="Docula">
400
+ <svg class="footer__bat" xmlns="http://www.w3.org/2000/svg" viewBox="70 110 360 205" fill="currentColor"><path d="M420,173c-16.76-18-72.78-36.65-131.59-30.58A160.15,160.15,0,0,0,269,120.55c-3.78,4.52-11.15,9.45-19,9.45s-15.23-4.93-19-9.45a160.15,160.15,0,0,0-19.4,21.83C152.78,136.31,96.76,154.94,80,173c48.86,5.86,79.5,34.09,79.5,72.06,52.37,0,77,28,90.5,59.94C263.54,273,288.13,245,340.5,245,340.5,207,371.14,178.8,420,173Z"/><circle cx="239.19" cy="145.36" r="3.32" fill="#fff"/><circle cx="260.81" cy="145.36" r="3.32" fill="#fff"/></svg>
401
+ </a>
402
+ <span class="footer__updated">updated: 2026-10-20</span>
403
+ </footer>
404
+ <script src="/css/highlight/highlight.min.js"></script>
405
+ <script>
406
+ document.addEventListener('DOMContentLoaded', () => {
407
+ document.querySelectorAll('pre code').forEach(el => { el.textContent = el.textContent.trimEnd(); });
408
+ hljs.highlightAll();
409
+ var kbd = document.querySelector('.search-button__shortcut');
410
+ if (kbd) kbd.textContent = navigator.platform.indexOf('Mac') > -1 ? '⌘K' : 'Ctrl K';
411
+
412
+ // Copy code buttons
413
+ const copyIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
414
+ const checkIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
415
+ document.querySelectorAll('pre').forEach(function(pre) {
416
+ const btn = document.createElement('button');
417
+ btn.className = 'copy-code-btn';
418
+ btn.innerHTML = copyIcon;
419
+ btn.setAttribute('aria-label', 'Copy code');
420
+ btn.addEventListener('click', function() {
421
+ const code = pre.querySelector('code');
422
+ const text = code ? code.textContent : pre.textContent;
423
+ navigator.clipboard.writeText(text || '').then(function() {
424
+ btn.innerHTML = checkIcon;
425
+ setTimeout(function() { btn.innerHTML = copyIcon; }, 2000);
426
+ });
427
+ });
428
+ pre.appendChild(btn);
429
+ });
430
+ });
431
+ </script>
432
+ <script>
433
+ (function() {
434
+ var themeKey = window.__doculaThemeKey;
435
+ function getMode() {
436
+ return localStorage.getItem(themeKey) || 'light';
437
+ }
438
+
439
+ function resolveTheme(mode) {
440
+ if (mode === 'system') {
441
+ return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
442
+ }
443
+ return mode;
444
+ }
445
+
446
+ var cachedEls = null;
447
+ function getThemeEls() {
448
+ if (!cachedEls) {
449
+ cachedEls = {
450
+ systemIcon: document.querySelector('.theme-button__icon--system'),
451
+ sunIcon: document.querySelector('.theme-button__icon--sun'),
452
+ moonIcon: document.querySelector('.theme-button__icon--moon'),
453
+ toggle: document.getElementById('theme-toggle')
454
+ };
455
+ }
456
+ return cachedEls;
457
+ }
458
+
459
+ function applyTheme(mode) {
460
+ var resolved = resolveTheme(mode);
461
+ if (resolved === 'light') {
462
+ document.documentElement.setAttribute('data-theme', 'light');
463
+ } else {
464
+ document.documentElement.removeAttribute('data-theme');
465
+ }
466
+
467
+ var els = getThemeEls();
468
+ if (els.systemIcon && els.sunIcon && els.moonIcon) {
469
+ els.systemIcon.classList.add('theme-button__icon--hidden');
470
+ els.sunIcon.classList.add('theme-button__icon--hidden');
471
+ els.moonIcon.classList.add('theme-button__icon--hidden');
472
+ if (mode === 'system') {
473
+ els.systemIcon.classList.remove('theme-button__icon--hidden');
474
+ } else if (mode === 'light') {
475
+ els.sunIcon.classList.remove('theme-button__icon--hidden');
476
+ } else {
477
+ els.moonIcon.classList.remove('theme-button__icon--hidden');
478
+ }
479
+ }
480
+
481
+ if (els.toggle) {
482
+ var labels = { system: 'Theme: system (following system)', light: 'Theme: light', dark: 'Theme: dark' };
483
+ els.toggle.setAttribute('aria-label', labels[mode] || 'Toggle theme');
484
+ }
485
+ }
486
+
487
+ function nextMode(current) {
488
+ var cycle = { system: 'light', light: 'dark', dark: 'system' };
489
+ return cycle[current] || 'system';
490
+ }
491
+
492
+ var currentMode = getMode();
493
+
494
+ document.addEventListener('DOMContentLoaded', function() {
495
+ applyTheme(currentMode);
496
+
497
+ var els = getThemeEls();
498
+ if (els.toggle) {
499
+ els.toggle.addEventListener('click', function() {
500
+ currentMode = nextMode(currentMode);
501
+ localStorage.setItem(themeKey, currentMode);
502
+ applyTheme(currentMode);
503
+ });
504
+ }
505
+ });
506
+
507
+ window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function() {
508
+ if (getMode() === 'system') {
509
+ applyTheme('system');
510
+ }
511
+ });
512
+ })();
513
+ </script>
514
+ <script defer>
515
+ document.addEventListener('DOMContentLoaded', () => {
516
+ const menuToggle = document.getElementById('mobile-menu-toggle');
517
+ const mobileSidebar = document.getElementById('mobile-sidebar');
518
+ const backdrop = document.getElementById('sidebar-backdrop');
519
+ if (menuToggle && mobileSidebar) {
520
+ menuToggle.addEventListener('click', () => {
521
+ mobileSidebar.classList.toggle('mobile-sidebar--open');
522
+ if (backdrop) backdrop.classList.toggle('sidebar-backdrop--visible');
523
+ });
524
+ if (backdrop) {
525
+ backdrop.addEventListener('click', () => {
526
+ mobileSidebar.classList.remove('mobile-sidebar--open');
527
+ backdrop.classList.remove('sidebar-backdrop--visible');
528
+ });
529
+ }
530
+ }
531
+
532
+ // Page dropdown toggle (docs pages)
533
+ const dropdownToggle = document.getElementById('page-dropdown-toggle');
534
+ const dropdownList = document.getElementById('page-dropdown-list');
535
+ if (dropdownToggle && dropdownList) {
536
+ dropdownToggle.addEventListener('click', () => {
537
+ dropdownList.classList.toggle('page-dropdown__list--open');
538
+ });
539
+ }
540
+
541
+ // Active sidebar link highlighting
542
+ const sidebarLinks = document.querySelectorAll('.nav-sidebar__item');
543
+ const currentPath = window.location.pathname;
544
+ sidebarLinks.forEach((link) => {
545
+ if (link.getAttribute('href') === currentPath || link.getAttribute('href') === currentPath.replace(/\/$/, '')) {
546
+ link.classList.add('nav-sidebar__item--active');
547
+ }
548
+ });
549
+
550
+ // Sidebar section collapse/expand
551
+ const SIDEBAR_STORAGE_KEY = 'docula:sidebar-sections';
552
+ const collapsibleSections = document.querySelectorAll('.nav-sidebar__section--collapsible');
553
+ if (collapsibleSections.length > 0) {
554
+ let storedSectionState = {};
555
+ try {
556
+ storedSectionState = JSON.parse(localStorage.getItem(SIDEBAR_STORAGE_KEY) || '{}');
557
+ } catch (e) { storedSectionState = {}; }
558
+
559
+ const setSectionOpen = (section, toggle, open) => {
560
+ toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
561
+ section.classList.toggle('nav-sidebar__section--collapsed', !open);
562
+ };
563
+
564
+ collapsibleSections.forEach((section, idx) => {
565
+ const toggle = section.querySelector('.nav-sidebar__toggle');
566
+ const list = section.querySelector('.nav-sidebar__list');
567
+ if (!toggle || !list) return;
568
+ const listId = 'nav-sidebar-section-' + idx;
569
+ list.id = listId;
570
+ toggle.setAttribute('aria-controls', listId);
571
+
572
+ const key = 'section-' + idx;
573
+ const hasStored = Object.prototype.hasOwnProperty.call(storedSectionState, key);
574
+ const isOpen = hasStored ? !!storedSectionState[key] : true;
575
+ setSectionOpen(section, toggle, isOpen);
576
+
577
+ toggle.addEventListener('click', () => {
578
+ const next = toggle.getAttribute('aria-expanded') !== 'true';
579
+ setSectionOpen(section, toggle, next);
580
+ storedSectionState[key] = next;
581
+ try {
582
+ localStorage.setItem(SIDEBAR_STORAGE_KEY, JSON.stringify(storedSectionState));
583
+ } catch (e) { /* storage unavailable */ }
584
+ });
585
+ });
586
+ }
587
+
588
+ // Active nav link highlighting (header + mobile). Highlight only the
589
+ // single most-specific link: the one whose path is the longest prefix of
590
+ // the current path. A plain startsWith() check lights up every link whose
591
+ // path merely prefixes the current one, so when the site is embedded under
592
+ // a base path (e.g. served at /developer with docs at that root) the
593
+ // Documentation link — whose path is a prefix of every page — would stay
594
+ // highlighted everywhere. Trailing slashes are normalized so matches stop
595
+ // at a path boundary. We read the anchor's resolved origin/pathname so
596
+ // absolute, relative, and same-origin hrefs all work, while cross-origin
597
+ // links and non-anchor items (e.g. the mobile logout button) are skipped.
598
+ const withTrailingSlash = (p) => (p.endsWith('/') ? p : p + '/');
599
+ const here = withTrailingSlash(currentPath);
600
+ const markActiveNav = (selector, activeClass) => {
601
+ let bestLink = null;
602
+ let bestLength = -1;
603
+ document.querySelectorAll(selector).forEach((link) => {
604
+ // Only same-origin anchors can match the current pathname.
605
+ if (!link.href || link.origin !== window.location.origin) return;
606
+ const base = withTrailingSlash(link.pathname);
607
+ if (here.startsWith(base) && base.length > bestLength) {
608
+ bestLink = link;
609
+ bestLength = base.length;
610
+ }
611
+ });
612
+ if (bestLink) bestLink.classList.add(activeClass);
613
+ };
614
+ markActiveNav('.header-bottom__item', 'header-bottom__item--active');
615
+ markActiveNav('.mobile-nav__item', 'mobile-nav__item--active');
616
+
617
+ // TOC extraction: move the inline TOC into the right sidebar and hide the original
618
+ const tocHeading = document.getElementById('table-of-contents');
619
+ if (tocHeading) {
620
+ const tocList = tocHeading.nextElementSibling;
621
+ const tocTarget = document.getElementById('toc-list');
622
+ if (tocList && tocList.tagName === 'UL' && tocTarget) {
623
+ tocTarget.innerHTML = tocList.innerHTML;
624
+ tocHeading.style.display = 'none';
625
+ tocList.style.display = 'none';
626
+ }
627
+ }
628
+
629
+ // Hide the TOC aside if it has no entries
630
+ const tocSidebar = document.getElementById('toc-list');
631
+ if (tocSidebar && tocSidebar.children.length === 0) {
632
+ const aside = tocSidebar.closest('.content-aside');
633
+ if (aside) { aside.style.display = 'none'; }
634
+ }
635
+
636
+ // Image lightbox
637
+ const lightboxOverlay = document.createElement('div');
638
+ lightboxOverlay.className = 'lightbox-overlay';
639
+ lightboxOverlay.addEventListener('click', function(e) {
640
+ if (e.target !== lightboxImg) lightboxOverlay.classList.remove('lightbox-overlay--visible');
641
+ });
642
+ const lightboxClose = document.createElement('button');
643
+ lightboxClose.className = 'lightbox-close';
644
+ lightboxClose.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
645
+ lightboxClose.addEventListener('click', function() {
646
+ lightboxOverlay.classList.remove('lightbox-overlay--visible');
647
+ });
648
+ const lightboxImg = document.createElement('img');
649
+ lightboxOverlay.appendChild(lightboxClose);
650
+ lightboxOverlay.appendChild(lightboxImg);
651
+ document.body.appendChild(lightboxOverlay);
652
+
653
+ document.querySelectorAll('.article__main img, .changelog-entry-body img').forEach(function(img) {
654
+ if (img.closest('a')) return;
655
+ img.addEventListener('click', function() {
656
+ lightboxImg.src = img.src;
657
+ lightboxOverlay.classList.add('lightbox-overlay--visible');
658
+ });
659
+ });
660
+ });
661
+ </script>
662
+ </body>
663
+
664
+ </html>