Healthchecks and fixes for QR API
This commit is contained in:
@@ -34,6 +34,11 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /mnt/config/docker/kutt/redis:/data
|
- /mnt/config/docker/kutt/redis:/data
|
||||||
command: redis-server --appendonly yes
|
command: redis-server --appendonly yes
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'redis-cli', 'ping']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
kutt:
|
kutt:
|
||||||
image: kutt/kutt:latest
|
image: kutt/kutt:latest
|
||||||
@@ -46,7 +51,17 @@ services:
|
|||||||
kutt_db:
|
kutt_db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
kutt_redis:
|
kutt_redis:
|
||||||
condition: service_started
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
'CMD-SHELL',
|
||||||
|
"node -e \"require('http').get('http://127.0.0.1:3000/', (r) => { r.resume(); process.exit(r.statusCode >= 200 && r.statusCode < 500 ? 0 : 1); }).on('error', () => process.exit(1))\"",
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
environment:
|
environment:
|
||||||
ADMIN_EMAILS: ${ADMIN_EMAILS:?Set ADMIN_EMAILS}
|
ADMIN_EMAILS: ${ADMIN_EMAILS:?Set ADMIN_EMAILS}
|
||||||
DEFAULT_DOMAIN: mifi.me
|
DEFAULT_DOMAIN: mifi.me
|
||||||
@@ -85,7 +100,8 @@ services:
|
|||||||
- 'traefik.http.routers.kutt-mifi.tls.certresolver=letsencrypt'
|
- 'traefik.http.routers.kutt-mifi.tls.certresolver=letsencrypt'
|
||||||
- 'traefik.http.routers.kutt-mifi.service=kutt-short'
|
- 'traefik.http.routers.kutt-mifi.service=kutt-short'
|
||||||
- 'traefik.http.services.kutt-short.loadbalancer.server.port=3000'
|
- 'traefik.http.services.kutt-short.loadbalancer.server.port=3000'
|
||||||
- 'traefik.http.services.kutt-short.loadbalancer.serversTransport=kutt-long-timeout'
|
# Backend timeout: use transport from file provider (see traefik-kutt-timeout.example.yml).
|
||||||
|
- 'traefik.http.services.kutt-short.loadbalancer.serversTransport=kutt-long-timeout@file'
|
||||||
|
|
||||||
qr_api:
|
qr_api:
|
||||||
image: ${REGISTRY:-git.mifi.dev}/mifi-holdings/shorty-qr-api:${IMAGE_TAG:-latest}
|
image: ${REGISTRY:-git.mifi.dev}/mifi-holdings/shorty-qr-api:${IMAGE_TAG:-latest}
|
||||||
@@ -103,6 +119,18 @@ services:
|
|||||||
KUTT_API_KEY: ${KUTT_API_KEY:-}
|
KUTT_API_KEY: ${KUTT_API_KEY:-}
|
||||||
KUTT_BASE_URL: http://kutt:3000
|
KUTT_BASE_URL: http://kutt:3000
|
||||||
SHORT_DOMAIN: https://mifi.me
|
SHORT_DOMAIN: https://mifi.me
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
'CMD',
|
||||||
|
'node',
|
||||||
|
'-e',
|
||||||
|
"require('http').get('http://127.0.0.1:8080/health', (r) => { r.resume(); process.exit(r.statusCode === 200 ? 0 : 1); }).on('error', () => process.exit(1))",
|
||||||
|
]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
qr_web:
|
qr_web:
|
||||||
image: ${REGISTRY:-git.mifi.dev}/mifi-holdings/shorty-qr-web:${IMAGE_TAG:-latest}
|
image: ${REGISTRY:-git.mifi.dev}/mifi-holdings/shorty-qr-web:${IMAGE_TAG:-latest}
|
||||||
@@ -112,9 +140,20 @@ services:
|
|||||||
- marina-net
|
- marina-net
|
||||||
- backend
|
- backend
|
||||||
depends_on:
|
depends_on:
|
||||||
- qr_api
|
qr_api:
|
||||||
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
QR_API_URL: http://qr_api:8080
|
QR_API_URL: http://qr_api:8080
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
'CMD-SHELL',
|
||||||
|
"node -e \"require('http').get('http://127.0.0.1:3000/', (r) => { r.resume(); process.exit(r.statusCode >= 200 && r.statusCode < 500 ? 0 : 1); }).on('error', () => process.exit(1))\"",
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
labels:
|
labels:
|
||||||
- 'traefik.enable=true'
|
- 'traefik.enable=true'
|
||||||
- 'docker.network=marina-net'
|
- 'docker.network=marina-net'
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /mnt/config/docker/kutt/redis:/data
|
- /mnt/config/docker/kutt/redis:/data
|
||||||
command: redis-server --appendonly yes
|
command: redis-server --appendonly yes
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'redis-cli', 'ping']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
kutt:
|
kutt:
|
||||||
image: kutt/kutt:latest
|
image: kutt/kutt:latest
|
||||||
@@ -39,7 +44,17 @@ services:
|
|||||||
kutt_db:
|
kutt_db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
kutt_redis:
|
kutt_redis:
|
||||||
condition: service_started
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
'CMD-SHELL',
|
||||||
|
"node -e \"require('http').get('http://127.0.0.1:3000/', (r) => { r.resume(); process.exit(r.statusCode >= 200 && r.statusCode < 500 ? 0 : 1); }).on('error', () => process.exit(1))\"",
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
environment:
|
environment:
|
||||||
DB_CLIENT: pg
|
DB_CLIENT: pg
|
||||||
DB_HOST: kutt_db
|
DB_HOST: kutt_db
|
||||||
@@ -85,6 +100,18 @@ services:
|
|||||||
KUTT_API_KEY: ${KUTT_API_KEY:-}
|
KUTT_API_KEY: ${KUTT_API_KEY:-}
|
||||||
KUTT_BASE_URL: http://kutt:3000
|
KUTT_BASE_URL: http://kutt:3000
|
||||||
SHORT_DOMAIN: https://mifi.me
|
SHORT_DOMAIN: https://mifi.me
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
'CMD',
|
||||||
|
'node',
|
||||||
|
'-e',
|
||||||
|
"require('http').get('http://127.0.0.1:8080/health', (r) => { r.resume(); process.exit(r.statusCode === 200 ? 0 : 1); }).on('error', () => process.exit(1))",
|
||||||
|
]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
qr_web:
|
qr_web:
|
||||||
build:
|
build:
|
||||||
@@ -95,9 +122,20 @@ services:
|
|||||||
- marina-net
|
- marina-net
|
||||||
- backend
|
- backend
|
||||||
depends_on:
|
depends_on:
|
||||||
- qr_api
|
qr_api:
|
||||||
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
QR_API_URL: http://qr_api:8080
|
QR_API_URL: http://qr_api:8080
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
'CMD-SHELL',
|
||||||
|
"node -e \"require('http').get('http://127.0.0.1:3000/', (r) => { r.resume(); process.exit(r.statusCode >= 200 && r.statusCode < 500 ? 0 : 1); }).on('error', () => process.exit(1))\"",
|
||||||
|
]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
labels:
|
labels:
|
||||||
- 'traefik.enable=true'
|
- 'traefik.enable=true'
|
||||||
- 'docker.network=marina-net'
|
- 'docker.network=marina-net'
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM node:20-alpine AS builder
|
# Use Debian-based image so better-sqlite3 prebuilds (glibc) work; Alpine/musl has no prebuilds.
|
||||||
|
FROM node:20-bookworm-slim AS builder
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json pnpm-lock.yaml* ./
|
COPY package.json pnpm-lock.yaml* ./
|
||||||
@@ -6,7 +7,7 @@ RUN pnpm install
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
|
||||||
FROM node:20-alpine
|
FROM node:20-bookworm-slim
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|||||||
@@ -18,7 +18,13 @@ export async function GET(
|
|||||||
}
|
}
|
||||||
return Response.json(data);
|
return Response.json(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.json({ error: String(e) }, { status: 502 });
|
return Response.json(
|
||||||
|
{
|
||||||
|
error: 'QR API unreachable',
|
||||||
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
|
},
|
||||||
|
{ status: 502 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +49,13 @@ export async function PUT(
|
|||||||
}
|
}
|
||||||
return Response.json(data);
|
return Response.json(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.json({ error: String(e) }, { status: 502 });
|
return Response.json(
|
||||||
|
{
|
||||||
|
error: 'QR API unreachable',
|
||||||
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
|
},
|
||||||
|
{ status: 502 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +77,12 @@ export async function DELETE(
|
|||||||
{ status: res.status },
|
{ status: res.status },
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.json({ error: String(e) }, { status: 502 });
|
return Response.json(
|
||||||
|
{
|
||||||
|
error: 'QR API unreachable',
|
||||||
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
|
},
|
||||||
|
{ status: 502 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,13 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
return Response.json(Array.isArray(data) ? data : data);
|
return Response.json(Array.isArray(data) ? data : data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.json({ error: String(e) }, { status: 502 });
|
return Response.json(
|
||||||
|
{
|
||||||
|
error: 'QR API unreachable',
|
||||||
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
|
},
|
||||||
|
{ status: 502 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +39,12 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
return Response.json(data);
|
return Response.json(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.json({ error: String(e) }, { status: 502 });
|
return Response.json(
|
||||||
|
{
|
||||||
|
error: 'QR API unreachable',
|
||||||
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
|
},
|
||||||
|
{ status: 502 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,13 @@ export async function GET(
|
|||||||
}
|
}
|
||||||
return Response.json(data);
|
return Response.json(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.json({ error: String(e) }, { status: 502 });
|
return Response.json(
|
||||||
|
{
|
||||||
|
error: 'QR API unreachable',
|
||||||
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
|
},
|
||||||
|
{ status: 502 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +61,13 @@ export async function PUT(
|
|||||||
}
|
}
|
||||||
return Response.json(data);
|
return Response.json(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.json({ error: String(e) }, { status: 502 });
|
return Response.json(
|
||||||
|
{
|
||||||
|
error: 'QR API unreachable',
|
||||||
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
|
},
|
||||||
|
{ status: 502 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +89,12 @@ export async function DELETE(
|
|||||||
{ status: res.status },
|
{ status: res.status },
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.json({ error: String(e) }, { status: 502 });
|
return Response.json(
|
||||||
|
{
|
||||||
|
error: 'QR API unreachable',
|
||||||
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
|
},
|
||||||
|
{ status: 502 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,13 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
return Response.json(Array.isArray(data) ? rewriteLogoUrl(data) : data);
|
return Response.json(Array.isArray(data) ? rewriteLogoUrl(data) : data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.json({ error: String(e) }, { status: 502 });
|
return Response.json(
|
||||||
|
{
|
||||||
|
error: 'QR API unreachable',
|
||||||
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
|
},
|
||||||
|
{ status: 502 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,6 +48,12 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
return Response.json(data);
|
return Response.json(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.json({ error: String(e) }, { status: 502 });
|
return Response.json(
|
||||||
|
{
|
||||||
|
error: 'QR API unreachable',
|
||||||
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
|
},
|
||||||
|
{ status: 502 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
return Response.json(data);
|
return Response.json(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return Response.json({ error: String(e) }, { status: 502 });
|
return Response.json(
|
||||||
|
{
|
||||||
|
error: 'QR API unreachable',
|
||||||
|
detail: e instanceof Error ? e.message : String(e),
|
||||||
|
},
|
||||||
|
{ status: 502 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
# Place this file in the same directory as your other dynamic config (e.g. /etc/traefik/conf.d/)
|
# Place this file in the same directory as your other dynamic config (e.g. /etc/traefik/conf.d/)
|
||||||
# so the file provider picks it up.
|
# so the file provider picks it up.
|
||||||
#
|
#
|
||||||
# Then uncomment the serversTransport label on the kutt service in docker-compose.portainer.yml.
|
# In docker-compose.portainer.yml set the kutt service label to use this transport with @file:
|
||||||
|
# traefik.http.services.kutt-short.loadbalancer.serversTransport=kutt-long-timeout@file
|
||||||
#
|
#
|
||||||
# responseHeaderTimeout: time to wait for backend response headers (0 = no timeout).
|
# responseHeaderTimeout: time to wait for backend response headers (0 = no timeout).
|
||||||
# 300s helps avoid 504 Gateway Timeout when Kutt or OIDC is slow.
|
# 300s helps avoid 504 Gateway Timeout when Kutt or OIDC is slow.
|
||||||
|
|||||||
Reference in New Issue
Block a user