Skip to content

Container Lifecycle

Casey edited this page Jul 29, 2025 · 1 revision

Container Lifecycle Management

This guide provides detailed technical documentation about how Flowcase manages Docker containers throughout their lifecycle, from creation to destruction.

🔄 Container Lifecycle Overview

Flowcase containers go through several distinct phases during their lifetime:

  1. Resource Validation - Check system resources before creation
  2. Image Verification - Ensure container images are available
  3. Container Creation - Create and configure the Docker container
  4. Network Configuration - Set up nginx proxy routing
  5. Runtime Management - Monitor and manage running containers
  6. Cleanup and Destruction - Proper container removal

[!INFO] Implementation Status: This documentation reflects the current implementation in Flowcase. Features marked as "planned" or "future" are not yet available.

📊 Resource Management

Resource Allocation Strategy

Before creating any container, Flowcase performs resource validation to prevent system overload:

# From routes/droplet.py - actual implementation
total_allocated_memory = 0
total_allocated_cores = 0

# Check all existing instances
for instance in instances:
    instance_droplet = droplet_dict.get(instance.droplet_id)
    if instance_droplet:
        total_allocated_cores += instance_droplet.container_cores
        total_allocated_memory += instance_droplet.container_memory

# System resource limits
system_cores = os.cpu_count()
total_memory = psutil.virtual_memory().total / 1024 / 1024  # MB

# Safety margins and oversubscription policies
max_allowed_memory = total_memory * 0.85  # 85% of total memory
max_allowed_cores = system_cores * 2.0    # 2x CPU oversubscription

Resource Allocation Rules

Resource Policy Reasoning
Memory 85% of total RAM Leave 15% for system operations
CPU 2x oversubscription Containers share CPU efficiently
Disk No limit (monitored) Dynamic based on available space
Network Shared bridge Isolated container network

Guacamole vs Container Droplets

Flowcase handles two types of droplets differently:

Container Droplets (desktop environments, applications):

  • Full resource validation
  • Memory and CPU limits enforced
  • Persistent storage support
  • VNC-based streaming

Guacamole Droplets (remote connections via VNC/RDP/SSH):

  • No resource checks (lightweight proxy)
  • Minimal resource usage
  • Connection to external systems
  • Built-in Guacamole protocol handling

🐳 Container Creation Process

Container Naming Convention

All Flowcase containers use a predictable naming pattern:

flowcase_generated_{instance_id}

Where instance_id is a UUID generated for each droplet instance.

Container Creation Flow

1. Image Verification

# Check if required image exists locally
image_exists = False
for image in docker_client.images.list():
    if image_name in image.tags:
        image_exists = True
        break

if not image_exists:
    return error("Docker image not found. Image might still be downloading.")

2. Persistent Storage Setup (Optional)

# Handle persistent profiles if configured
if droplet.container_persistent_profile_path:
    profilePath = droplet.container_persistent_profile_path
    # Variable substitution
    profilePath = profilePath.replace("{user_id}", str(current_user.id))
    profilePath = profilePath.replace("{username}", current_user.username)
    profilePath = profilePath.replace("{droplet_id}", str(droplet_id))
    
    mount = docker.types.Mount(
        target="/home/flowcase-user", 
        source=profilePath, 
        type="bind", 
        consistency="[r]private"
    )

3. Container Deployment

Standard Container Droplets:

container = docker_client.containers.run(
    image=image_name,
    name=f"flowcase_generated_{instance.id}",
    environment={
        "DISPLAY": ":1", 
        "VNC_PW": current_user.auth_token, 
        "VNC_RESOLUTION": resolution
    },
    detach=True,
    network="flowcase_default_network",
    mem_limit=f"{droplet.container_memory}000000",  # MB to bytes
    cpu_shares=int(droplet.container_cores * 1024),  # CPU shares
    mounts=[mount] if mount else None,
)

Guacamole Droplets:

container = docker_client.containers.run(
    image=f"flowcaseweb/flowcase-guac:{__version__}",
    name=f"flowcase_generated_{instance.id}",
    environment={"GUAC_KEY": current_user.auth_token[:32]},
    detach=True,
    network="flowcase_default_network",
)

🌐 Network Configuration

Nginx Proxy Setup

After container creation, Flowcase automatically configures nginx routing:

1. Container IP Detection

