AWS
intermediate
60-90 minutes

Lightsail: Docker Deployment

Deploy containerized applications to AWS Lightsail. Use Docker for consistent, reproducible deployments with easy rollbacks.

What you'll accomplish

Your Dockerized application running on Lightsail with automatic restarts and SSL

Prerequisites

  • AWS Lightsail instance running (previous lesson)
  • SSH access configured
  • Docker installed locally
  • Basic Docker knowledge

Why Docker on Lightsail?

Docker provides several advantages over traditional deployment:

TraditionalDocker
Install dependencies on serverDependencies bundled in image
"Works on my machine" issuesIdentical everywhere
Manual rollbacksSwitch to previous image tag
Complex multi-app setupIsolated containers
Environment drift over timeImmutable infrastructure
Note

Docker adds a learning curve but pays off quickly. Once set up, deployments become simpler and more reliable.

Step 1: Prepare Your Lightsail Instance

1.1 Connect to Your Server

bash
ssh ubuntu@YOUR_STATIC_IP

1.2 Install Docker

If you created a plain Ubuntu instance, install Docker:

bash
# Update system
sudo apt update && sudo apt upgrade -y

# Install Docker
curl -fsSL https://get.docker.com | sudo sh

# Add your user to docker group (no sudo needed for docker commands)
sudo usermod -aG docker ubuntu

# Log out and back in for group change to take effect
exit

Reconnect and verify:

bash
ssh ubuntu@YOUR_STATIC_IP

# Verify Docker works
docker --version
docker run hello-world

1.3 Install Docker Compose

bash
# Install Docker Compose plugin
sudo apt install -y docker-compose-plugin

# Verify
docker compose version
Note

Lightsail also offers a "Docker on Ubuntu" blueprint that comes with Docker pre-installed. Select it when creating your instance to skip manual installation.

Step 2: Create a Dockerfile

On your local machine, create a Dockerfile for your application.

Node.js Example

dockerfile
# Dockerfile
FROM node:20-alpine

# Set working directory
WORKDIR /app

# Copy package files first (for better caching)
COPY package*.json ./

# Install production dependencies
RUN npm ci --only=production

# Copy application code
COPY . .

# Build if needed (for Next.js, etc.)
RUN npm run build

# Expose the port your app uses
EXPOSE 3000

# Set environment
ENV NODE_ENV=production

# Start the application
CMD ["npm", "start"]

Python/Flask Example

dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

Create .dockerignore

text
# .dockerignore
node_modules
.git
.gitignore
.env.local
.env.development
*.log
.DS_Store
Dockerfile
docker-compose*.yml
README.md
.next
dist
coverage

Step 3: Build and Test Locally

Before deploying, verify your container works locally:

Step 4: Push to a Container Registry

You need somewhere to store your Docker image. Options:

RegistryProsCons
Docker HubFree tier, simplePublic by default on free
AWS ECRPrivate, AWS integrationRequires AWS CLI setup
GitHub Container RegistryFree for public reposTied to GitHub

Option A: Docker Hub (Simplest)

Option B: AWS ECR (Private)

bash
# Create ECR repository (run once)
aws ecr create-repository --repository-name myapp --region us-east-1

# Get login command
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin YOUR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com

# Tag for ECR
docker tag myapp YOUR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/myapp:latest

# Push
docker push YOUR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
Note

Find your AWS Account ID in the AWS Console (top right dropdown) or run aws sts get-caller-identity.

Step 5: Deploy to Lightsail

5.1 Pull and Run (Simple Method)

SSH into your server and pull the image:

bash
ssh ubuntu@YOUR_STATIC_IP

# Pull the image
docker pull yourusername/myapp:latest

# Run the container
docker run -d \
  --name myapp \
  --restart unless-stopped \
  -p 3000:3000 \
  -e NODE_ENV=production \
  -e DATABASE_URL="your-database-url" \
  yourusername/myapp:latest

# Verify it's running
docker ps
docker logs myapp

5.2 Using Docker Compose (Recommended)

Create a docker-compose.yml on the server:

bash
ssh ubuntu@YOUR_STATIC_IP

# Create app directory
mkdir -p ~/myapp
cd ~/myapp

# Create docker-compose.yml
nano docker-compose.yml

Add this configuration:

yaml
version: '3.8'

services:
  app:
    image: yourusername/myapp:latest
    container_name: myapp
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
    env_file:
      - .env
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  # Optional: Add a database
  # postgres:
  #   image: postgres:15-alpine
  #   container_name: myapp-db
  #   restart: unless-stopped
  #   volumes:
  #     - postgres_data:/var/lib/postgresql/data
  #   environment:
  #     - POSTGRES_USER=myapp
  #     - POSTGRES_PASSWORD=secretpassword
  #     - POSTGRES_DB=myapp
  #   ports:
  #     - "5432:5432"

# volumes:
#   postgres_data:

Create the .env file:

bash
nano .env
env
NODE_ENV=production
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
API_KEY=your-secret-key

Start the application:

bash
# Start in detached mode
docker compose up -d

# View logs
docker compose logs -f

