Frontend foundation built with Next.js 15 + TypeScript (ESM) — containerized and ready to connect with the existing Traefik infrastructure.
A minimal “Hello World” app designed to run locally as if it were in production.
FROM LOCALHOST TO PRODUCTION — BUILT LIKE A HACKER
This repository represents the frontend core of the Leonobitech full-stack architecture.
It uses Next.js 15 + TypeScript (ESM), runs in Docker, and is automatically discoverable by Traefik for HTTPS routing (app.localhost → frontend, api.localhost → backend).
It’s minimal by design — just enough to prove your infrastructure works end-to-end.
| Layer | Component | Purpose |
|---|---|---|
| ⚛️ Next.js 15 | React framework | SSR + static pages + API routes |
| 🦾 TypeScript (ESM) | Modern language support | Type safety + cleaner DX |
| 🐳 Docker | Runtime isolation | Production-like local execution |
| ⚡ Traefik 3.x | Reverse proxy | HTTPS, domain routing |
| 🔐 mkcert | Local TLS | Trusted local HTTPS certificates |
This project is designed to sit inside the same root alongside infra and backend:
root/
├─ assets/
├─ Docs/
│ ├─ README_BACKEND.md
│ └─ README_INFRA.md
├─ repositories/
│ ├─ core/ # backend (Node + TS + Hexagonal)
│ └─ frontend/ # <-- we will create this now
├─ traefik/
├─ .env
├─ .env.example
├─ docker-compose.yml
├─ docker-compose.local.yml
├─ docker-compose.prod.yml
├─ LICENSE
├─ Makefile
└─ README.md
From the root of your stack:
# 1) Ensure the parent folder exists
mkdir -p repositories
cd repositories
# 2) Create a Next.js app in "frontend"
# (pick one: npm / pnpm / yarn)
npx create-next-app@latest frontend \
--ts --eslint --app --src-dir false --tailwind \
--use-npm --turbopack --import-alias "@/*"
# If you prefer pnpm:
pnpm dlx create-next-app@latest frontend \
--ts --eslint --app --src-dir false --tailwind \
--use-pnpm --turbopack --import-alias "@/*"
Why this?
- App Router and ESM by default.
- Tailwind ready out of the box.
- Turbopack for a faster dev server.
- Keeps the default alias
@/*across the project.
We prefer CNA + shadcn init (more control) over opinionated templates.
cd repositories/frontend
npx shadcn@latest init- Select Base color:
zincorslate( Optional )
npx shadcn@latest add button card input textarea select dialog sonnerapp/layout.tsx
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Frontend Core — Leonobitech",
description: "Next.js + TypeScript + Tailwind + shadcn/ui minimal core",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} min-h-dvh bg-background text-foreground antialiased p-6`}
>
{children}
</body>
</html>
);
}app/page.tsx
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export default function HomePage() {
return (
<main className="max-w-xl mx-auto grid gap-4">
<h1 className="text-2xl font-semibold">🚀 Frontend Core — Hello World</h1>
<Card>
<CardHeader>
<CardTitle>Stack</CardTitle>
</CardHeader>
<CardContent className="space-y-2">
<p>Next.js 15 + TypeScript + Tailwind + Turbopack + shadcn/ui</p>
<Button>It works</Button>
</CardContent>
</Card>
</main>
);
}repositories/frontend/
├─ public/
├─ src/app/
│ ├─ page.tsx
│ └─ layout.tsx
├─ .gitignore
├─ components.json
├─ eslint.config.mjs
├─ next-env.d.ts
├─ next.config.ts
├─ package.json
├─ postcss.config.mjs
├─ README.md
└─ tsconfig.json
From repositories/frontend/:
npm run dev
# visit http://localhost:3000Build & start (prod mode, still without Docker):
npm run build && npm startCreate a Dockerfile inside repositories/frontend/:
# --- Builder ---
FROM node:22-alpine AS builder
WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# --- Runtime ---
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# curl para healthcheck
RUN apk add --no-cache curl
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["npm", "run", "start"]
.dockerignore
node_modules
.next
.git
.gitignore
Dockerfile
README.md
.env
.env.*
*.log
To make healthcheck faster and more stable, create a lightweight endpoint that always responds 200
// src/app/healthz/route.ts
export function GET() {
return new Response("ok", {
status: 200,
headers: {
"content-type": "text/plain; charset=utf-8",
"cache-control": "no-store",
},
});
}healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:3000/healthz >/dev/null || exit 1"]
interval: 15s
timeout: 3s
retries: 3
start_period: 10sWe’ll connect to the same Traefik network used by the rest of the stack (replace the network name if yours differs — e.g., proxy vs leonobitech-net). The example below assumes an external network named leonobitech-net and an .env with FRONTEND_DOMAIN=app.localhost.
frontend:
build:
context: ./repositories/frontend
dockerfile: Dockerfile
image: frontend:v1.0.0
container_name: frontend
restart: unless-stopped
environment:
- NODE_ENV=production
networks:
- leonobitech-net
depends_on:
traefik:
condition: service_started
# 🔍 Healthcheck: using a lightweight endpoint
healthcheck:
test:
[
"CMD-SHELL",
"curl -fsS http://localhost:3000/healthz >/dev/null || exit 1",
]
interval: 15s
timeout: 3s
retries: 3
start_period: 10s
labels:
- "traefik.enable=true"
# HTTPS router: https://app.localhost
- "traefik.http.routers.frontend.rule=Host(`${FRONTEND_DOMAIN}`)"
- "traefik.http.routers.frontend.entrypoints=websecure"
- "traefik.http.routers.frontend.tls=true"
# Forward to container port 3000
- "traefik.http.services.frontend.loadbalancer.server.port=3000"
# Optionally attach middlewares defined under traefik/dynamic
# - "traefik.http.routers.frontend.middlewares=secure-strict@file"Ensure your root .env contains:
FRONTEND_DOMAIN=app.localhost
From the root:
docker compose up -d --build frontend
# or build the whole stack if Traefik is not up yet:
# docker compose up -d --build
# Options:
docker ps
docker logs -f frontendOpen:
- https://app.localhost → Frontend Core
- https://traefik.localhost → Dashboard (if enabled)
If you’re using mkcert from the infra repo, the cert will be trusted and the browser will show the lock icon.
- 404 from Traefik → Check labels and the external network name.
- TLS warning → Re-run mkcert and reload Traefik (see infra README).
- Port conflict 3000 → Stop local
npm run devwhen testing the container. - Not using your host → Confirm
.env FRONTEND_DOMAINand the router rule.
“Production is not a deployment — it’s a mindset.”
This repository completes the local triad:
| Repo | Role |
|---|---|
🧱 fullstack-infrastructure-blueprint |
Traefik + mkcert base |
⚙️ fullstack-backend-core |
API core (Node + Express + Hexagonal) |
🖥️ fullstack-frontend-core |
Frontend (Next.js + TypeScript) |
Together, they simulate a real production-grade full stack, entirely on your laptop.
docker compose up -d --build traefik core frontendAfter all three repos are up:
https://traefik.localhost→ Traefik dashboardhttps://api.localhost→ Backend corehttps://app.localhost→ Frontend core
✅ Everything runs locally under HTTPS — just like in production.
frontend, nextjs, typescript, esm, docker, traefik, mkcert, production-like, leonobitech, fullstack, infrastructure
MIT © 2025 — Felix Figueroa @ Leonobitech
🥷 Leonobitech Dev Team
https://www.leonobitech.com
Made with 🧠, 🥷, and Docker love 🐳
🔥 This isn’t just a frontend. It’s your bridge between infrastructure and imagination.
