Healthchecks and fixes for QR API
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/deploy unknown status

This commit is contained in:
2026-02-07 14:45:02 -03:00
parent 771d0ccf27
commit ace33435fb
9 changed files with 164 additions and 19 deletions

View File

@@ -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'

View File

@@ -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'

View File

@@ -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

View File

@@ -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 },
);
} }
} }

View File

@@ -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 },
);
} }
} }

View File

@@ -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 },
);
} }
} }

View File

@@ -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 },
);
} }
} }

View File

@@ -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 },
);
} }
} }

View File

@@ -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.