Back to Blog
Tutorial

How to Deploy a Docker Application to the Cloud

Daniel Brooks8 min read
How to Deploy a Docker Application to the Cloud

Docker solved the "works on my machine" problem. Your application and all its dependencies are packaged into a single, portable image that runs identically across any environment. The harder part has always been what comes next: getting that container into production without spending days configuring servers, load balancers, TLS certificates, and container orchestration.

This guide shows you how to deploy a Docker application to a live production URL in under five minutes. We cover writing a production-ready Dockerfile for Node.js, Python, and Go, then walk through every step of the deployment process — from connecting your GitHub repository to accessing your app over HTTPS.

What You'll Learn

  • How to write production-ready Dockerfiles for Node.js, Python, and Go
  • Multi-stage build patterns that keep image sizes small and deploys fast
  • How to configure and deploy a Docker application through the Out Plane console
  • How to set environment variables, configure scaling, and map a custom domain
  • Common Docker production deployment issues and how to resolve them

Prerequisites

Before starting, make sure you have:

  • A GitHub account with your application code in a repository
  • A working Dockerfile in your project root (or a monorepo with a Dockerfile in a subdirectory)
  • An Out Plane account — free to create at console.outplane.com

Out Plane uses GitHub OAuth for authentication. No CLI tool is required. Everything in this guide is done through the web console.

Writing a Production-Ready Dockerfile

A production Dockerfile is not the same as a development one. It needs to produce a small, secure image that starts quickly and handles signals cleanly. The patterns below apply regardless of which language you use.

Node.js Application

This multi-stage Dockerfile separates the build environment from the runtime image. The final image contains only the compiled output and production dependencies — no build tools, no source files.

dockerfile
# Stage 1: Install dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --only=production

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

# Stage 3: Production runtime
FROM node:20-alpine AS runner
WORKDIR /app

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 appuser

COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json ./

USER appuser

EXPOSE 8080

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

Create a .dockerignore file at your project root to prevent unnecessary files from being copied into the build context:

text
node_modules
.git
.env
.env.*
*.log
dist
coverage
.next

Python Application

For Python, the slim base image removes the build toolchain while keeping the interpreter. Gunicorn handles production traffic; Flask's built-in server is not suitable for production.

dockerfile
FROM python:3.12-slim

WORKDIR /app

# Install dependencies first to leverage layer caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN addgroup --system --gid 1001 appgroup && \
    adduser --system --uid 1001 --ingroup appgroup appuser

USER appuser

EXPOSE 8080

CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8080", "--workers", "4", "--timeout", "120"]

Your requirements.txt must include gunicorn:

text
flask==3.1.0
gunicorn==22.0.0

Go Application

Go compiles to a single static binary, which means the runtime image can be built from scratch — no operating system, no shell, nothing except the binary itself. This produces images under 15MB.

dockerfile
# Stage 1: Build
FROM golang:1.23-alpine AS builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server .

# Stage 2: Minimal runtime
FROM scratch

COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

EXPOSE 8080

CMD ["/server"]

Note: the scratch base image has no shell. If you need a shell for debugging, use alpine:latest as the runtime base instead.

Dockerfile Best Practices for Production

Regardless of the language, these practices apply to every production Dockerfile:

  • Use multi-stage builds. Separate the build environment from the runtime image. A Node.js build stage with dev dependencies can exceed 800MB; the final runtime image should be under 100MB.
  • Run as a non-root user. Container escape vulnerabilities are significantly harder to exploit when the process runs as an unprivileged user. Always create a system user and switch to it before the CMD instruction.
  • Pin specific version tags. Never use :latest. Pin to a specific version like node:20-alpine to ensure reproducible builds. Base images update without warning, and :latest can silently break your application.
  • Copy dependency files first. Copy package.json, requirements.txt, or go.mod before copying the rest of your source code. Docker caches each layer; this pattern means dependency installation is only re-run when dependencies actually change.
  • Use a .dockerignore file. Excluding node_modules, .git, and local .env files keeps build context small and prevents secrets from reaching the image.
  • Use the exec form of CMD. Write CMD ["node", "server.js"] instead of CMD node server.js. The exec form passes signals directly to your process, enabling graceful shutdown.
  • EXPOSE the correct port. The EXPOSE directive documents which port your application listens on. You will need this value during the deployment configuration step.

