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
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 --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
EXPOSEa port — Temps reads theEXPOSEdirective to know which port your application listens on. If you omit it, Temps falls back to the port configured in the dashboard (default3000).- Listen on
0.0.0.0— Containers that bind tolocalhostor127.0.0.1only 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.
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 port Temps expects your application to listen on. Matches the
EXPOSEdirective 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
- Temps waits for the container to enter
Runningstate (up to 5 minutes) - Sends HTTP
GETrequests to/on the container port - Requires 2 consecutive
2xxor3xxresponses to pass - Connection errors during startup are retried every 5 seconds without penalty
- If
4xxor5xxresponses persist for 60 seconds, the deployment fails - 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
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:
| Resource | Default | Description |
|---|---|---|
| CPU | 1 core | Maximum CPU allocation |
| Memory | 512 MB | Maximum memory allocation |
| PIDs | 512 | Maximum 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-privilegessecurity 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:
- Build — The new image is built from your latest commit
- Start — The new container starts alongside the existing one
- Health check — Temps verifies the new container is healthy
- Switch — The route table is updated to point to the new container
- Confirm — Temps waits for the proxy to confirm the route change (up to 30 seconds)
- 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
EXPOSEport - 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
EXPOSEdeclares - 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, notlocalhost - Verify the port in your code matches the
EXPOSEdirective - 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-alpineinstead ofnode:22) - Add a
.dockerignoreto excludenode_modules,.git, and test files - Clean up package manager caches (
apt-get clean,rm -rf /var/cache/apk/*) - Combine
RUNcommands to reduce layers