mirror of
https://github.com/hoppscotch/hoppscotch.git
synced 2026-07-16 16:57:05 +02:00
feat: allow containers to run with arbitrary non-root UIDs (#6481)
This commit is contained in:
@@ -44,3 +44,13 @@ VITE_PROXYSCOTCH_ACCESS_TOKEN=
|
||||
|
||||
# Set to `true` for subpath based access
|
||||
ENABLE_SUBPATH_BASED_ACCESS=false
|
||||
|
||||
#-----------------------Container Runtime Config---------------------#
|
||||
|
||||
# Caddy's in-container HTTP port (default 80). Set a free port (e.g. 8000) to run
|
||||
# under a non-root UID like OpenShift (ports < 1024 need root or CAP_NET_BIND_SERVICE),
|
||||
# and update your published mapping too. Avoid internal ports 8080, 3200, 3000, 3100,
|
||||
# 3170. Non-root runs expect GID 0. On the AIO image this applies only when
|
||||
# ENABLE_SUBPATH_BASED_ACCESS=true; the legacy HOPP_AIO_ALTERNATE_PORT is honoured
|
||||
# there as a fallback.
|
||||
# HOPP_ALTERNATE_PORT=8000
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
persist_config off
|
||||
}
|
||||
|
||||
:{$HOPP_AIO_ALTERNATE_PORT:80} {
|
||||
:{$HOPP_ALTERNATE_PORT:80} {
|
||||
# Serve the `selfhost-web` SPA by default
|
||||
root * /site/selfhost-web
|
||||
file_server
|
||||
|
||||
+76
-9
@@ -1,10 +1,70 @@
|
||||
#!/usr/local/bin/node
|
||||
// @ts-check
|
||||
|
||||
import { execSync, spawn } from "child_process"
|
||||
import { execFileSync, spawn } from "child_process"
|
||||
import fs from "fs"
|
||||
import net from "net"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import process from "process"
|
||||
|
||||
// Probe the real bind so the kernel decides (CAP_NET_BIND_SERVICE, port sysctls),
|
||||
// not a UID guess.
|
||||
async function assertPortBindable(port) {
|
||||
await new Promise((resolve) => {
|
||||
const probe = net.createServer()
|
||||
probe.once("error", (err) => {
|
||||
if (err.code === "EACCES") {
|
||||
console.error(`Cannot bind port ${port} as the current user: set HOPP_ALTERNATE_PORT to a free port >= 1024, or run as root / grant CAP_NET_BIND_SERVICE.`)
|
||||
process.exit(1)
|
||||
}
|
||||
if (err.code === "EADDRINUSE") {
|
||||
console.error(`Port ${port} is already in use inside the container: set HOPP_ALTERNATE_PORT to a free port.`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.warn(`Skipping bind preflight for port ${port} (${err.code})`)
|
||||
resolve()
|
||||
})
|
||||
probe.listen(port, () => probe.close(resolve))
|
||||
})
|
||||
}
|
||||
|
||||
// Empty means unset (compose passes undefined vars as ""), so the :80 default applies.
|
||||
if (process.env.HOPP_ALTERNATE_PORT === "") delete process.env.HOPP_ALTERNATE_PORT
|
||||
if (process.env.HOPP_AIO_ALTERNATE_PORT === "") delete process.env.HOPP_AIO_ALTERNATE_PORT
|
||||
|
||||
// Back-compat: fall back to the legacy var when the new one is unset.
|
||||
const legacyPortApplied = !process.env.HOPP_ALTERNATE_PORT && !!process.env.HOPP_AIO_ALTERNATE_PORT
|
||||
if (legacyPortApplied) {
|
||||
process.env.HOPP_ALTERNATE_PORT = process.env.HOPP_AIO_ALTERNATE_PORT
|
||||
}
|
||||
|
||||
const useSubpathAccess = process.env.ENABLE_SUBPATH_BASED_ACCESS === "true"
|
||||
|
||||
// Sanity-check the value; real bindability is probed below.
|
||||
const RESERVED_PORTS = ["8080", "3200"]
|
||||
const altPort = process.env.HOPP_ALTERNATE_PORT
|
||||
// Name whichever var the operator actually set.
|
||||
const altPortVar = legacyPortApplied ? "HOPP_AIO_ALTERNATE_PORT" : "HOPP_ALTERNATE_PORT"
|
||||
if (altPort !== undefined) {
|
||||
if (!(/^[0-9]+$/.test(altPort) && +altPort >= 1 && +altPort <= 65535)) {
|
||||
console.error(`${altPortVar}="${altPort}" is invalid: use an integer in 1-65535 (e.g. 8000).`)
|
||||
process.exit(1)
|
||||
}
|
||||
if (RESERVED_PORTS.includes(String(+altPort))) {
|
||||
console.error(`${altPortVar}="${altPort}" is already used by this image (${RESERVED_PORTS.join(", ")}); pick another port (e.g. 8000).`)
|
||||
process.exit(1)
|
||||
}
|
||||
if (!useSubpathAccess) {
|
||||
console.warn(`${altPortVar} has no effect in multiport mode (Caddy binds 3000/3100/3170); it applies only when ENABLE_SUBPATH_BASED_ACCESS=true.`)
|
||||
}
|
||||
}
|
||||
|
||||
// Only subpath mode binds the configurable port; multiport uses fixed ports.
|
||||
if (useSubpathAccess) {
|
||||
await assertPortBindable(+(process.env.HOPP_ALTERNATE_PORT ?? 80))
|
||||
}
|
||||
|
||||
function runChildProcessWithPrefix(command, args, prefix) {
|
||||
const childProcess = spawn(command, args);
|
||||
|
||||
@@ -44,30 +104,37 @@ const envFileContent = Object.entries(process.env)
|
||||
}`)
|
||||
.join("\n")
|
||||
|
||||
fs.writeFileSync("build.env", envFileContent)
|
||||
// Write to a temp dir (not cwd) so a non-root UID needn't own the working directory.
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "hopp-env-"))
|
||||
const buildEnvPath = path.join(tmpDir, "build.env")
|
||||
|
||||
execSync(`npx import-meta-env -x build.env -e build.env -p "/site/**/*"`)
|
||||
try {
|
||||
fs.writeFileSync(buildEnvPath, envFileContent)
|
||||
// Call the global binary directly (not npx, which needs a writable $HOME cache).
|
||||
execFileSync("import-meta-env", ["-x", buildEnvPath, "-e", buildEnvPath, "-p", "/site/**/*"], { stdio: "inherit" })
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
fs.rmSync("build.env")
|
||||
|
||||
const caddyFileName = process.env.ENABLE_SUBPATH_BASED_ACCESS === 'true' ? 'aio-subpath-access.Caddyfile' : 'aio-multiport-setup.Caddyfile'
|
||||
const caddyFileName = useSubpathAccess ? 'aio-subpath-access.Caddyfile' : 'aio-multiport-setup.Caddyfile'
|
||||
const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", `/etc/caddy/${caddyFileName}`, "--adapter", "caddyfile"], "App/Admin Dashboard Caddy")
|
||||
const backendProcess = runChildProcessWithPrefix("node", ["/dist/backend/dist/src/main.js"], "Backend Server")
|
||||
const webappProcess = runChildProcessWithPrefix("webapp-server", [], "Webapp Server")
|
||||
|
||||
caddyProcess.on("exit", (code) => {
|
||||
console.log(`Exiting process because Caddy Server exited with code ${code}`)
|
||||
process.exit(code)
|
||||
// code is null on signal death; report failure, not success.
|
||||
process.exit(code ?? 1)
|
||||
})
|
||||
|
||||
backendProcess.on("exit", (code) => {
|
||||
console.log(`Exiting process because Backend Server exited with code ${code}`)
|
||||
process.exit(code)
|
||||
process.exit(code ?? 1)
|
||||
})
|
||||
|
||||
webappProcess.on("exit", (code) => {
|
||||
console.log(`Exiting process because Webapp Server exited with code ${code}`)
|
||||
process.exit(code)
|
||||
process.exit(code ?? 1)
|
||||
})
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
|
||||
+5
-5
@@ -51,7 +51,7 @@ services:
|
||||
hoppscotch-db:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "3180:80"
|
||||
- "3180:${HOPP_ALTERNATE_PORT:-80}"
|
||||
- "3170:3170"
|
||||
|
||||
# The main hoppscotch app with integrated webapp server. This will be hosted at port 3000
|
||||
@@ -70,7 +70,7 @@ services:
|
||||
depends_on:
|
||||
- hoppscotch-backend
|
||||
ports:
|
||||
- "3080:80"
|
||||
- "3080:${HOPP_ALTERNATE_PORT:-80}"
|
||||
- "3000:3000"
|
||||
- "3200:3200"
|
||||
|
||||
@@ -89,7 +89,7 @@ services:
|
||||
depends_on:
|
||||
- hoppscotch-backend
|
||||
ports:
|
||||
- "3280:80"
|
||||
- "3280:${HOPP_ALTERNATE_PORT:-80}"
|
||||
- "3100:3100"
|
||||
|
||||
# The service that spins up all services at once in one container
|
||||
@@ -111,7 +111,7 @@ services:
|
||||
- "3100:3100"
|
||||
- "3170:3170"
|
||||
- "3200:3200"
|
||||
- "3080:80"
|
||||
- "3080:${HOPP_ALTERNATE_PORT:-80}"
|
||||
|
||||
# Profile with no database dependency (purely developmental)
|
||||
hoppscotch-aio-no-db:
|
||||
@@ -129,7 +129,7 @@ services:
|
||||
- "3100:3100"
|
||||
- "3170:3170"
|
||||
- "3200:3200"
|
||||
- "3080:80"
|
||||
- "3080:${HOPP_ALTERNATE_PORT:-80}"
|
||||
|
||||
# The preset DB service, you can delete/comment the below lines if
|
||||
# you are using an external postgres instance
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ if [ "$UPTIME" -lt 15 ]; then
|
||||
fi
|
||||
|
||||
if [ "$ENABLE_SUBPATH_BASED_ACCESS" = "true" ]; then
|
||||
curlCheck "http://localhost:${HOPP_AIO_ALTERNATE_PORT:-80}/backend/ping" || exit 1
|
||||
curlCheck "http://localhost:${HOPP_ALTERNATE_PORT:-${HOPP_AIO_ALTERNATE_PORT:-80}}/backend/ping" || exit 1
|
||||
else
|
||||
curlCheck "http://localhost:3000" || exit 1
|
||||
curlCheck "http://localhost:3100" || exit 1
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
persist_config off
|
||||
}
|
||||
|
||||
:80 :3170 {
|
||||
:{$HOPP_ALTERNATE_PORT:80} :3170 {
|
||||
@mock {
|
||||
header_regexp host Host ^[^.]+\.mock\..*$
|
||||
}
|
||||
|
||||
@@ -2,8 +2,50 @@
|
||||
// @ts-check
|
||||
|
||||
import { spawn } from 'child_process';
|
||||
import net from 'net';
|
||||
import process from 'process';
|
||||
|
||||
// Probe the real bind so the kernel decides (CAP_NET_BIND_SERVICE, port sysctls),
|
||||
// not a UID guess.
|
||||
async function assertPortBindable(port) {
|
||||
await new Promise((resolve) => {
|
||||
const probe = net.createServer();
|
||||
probe.once('error', (err) => {
|
||||
if (err.code === 'EACCES') {
|
||||
console.error(`Cannot bind port ${port} as the current user: set HOPP_ALTERNATE_PORT to a free port >= 1024, or run as root / grant CAP_NET_BIND_SERVICE.`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.error(`Port ${port} is already in use inside the container: set HOPP_ALTERNATE_PORT to a free port.`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.warn(`Skipping bind preflight for port ${port} (${err.code})`);
|
||||
resolve();
|
||||
});
|
||||
probe.listen(port, () => probe.close(resolve));
|
||||
});
|
||||
}
|
||||
|
||||
// Empty means unset (compose passes undefined vars as ""), so the :80 default applies.
|
||||
if (process.env.HOPP_ALTERNATE_PORT === '') delete process.env.HOPP_ALTERNATE_PORT;
|
||||
|
||||
// Sanity-check the value; real bindability is probed below.
|
||||
const RESERVED_PORTS = ['3170', '8080'];
|
||||
const altPort = process.env.HOPP_ALTERNATE_PORT;
|
||||
if (altPort !== undefined) {
|
||||
if (!(/^[0-9]+$/.test(altPort) && +altPort >= 1 && +altPort <= 65535)) {
|
||||
console.error(`HOPP_ALTERNATE_PORT="${altPort}" is invalid: use an integer in 1-65535 (e.g. 8000).`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (RESERVED_PORTS.includes(String(+altPort))) {
|
||||
console.error(`HOPP_ALTERNATE_PORT="${altPort}" is already used by this image (${RESERVED_PORTS.join(', ')}); pick another port (e.g. 8000).`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Caddy always binds this port (env value or the :80 default).
|
||||
await assertPortBindable(+(process.env.HOPP_ALTERNATE_PORT ?? 80));
|
||||
|
||||
function runChildProcessWithPrefix(command, args, prefix) {
|
||||
const childProcess = spawn(command, args);
|
||||
|
||||
@@ -46,14 +88,15 @@ const backendProcess = runChildProcessWithPrefix(
|
||||
|
||||
caddyProcess.on('exit', (code) => {
|
||||
console.log(`Exiting process because Caddy Server exited with code ${code}`);
|
||||
process.exit(code);
|
||||
// code is null on signal death; report failure, not success.
|
||||
process.exit(code ?? 1);
|
||||
});
|
||||
|
||||
backendProcess.on('exit', (code) => {
|
||||
console.log(
|
||||
`Exiting process because Backend Server exited with code ${code}`,
|
||||
);
|
||||
process.exit(code);
|
||||
process.exit(code ?? 1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
|
||||
@@ -1,6 +1,51 @@
|
||||
#!/usr/local/bin/node
|
||||
import { execSync } from "child_process"
|
||||
import { execFileSync } from "child_process"
|
||||
import fs from "fs"
|
||||
import net from "net"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
|
||||
// Probe the real bind so the kernel decides (CAP_NET_BIND_SERVICE, port sysctls),
|
||||
// not a UID guess.
|
||||
async function assertPortBindable(port) {
|
||||
await new Promise((resolve) => {
|
||||
const probe = net.createServer()
|
||||
probe.once("error", (err) => {
|
||||
if (err.code === "EACCES") {
|
||||
console.error(`Cannot bind port ${port} as the current user: set HOPP_ALTERNATE_PORT to a free port >= 1024, or run as root / grant CAP_NET_BIND_SERVICE.`)
|
||||
process.exit(1)
|
||||
}
|
||||
if (err.code === "EADDRINUSE") {
|
||||
console.error(`Port ${port} is already in use inside the container: set HOPP_ALTERNATE_PORT to a free port.`)
|
||||
process.exit(1)
|
||||
}
|
||||
console.warn(`Skipping bind preflight for port ${port} (${err.code})`)
|
||||
resolve()
|
||||
})
|
||||
probe.listen(port, () => probe.close(resolve))
|
||||
})
|
||||
}
|
||||
|
||||
// Empty means unset (compose passes undefined vars as ""), so the :80 default
|
||||
// applies. The image CMD also unsets an empty value before launching Caddy.
|
||||
if (process.env.HOPP_ALTERNATE_PORT === "") delete process.env.HOPP_ALTERNATE_PORT
|
||||
|
||||
// Sanity-check the value; real bindability is probed below.
|
||||
const RESERVED_PORTS = ["3000", "3200"]
|
||||
const altPort = process.env.HOPP_ALTERNATE_PORT
|
||||
if (altPort !== undefined) {
|
||||
if (!(/^[0-9]+$/.test(altPort) && +altPort >= 1 && +altPort <= 65535)) {
|
||||
console.error(`HOPP_ALTERNATE_PORT="${altPort}" is invalid: use an integer in 1-65535 (e.g. 8000).`)
|
||||
process.exit(1)
|
||||
}
|
||||
if (RESERVED_PORTS.includes(String(+altPort))) {
|
||||
console.error(`HOPP_ALTERNATE_PORT="${altPort}" is already used by this image (${RESERVED_PORTS.join(", ")}); pick another port (e.g. 8000).`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Caddy always binds this port (env value or the :80 default).
|
||||
await assertPortBindable(+(process.env.HOPP_ALTERNATE_PORT ?? 80))
|
||||
|
||||
const envFileContent = Object.entries(process.env)
|
||||
.filter(([env]) => env.startsWith("VITE_"))
|
||||
@@ -11,8 +56,14 @@ const envFileContent = Object.entries(process.env)
|
||||
)
|
||||
.join("\n")
|
||||
|
||||
fs.writeFileSync("build.env", envFileContent)
|
||||
// Write to a temp dir (not cwd) so a non-root UID needn't own the working directory.
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "hopp-env-"))
|
||||
const buildEnvPath = path.join(tmpDir, "build.env")
|
||||
|
||||
execSync(`npx import-meta-env -x build.env -e build.env -p "/site/**/*"`)
|
||||
|
||||
fs.rmSync("build.env")
|
||||
try {
|
||||
fs.writeFileSync(buildEnvPath, envFileContent)
|
||||
// Call the global binary directly (not npx, which needs a writable $HOME cache).
|
||||
execFileSync("import-meta-env", ["-x", buildEnvPath, "-e", buildEnvPath, "-p", "/site/**/*"], { stdio: "inherit" })
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
persist_config off
|
||||
}
|
||||
|
||||
:80 :3000 {
|
||||
:{$HOPP_ALTERNATE_PORT:80} :3000 {
|
||||
try_files {path} /
|
||||
root * /site/selfhost-web
|
||||
file_server
|
||||
|
||||
@@ -1,8 +1,52 @@
|
||||
#!/usr/local/bin/node
|
||||
import { execSync, spawn } from 'child_process';
|
||||
import { execFileSync, spawn } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import net from 'net';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import process from 'process';
|
||||
|
||||
// Probe the real bind so the kernel decides (CAP_NET_BIND_SERVICE, port sysctls),
|
||||
// not a UID guess.
|
||||
async function assertPortBindable(port) {
|
||||
await new Promise((resolve) => {
|
||||
const probe = net.createServer();
|
||||
probe.once('error', (err) => {
|
||||
if (err.code === 'EACCES') {
|
||||
console.error(`Cannot bind port ${port} as the current user: set HOPP_ALTERNATE_PORT to a free port >= 1024, or run as root / grant CAP_NET_BIND_SERVICE.`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
console.error(`Port ${port} is already in use inside the container: set HOPP_ALTERNATE_PORT to a free port.`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.warn(`Skipping bind preflight for port ${port} (${err.code})`);
|
||||
resolve();
|
||||
});
|
||||
probe.listen(port, () => probe.close(resolve));
|
||||
});
|
||||
}
|
||||
|
||||
// Empty means unset (compose passes undefined vars as ""), so the :80 default applies.
|
||||
if (process.env.HOPP_ALTERNATE_PORT === '') delete process.env.HOPP_ALTERNATE_PORT;
|
||||
|
||||
// Sanity-check the value; real bindability is probed below.
|
||||
const RESERVED_PORTS = ['3100'];
|
||||
const altPort = process.env.HOPP_ALTERNATE_PORT;
|
||||
if (altPort !== undefined) {
|
||||
if (!(/^[0-9]+$/.test(altPort) && +altPort >= 1 && +altPort <= 65535)) {
|
||||
console.error(`HOPP_ALTERNATE_PORT="${altPort}" is invalid: use an integer in 1-65535 (e.g. 8000).`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (RESERVED_PORTS.includes(String(+altPort))) {
|
||||
console.error(`HOPP_ALTERNATE_PORT="${altPort}" is already used by this image (${RESERVED_PORTS.join(', ')}); pick another port (e.g. 8000).`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Caddy always binds this port (env value or the :80 default).
|
||||
await assertPortBindable(+(process.env.HOPP_ALTERNATE_PORT ?? 80));
|
||||
|
||||
function runChildProcessWithPrefix(command, args, prefix) {
|
||||
const childProcess = spawn(command, args);
|
||||
|
||||
@@ -41,11 +85,17 @@ const envFileContent = Object.entries(process.env)
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
fs.writeFileSync('build.env', envFileContent);
|
||||
// Write to a temp dir (not cwd) so a non-root UID needn't own the working directory.
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hopp-env-'));
|
||||
const buildEnvPath = path.join(tmpDir, 'build.env');
|
||||
|
||||
execSync(`npx import-meta-env -x build.env -e build.env -p "/site/**/*"`);
|
||||
|
||||
fs.rmSync('build.env');
|
||||
try {
|
||||
fs.writeFileSync(buildEnvPath, envFileContent);
|
||||
// Call the global binary directly (not npx, which needs a writable $HOME cache).
|
||||
execFileSync('import-meta-env', ['-x', buildEnvPath, '-e', buildEnvPath, '-p', '/site/**/*'], { stdio: 'inherit' });
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const caddyFileName =
|
||||
process.env.ENABLE_SUBPATH_BASED_ACCESS === 'true'
|
||||
@@ -59,7 +109,8 @@ const caddyProcess = runChildProcessWithPrefix(
|
||||
|
||||
caddyProcess.on('exit', (code) => {
|
||||
console.log(`Exiting process because Caddy Server exited with code ${code}`);
|
||||
process.exit(code);
|
||||
// code is null on signal death; report failure, not success.
|
||||
process.exit(code ?? 1);
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
persist_config off
|
||||
}
|
||||
|
||||
:80 :3100 {
|
||||
:{$HOPP_ALTERNATE_PORT:80} :3100 {
|
||||
try_files {path} /
|
||||
root * /site/sh-admin-multiport-setup
|
||||
file_server
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
persist_config off
|
||||
}
|
||||
|
||||
:80 :3100 {
|
||||
:{$HOPP_ALTERNATE_PORT:80} :3100 {
|
||||
handle_path /admin* {
|
||||
root * /site/sh-admin-subpath-access
|
||||
file_server
|
||||
|
||||
+57
-10
@@ -155,6 +155,10 @@ COPY --from=base_builder /usr/src/app/packages/hoppscotch-backend/prod_run.mjs /
|
||||
ENV PRODUCTION="true"
|
||||
ENV PORT=8080
|
||||
|
||||
# Writable Caddy storage for non-root UIDs (no writable $HOME needed).
|
||||
ENV XDG_DATA_HOME=/tmp
|
||||
ENV XDG_CONFIG_HOME=/tmp
|
||||
|
||||
WORKDIR /dist/backend
|
||||
|
||||
CMD ["node", "prod_run.mjs"]
|
||||
@@ -166,6 +170,10 @@ EXPOSE 3170
|
||||
FROM base_builder AS fe_builder
|
||||
WORKDIR /usr/src/app/packages/hoppscotch-selfhost-web
|
||||
RUN pnpm run generate
|
||||
# Group-writable (GID 0) so a non-root UID (OpenShift runs as GID 0) can rewrite
|
||||
# these files during env injection. Done here (not in the runtime stage) so the
|
||||
# perms travel with the COPY instead of duplicating the layer with a chmod there.
|
||||
RUN chmod -R g=rwX /usr/src/app/packages/hoppscotch-selfhost-web/dist
|
||||
|
||||
|
||||
|
||||
@@ -178,11 +186,25 @@ COPY --from=webapp_server_builder /usr/src/app/packages/hoppscotch-selfhost-web/
|
||||
|
||||
COPY --from=fe_builder /usr/src/app/packages/hoppscotch-selfhost-web/prod_run.mjs /site/prod_run.mjs
|
||||
COPY --from=fe_builder /usr/src/app/packages/hoppscotch-selfhost-web/selfhost-web.Caddyfile /etc/caddy/selfhost-web.Caddyfile
|
||||
COPY --from=fe_builder /usr/src/app/packages/hoppscotch-selfhost-web/dist/ /site/selfhost-web
|
||||
COPY --chown=root:0 --from=fe_builder /usr/src/app/packages/hoppscotch-selfhost-web/dist/ /site/selfhost-web
|
||||
|
||||
# Writable Caddy storage for non-root UIDs (no writable $HOME needed).
|
||||
ENV XDG_DATA_HOME=/tmp
|
||||
ENV XDG_CONFIG_HOME=/tmp
|
||||
|
||||
# Files keep g=rwX from the builder; only the COPY-created dirs need it. List the
|
||||
# dirs (not /site/*) so the layer stays metadata-only and prod_run.mjs stays 755.
|
||||
RUN chmod g=rwX /site /site/selfhost-web
|
||||
|
||||
# Pre-create /data group-writable so webapp-server's signing key persists across
|
||||
# restarts under a non-root UID (else it's regenerated and logged each start).
|
||||
RUN mkdir -p /data/webapp-server && chmod g=rwX /data /data/webapp-server
|
||||
|
||||
WORKDIR /site
|
||||
# Run both webapp-server and Caddy after env processing (NOTE: env processing is required by both)
|
||||
CMD ["/bin/sh", "-c", "node /site/prod_run.mjs && (webapp-server & caddy run --config /etc/caddy/selfhost-web.Caddyfile --adapter caddyfile)"]
|
||||
# An empty HOPP_ALTERNATE_PORT (compose passthrough of an undefined var) means unset,
|
||||
# so the Caddyfile default (:80) applies.
|
||||
CMD ["/bin/sh", "-c", "[ -n \"$HOPP_ALTERNATE_PORT\" ] || unset HOPP_ALTERNATE_PORT; node /site/prod_run.mjs && (webapp-server & caddy run --config /etc/caddy/selfhost-web.Caddyfile --adapter caddyfile)"]
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 3000
|
||||
@@ -195,6 +217,10 @@ WORKDIR /usr/src/app/packages/hoppscotch-sh-admin
|
||||
# Generate two builds for `sh-admin`, one based on subpath-access and the regular build
|
||||
RUN pnpm run build --outDir dist-multiport-setup
|
||||
RUN pnpm run build --outDir dist-subpath-access --base /admin/
|
||||
# Group-writable (GID 0) so a non-root UID (OpenShift runs as GID 0) can rewrite
|
||||
# these files during env injection. Done here (not in the runtime stage) so the
|
||||
# perms travel with the COPY instead of duplicating the layer with a chmod there.
|
||||
RUN chmod -R g=rwX dist-multiport-setup dist-subpath-access
|
||||
|
||||
|
||||
FROM node_base AS sh_admin
|
||||
@@ -204,8 +230,16 @@ COPY --from=caddy_builder /tmp/caddy-build/cmd/caddy/caddy /usr/bin/caddy
|
||||
COPY --from=sh_admin_builder /usr/src/app/packages/hoppscotch-sh-admin/prod_run.mjs /site/prod_run.mjs
|
||||
COPY --from=sh_admin_builder /usr/src/app/packages/hoppscotch-sh-admin/sh-admin-multiport-setup.Caddyfile /etc/caddy/sh-admin-multiport-setup.Caddyfile
|
||||
COPY --from=sh_admin_builder /usr/src/app/packages/hoppscotch-sh-admin/sh-admin-subpath-access.Caddyfile /etc/caddy/sh-admin-subpath-access.Caddyfile
|
||||
COPY --from=sh_admin_builder /usr/src/app/packages/hoppscotch-sh-admin/dist-multiport-setup /site/sh-admin-multiport-setup
|
||||
COPY --from=sh_admin_builder /usr/src/app/packages/hoppscotch-sh-admin/dist-subpath-access /site/sh-admin-subpath-access
|
||||
COPY --chown=root:0 --from=sh_admin_builder /usr/src/app/packages/hoppscotch-sh-admin/dist-multiport-setup /site/sh-admin-multiport-setup
|
||||
COPY --chown=root:0 --from=sh_admin_builder /usr/src/app/packages/hoppscotch-sh-admin/dist-subpath-access /site/sh-admin-subpath-access
|
||||
|
||||
# Writable Caddy storage for non-root UIDs (no writable $HOME needed).
|
||||
ENV XDG_DATA_HOME=/tmp
|
||||
ENV XDG_CONFIG_HOME=/tmp
|
||||
|
||||
# Files keep g=rwX from the builder; only the COPY-created dirs need it. List the
|
||||
# dirs (not /site/*) so the layer stays metadata-only and prod_run.mjs stays 755.
|
||||
RUN chmod g=rwX /site /site/sh-admin-multiport-setup /site/sh-admin-subpath-access
|
||||
|
||||
WORKDIR /site
|
||||
CMD ["node","/site/prod_run.mjs"]
|
||||
@@ -236,17 +270,27 @@ COPY --from=base_builder /usr/src/app/packages/hoppscotch-backend/prod_run.mjs /
|
||||
|
||||
# Static Server
|
||||
COPY --from=webapp_server_builder /usr/src/app/packages/hoppscotch-selfhost-web/webapp-server/webapp-server /usr/local/bin/
|
||||
RUN mkdir -p /site/selfhost-web
|
||||
COPY --from=fe_builder /usr/src/app/packages/hoppscotch-selfhost-web/dist /site/selfhost-web
|
||||
COPY --chown=root:0 --from=fe_builder /usr/src/app/packages/hoppscotch-selfhost-web/dist /site/selfhost-web
|
||||
|
||||
# FE Files
|
||||
COPY --from=base_builder /usr/src/app/aio_run.mjs /usr/src/app/aio_run.mjs
|
||||
COPY --from=fe_builder /usr/src/app/packages/hoppscotch-selfhost-web/dist /site/selfhost-web
|
||||
COPY --from=sh_admin_builder /usr/src/app/packages/hoppscotch-sh-admin/dist-multiport-setup /site/sh-admin-multiport-setup
|
||||
COPY --from=sh_admin_builder /usr/src/app/packages/hoppscotch-sh-admin/dist-subpath-access /site/sh-admin-subpath-access
|
||||
COPY --chown=root:0 --from=sh_admin_builder /usr/src/app/packages/hoppscotch-sh-admin/dist-multiport-setup /site/sh-admin-multiport-setup
|
||||
COPY --chown=root:0 --from=sh_admin_builder /usr/src/app/packages/hoppscotch-sh-admin/dist-subpath-access /site/sh-admin-subpath-access
|
||||
COPY aio-multiport-setup.Caddyfile /etc/caddy/aio-multiport-setup.Caddyfile
|
||||
COPY aio-subpath-access.Caddyfile /etc/caddy/aio-subpath-access.Caddyfile
|
||||
|
||||
# Writable Caddy storage for non-root UIDs (no writable $HOME needed).
|
||||
ENV XDG_DATA_HOME=/tmp
|
||||
ENV XDG_CONFIG_HOME=/tmp
|
||||
|
||||
# Files keep g=rwX from the builders; only the COPY-created dirs need it. List the
|
||||
# dirs (not /site/*) so the layer stays metadata-only.
|
||||
RUN chmod g=rwX /site /site/selfhost-web /site/sh-admin-multiport-setup /site/sh-admin-subpath-access
|
||||
|
||||
# Pre-create /data group-writable so webapp-server's signing key persists across
|
||||
# restarts under a non-root UID (else it's regenerated and logged each start).
|
||||
RUN mkdir -p /data/webapp-server && chmod g=rwX /data /data/webapp-server
|
||||
|
||||
ENTRYPOINT [ "tini", "--" ]
|
||||
COPY --chmod=755 healthcheck.sh /
|
||||
HEALTHCHECK --interval=2s --start-period=15s CMD /bin/sh /healthcheck.sh
|
||||
@@ -254,7 +298,10 @@ HEALTHCHECK --interval=2s --start-period=15s CMD /bin/sh /healthcheck.sh
|
||||
WORKDIR /dist/backend
|
||||
CMD ["node", "/usr/src/app/aio_run.mjs"]
|
||||
|
||||
# NOTE: Although these ports are exposed, the HOPP_ALTERNATE_AIO_PORT variable can be used to assign a user-specified port
|
||||
# NOTE: In subpath mode (ENABLE_SUBPATH_BASED_ACCESS=true) HOPP_ALTERNATE_PORT sets
|
||||
# Caddy's HTTP port (default 80). In multiport mode (default) Caddy uses the
|
||||
# fixed ports 3000/3100/3170 and it has no effect. Legacy
|
||||
# HOPP_AIO_ALTERNATE_PORT is still honoured.
|
||||
EXPOSE 3170
|
||||
EXPOSE 3000
|
||||
EXPOSE 3100
|
||||
|
||||
Reference in New Issue
Block a user