Step 1: Prepare Your Repository

Before deploying, verify your repository is ready:

  1. Dockerfile is in the right location. For a standard project, the Dockerfile should be at the repository root. For a monorepo, it can be in a subdirectory — you will set the root directory in the deployment configuration.
  2. .dockerignore is committed. This prevents secrets and large directories from inflating your build context.
  3. The correct port is EXPOSEd. Note the port number — you will enter it during configuration. The default on Out Plane is 8080.
  4. Your Dockerfile builds locally. Run docker build -t my-app . and docker run -p 8080:8080 my-app to verify the image works before deploying.

Push your code to GitHub. Out Plane deploys directly from your repository on every push to the configured branch.

Step 2: Connect Your Repository

  1. Go to console.outplane.com
  2. Sign in with your GitHub account
  3. Click New Application
  4. Select your repository from the list

Out Plane reads your repository through the GitHub OAuth connection you authorized during sign-in. If your repository does not appear in the list, verify it is accessible under your GitHub account or organization.

Step 3: Configure Your Application

The create application form gives you control over how your Docker application is built and run.

Build Method

Set Build Method to Dockerfile. This instructs Out Plane to locate your Dockerfile and build the image directly, rather than using Paketo Buildpacks auto-detection.

If your Dockerfile is in a subdirectory (monorepo), set the Root Directory field to the path of that subdirectory — for example, services/api. The build context will be relative to that directory.

Port

Enter the port your application listens on. This must match the EXPOSE directive in your Dockerfile. The default is 8080. If your application uses a different port — for example, a Next.js app on 3000 — enter that value here.

Branch

Select the branch Out Plane should deploy from. The default is main or master. You can change this to any branch in your repository — useful for deploying a staging environment from a staging branch.

Scaling

Configure minimum and maximum instances:

  • Min instances: The number of instances that remain running at all times. Set to 1 for continuous availability. Set to 0 to scale to zero when the application receives no traffic (cold start applies).
  • Max instances: The upper limit for auto-scaling under load. Out Plane scales up automatically when CPU or memory pressure increases.

Instance Type

Out Plane instance types range from op-20 (0.5 vCPU, 512MB RAM) to op-94 (32 vCPU, 64GB RAM). For most web applications, op-20 or op-21 (1 vCPU, 1GB RAM) is sufficient to start. You can change the instance type at any time without redeploying.

Region

The default deployment region is Nuremberg. Enterprise regions are available if you need deployment closer to specific geographic markets. Choose the region nearest to the majority of your users.

Environment Variables

Add any configuration your application reads from the environment. Click Add to enter variables one at a time, or click Raw Edit to paste multiple variables at once in KEY=VALUE format.

Common variables for Docker applications:

text
DATABASE_URL=postgres://user:password@host:5432/database
API_SECRET=your-secret-key
NODE_ENV=production

Never bake secrets into your Docker image. Environment variables set in the console are injected at runtime and are never stored in the image layer history.

Step 4: Deploy

Click Deploy Application. Out Plane queues the build and you can watch the status progress in real time:

  1. Queued — The build is waiting for an available build runner
  2. Building — Out Plane is executing your Dockerfile and building the image
  3. Deploying — The built image is being started on the selected instance type
  4. Ready — Your application is live

The build log streams in real time during the Building phase. If the build fails, the log shows the exact Dockerfile instruction that failed and the error output.

Once the status shows Ready, your application URL appears at the top of the deployment page. It follows the format your-app.outplane.app. Click it to open your application in a browser. HTTPS is configured automatically — no certificate management required.

Every push to your configured branch triggers a new deployment automatically.

Step 5: Configure a Custom Domain

The default .outplane.app URL is ready for production, but most applications need a branded domain. To map your own domain:

  1. Navigate to Domains in the sidebar
  2. Click Map Domain
  3. Enter your domain name (for example, app.yourdomain.com)

