Deploy Docker Containers

When auto-detection is not enough, bring your own Dockerfile. Temps builds and deploys it as a container with health checks, zero-downtime rollouts, and automatic TLS — no extra configuration required.


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 is not auto-detected (e.g. Elixir, Haskell, custom C++ service)
  • 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

Quick start

Create a Dockerfile in your repository root:

Dockerfile

FROM node:22-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --production
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. EXPOSE a port — Temps reads the EXPOSE directive to know which port your application listens on. If you omit it, Temps falls back to the port configured in the dashboard (default 3000).
  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 injects a PORT environment variable that matches the EXPOSE value. You can use it, but it is not required — as long as the port in your code matches EXPOSE, traffic will be routed correctly.


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.


.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 port Temps expects your application to listen on. Matches the EXPOSE directive in your Dockerfile.

  • 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.

Default behavior

  1. Temps waits for the container to enter Running state (up to 5 minutes)
  2. Sends HTTP GET requests to / on the container port
  3. Requires 2 consecutive 2xx or 3xx responses to pass
  4. Connection errors during startup are retried every 5 seconds without penalty
  5. If 4xx or 5xx responses persist for 60 seconds, the deployment fails
  6. If no healthy response within 5 minutes total, the deployment times out

Custom health check

Override the defaults in .temps.yaml:

.temps.yaml

health:
  path: /health
  timeout: 10
  interval: 30
  retries: 3
  • Name
    path
    Type
    string
    Description

    HTTP path to check. Defaults to /.

  • Name
    timeout
    Type
    integer
    Description

    Request timeout in seconds. Defaults to 5.

  • Name
    interval
    Type
    integer
    Description

    Seconds between checks. Defaults to 30.

  • Name
    retries
    Type
    integer
    Description

    Consecutive failures before marking unhealthy. Defaults to 3.

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

Resource limits

Every container runs with default resource limits:

ResourceDefaultDescription
CPU1 coreMaximum CPU allocation
Memory512 MBMaximum memory allocation
PIDs512Maximum number of processes

These limits prevent a single container from consuming all server resources. Configure different limits per environment in the Temps dashboard.

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:

  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 30 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.


Networking

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

  • Gets a random host port mapped to its EXPOSE 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 EXPOSE declares
  • 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 EXPOSE directive
  • If your application takes a long time to start (loading ML models, running migrations), set a custom health check with a longer timeout in .temps.yaml
  • Add a dedicated health endpoint that responds quickly, even while the app is still warming up

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

Was this page helpful?