feat: add Docker containerization with multi-stage build

Includes .dockerignore, Dockerfile with cargo-chef caching, and Justfile commands for building/running containerized app. Updates console-logger to support both JSON and pretty-printed logs based on LOG_JSON env var.
This commit is contained in:
2026-01-04 20:06:56 -06:00
parent edf271bcc6
commit 9de3c84f00
15 changed files with 238 additions and 244 deletions
-3
View File
@@ -3,13 +3,11 @@ import { dev } from "$app/environment";
import { initLogger } from "$lib/logger";
import { getLogger } from "@logtape/logtape";
// Initialize logger on server startup
await initLogger();
const logger = getLogger(["ssr", "error"]);
export const handle: Handle = async ({ event, resolve }) => {
// Handle DevTools request silently to prevent console.log spam
if (
dev &&
event.url.pathname === "/.well-known/appspecific/com.chrome.devtools.json"
@@ -26,7 +24,6 @@ export const handleError: HandleServerError = async ({
status,
message,
}) => {
// Use structured logging via LogTape instead of console.error
logger.error(message, {
status,
method: event.request.method,
-11
View File
@@ -3,20 +3,11 @@ import { env } from "$env/dynamic/private";
const logger = getLogger(["ssr", "lib", "api"]);
// Compute upstream configuration once at module load
const upstreamUrl = env.UPSTREAM_URL;
const isUnixSocket =
upstreamUrl?.startsWith("/") || upstreamUrl?.startsWith("./");
const baseUrl = isUnixSocket ? "http://localhost" : upstreamUrl;
/**
* Fetch utility for calling the Rust backend API.
* Automatically prefixes requests with the upstream URL from environment.
* Supports both HTTP URLs and Unix socket paths.
*
* Connection pooling and keep-alive are handled automatically by Bun.
* Default timeout is 30 seconds unless overridden via init.signal.
*/
export async function apiFetch<T>(
path: string,
init?: RequestInit,
@@ -29,10 +20,8 @@ export async function apiFetch<T>(
const url = `${baseUrl}${path}`;
const method = init?.method ?? "GET";
// Build fetch options with 30s default timeout and unix socket support
const fetchOptions: RequestInit & { unix?: string } = {
...init,
// Respect caller-provided signal, otherwise default to 30s timeout
signal: init?.signal ?? AbortSignal.timeout(30_000),
};
-4
View File
@@ -195,7 +195,6 @@
float closestRad = 0.0;
float pointOpacity = 0.0;
// Check 9 neighboring grid points
for (float dx = -1.0; dx <= 1.0; dx += 1.0) {
for (float dy = -1.0; dy <= 1.0; dy += 1.0) {
vec2 testGrid = gridCoord + vec2(dx * spacing, dy * spacing);
@@ -296,7 +295,6 @@
const { gl, program } = context;
// Setup fullscreen quad geometry
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(
@@ -309,7 +307,6 @@
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
// Setup uniform manager
const uniforms = new UniformManager(gl, program, [
"u_resolution",
"u_time",
@@ -335,7 +332,6 @@
const dpr = window.devicePixelRatio || 1;
// Set static uniforms
uniforms.setStatic({
u_seed: Math.random() * 1000,
u_dpr: dpr,
-20
View File
@@ -8,15 +8,6 @@ interface RailwayLogEntry {
[key: string]: unknown;
}
/**
* Custom formatter that outputs Railway-compatible JSON logs.
* Format: { timestamp, level, message, target, ...attributes }
*
* The target field is constructed from the logger category:
* - ["ssr"] -> "ssr"
* - ["ssr", "routes"] -> "ssr:routes"
* - ["ssr", "api", "auth"] -> "ssr:api:auth"
*/
function railwayFormatter(record: LogRecord): string {
const entry: RailwayLogEntry = {
timestamp: new Date().toISOString(),
@@ -25,7 +16,6 @@ function railwayFormatter(record: LogRecord): string {
target: record.category.join(":"),
};
// Flatten properties to root level (custom attributes)
if (record.properties && Object.keys(record.properties).length > 0) {
Object.assign(entry, record.properties);
}
@@ -33,18 +23,12 @@ function railwayFormatter(record: LogRecord): string {
return JSON.stringify(entry) + "\n";
}
/**
* Initialize LogTape with Railway-compatible JSON logging.
* Only outputs logs when LOG_JSON=true or LOG_JSON=1 is set.
* Safe to call multiple times (idempotent - will silently skip if already configured).
*/
export async function initLogger() {
const useJsonLogs =
process.env.LOG_JSON === "true" || process.env.LOG_JSON === "1";
try {
if (!useJsonLogs) {
// In development, use default console logging with nice formatting
await configure({
sinks: {
console: getConsoleSink(),
@@ -66,7 +50,6 @@ export async function initLogger() {
return;
}
// In production/JSON mode, use Railway-compatible JSON formatter
await configure({
sinks: {
json: (record: LogRecord) => {
@@ -75,13 +58,11 @@ export async function initLogger() {
},
filters: {},
loggers: [
// Meta logger for LogTape's internal messages
{
category: ["logtape", "meta"],
lowestLevel: "warning",
sinks: ["json"],
},
// SSR application logs
{
category: ["ssr"],
lowestLevel: "info",
@@ -90,7 +71,6 @@ export async function initLogger() {
],
});
} catch (error) {
// Already configured (HMR in dev mode), silently ignore
if (
error instanceof Error &&
error.message.includes("Already configured")
+1 -6
View File
@@ -8,17 +8,12 @@
</script>
<AppWrapper class="overflow-x-hidden font-schibsted">
<!-- Top Navigation Bar -->
<div class="flex w-full justify-end items-center pt-5 px-6 pb-9">
<!-- <div class="flex gap-4 items-center"></div> -->
</div>
<div class="flex w-full justify-end items-center pt-5 px-6 pb-9"></div>
<!-- Main Content -->
<div class="flex items-center flex-col">
<div
class="max-w-2xl mx-6 border-b border-zinc-700 divide-y divide-zinc-700"
>
<!-- Name & Occupation -->
<div class="flex flex-col pb-4">
<span class="text-3xl font-bold text-white">Ryan Walters,</span>
<span class="text-2xl font-normal text-zinc-400">