container = docker_client.containers.get(f"flowcase_generated_{instance.id}")
ip = container.attrs['NetworkSettings']['Networks']['flowcase_default_network']['IPAddress']

2. Authentication Header Generation

authHeader = base64.b64encode(
    b'flowcase_user:' + current_user.auth_token.encode()
).decode('utf-8')

3. Nginx Configuration Generation

For Container Droplets:

location /desktop/{instance_id}/vnc/ {
    auth_request /droplet_connect;
    proxy_pass https://{container_ip}:6901/;
    proxy_set_header Authorization "Basic {authHeader}";
}

location /desktop/{instance_id}/vnc/websockify {
    auth_request /droplet_connect;
    proxy_pass https://{container_ip}:6901/websockify/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_read_timeout 86400s;
    proxy_buffering off;
}

location /desktop/{instance_id}/uploads/ {
    auth_request /droplet_connect;
    proxy_pass https://{container_ip}:4902/;
    proxy_set_header Authorization "Basic {authHeader}";
}

For Guacamole Droplets:

location /desktop/{instance_id}/vnc/ {
    auth_request /droplet_connect;
    proxy_pass http://{container_ip}:8080/;
}

location /desktop/{instance_id}/vnc/websockify {
    auth_request /droplet_connect;
    proxy_pass http://{container_ip}:8080/websockify/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
}

4. Dynamic Nginx Reload

nginx_container = docker_client.containers.get("flowcase-nginx")
result = nginx_container.exec_run("nginx -s reload")

Port Mapping Strategy

Service Container Port Purpose
VNC 6901 Desktop streaming (HTTPS)
Audio 4901 Audio streaming
File Upload 4902 File management
Guacamole 8080 Protocol gateway

🔍 Container Monitoring

Container Health Checks

Flowcase monitors containers through several mechanisms:

1. Database Instance Tracking

# DropletInstance model tracks:
- instance.id (UUID)
- instance.droplet_id (foreign key)
- instance.user_id (foreign key) 
- instance.created_at (timestamp)
- instance.updated_at (timestamp)

2. Docker Container Status

# Admin panel checks container status
try:
    container = docker_client.containers.get(f"flowcase_generated_{instance.id}")
    status = container.status
    # Container found and status available
except docker.errors.NotFound:
    # Container no longer exists
    # Instance should be cleaned up

3. Resource Usage Monitoring

# Available through docker stats
docker stats --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}"

Cleanup and Orphan Detection

Startup Cleanup

# From utils/docker.py - cleanup_containers()
containers = docker_client.containers.list(all=True)
for container in containers:
    regex = re.compile(r"flowcase_generated_([a-z0-9]+(-[a-z0-9]+)+)", re.IGNORECASE)
    if regex.match(container.name):
        container.stop()
        container.remove()

🗑️ Container Destruction

User-Initiated Destruction

When a user destroys an instance through the UI:

1. Permission Validation

if instance.user_id != current_user.id:
    return jsonify({"success": False, "error": "Unauthorized"}), 403

2. Container Removal

try:
    container = docker_client.containers.get(f"flowcase_generated_{instance.id}")
    container.remove(force=True)  # Force removal even if running
except Exception as e:
    log("ERROR", f"Error removing container: {str(e)}")

3. Nginx Configuration Cleanup

config_path = f"/flowcase/nginx/containers.d/{instance.id}.conf"
if os.path.exists(config_path):
    os.remove(config_path)

4. Database Cleanup

db.session.delete(instance)
db.session.commit()

Administrative Destruction

Administrators can force-destroy any instance:

# From routes/admin.py - api_admin_delete_instance()
# Similar process but without user ownership check

🔧 Container Image Management

Image Pulling Strategy

Flowcase automatically manages container images:

1. Background Image Pulling

# From utils/docker.py - pull_images()
def pull_images():
    droplets = Droplet.query.all()
    
    # Add Guacamole image to pull list
    droplets.append(Droplet(
        container_docker_registry="https://index.docker.io/v1/",
        container_docker_image="flowcaseweb/flowcase-guac:" + __version__
    ))
    
    for droplet in droplets:
        if droplet.container_docker_registry and "docker.io" not in droplet.container_docker_registry:
            image = f"{registry}/{droplet.container_docker_image}"
        else:
            image = droplet.container_docker_image
            
        try:
            docker_client.images.pull(image)
        except Exception as e:
            log("ERROR", f"Error pulling image {image}: {e}")

