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:

  1. Clone — Temps pulls your code from GitHub, GitLab, Bitbucket, or Gitea
  2. Detect — The build system inspects your files to determine the language and framework
  3. Build — Dependencies are installed, the project is compiled, and a container image is created
  4. Deploy — The container starts, Temps injects environment variables (including PORT), and runs health checks
  5. 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)

Framework detection

Temps inspects your repository files and auto-detects the framework. Detection happens in priority order — the first match wins.

PriorityFrameworkDetection signalDefault port
1DockerfileDockerfile in repo rootFrom EXPOSE directive
2Docusaurusdocusaurus.config.js or .ts3000
3Next.jsnext.config.js, .mjs, or .ts3000
4Vitevite.config.js or .ts80 (static, Temps)
5Create React Appreact-scripts in package.json3000
6Rsbuildrsbuild.config.ts3000
7RustCargo.toml8080
8Gogo.mod8080
9Pythonrequirements.txt, pyproject.toml, setup.py, or Pipfile8000
10Javapom.xml, build.gradle, or build.gradle.kts8080
PHPcomposer.json (via Nixpacks)8080
RubyGemfile (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 filePackage managerInstall command
pnpm-lock.yamlpnpmpnpm install --frozen-lockfile
package-lock.jsonnpmnpm install
yarn.lockYarnyarn install --frozen-lockfile
bun.lockb or bun.lockBunbun 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"]

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"]

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

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

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:

VariableValueDescription
PORTResolved portThe port your app must listen on
HOST0.0.0.0Bind address
SENTRY_DSNAuto-generatedError tracking endpoint
TEMPS_API_URLYour Temps URLPlatform API endpoint
TEMPS_API_TOKENDeployment tokenAuthentication for Temps SDKs
OTEL_EXPORTER_OTLP_ENDPOINTYour Temps OTLP URLOpenTelemetry trace collection
OTEL_EXPORTER_OTLP_PROTOCOLhttp/protobufOTLP protocol
OTEL_SERVICE_NAMEProject nameService 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 /health endpoint that returns a 200 status code
  • Configure health.path in .temps.yaml to 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.


What to explore next

Was this page helpful?