Learn how to containerize Deno applications with Docker using multi-stage builds, security hardening, and production-ready configuration. This tutorial covers creating optimized Dockerfiles, implementing proper caching strategies, and deploying with Docker Compose for scalable web applications.
Prerequisites
- Root or sudo access
- At least 2GB RAM
- 10GB free disk space
- Internet connection for downloading Docker and Deno
What this solves
Containerizing Deno applications provides consistent deployment environments across development, staging, and production systems. Docker containers eliminate the "it works on my machine" problem by packaging your Deno runtime, dependencies, and application code into portable, isolated units that run identically everywhere.
This approach is essential for teams deploying modern web applications that need predictable scaling, automated deployments, and infrastructure-as-code practices. You'll learn to create production-optimized containers with proper security boundaries and resource management.
Step-by-step installation
Update system packages and install Docker
Start by updating your package manager and installing Docker Engine with the official repository for the latest stable version.
sudo apt update && sudo apt upgrade -y
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Configure Docker service and user permissions
Enable Docker to start automatically on boot and add your user to the docker group to run commands without sudo.
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker
docker --version
Install Deno for local development
Install Deno on your host system for development and testing before containerization.
curl -fsSL https://deno.land/install.sh | sh
echo 'export DENO_INSTALL="$HOME/.deno"' >> ~/.bashrc
echo 'export PATH="$DENO_INSTALL/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
deno --version
Create a sample Deno web application
Create a directory structure for your Deno application with a simple HTTP server that we'll containerize.
mkdir -p ~/deno-docker-app/src
cd ~/deno-docker-app
import { serve } from "https://deno.land/std@0.208.0/http/server.ts";
const port = parseInt(Deno.env.get("PORT") || "8000");
const handler = (request: Request): Response => {
const url = new URL(request.url);
if (url.pathname === "/health") {
return new Response(JSON.stringify({ status: "healthy", timestamp: Date.now() }), {
headers: { "content-type": "application/json" },
});
}
if (url.pathname === "/api/info") {
return new Response(JSON.stringify({
message: "Deno Docker Application",
version: "1.0.0",
deno: Deno.version.deno,
env: Deno.env.get("ENVIRONMENT") || "development"
}), {
headers: { "content-type": "application/json" },
});
}
return new Response("Welcome to Deno Docker App!", {
headers: { "content-type": "text/plain" },
});
};
console.log(HTTP server running on port ${port});
await serve(handler, { port });
Create production-optimized Dockerfile with multi-stage build
Build a multi-stage Dockerfile that creates a minimal production image with security hardening and dependency caching.
# Build stage
FROM denoland/deno:1.38.3 AS builder
Set working directory
WORKDIR /app
Copy dependency files first for better caching
COPY src/deps.ts ./src/
Cache dependencies
RUN deno cache src/deps.ts
Copy source code
COPY src/ ./src/
Cache and compile the application
RUN deno cache src/main.ts
RUN deno compile --allow-net --allow-env --output=server src/main.ts
Production stage
FROM debian:12-slim AS production
Install ca-certificates for HTTPS requests
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
Create non-root user for security
RUN groupadd -r denoapp && useradd -r -g denoapp -s /bin/false denoapp
Create app directory with correct permissions
RUN mkdir -p /app && chown denoapp:denoapp /app
Copy compiled binary from builder stage
COPY --from=builder --chown=denoapp:denoapp /app/server /app/server
Switch to non-root user
USER denoapp
Set working directory
WORKDIR /app
Expose port
EXPOSE 8000
Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
Run the server
CMD ["./server"]
Create dependency management file
Create a dedicated deps.ts file to centralize and cache external dependencies efficiently.
// HTTP server dependencies
export { serve } from "https://deno.land/std@0.208.0/http/server.ts";
// Additional utilities
export { parse } from "https://deno.land/std@0.208.0/flags/mod.ts";
Update main.ts to use deps.ts
Modify the main application file to import from the centralized dependencies file.
import { serve } from "./deps.ts";
const port = parseInt(Deno.env.get("PORT") || "8000");
const handler = (request: Request): Response => {
const url = new URL(request.url);
if (url.pathname === "/health") {
return new Response(JSON.stringify({ status: "healthy", timestamp: Date.now() }), {
headers: { "content-type": "application/json" },
});
}
if (url.pathname === "/api/info") {
return new Response(JSON.stringify({
message: "Deno Docker Application",
version: "1.0.0",
deno: Deno.version.deno,
env: Deno.env.get("ENVIRONMENT") || "development",
uptime: performance.now()
}), {
headers: { "content-type": "application/json" },
});
}
return new Response("Welcome to Deno Docker App!", {
headers: { "content-type": "text/plain" },
});
};
console.log(HTTP server running on port ${port});
await serve(handler, { port });
Create Docker Compose configuration
Set up Docker Compose for production deployment with environment variables, health checks, and restart policies.
version: '3.8'
services:
deno-app:
build:
context: .
dockerfile: Dockerfile
target: production
container_name: deno-production-app
ports:
- "8000:8000"
environment:
- PORT=8000
- ENVIRONMENT=production
- NODE_ENV=production
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp:size=100M,noexec,nosuid,nodev
ulimits:
nproc: 65535
nofile:
soft: 65535
hard: 65535
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
- deno-network
networks:
deno-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
Create .dockerignore for build optimization
Exclude unnecessary files from the Docker build context to reduce image size and build time.
node_modules
.git
.gitignore
README.md
.env*
*.log
docker-compose.yml
.dockerignore
Dockerfile
.vscode
.idea
*.md
logs/
temp/
*.tmp
.DS_Store
Build and run the containerized application
Build the Docker image and start the application using Docker Compose with production optimizations.
docker compose build --no-cache
docker compose up -d
docker compose ps
Configure production environment variables
Create an environment file for production secrets and configuration management.
PORT=8000
ENVIRONMENT=production
LOG_LEVEL=info
TZ=UTC
Security headers
SECURE_HEADERS=true
CORS_ORIGIN=https://example.com
Resource limits
MAX_CONNECTIONS=1000
REQUEST_TIMEOUT=30000
Set up production monitoring and logging
Configure structured logging and health monitoring for production deployments.
version: '3.8'
services:
deno-app:
extends:
file: docker-compose.yml
service: deno-app
env_file:
- .env.production
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.5'
memory: 256M
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
labels:
- "traefik.enable=true"
- "traefik.http.routers.deno-app.rule=Host(example.com)"
- "traefik.http.routers.deno-app.tls=true"
- "traefik.http.services.deno-app.loadbalancer.server.port=8000"
prometheus:
image: prom/prometheus:latest
container_name: deno-prometheus
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
networks:
- deno-network
networks:
deno-network:
external: true
Verify your setup
Test that your containerized Deno application is running correctly and responding to requests.
docker compose logs deno-app
curl -i http://localhost:8000
curl -i http://localhost:8000/health
curl -i http://localhost:8000/api/info
docker compose exec deno-app ps aux
Check resource usage and container security:
docker stats deno-production-app
docker inspect deno-production-app | grep -A 5 SecurityOpt
docker compose top
Production optimization and security hardening
Implement resource limits and monitoring
Add CPU and memory limits to prevent resource exhaustion and configure monitoring endpoints.
let requestCount = 0;
let errorCount = 0;
const startTime = Date.now();
export function incrementRequestCount() {
requestCount++;
}
export function incrementErrorCount() {
errorCount++;
}
export function getMetrics() {
return {
requests_total: requestCount,
errors_total: errorCount,
uptime_seconds: Math.floor((Date.now() - startTime) / 1000),
memory_usage: Deno.memoryUsage(),
};
}
Add security headers and rate limiting
Enhance the application with security headers and basic rate limiting for production use.
const rateLimitMap = new Map();
export function getSecurityHeaders(): Record {
return {
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"X-XSS-Protection": "1; mode=block",
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
"Referrer-Policy": "strict-origin-when-cross-origin",
"Content-Security-Policy": "default-src 'self'",
};
}
export function rateLimit(clientIP: string, limit = 100, windowMs = 60000): boolean {
const now = Date.now();
const client = rateLimitMap.get(clientIP);
if (!client || now > client.resetTime) {
rateLimitMap.set(clientIP, { count: 1, resetTime: now + windowMs });
return true;
}
if (client.count >= limit) {
return false;
}
client.count++;
return true;
}
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Permission denied errors | Container running as root or wrong file ownership | Use non-root USER in Dockerfile and chown denoapp:denoapp for app files |
| Slow container startup | Dependencies downloaded at runtime | Cache dependencies in build stage with deno cache |
| Large image size | Using full Deno image in production | Use multi-stage build with minimal base image like debian:slim |
| Health check failures | Missing curl in production image | Install ca-certificates and add curl to Dockerfile or use Deno's fetch |
| Out of memory errors | No resource limits set | Add memory limits in docker-compose.yml deploy section |
| Cannot connect to external APIs | Missing CA certificates | Install ca-certificates package in production stage |
Next steps
- Install and configure Deno for web development with systemd and reverse proxy
- Setup nginx reverse proxy with SSL certificates and security hardening
- Monitor Docker containers with Prometheus and Grafana using cAdvisor for comprehensive metrics collection
- Configure Deno application clustering with load balancing for high availability
- Implement Deno JWT authentication with OAuth2 integration for secure APIs
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Production Deno Docker Deployment Script
# Supports Ubuntu/Debian and RHEL-based distributions
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Configuration
APP_NAME="${1:-deno-docker-app}"
APP_PORT="${2:-8000}"
APP_ENV="${3:-production}"
# Usage function
usage() {
echo "Usage: $0 [app-name] [port] [environment]"
echo " app-name: Application name (default: deno-docker-app)"
echo " port: Application port (default: 8000)"
echo " environment: Environment (default: production)"
exit 1
}
# Logging functions
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Cleanup function for rollback
cleanup() {
log_error "Installation failed. Performing cleanup..."
systemctl stop docker 2>/dev/null || true
rm -rf /home/$SUDO_USER/$APP_NAME 2>/dev/null || true
}
trap cleanup ERR
# Check prerequisites
check_prerequisites() {
if [[ $EUID -eq 0 ]]; then
log_error "Do not run as root. Use a user with sudo privileges."
exit 1
fi
if ! command -v sudo &> /dev/null; then
log_error "sudo is required but not installed."
exit 1
fi
if ! sudo -n true 2>/dev/null; then
log_error "User must have passwordless sudo or be in sudoers."
exit 1
fi
}
# Auto-detect distribution
detect_distro() {
if [ ! -f /etc/os-release ]; then
log_error "Cannot detect distribution. /etc/os-release not found."
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
PKG_UPGRADE="apt upgrade -y"
DOCKER_REPO_SETUP="setup_docker_apt"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf check-update || true"
PKG_INSTALL="dnf install -y"
PKG_UPGRADE="dnf update -y"
DOCKER_REPO_SETUP="setup_docker_dnf"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum check-update || true"
PKG_INSTALL="yum install -y"
PKG_UPGRADE="yum update -y"
DOCKER_REPO_SETUP="setup_docker_yum"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
log_info "Detected distribution: $PRETTY_NAME"
}
# Docker repository setup functions
setup_docker_apt() {
sudo $PKG_INSTALL curl gnupg lsb-release
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/$ID/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod 644 /etc/apt/keyrings/docker.gpg
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$ID $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo chmod 644 /etc/apt/sources.list.d/docker.list
sudo $PKG_UPDATE
}
setup_docker_dnf() {
sudo $PKG_INSTALL curl
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
}
setup_docker_yum() {
sudo $PKG_INSTALL curl
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
}
# Main installation steps
step1_update_system() {
log_info "[1/7] Updating system packages..."
sudo $PKG_UPDATE
sudo $PKG_UPGRADE
log_success "System packages updated"
}
step2_install_docker() {
log_info "[2/7] Installing Docker..."
$DOCKER_REPO_SETUP
sudo $PKG_INSTALL docker-ce docker-ce-cli containerd.io docker-compose-plugin
log_success "Docker installed"
}
step3_configure_docker() {
log_info "[3/7] Configuring Docker service..."
sudo systemctl enable --now docker
sudo usermod -aG docker $USER
# Configure Docker daemon for production
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json > /dev/null <<EOF
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2"
}
EOF
sudo chmod 644 /etc/docker/daemon.json
sudo systemctl restart docker
log_success "Docker configured"
}
step4_install_deno() {
log_info "[4/7] Installing Deno..."
curl -fsSL https://deno.land/install.sh | sh
echo 'export DENO_INSTALL="$HOME/.deno"' >> ~/.bashrc
echo 'export PATH="$DENO_INSTALL/bin:$PATH"' >> ~/.bashrc
export DENO_INSTALL="$HOME/.deno"
export PATH="$DENO_INSTALL/bin:$PATH"
log_success "Deno installed"
}
step5_create_app() {
log_info "[5/7] Creating Deno application..."
mkdir -p ~/$APP_NAME/src
cd ~/$APP_NAME
# Create deps.ts for better caching
cat > src/deps.ts <<EOF
export { serve } from "https://deno.land/std@0.208.0/http/server.ts";
EOF
# Create main application
cat > src/main.ts <<EOF
import { serve } from "./deps.ts";
const port = parseInt(Deno.env.get("PORT") || "$APP_PORT");
const handler = (request: Request): Response => {
const url = new URL(request.url);
if (url.pathname === "/health") {
return new Response(JSON.stringify({ status: "healthy", timestamp: Date.now() }), {
headers: { "content-type": "application/json" },
});
}
if (url.pathname === "/api/info") {
return new Response(JSON.stringify({
message: "Deno Docker Application",
version: "1.0.0",
deno: Deno.version.deno,
env: Deno.env.get("ENVIRONMENT") || "$APP_ENV"
}), {
headers: { "content-type": "application/json" },
});
}
return new Response("Welcome to Deno Docker App!", {
headers: { "content-type": "text/plain" },
});
};
console.log(\`HTTP server running on port \${port}\`);
await serve(handler, { port });
EOF
chmod 644 src/*.ts
log_success "Application created"
}
step6_create_dockerfile() {
log_info "[6/7] Creating production Dockerfile..."
cat > Dockerfile <<EOF
# Build stage
FROM denoland/deno:1.38.3 AS builder
WORKDIR /app
COPY src/deps.ts ./src/
RUN deno cache src/deps.ts
COPY src/ ./src/
RUN deno cache src/main.ts
RUN deno compile --allow-net --allow-env --output=server src/main.ts
# Production stage
FROM debian:12-slim AS production
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
RUN groupadd -r denouser && useradd -r -g denouser denouser
COPY --from=builder --chown=denouser:denouser /app/server /usr/local/bin/server
RUN chmod 755 /usr/local/bin/server
USER denouser
EXPOSE $APP_PORT
ENV PORT=$APP_PORT
ENV ENVIRONMENT=$APP_ENV
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:$APP_PORT/health || exit 1
CMD ["/usr/local/bin/server"]
EOF
cat > docker-compose.yml <<EOF
version: '3.8'
services:
$APP_NAME:
build: .
ports:
- "$APP_PORT:$APP_PORT"
environment:
- PORT=$APP_PORT
- ENVIRONMENT=$APP_ENV
restart: unless-stopped
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
EOF
chmod 644 Dockerfile docker-compose.yml
log_success "Docker files created"
}
step7_build_deploy() {
log_info "[7/7] Building and deploying application..."
newgrp docker <<EOF
docker build -t $APP_NAME .
docker compose up -d
EOF
log_success "Application deployed"
}
verify_installation() {
log_info "Verifying installation..."
if ! command -v docker &> /dev/null; then
log_error "Docker installation failed"
return 1
fi
if ! $DENO_INSTALL/bin/deno --version &> /dev/null; then
log_error "Deno installation failed"
return 1
fi
sleep 5
if curl -f http://localhost:$APP_PORT/health &> /dev/null; then
log_success "Application is running and healthy"
echo -e "${GREEN}Access your application at: http://localhost:$APP_PORT${NC}"
echo -e "${GREEN}Health check: http://localhost:$APP_PORT/health${NC}"
echo -e "${GREEN}API info: http://localhost:$APP_PORT/api/info${NC}"
else
log_warning "Application may still be starting. Check with: docker compose logs"
fi
}
# Main execution
main() {
log_info "Starting Deno Docker deployment for application: $APP_NAME"
check_prerequisites
detect_distro
step1_update_system
step2_install_docker
step3_configure_docker
step4_install_deno
step5_create_app
step6_create_dockerfile
step7_build_deploy
verify_installation
log_success "Deno Docker deployment completed successfully!"
log_info "Note: You may need to log out and back in for Docker group membership to take effect"
}
# Validate arguments
if [[ $# -gt 3 ]]; then
usage
fi
main "$@"
Review the script before running. Execute with: bash install.sh