2. Registry Support

  • Docker Hub: Default registry
  • Private Registries: Custom registry URLs supported
  • Flowcase Registry: Default at https://registry.flowcase.org

3. Image Verification Before Launch

# Check if image exists before creating container
image_exists = False
for image in docker_client.images.list():
    if image_name in image.tags:
        image_exists = True
        break

📁 Persistent Storage

Storage Types

1. Ephemeral Storage (Default)

  • Container filesystem is temporary
  • Data lost when container is destroyed
  • Faster startup times
  • No configuration required

2. Persistent Profiles

# Configuration in droplet settings
container_persistent_profile_path = "/data/users/{user_id}/{droplet_id}"

# Variable substitution:
# {user_id} -> User's UUID
# {username} -> User's username  
# {droplet_id} -> Droplet's UUID

Important

Storage Default: All containers use ephemeral storage by default. Persistent storage must be explicitly configured per droplet by administrators and requires proper host directory setup.

3. Bind Mount Implementation

mount = docker.types.Mount(
    target="/home/flowcase-user",    # Inside container
    source=profilePath,              # Host system path
    type="bind",                     # Bind mount type
    consistency="[r]private"         # Read-only private consistency
)

Warning

Host Requirements: Persistent storage requires the host directory to exist and be accessible by the Docker daemon. Incorrect permissions will prevent container startup.

Storage Initialization

First-time persistent storage setup includes a "warm-up" container:

# Create temporary container to initialize storage
if not os.path.exists(profilePath + ".bashrc"):
    container = docker_client.containers.run(
        image=image_name,
        detach=True,
        mem_limit="512000000",  # 512MB for init
        cpu_shares=int(droplet.container_cores * 1024),
        mounts=[mount],
    )
    time.sleep(1)
    container.stop()
    container.remove(force=True)

Tip

Initialization Process: The warm-up container ensures proper file permissions and directory structure for persistent storage before the actual user container starts.

🚨 Error Handling

Container Creation Failures

Common failure scenarios and handling:

1. Resource Exhaustion

if projected_memory_usage > max_allowed_memory:
    log("ERROR", f"Insufficient memory for user {current_user.username}")
    return jsonify({"success": False, "error": "Insufficient memory"}), 400

2. Image Not Available

if not image_exists:
    log("WARNING", f"Docker image {droplet.container_docker_image} not found")
    return jsonify({"success": False, "error": "Docker image not found"}), 400

3. Docker Daemon Unavailable

if not utils.docker.docker_client:
    log("ERROR", "Docker client not available")
    return jsonify({"success": False, "error": "Docker service unavailable"}), 500

4. Network Configuration Failures

try:
    nginx_container.exec_run("nginx -s reload")
except Exception as e:
    log("ERROR", f"Error reloading Nginx configuration: {str(e)}")
    # Container created but networking may not work

📝 Container Logs

Logging Strategy

Flowcase implements comprehensive logging for container operations:

# Creation logging
log("INFO", f"Creating new instance for user {current_user.username} with droplet {droplet.display_name}")
log("INFO", f"Instance created for user {current_user.username} with droplet {droplet.display_name}")

# Error logging  
log("ERROR", f"Insufficient memory for user {current_user.username} to request droplet {droplet.display_name}")
log("WARNING", f"Docker image {droplet.container_docker_image} not found")
log("ERROR", f"Error removing container: {str(e)}")

Log Access

  • Database Logs: Available through Admin Panel → Logs
  • Container Logs: docker logs flowcase_generated_{instance_id}
  • System Logs: docker compose logs -f web

🔍 Troubleshooting Container Issues

Common Problems

1. Container Fails to Start

# Check container logs
docker logs flowcase_generated_{instance_id}

# Check resource availability
free -h
df -h

# Verify image exists
docker images | grep {image_name}

2. Networking Issues

# Check container network
docker inspect flowcase_generated_{instance_id} | grep NetworkMode

# Verify nginx configuration
ls -la nginx/containers.d/{instance_id}.conf

# Test nginx reload
docker exec flowcase-nginx nginx -t

3. Persistent Storage Problems

# Check mount permissions
ls -la /path/to/persistent/storage

# Verify bind mount in container
docker exec flowcase_generated_{instance_id} mount | grep flowcase-user

This container lifecycle documentation reflects the actual implementation in Flowcase and provides administrators with detailed understanding of how containers are managed throughout their lifetime.

Clone this wiki locally