Example Projects
Minimal, working apps for the stacks Temps deploys most often, with the two things every app needs: listen on the PORT Temps sets, on 0.0.0.0, and pick the right preset when the stack isn't auto-detected. Each heading links to the full deploy guide for that framework.
Most examples here are inline snippets you can copy. Where the gotempsh/temps repository has a complete project, it's linked: Next.js, Vite + React, Flask, Gin, Axum, NestJS, Java and C#.
Node.js
Express (npm)
A minimal Express server. Temps doesn't detect a plain Node.js service from package.json, so choose the Autopack (Node.js) preset (nixpacks-node); it starts your start script. Full guide: Deploy Node.js.
server.js
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({ message: 'Hello from Express on Temps!' });
});
app.listen(port, () => {
console.log(`Listening on port ${port}`);
});
package.json
{
"name": "temps-express-example",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^5.1.0"
}
}
Key points:
- Listen on
process.env.PORT— Temps injects this automatically - Include a
startscript inpackage.json - No Dockerfile needed
Fastify (pnpm)
A Fastify server using pnpm, with the same nixpacks-node preset. Temps installs with pnpm when it finds pnpm-lock.yaml.
server.js
const fastify = require('fastify')({ logger: true });
const port = process.env.PORT || 3000;
fastify.get('/', async () => {
return { message: 'Hello from Fastify on Temps!' };
});
fastify.listen({ port, host: '0.0.0.0' });
package.json
{
"name": "temps-fastify-example",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"fastify": "^5.3.3"
}
}
Key points:
- Bind to
0.0.0.0— required for Fastify to accept connections from the Temps proxy pnpm-lock.yamltriggers pnpm instead of npm
Bun
Bun HTTP server
A native Bun server. Choose the nixpacks-node preset; a bun.lock or bun.lockb makes Temps install and run the app with Bun.
index.ts
const port = process.env.PORT || 3000;
const server = Bun.serve({
port,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === '/') {
return Response.json({ message: 'Hello from Bun on Temps!' });
}
return new Response('Not Found', { status: 404 });
},
});
console.log(`Listening on port ${server.port}`);
package.json
{
"name": "temps-bun-example",
"scripts": {
"start": "bun run index.ts"
}
}
Key points:
- Bun runs TypeScript natively — no build step required
- Generate a lock file with
bun installbefore pushing
Elysia (Bun)
Elysia is a fast web framework for Bun with end-to-end type safety.
src/index.ts
import { Elysia } from 'elysia';
const port = process.env.PORT || 3000;
new Elysia()
.get('/', () => ({ message: 'Hello from Elysia on Temps!' }))
.get('/health', () => ({ status: 'ok' }))
.listen(port);
console.log(`Listening on port ${port}`);
package.json
{
"name": "temps-elysia-example",
"scripts": {
"start": "bun run src/index.ts"
},
"dependencies": {
"elysia": "^1.3.2"
}
}
Next.js
Next.js App Router
Next.js is auto-detected from next.config.js, .mjs or .ts and built with the nextjs preset, which runs next start on Node.js 22. Full guide: Deploy Next.js. Repository example: nextjs/basic.
app/page.tsx
export default function Home() {
return (
<main>
<h1>Hello from Next.js on Temps!</h1>
</main>
);
}
app/api/health/route.ts
export function GET() {
return Response.json({ status: 'ok' });
}
Key points:
- No
next.config.jschanges needed — Temps runsnext start, sooutput: 'standalone'isn't required - API routes work out of the box — they run inside the same container
- No Dockerfile needed — Temps generates one
Vite
Vite + React
Vite projects are detected from vite.config.ts or vite.config.js and deployed as static sites — no container runs at runtime. Full guide: Deploy React. Repository example: vite/react-basic.
src/App.tsx
function App() {
return <h1>Hello from Vite + React on Temps!</h1>;
}
export default App;
vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});
Key points:
- Static deployment — Temps builds the
dist/folder, extracts the files, and serves them from the proxy - Zero runtime memory and CPU usage
- SPA routing and cache headers are handled by the Temps proxy
Python
FastAPI
A FastAPI application. Temps detects Python from requirements.txt and uses the python preset, which finds app = FastAPI() in main.py and starts it with Uvicorn on $PORT. Full guide: Deploy FastAPI.
main.py
from fastapi import FastAPI
import os
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello from FastAPI on Temps!"}
@app.get("/health")
def health():
return {"status": "ok"}
if __name__ == "__main__":
import uvicorn
port = int(os.environ.get("PORT", 8000))
uvicorn.run(app, host="0.0.0.0", port=port)
requirements.txt
fastapi>=0.115.0
uvicorn[standard]>=0.34.0
Key points:
- No start command needed when the app object is named
appinmain.py,app.py,asgi.py,wsgi.pyorserver.py - Otherwise, set one with a
Procfileline such asweb: uvicorn api.main:app --host 0.0.0.0 --port $PORT PORTis injected by Temps
Flask
Detected the same way; the python preset starts a Flask app with Gunicorn. Repository example: python/flask-basic.
app.py
from flask import Flask, jsonify
import os
app = Flask(__name__)
@app.route("/")
def hello():
return jsonify(message="Hello from Flask on Temps!")
@app.route("/health")
def health():
return jsonify(status="ok")
if __name__ == "__main__":
port = int(os.environ.get("PORT", 5000))
app.run(host="0.0.0.0", port=port)
requirements.txt
flask>=3.1.0
gunicorn>=23.0.0
Key points:
- Temps runs
gunicorn app:app --bind 0.0.0.0:$PORT, adding Gunicorn to the build if it isn't in your dependencies; Flask's built-in server isn't meant for production traffic - A
Procfileweb:line replaces the start command
Django
The python preset finds manage.py and <project>/wsgi.py and starts Gunicorn. Full guide: Deploy Django.
mysite/wsgi.py
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
application = get_wsgi_application()
requirements.txt
django>=5.2
gunicorn>=23.0.0
psycopg2-binary>=2.9.0
whitenoise>=6.9.0
Key points:
- Temps runs
gunicorn mysite.wsgi:applicationon$PORT; no start command needed - Don't chain
migrateinto the start command: with restarts or several replicas it can run more than once at the same time. Run it as a one-off step after the deploy, as shown in Django migrations - Use WhiteNoise for static files in production and set
STATIC_ROOT - For the database, create a PostgreSQL service and link it to the project — Temps injects
POSTGRES_URL, notDATABASE_URL
Go
Go net/http
Detected from go.mod. Temps compiles a static binary and runs it on a Debian slim image. Full guide: Deploy Go.
main.go
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"message": "Hello from Go on Temps!",
})
})
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
fmt.Printf("Listening on port %s\n", port)
http.ListenAndServe(":"+port, nil)
}
go.mod
module temps-go-example
go 1.27
Key points:
- Listen on
:PORT, notlocalhost:PORT— the container must accept connections from the Temps proxy - Go compiles to a static binary — the final container is minimal
Gin
Repository example: go/gin-basic.
main.go
package main
import (
"net/http"
"os"
"github.com/gin-gonic/gin"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello from Gin on Temps!"})
})
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
r.Run(":" + port)
}
Key points:
- Gin listens on all interfaces by default when using
r.Run(":port") - Set
GIN_MODE=releaseas an environment variable in Temps for production
PHP
Laravel
PHP isn't auto-detected: neither artisan nor composer.json is a detection signal, so choose the Autopack (PHP) preset (nixpacks-php). Temps serves public/ with FrankenPHP on $PORT. Full guide: Deploy Laravel.
routes/api.php
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return response()->json(['message' => 'Hello from Laravel on Temps!']);
});
Route::get('/health', function () {
return response()->json(['status' => 'ok']);
});
composer.json (excerpt)
{
"name": "temps/laravel-example",
"require": {
"php": "^8.3",
"laravel/framework": "^12.0"
},
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
]
}
}
Key points:
- No start command needed: FrankenPHP serves
public/index.php. Don't usephp artisan serve, which is a development server - Don't chain
migrateinto the start command; run it as a one-off step after the deploy, as shown in Laravel migrations - PHP extensions listed as
ext-*incomposer.jsonrequire(for exampleext-pdo_pgsql) are built into the image - Set
APP_KEYandAPP_URLas environment variables in Temps;APP_ENV=productionis set for you
PHP (vanilla)
A plain PHP application. With the nixpacks-php preset, FrankenPHP serves index.php from the repository root (or from public/ if it has one), so no start command is needed.
index.php
<?php
header('Content-Type: application/json');
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ($path === '/health') {
echo json_encode(['status' => 'ok']);
exit;
}
echo json_encode(['message' => 'Hello from PHP on Temps!']);
Rust
Actix Web
Detected from Cargo.toml. Temps runs cargo build --release with cached dependencies and runs the binary on a Debian slim image. Full guide: Deploy Rust. The repository has an Axum version: rust/axum-basic.
src/main.rs
use actix_web::{web, App, HttpServer, HttpResponse};
use serde_json::json;
use std::env;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let port: u16 = env::var("PORT")
.unwrap_or_else(|_| "8080".to_string())
.parse()
.expect("PORT must be a number");
println!("Listening on port {}", port);
HttpServer::new(|| {
App::new()
.route("/", web::get().to(|| async {
HttpResponse::Ok().json(json!({"message": "Hello from Actix on Temps!"}))
}))
.route("/health", web::get().to(|| async {
HttpResponse::Ok().json(json!({"status": "ok"}))
}))
})
.bind(("0.0.0.0", port))?
.run()
.await
}
Cargo.toml
[package]
name = "temps-actix-example"
version = "0.1.0"
edition = "2024"
[dependencies]
actix-web = "4"
serde_json = "1"
Key points:
- Bind to
0.0.0.0, not127.0.0.1 - Rust builds take longer than most languages — the first deployment may take several minutes while dependencies compile. Subsequent builds are faster thanks to BuildKit caching.
Dockerfile
Custom Dockerfile
If your stack has no preset, or you need full control, add a Dockerfile to your repository. Temps detects it before any framework. Full guide: Deploy Docker Containers.
Dockerfile
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
RUN addgroup -g 1001 appgroup && adduser -D -u 1001 -G appgroup appuser
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER appuser
EXPOSE 3000
CMD ["node", "dist/index.js"]
Key points:
- Multi-stage builds keep the final image small
- Run as a non-root user for security
- Match
EXPOSEto the port your app listens on. In v0.1.0-beta.56 anEXPOSEin the image decides which port Temps routes to; see Ports - A
Dockerfilein the repo root is detected before any framework config
Other stacks
These have full guides with their own examples: Java and Spring Boot, .NET, Elixir and Phoenix, Ruby on Rails, Astro, SvelteKit, Remix, Vue and Nuxt, Angular and Docusaurus.
Common patterns
Listening on the right port
Every example above uses process.env.PORT (or the equivalent in each language). This is the single most important pattern — Temps injects the PORT variable and routes all traffic to it.
Node.js
const port = process.env.PORT || 3000;
Python
port = int(os.environ.get("PORT", 8000))
Go
port := os.Getenv("PORT")
Rust
let port: u16 = env::var("PORT").unwrap_or_else(|_| "8080".into()).parse().unwrap();
PHP
$port = getenv('PORT') ?: 8000;
Binding to 0.0.0.0
Your application must bind to 0.0.0.0, not 127.0.0.1 or localhost. The Temps proxy routes traffic to your container over the Docker network — binding to localhost makes your app unreachable.
Health checks
Temps runs HTTP health checks against your container before routing traffic to it. A GET / that returns a 2xx or 3xx status is enough. If your root route needs the database or authentication, add a dedicated /health endpoint and set it as health.path in .temps.yaml.
Start command
With a Dockerfile, Temps runs its CMD. Without one, the preset decides: a web: line in a Procfile or a [start] cmd in nixpacks.toml sets the command explicitly; otherwise Temps uses the framework default, such as your start script for Node.js or Uvicorn or Gunicorn for Python.
If your app doesn't start correctly, add a Procfile with an explicit web: command. See Custom Builds.