Deploy Web Applications
Temps deploys web applications from a Git repository. Push your code, and Temps detects your language, installs dependencies, builds the project, and starts it in a container. This page covers every supported language and what your application needs to work.
How it works
When you connect a repository to Temps and trigger a deployment, the following happens:
- Clone — Temps pulls your code from GitHub, GitLab, Bitbucket, or Gitea
- Detect — The build system inspects your files to determine the language and framework
- Build — Dependencies are installed, the project is compiled, and a container image is created
- Deploy — The container starts, Temps injects environment variables (including
PORT), and runs health checks - Route — Once healthy, traffic is routed to the new container and the old one is removed
You do not need to write a Dockerfile. Temps generates one automatically based on your framework. If you prefer full control, you can add your own Dockerfile and it will take priority over auto-detection.
The one requirement
Your application must listen on the port provided in the PORT environment variable. Temps sets this automatically and routes all traffic to it.
Node.js
const port = process.env.PORT || 3000;
app.listen(port, '0.0.0.0');
Python
import os
port = int(os.environ.get('PORT', 8000))
Go
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.ListenAndServe("0.0.0.0:"+port, router)
Binding to 0.0.0.0 instead of localhost or 127.0.0.1 is required. Containers that bind to localhost only accept connections from inside the container, so Temps cannot route traffic to them.
Framework detection
Temps inspects your repository files and auto-detects the framework. Detection happens in priority order — the first match wins.
| Priority | Framework | Detection signal | Default port |
|---|---|---|---|
| 1 | Dockerfile | Dockerfile in repo root | From EXPOSE directive |
| 2 | Docusaurus | docusaurus.config.js or .ts | 3000 |
| 3 | Next.js | next.config.js, .mjs, or .ts | 3000 |
| 4 | Vite | vite.config.js or .ts | 80 (static, Temps) |
| 5 | Create React App | react-scripts in package.json | 3000 |
| 6 | Rsbuild | rsbuild.config.ts | 3000 |
| 7 | Rust | Cargo.toml | 8080 |
| 8 | Go | go.mod | 8080 |
| 9 | Python | requirements.txt, pyproject.toml, setup.py, or Pipfile | 8000 |
| 10 | Java | pom.xml, build.gradle, or build.gradle.kts | 8080 |
| — | PHP | composer.json (via Nixpacks) | 8080 |
| — | Ruby | Gemfile (via Nixpacks) | 3000 |
| — | C# / .NET | .csproj (via Nixpacks) | 8080 |
Languages marked with — in priority are not in the auto-detection chain. They are supported via Nixpacks and can be selected manually in the project settings, or you can add a custom Dockerfile for full control (see examples below).
If a Dockerfile is present alongside framework config files (e.g., both Dockerfile and next.config.ts), the Dockerfile always takes priority.
For Node.js projects, Temps further detects the specific framework from your package.json dependencies: Next.js, NestJS, Nuxt, Remix, Astro, SvelteKit, Vite, Vue, Express, or generic Node.js.
For Python projects, Temps checks your requirements for Django, FastAPI, Flask, or Streamlit.
Package manager detection
For Node.js projects, Temps detects your package manager from the lock file:
| Lock file | Package manager | Install command |
|---|---|---|
pnpm-lock.yaml | pnpm | pnpm install --frozen-lockfile |
package-lock.json | npm | npm install |
yarn.lock | Yarn | yarn install --frozen-lockfile |
bun.lockb or bun.lock | Bun | bun install |
If no lock file is found, npm is used as the default.
Node.js
Temps supports Express, Fastify, NestJS, Hono, and any Node.js HTTP server.
Express
server.js
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.json({ message: 'Hello from Express' });
});
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
const port = process.env.PORT || 3000;
app.listen(port, '0.0.0.0', () => {
console.log(`Server running on port ${port}`);
});
package.json
{
"name": "my-api",
"scripts": {
"start": "node server.js",
"build": "tsc"
},
"dependencies": {
"express": "^4.18.0"
}
}
Fastify
server.js
const fastify = require('fastify')({ logger: true });
fastify.get('/', async () => {
return { message: 'Hello from Fastify' };
});
fastify.get('/health', async () => {
return { status: 'ok' };
});
const start = async () => {
const port = process.env.PORT || 3000;
await fastify.listen({ port: Number(port), host: '0.0.0.0' });
};
start();
NestJS
NestJS is auto-detected when @nestjs/core is in your dependencies. Ensure your main.ts reads the port from the environment:
src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
TypeScript projects
If your project uses TypeScript, ensure you have a build script in package.json:
package.json
{
"scripts": {
"build": "tsc",
"start": "node dist/server.js"
}
}
Temps runs npm run build (or equivalent for your package manager) during the build phase, then npm start to launch the application.
Python
Temps supports Flask, FastAPI, Django, and any Python WSGI/ASGI application.
Flask
main.py
from flask import Flask, jsonify
import os
app = Flask(__name__)
@app.route('/')
def hello():
return jsonify({
'message': 'Hello from Flask',
'status': 'healthy'
})
@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.0.0
gunicorn==22.0.0
Temps detects Flask from your requirements.txt and starts the application with Gunicorn in production.
FastAPI
main.py
from fastapi import FastAPI
app = FastAPI()
@app.get('/')
async def root():
return {'message': 'Hello from FastAPI'}
@app.get('/health')
async def health():
return {'status': 'ok'}
requirements.txt
fastapi==0.115.0
uvicorn[standard]==0.30.0
FastAPI apps are started with Uvicorn automatically.
Django
For Django projects, Temps detects django in your requirements and the presence of manage.py. The application is started with Gunicorn using your WSGI module:
requirements.txt
Django==5.1.0
gunicorn==22.0.0
psycopg2-binary==2.9.9
Ensure ALLOWED_HOSTS reads from the environment:
settings.py
import os
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '*').split(',')
Go
Temps detects Go projects by the presence of go.mod. The project is compiled with go build and the resulting binary is executed.
main.go
package main
import (
"net/http"
"os"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Hello from Go",
})
})
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
})
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
r.Run("0.0.0.0:" + port)
}
This works with Gin, Echo, Chi, the standard library net/http, or any Go HTTP framework. The key requirement is reading the port from PORT and binding to 0.0.0.0.
Rust
Temps detects Rust projects by Cargo.toml. The project is compiled with cargo build --release via Nixpacks.
src/main.rs
use axum::{routing::get, Json, Router};
use serde::Serialize;
use std::net::SocketAddr;
#[derive(Serialize)]
struct Response {
message: String,
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async {
Json(Response { message: "Hello from Rust".into() })
}))
.route("/health", get(|| async {
Json(serde_json::json!({"status": "ok"}))
}));
let port: u16 = std::env::var("PORT")
.unwrap_or_else(|_| "3000".into())
.parse()
.unwrap_or(3000);
let addr = SocketAddr::from(([0, 0, 0, 0], port));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Cargo.toml
[package]
name = "my-api"
version = "1.0.0"
edition = "2021"
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Java
Temps detects Java projects from pom.xml (Maven) or build.gradle / build.gradle.kts (Gradle).
Spring Boot
src/main/java/com/example/Application.java
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
src/main/java/com/example/HelloController.java
package com.example;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class HelloController {
@GetMapping("/")
public Map<String, String> hello() {
return Map.of("message", "Hello from Spring Boot");
}
@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "ok");
}
}
Configure Spring Boot to read the port from the environment:
src/main/resources/application.properties
server.port=${PORT:8080}
Temps uses Nixpacks to build Java projects. For full control, add your own Dockerfile:
FROM maven:3.9-eclipse-temurin-21-alpine AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
The multi-stage build separates dependency resolution from source compilation. This means dependencies are cached and only re-downloaded when pom.xml or build.gradle.kts changes.
C# / .NET
Temps supports .NET applications via Nixpacks. Detection is based on .csproj files in the project directory.
Minimal API
Program.cs
var builder = WebApplication.CreateBuilder(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "8080";
builder.WebHost.UseUrls($"http://0.0.0.0:{port}");
var app = builder.Build();
app.MapGet("/", () => new { message = "Hello from .NET", status = "healthy" });
app.MapGet("/health", () => new { status = "ok" });
app.Run();
MyApp.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Custom Dockerfile
For production .NET deployments, a custom Dockerfile gives you control over the SDK version and runtime image:
Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build
WORKDIR /app
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /out
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine
WORKDIR /app
COPY --from=build /out .
EXPOSE 8080
ENTRYPOINT ["dotnet", "MyApp.dll"]
The aspnet runtime image is much smaller than the sdk image. The multi-stage build uses the SDK for compilation and the runtime for execution.
Ruby
Temps detects Ruby projects from a Gemfile. Rails and Sinatra are both supported via Nixpacks.
Rails
Ensure your Gemfile includes rails and that config/puma.rb binds to the correct port:
config/puma.rb
port ENV.fetch('PORT') { 3000 }
bind "tcp://0.0.0.0:#{ENV.fetch('PORT') { 3000 }}"
Sinatra
app.rb
require 'sinatra'
set :port, ENV['PORT'] || 4567
set :bind, '0.0.0.0'
get '/' do
content_type :json
{ message: 'Hello from Sinatra' }.to_json
end
Custom Dockerfile
For Rails applications that need specific system dependencies (image processing, PDF generation, etc.), use a custom Dockerfile:
Dockerfile (Rails)
FROM ruby:3.3-slim AS build
WORKDIR /app
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
nodejs \
&& rm -rf /var/lib/apt/lists/*
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment true && \
bundle config set --local without 'development test' && \
bundle install
COPY . .
RUN SECRET_KEY_BASE=placeholder bundle exec rake assets:precompile
FROM ruby:3.3-slim
WORKDIR /app
RUN apt-get update && apt-get install -y libpq5 && rm -rf /var/lib/apt/lists/*
COPY --from=build /app /app
ENV RAILS_ENV=production \
RAILS_SERVE_STATIC_FILES=true \
RAILS_LOG_TO_STDOUT=true
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
PHP
Temps detects PHP projects from composer.json. Laravel is specifically detected when laravel/framework is a dependency and an artisan file exists. PHP support is provided via Nixpacks.
Laravel
Set essential environment variables in the Temps dashboard:
APP_KEY=base64:your-app-key-here
APP_URL=https://your-app.yourdomain.com
DB_CONNECTION=pgsql
DATABASE_URL=postgres://user:pass@host:5432/dbname
Never commit APP_KEY to your repository. Generate it locally with php artisan key:generate --show and add it as an environment variable in Temps.
Custom Dockerfile (Laravel)
For production Laravel deployments, a custom Dockerfile gives you full control over PHP extensions, web server configuration, and optimization:
Dockerfile (Laravel)
FROM php:8.3-fpm-alpine AS base
# Install system dependencies and PHP extensions
RUN apk add --no-cache \
nginx \
supervisor \
libpng-dev \
libjpeg-turbo-dev \
libzip-dev \
postgresql-dev \
icu-dev \
&& docker-php-ext-configure gd --with-jpeg \
&& docker-php-ext-install \
pdo_pgsql \
gd \
zip \
intl \
opcache \
bcmath \
&& rm -rf /var/cache/apk/*
# Install Composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
# Install dependencies (cached layer)
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
# Copy application
COPY . .
RUN composer dump-autoload --optimize && \
php artisan config:clear
# Set permissions
RUN chown -R www-data:www-data storage bootstrap/cache
# Nginx configuration
COPY docker/nginx.conf /etc/nginx/http.d/default.conf
# Supervisor to run nginx + php-fpm
COPY docker/supervisord.conf /etc/supervisord.conf
# PHP production settings
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY docker/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
EXPOSE 8080
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
Supporting configuration files:
docker/nginx.conf
server {
listen 8080;
server_name _;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
docker/supervisord.conf
[supervisord]
nodaemon=true
logfile=/dev/stdout
logfile_maxbytes=0
[program:nginx]
command=nginx -g "daemon off;"
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
[program:php-fpm]
command=php-fpm --nodaemonize
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
docker/opcache.ini
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000
opcache.validate_timestamps=0
Generic PHP
For non-Laravel PHP applications, use a simpler Dockerfile:
Dockerfile (PHP + Apache)
FROM php:8.3-apache
RUN a2enmod rewrite && \
docker-php-ext-install pdo_mysql
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY composer.json composer.lock ./
RUN composer install --no-dev --prefer-dist
COPY . .
RUN sed -i 's|80|${PORT}|g' /etc/apache2/sites-available/000-default.conf && \
sed -i 's|80|${PORT}|g' /etc/apache2/ports.conf
EXPOSE 8080
CMD ["apache2-foreground"]
Elixir
Temps supports Elixir projects via Nixpacks (select the nixpacks-elixir preset manually). For Phoenix applications, a custom Dockerfile is recommended:
Dockerfile (Phoenix)
FROM elixir:1.17-alpine AS build
RUN apk add --no-cache build-base git
WORKDIR /app
ENV MIX_ENV=prod
COPY mix.exs mix.lock ./
RUN mix deps.get --only prod && mix deps.compile
COPY lib ./lib
COPY priv ./priv
COPY config ./config
RUN mix compile && mix release
FROM alpine:3.20
RUN apk add --no-cache libstdc++ openssl ncurses-libs
WORKDIR /app
COPY --from=build /app/_build/prod/rel/my_app ./
ENV PORT=4000
EXPOSE 4000
CMD ["bin/my_app", "start"]
Make sure your Phoenix endpoint reads the port from the environment:
config/runtime.exs
config :my_app, MyAppWeb.Endpoint,
url: [host: System.get_env("APP_HOST") || "localhost"],
http: [ip: {0, 0, 0, 0}, port: String.to_integer(System.get_env("PORT") || "4000")]
Health checks
After starting your container, Temps sends HTTP GET requests to verify the application is healthy before routing traffic to it.
Default behavior
- Path:
/(the root of your application) - Success: 2 consecutive responses with a 2xx or 3xx status code
- Timeout: 300 seconds (5 minutes) for the application to become healthy
- Retry interval: every 5 seconds
If your application returns 4xx or 5xx errors for 60 consecutive seconds, the deployment fails. Connection errors (application still starting up) are retried without penalty.
Custom health check path
Add a .temps.yaml file to your repository root to customize the health check:
.temps.yaml
health:
path: /health
status: 200
interval: 30
timeout: 5
retries: 3
Adding a dedicated /health endpoint that returns a simple 200 response is recommended. This avoids issues where your root path (/) requires authentication or returns a redirect.
Configuration
Override build and start commands
You can override the auto-detected commands in the Temps dashboard under your project settings, or via .temps.yaml:
.temps.yaml
build:
install_command: npm ci
build_command: npm run build
output_dir: dist
Environment variables
Set environment variables in the Temps dashboard under Project > Environment Variables. Variables can be scoped to specific environments (production, staging, etc.).
Common patterns:
# Database connection
DATABASE_URL=postgresql://user:pass@host:5432/dbname
# API keys (never commit these to Git)
STRIPE_SECRET_KEY=sk_live_...
SENDGRID_API_KEY=SG...
# Application config
NODE_ENV=production
LOG_LEVEL=info
Automatically injected variables
Temps injects these environment variables into every deployment:
| Variable | Value | Description |
|---|---|---|
PORT | Resolved port | The port your app must listen on |
HOST | 0.0.0.0 | Bind address |
SENTRY_DSN | Auto-generated | Error tracking endpoint |
TEMPS_API_URL | Your Temps URL | Platform API endpoint |
TEMPS_API_TOKEN | Deployment token | Authentication for Temps SDKs |
OTEL_EXPORTER_OTLP_ENDPOINT | Your Temps OTLP URL | OpenTelemetry trace collection |
OTEL_EXPORTER_OTLP_PROTOCOL | http/protobuf | OTLP protocol |
OTEL_SERVICE_NAME | Project name | Service identifier for traces |
You do not need to configure these manually. They are available in process.env (Node.js), os.environ (Python), os.Getenv (Go), and equivalent in other languages.
Connecting to databases
Temps can provision managed PostgreSQL, Redis, MongoDB, and S3-compatible storage as services linked to your project. When a service is linked, connection variables are injected automatically:
PostgreSQL:
POSTGRES_URL=postgresql://user:pass@host:5432/dbname
POSTGRES_HOST=host
POSTGRES_PORT=5432
POSTGRES_USER=user
POSTGRES_PASSWORD=pass
POSTGRES_DATABASE=dbname
Redis:
REDIS_URL=redis://host:6379
REDIS_HOST=host
REDIS_PORT=6379
You can also connect to external databases by setting the connection URL as an environment variable in the dashboard.
For a complete walkthrough, see Deploy with a Database.
Troubleshooting
Application exits immediately
Your application is binding to localhost or 127.0.0.1 instead of 0.0.0.0. Temps runs your application in a container — binding to localhost means it only accepts connections from inside the container.
Fix: Bind to 0.0.0.0 or use the HOST environment variable.
Health check timeout
The application takes longer than 300 seconds to start, or the health check path returns an error.
Fix:
- Add a
/healthendpoint that returns a 200 status code - Configure
health.pathin.temps.yamlto point to your health endpoint - If your application needs more startup time (e.g., database migrations), consider running migrations as a separate step
Build fails with "command not found"
The auto-detected build command does not match your project setup.
Fix: Override the build command in the Temps dashboard or in .temps.yaml:
.temps.yaml
build:
build_command: yarn build
install_command: yarn install --frozen-lockfile
Port mismatch
Your application listens on a different port than what Temps expects.
Fix: Always read from the PORT environment variable. Do not hardcode ports. If your framework uses a different default (e.g., Flask uses 5000, Go uses 8080), that is fine — Temps sets PORT to the correct value and your code should use it.