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 start script in package.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.yaml triggers 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 install before 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.js changes needed — Temps runs next start, so output: '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 app in main.py, app.py, asgi.py, wsgi.py or server.py
  • Otherwise, set one with a Procfile line such as web: uvicorn api.main:app --host 0.0.0.0 --port $PORT
  • PORT is 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 Procfile web: 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:application on $PORT; no start command needed
  • Don't chain migrate into 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, not DATABASE_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, not localhost: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=release as 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 use php artisan serve, which is a development server
  • Don't chain migrate into the start command; run it as a one-off step after the deploy, as shown in Laravel migrations
  • PHP extensions listed as ext-* in composer.json require (for example ext-pdo_pgsql) are built into the image
  • Set APP_KEY and APP_URL as environment variables in Temps; APP_ENV=production is 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, not 127.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 EXPOSE to the port your app listens on. In v0.1.0-beta.56 an EXPOSE in the image decides which port Temps routes to; see Ports
  • A Dockerfile in 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.


What to explore next

Supported languages and frameworks Dockerfile deployment guide Add a database to your project

Last updated

Was this page helpful?