Deno and Docker
Docker is a common way to package and ship a Deno app: you build a reproducible image once and run it the same way everywhere. Deno publishes official base images, so the Dockerfile is usually short. This page covers writing that Dockerfile, keeping the image small with multi-stage builds, and the Compose, workspace, security, and health-check setups you reach for in production.
Using Deno with Docker Jump to heading
Deno provides official Docker files and images.
To use the official image, create a Dockerfile in your project directory:
FROM denoland/deno:latest
WORKDIR /app
# Copy manifests first so the dependency install layer caches across
# source-only edits
COPY deno.json deno.lock package.json* ./
RUN deno ci --prod --skip-types
# Then copy the rest of the source
COPY . .
CMD ["deno", "run", "--allow-net", "main.ts"]
deno ci performs a reproducible install from
deno.lock. --prod skips devDependencies and --skip-types drops
@types/* packages. Both shrink the resulting image without affecting runtime
behavior.
Best practices Jump to heading
Use multi-stage builds Jump to heading
For smaller production images:
# Build stage
FROM denoland/deno:latest AS builder
# Point Deno's cache at a known location so it can be copied to the next stage
ENV DENO_DIR=/deno-dir
WORKDIR /app
# Copy manifests first so the dependency install layer caches across
# source-only edits
COPY deno.json deno.lock package.json* ./
RUN deno ci --prod --skip-types
# Then copy the rest of the source
COPY . .
# Production stage
FROM denoland/deno:latest
ENV DENO_DIR=/deno-dir
WORKDIR /app
COPY --from=builder /app .
# Copy the populated Deno cache so the runtime stage has the dependencies
COPY --from=builder /deno-dir /deno-dir
CMD ["deno", "run", "--allow-net", "main.ts"]
Without copying $DENO_DIR, deno ci only writes to Deno's global cache inside
the builder stage. Those files do not travel with COPY --from=builder /app .,
so the container re-downloads dependencies on first run.
Permission flags Jump to heading
Specify required permissions explicitly:
CMD ["deno", "run", "--allow-net=api.example.com", "--allow-read=/data", "main.ts"]
Development container Jump to heading
For development with hot-reload:
FROM denoland/deno:latest
WORKDIR /app
COPY . .
CMD ["deno", "run", "--watch", "--allow-net", "main.ts"]
Common issues and solutions Jump to heading
-
Permission Denied Errors
- Use
--allow-*flags appropriately - Consider using
deno.jsonpermissions
- Use
-
Large Image Sizes
- Use multi-stage builds
- Include only necessary files
- Add proper
.dockerignore
-
Cache Invalidation
- Separate dependency caching
- Use proper layer ordering
Example .dockerignore Jump to heading
.git
.gitignore
Dockerfile
README.md
*.log
_build/
node_modules/
Available Docker tags Jump to heading
Deno provides several official tags:
denoland/deno:latest- Latest stable releasedenoland/deno:alpine- Alpine-based smaller imagedenoland/deno:distroless- Google's distroless-based imagedenoland/deno:ubuntu- Ubuntu-based imagedenoland/deno:2.x- Pin to a specific release line (use the version you target)
Environment variables Jump to heading
Common environment variables for Deno in Docker:
ENV DENO_DIR=/deno-dir/
ENV DENO_INSTALL_ROOT=/usr/local
ENV PATH=${DENO_INSTALL_ROOT}/bin:${PATH}
# Optional environment variables
ENV DENO_NO_UPDATE_CHECK=1
ENV DENO_NO_PROMPT=1
Running tests in Docker Jump to heading
FROM denoland/deno:latest
WORKDIR /app
COPY . .
# Run tests
CMD ["deno", "test", "--allow-none"]
Using Docker Compose Jump to heading
A basic Compose file for development with hot-reload:
services:
deno-app:
build: .
volumes:
- .:/app
ports:
- "8000:8000"
environment:
- DENO_ENV=development
command: ["deno", "run", "--watch", "--allow-net", "main.ts"]
For a more realistic setup with a database:
services:
app:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgres://deno:${POSTGRES_PASSWORD}@db:5432/app
depends_on:
db:
condition: service_healthy
restart: unless-stopped
command:
[
"deno",
"run",
"--allow-net=db:5432",
"--allow-env=DATABASE_URL",
"main.ts",
]
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: deno
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U deno"]
interval: 5s
timeout: 3s
retries: 5
restart: unless-stopped
volumes:
pgdata:
Put secrets like POSTGRES_PASSWORD in a .env file next to
docker-compose.yml. Compose loads it automatically. Don't commit it.
Start the services with docker compose up, or run in the background with
docker compose up -d.
Health checks Jump to heading
HEALTHCHECK --interval=30s --timeout=3s \
CMD deno eval "try { await fetch('http://localhost:8000/health'); } catch { Deno.exit(1); }"
Common development workflow Jump to heading
For local development:
- Build the image:
docker build -t my-deno-app . - Run with volume mount:
docker run -it --rm \
-v ${PWD}:/app \
-p 8000:8000 \
my-deno-app
Security considerations Jump to heading
- Run as non-root user:
# Create deno user
RUN addgroup --system deno && \
adduser --system --ingroup deno deno
# Switch to deno user
USER deno
# Continue with rest of Dockerfile
- Use minimal permissions:
CMD ["deno", "run", "--allow-net=api.example.com", "--allow-read=/app", "main.ts"]
- Consider using
--deny-*flags for additional security
Working with workspaces in Docker Jump to heading
When working with Deno workspaces (monorepos) in Docker, there are two main approaches:
1. Full workspace containerization Jump to heading
Include the entire workspace when you need all dependencies:
FROM denoland/deno:latest
WORKDIR /app
# Copy entire workspace
COPY deno.json .
COPY project-a/ ./project-a/
COPY project-b/ ./project-b/
WORKDIR /app/project-a
CMD ["deno", "run", "-A", "mod.ts"]
2. Minimal workspace containerization Jump to heading
For smaller images, include only required workspace members:
- Create a build context structure:
project-root/
├── docker/
│ └── project-a/
│ ├── Dockerfile
│ ├── .dockerignore
│ └── build-context.sh
├── deno.json
├── project-a/
└── project-b/
- Create a
.dockerignore:
*
!deno.json
!project-a/**
!project-b/** # Only if needed
- Create a build context script:
#!/bin/bash
# Create temporary build context
BUILD_DIR="./tmp-build-context"
mkdir -p $BUILD_DIR
# Copy workspace configuration
cp ../../deno.json $BUILD_DIR/
# Copy main project
cp -r ../../project-a $BUILD_DIR/
# Copy only required dependencies
if grep -q "\"@scope/project-b\"" "../../project-a/mod.ts"; then
cp -r ../../project-b $BUILD_DIR/
fi
- Create a minimal Dockerfile:
FROM denoland/deno:latest
WORKDIR /app
# Copy only necessary files
COPY deno.json .
COPY project-a/ ./project-a/
# Copy dependencies only if required
COPY project-b/ ./project-b/
WORKDIR /app/project-a
CMD ["deno", "run", "-A", "mod.ts"]
- Build the container:
cd docker/project-a
./build-context.sh
docker build -t project-a -f Dockerfile tmp-build-context
rm -rf tmp-build-context
Best practices Jump to heading
- Always include the root
deno.jsonfile - Maintain the same directory structure as development
- Document workspace dependencies clearly
- Use build scripts to manage context
- Include only required workspace members
- Update
.dockerignorewhen dependencies change