Deploy Docker Containers

Add a Dockerfile to your repository and Temps builds it, health-checks the new container and switches traffic to it only once it passes, behind automatic HTTPS. You can also deploy a pre-built image from any registry. Your app has to listen on 0.0.0.0 on the port Temps routes to.


When to use a Dockerfile

Temps auto-detects most frameworks and generates a Dockerfile for you. You only need a custom Dockerfile when:

  • Your stack has no preset (e.g. Haskell or a custom C++ service). Many stacks that aren't auto-detected still build without one: see the .NET, Elixir, Rails and Laravel guides
  • You need system packages that the generated image does not include
  • You want a specific base image (Debian, Ubuntu, distroless)
  • Your build has multiple stages with non-trivial caching
  • You are migrating from another container platform and already have a Dockerfile

If a Dockerfile exists in your repository root, Temps always uses it — even if auto-detection would match a framework like Next.js or Flask. Remove or rename the file to fall back to auto-detection.


Quick start

Create a Dockerfile in your repository root:

Dockerfile

FROM node:22-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .

EXPOSE 3000
CMD ["node", "server.js"]

Push to your connected repository. Temps detects the Dockerfile, builds the image, runs health checks, and routes traffic to the new container.

The two requirements

  1. Listen on the port Temps routes to. In v0.1.0-beta.56, an EXPOSE in your image decides the port and wins over the port configured for the project; without EXPOSE, Temps uses the configured port (default 3000). From the next release, a configured port wins over EXPOSE. See Ports for the full order.
  2. Listen on 0.0.0.0 — Containers that bind to localhost or 127.0.0.1 only accept connections from inside the container. Temps routes traffic from the host, so your application must bind to all interfaces.

server.js

const port = process.env.PORT || 3000;
app.listen(port, '0.0.0.0');

Temps sets a PORT environment variable to the configured port (3000 if none is set), not to the EXPOSE value. If your image exposes a different port, make your app listen on that port, or set the project's port to match it under Project → Settings → Build & deploy → Deploy. The simplest setup is to EXPOSE and listen on the same port you configure.


Multi-stage builds

Multi-stage builds produce smaller images by separating the build toolchain from the runtime. Temps supports them natively.

# Stage 1: Build
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Production
FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

EXPOSE 3000
CMD ["node", "dist/index.js"]

Custom Dockerfile path

If your Dockerfile is not in the repository root, specify its location in .temps.yaml:

.temps.yaml

build:
  dockerfile: docker/Dockerfile.production
  context: .
  • Name
    dockerfile
    Type
    string
    Description

    Path to the Dockerfile relative to the repository root. Defaults to Dockerfile.

  • Name
    context
    Type
    string
    Description

    Build context directory relative to the repository root. Defaults to the project directory.

The build context controls which files are available during COPY and ADD instructions. Setting context: . when your Dockerfile is in a subdirectory ensures the build can access files from the repository root.


Build arguments

Pass build-time variables to your Dockerfile via .temps.yaml:

.temps.yaml

build:
  args:
    NODE_ENV: production
    API_VERSION: v2
    ENABLE_FEATURE_X: "true"

Use them in your Dockerfile with ARG:

Dockerfile

FROM node:22-alpine

ARG NODE_ENV=development
ARG API_VERSION=v1

ENV NODE_ENV=$NODE_ENV

WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build

EXPOSE 3000
CMD ["node", "dist/index.js"]

Environment variables configured in the Temps dashboard are also passed as build arguments automatically. User-provided args take precedence over any args generated by presets.

Build arguments are visible in the image history. Do not use them for secrets. Use environment variables (set in the dashboard) for sensitive values — they are injected at runtime, not baked into the image.


.dockerignore

A .dockerignore file reduces build context size and prevents sensitive files from being included in the image:

.dockerignore

node_modules
.git
.env
.env.*
dist
build
*.md
.DS_Store
.vscode
.idea

Smaller build contexts mean faster builds. Always exclude node_modules, .git, and any build output that gets regenerated during the build.


Environment variables

Temps injects environment variables into your container at runtime. Variables configured in the dashboard are available via process.env (Node.js), os.environ (Python), os.Getenv (Go), etc.

Auto-injected variables

These variables are always available — you do not need to configure them:

  • Name
    PORT
    Type
    string
    Description

    The configured port (3000 unless you change it). See Ports for how it relates to EXPOSE.

  • Name
    HOST
    Type
    string
    Description

    Always 0.0.0.0.

  • Name
    SENTRY_DSN
    Type
    string
    Description

    Sentry DSN for error tracking, if configured on your project.

  • Name
    OTEL_EXPORTER_OTLP_ENDPOINT
    Type
    string
    Description

    OpenTelemetry collector endpoint for traces and metrics.

  • Name
    OTEL_SERVICE_NAME
    Type
    string
    Description

    Set to your project name. Used by OpenTelemetry SDKs automatically.


Health checks

After starting your container, Temps verifies it is healthy before routing traffic to it.

After your container starts, Temps sends HTTP GET requests to verify it is healthy before routing traffic to it.

  • Path: / (the root of your application) by default
  • Success: 2 consecutive responses with a 2xx, 3xx, 404 or 405 status code
  • Timeout: 300 seconds (5 minutes) for the app to become healthy, fixed in v0.1.0-beta.56. If the app answers with error responses (other 4xx, or 5xx) for 60 seconds in a row, the deployment fails sooner. From the next release the timeout is configurable from 30 to 3600 seconds under Project → Settings → Build & deploy
  • Retry interval: 2 seconds between successful checks, 5 seconds after an error response or a failed connection

Connection errors and other 4xx or 5xx responses are retried until the timeout; if the app hasn't passed by then, the deployment fails. The check runs once, at deploy time. To check a different path, add a .temps.yaml to your repository root:

.temps.yaml
health:
  path: /health

Only path is used. Other health fields (status, interval, timeout, retries) are accepted but currently have no effect.

Add a dedicated /health endpoint that returns a simple 200. This avoids issues where / requires authentication or returns a redirect.

Keep the health check fast. If your app takes a while to start, add a dedicated endpoint that answers as soon as the server is listening and point health.path at it.

A minimal health endpoint:

app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok' });
});

Pre-built images

You can deploy images that were built elsewhere — from Docker Hub, GitHub Container Registry, or any private registry.

Configure a project to use an external image instead of building from source. Temps pulls the image, runs health checks, and deploys it identically to a source-built container.

Your Docker daemon credentials (~/.docker/config.json on the Temps server) are used for authentication. For private registries, ensure the Temps server has docker login configured for your registry.

# Supported image reference formats
nginx:latest
ghcr.io/your-org/your-app:v1.2.3
registry.example.com:5000/app:latest

Pre-built image deployments skip the build step entirely. Temps pulls the image, inspects it for EXPOSE directives, and deploys it directly.


Resource limits

Every container runs with default resource limits:

ResourceDefaultDescription
CPUUncappedSet a limit in cores per environment
MemoryUncappedSet a limit in MB per environment (0 means uncapped)
PIDs512Maximum number of processes

CPU and memory are uncapped until you set a limit for the environment in the dashboard. Only limits are applied; requests are recorded but not enforced. See Scaling.

Security hardening

Temps applies security best practices to every container automatically:

  • All Linux capabilities dropped — containers run with the minimum privilege set
  • No privilege escalation — the no-new-privileges security option is enforced
  • PID limit — prevents fork bombs with a 512-process cap
  • Init process — proper signal handling and zombie process reaping via --init
  • Log rotation — container logs are capped at 150 MB (3 files x 50 MB) to prevent disk exhaustion

Zero-downtime deployments

Temps deploys your new container before removing the old one. This applies to single-container deploys; a Docker Compose stack is stopped and restarted on each redeploy.

  1. Build — The new image is built from your latest commit
  2. Start — The new container starts alongside the existing one
  3. Health check — Temps verifies the new container is healthy
  4. Switch — The route table is updated to point to the new container
  5. Confirm — Temps waits for the proxy to confirm the route change (up to 60 seconds)
  6. Teardown — Only after confirmation, the old container is stopped and removed

If the route confirmation times out, Temps reverts the route table to the previous deployment. The old container keeps running. The failed deployment is marked as failed — nothing breaks. There is no automatic rollback after a deployment succeeds; to go back, redeploy a previous deployment as described in Rollbacks.


Networking

Containers run on an isolated Docker bridge network (temps-app-network). Each container:

  • Gets a random host port mapped to its container port
  • Is accessible from the Temps proxy via 127.0.0.1:{host_port}
  • Can communicate with other containers on the same network by container name
  • Is not directly accessible from the internet — all external traffic goes through the Temps proxy

If your application needs to connect to a database or Redis service managed by Temps, use the service name as the hostname. Temps DNS resolution handles the rest within the Docker network.


Build caching

Temps uses Docker BuildKit for all image builds. BuildKit provides automatic layer caching — unchanged layers are reused from the cache, and you will see [CACHED] in the build logs for skipped steps.

To maximize cache hits:

Good: dependencies cached separately

FROM node:22-alpine
WORKDIR /app

# Copy dependency files first
COPY package.json package-lock.json ./
RUN npm ci

# Copy application code (changes more often)
COPY . .
RUN npm run build

EXPOSE 3000
CMD ["node", "dist/index.js"]

Bad: cache busted on every change

FROM node:22-alpine
WORKDIR /app

# Copying everything first means any file change
# invalidates the npm ci cache
COPY . .
RUN npm ci
RUN npm run build

EXPOSE 3000
CMD ["node", "dist/index.js"]

The key principle: copy files that change less often (lockfiles, dependency manifests) before files that change frequently (source code).


Troubleshooting

Build fails with "no such file or directory"

Your COPY or ADD instruction references a file outside the build context. Check your .temps.yaml build.context setting and make sure the file exists relative to the context directory.

Container exits immediately

Check the deployment logs for the exit code and error output. Common causes:

  • Missing environment variable that your application requires at startup
  • Port mismatch — your app listens on a different port than the one Temps routes to (see Ports)
  • Missing runtime dependency — the production image is missing a library or binary the app needs

Health check times out

Your container starts but Temps cannot reach it on the expected port:

  • Verify your application binds to 0.0.0.0, not localhost
  • Verify the port in your code matches the port Temps routes to (the EXPOSE port on v0.1.0-beta.56)
  • Don't run migrations or other slow work before the server starts listening; the app has 5 minutes to become healthy, and a server that answers with errors for 60 seconds in a row fails the deploy sooner (from the next release the limit is a configurable Startup timeout (seconds) under Project → Settings → Build & deploy)
  • Add a dedicated health endpoint that responds quickly, even while the app is still warming up, and set it as health.path in .temps.yaml

Image too large

Use multi-stage builds to separate build dependencies from the runtime image. A Node.js application that is 1.2 GB in a single stage typically shrinks to 150-200 MB with a multi-stage build.

Other tips:

  • Use Alpine-based images (node:22-alpine instead of node:22)
  • Add a .dockerignore to exclude node_modules, .git, and test files
  • Clean up package manager caches (apt-get clean, rm -rf /var/cache/apk/*)
  • Combine RUN commands to reduce layers

What to explore next

Deploy without a Dockerfile Configure environment variables Set up a custom domain Add error tracking

Last updated

Was this page helpful?