Out Plane provides the DNS records you need to add at your domain registrar:

  • Subdomain (CNAME): Point app.yourdomain.com to domains-management.outplane.app
  • Apex domain (A record): Point yourdomain.com to 49.13.46.108

Once DNS propagates — typically within a few minutes, up to 24 hours for some registrars — SSL is provisioned automatically via Let's Encrypt. No manual certificate renewal is required.

Common Issues and Solutions

Port Mismatch

Symptom: The deployment shows Ready but the application returns a connection error or 502.

Cause: The port configured in Out Plane does not match the port your application actually listens on.

Fix: Check your Dockerfile's EXPOSE directive and your application code. If your app listens on 3000 but you configured 8080 in Out Plane, update the port in your application settings and redeploy.

Build Failures from Missing Dependencies

Symptom: The build log shows a module not found or dependency installation error.

Cause: System-level build dependencies (such as gcc, libpq-dev, or python3-dev) are not available in the base image.

Fix: Add the necessary system packages before your dependency installation step. For Alpine-based images, use apk add --no-cache. For Debian-based images, use apt-get install -y --no-install-recommends.

dockerfile
# Alpine example
RUN apk add --no-cache gcc musl-dev

# Debian example
RUN apt-get update && apt-get install -y --no-install-recommends libpq-dev && rm -rf /var/lib/apt/lists/*

Permission Errors at Runtime

Symptom: The application starts but crashes with EACCES or Permission denied errors when trying to write to the filesystem.

Cause: The application was written assuming it would run as root. Files in the image are owned by root, but the process runs as an unprivileged user.

Fix: Ensure any directories your application writes to are owned by the non-root user. Set ownership during the build stage:

dockerfile
RUN mkdir -p /app/uploads && chown appuser:appgroup /app/uploads
USER appuser

For applications that genuinely need to write to the filesystem, prefer writing to /tmp (which is world-writable) or using external storage like an S3-compatible object store.

Large Image Sizes Slowing Deploys

Symptom: The Building phase takes several minutes. Logs show the image push step is slow.

Cause: The image includes development dependencies, build artifacts, or large assets that are not needed at runtime.

Fix: Audit your Dockerfile for multi-stage build opportunities. Common culprits:

  • node_modules from a development install (use npm ci --only=production in the final stage)
  • Build tools like gcc, make, or cmake that are only needed to compile dependencies
  • Test fixtures, documentation, or .git history copied into the image

A well-optimized Node.js image should be under 200MB. A Go binary in scratch should be under 20MB.

Next Steps

Your Docker application is now running in production with automatic HTTPS, per-second billing, and auto-scaling. Here's what to explore next:

  • Deploy a specific framework: See How to Deploy a Next.js Application for a framework-specific guide with SSR configuration
  • Zero-downtime deployments: Learn how to configure health checks and rolling updates in the Zero-Downtime Deployment Guide
  • Add a database: Out Plane provides managed PostgreSQL (versions 14–18). Navigate to Databases in the sidebar to provision one and connect it via a DATABASE_URL environment variable
  • Adjust scaling: Start with a small instance type and scale up based on the metrics shown in your application dashboard

Summary

Deploying a Docker application to Out Plane comes down to five steps:

  1. Write a production-ready Dockerfile with multi-stage builds and a non-root user
  2. Prepare your repository with a .dockerignore and verify the build runs locally
  3. Connect your GitHub repository at console.outplane.com
  4. Configure the build method as Dockerfile, set the correct port, and add environment variables
  5. Deploy and access your application at your-app.outplane.app with HTTPS

No server provisioning, no certificate management, no infrastructure to maintain. Push your Dockerfile and get a production URL.

Ready to deploy your Docker application? Get started with Out Plane and receive $20 in free credit. Pay only for what you use with per-second billing.


Tags

docker
deployment
containers
devops
tutorial

Start deploying in minutes

Connect your GitHub repository and deploy your first application today. $20 free credit. No credit card required.