# Check status
docker compose ps

Step 6: Set Up Nginx Reverse Proxy

6.1 Install Nginx

bash
sudo apt install -y nginx
sudo systemctl enable nginx

6.2 Configure Nginx

bash
sudo nano /etc/nginx/sites-available/myapp
nginx
server {
    listen 80;
    server_name YOUR_DOMAIN_OR_IP;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
        proxy_read_timeout 86400;
    }
}

Enable the site:

bash
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginx

6.3 Alternative: Nginx in Docker

You can also run Nginx as a Docker container:

yaml
# docker-compose.yml with Nginx
version: '3.8'

services:
  app:
    image: yourusername/myapp:latest
    container_name: myapp
    restart: unless-stopped
    expose:
      - "3000"
    environment:
      - NODE_ENV=production
    env_file:
      - .env

  nginx:
    image: nginx:alpine
    container_name: nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certbot/conf:/etc/letsencrypt:ro
      - ./certbot/www:/var/www/certbot:ro
    depends_on:
      - app

Step 7: Add SSL with Let's Encrypt

Note

SSL requires a domain name. If you're using just an IP address, skip this step.

Using Certbot (Nginx installed on host)

bash
# Install Certbot
sudo apt install -y certbot python3-certbot-nginx

# Get certificate
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

# Test auto-renewal
sudo certbot renew --dry-run

Using Docker (Nginx in container)

Create a script to manage certificates:

bash
# init-letsencrypt.sh
#!/bin/bash

domains=(yourdomain.com www.yourdomain.com)
email="[email protected]"

# Create directories
mkdir -p ./certbot/conf ./certbot/www

# Get initial certificate
docker run --rm \
  -v $(pwd)/certbot/conf:/etc/letsencrypt \
  -v $(pwd)/certbot/www:/var/www/certbot \
  certbot/certbot certonly \
  --webroot \
  --webroot-path=/var/www/certbot \
  --email $email \
  --agree-tos \
  --no-eff-email \
  -d ${domains[0]} -d ${domains[1]}

Step 8: Create Deployment Script

Automate your deployment process.

On Your Local Machine

Step 9: Useful Docker Commands

Managing Containers

bash
# View running containers
docker ps

# View all containers (including stopped)
docker ps -a

# Stop a container
docker stop myapp

# Start a container
docker start myapp

# Restart a container
docker restart myapp

# Remove a container
docker rm myapp

# View container logs
docker logs myapp
docker logs -f myapp           # Follow logs
docker logs --tail 100 myapp   # Last 100 lines

Docker Compose Commands

bash
# Start services
docker compose up -d

# Stop services
docker compose down

# View logs
docker compose logs -f

# Restart a specific service
docker compose restart app

# Pull latest images
docker compose pull

# Rebuild and restart
docker compose up -d --build

Cleanup

bash
# Remove unused images
docker image prune -f

# Remove all unused resources
docker system prune -f

# Remove everything (including volumes - CAREFUL!)
docker system prune -a --volumes

Step 10: Zero-Downtime Deployments

For production, use rolling updates:

yaml
# docker-compose.yml with healthcheck
version: '3.8'

services:
  app:
    image: yourusername/myapp:latest
    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 10s
        failure_action: rollback
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 30s

Deploy with:

bash
docker compose up -d --scale app=2

Rollbacks

One of Docker's best features - easy rollbacks:

bash
# Deploy a specific version
docker compose down
# Edit docker-compose.yml to use yourusername/myapp:v1.0.0
docker compose up -d

# Or pull and run specific tag
docker pull yourusername/myapp:v1.0.0
docker stop myapp
docker rm myapp
docker run -d --name myapp ... yourusername/myapp:v1.0.0

Monitoring

Basic Monitoring

bash
# Container resource usage
docker stats

# Container processes
docker top myapp

# Container details
docker inspect myapp

Log Management

For production, consider using a log driver:

yaml
# docker-compose.yml
services:
  app:
    image: yourusername/myapp:latest
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Troubleshooting

Container Won't Start

bash
# Check logs
docker logs myapp

# Run interactively to debug
docker run -it yourusername/myapp:latest /bin/sh

Port Already in Use

bash
# Find what's using the port
sudo lsof -i :3000

# Kill the process or change your port

Out of Disk Space

bash
# Check disk usage
df -h

# Clean up Docker
docker system prune -a

Image Pull Fails

bash
# Re-login to registry
docker login

# For ECR
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin YOUR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com

Cost Summary

ComponentCost
Lightsail Instance ($5 plan)$5/month
Docker Hub (free tier)$0
ECR (first 500MB free)~$0-1/month
SSL CertificateFree (Let's Encrypt)
Total~$5-6/month

What's Next?

You now have a containerized deployment on Lightsail. For larger applications or when you need auto-scaling, consider:

  • ECS Fargate - AWS-managed container orchestration
  • App Runner - Even simpler container deployment
  • Kubernetes (EKS) - For complex microservices
Note

Checkpoint: Visit http://YOUR_STATIC_IP or https://yourdomain.com to verify your Dockerized application is running.