Container Engine Guide

    Deploying Podman

    Podman is an open-source, OCI-compliant container engine that runs rootless by default — no daemon required. Deploy secure, Docker-compatible containers on RamNode's reliable VPS hosting.

    Ubuntu 24.04 LTS
    Podman 4.x
    Rootless by Default
    15–20 min setup

    Podman vs Docker

    FeaturePodmanDocker
    ArchitectureDaemonless (fork/exec)Client-server (dockerd daemon)
    Root requirementRootless by defaultRequires root or docker group
    Systemd integrationNativeRequires additional config
    Docker CLI compatibilityFull (alias docker=podman)Native
    PodsNative supportRequires Docker Compose
    Resource overheadLower (no daemon)Higher (persistent daemon)

    Why RamNode for Podman

    • KVM virtualization provides full kernel access for rootless containers
    • Starting at $4/month with generous bandwidth allocations
    • $500 annual credit for new accounts
    • Multiple datacenter locations (US, NL) for low-latency deployments

    Prerequisites

    • A RamNode KVM VPS with at least 2 GB RAM (4 GB recommended)
    • Ubuntu 24.04 LTS installed
    • SSH access with a non-root sudo user configured
    • Basic Linux command-line familiarity
    1

    Initial Server Setup

    Update the system
    sudo apt update && sudo apt upgrade -y
    sudo reboot

    Create a Dedicated User

    If logged in as root, create a non-root user for rootless Podman:

    Create user
    adduser poduser
    usermod -aG sudo poduser
    su - poduser

    Configure Firewall

    UFW setup
    sudo ufw allow OpenSSH
    sudo ufw enable
    sudo ufw status
    2

    Installing Podman

    Ubuntu 24.04 includes Podman in its default repositories:

    Install Podman and companion tools
    sudo apt install -y podman podman-compose slirp4netns uidmap fuse-overlayfs
    Verify installation
    podman --version
    podman info --format '{{.Host.Security.Rootless}}'

    The second command should return true, confirming rootless mode.

    Configure Subuid/Subgid Ranges

    Check and configure UID/GID mapping
    grep poduser /etc/subuid
    grep poduser /etc/subgid
    
    # If no entries appear, add them:
    sudo usermod --add-subuids 100000-165535 poduser
    sudo usermod --add-subgids 100000-165535 poduser

    Configure Registries

    Set default registries
    mkdir -p ~/.config/containers
    cat > ~/.config/containers/registries.conf << 'EOF'
    [registries.search]
    registries = ['docker.io', 'quay.io', 'ghcr.io']
    
    [registries.insecure]
    registries = []
    EOF

    Docker CLI Compatibility (Optional)

    Create alias
    echo 'alias docker=podman' >> ~/.bashrc
    source ~/.bashrc
    3

    Container Management Basics

    Pull and run your first container
    # Pull the latest Nginx image
    podman pull docker.io/library/nginx:latest
    
    # Run in detached mode with port mapping
    podman run -d --name webserver -p 8080:80 nginx:latest
    
    # Verify it's running
    podman ps
    curl http://localhost:8080

    Volume Management

    Persistent data with named volumes
    # Create a named volume
    podman volume create app_data
    
    # Mount the volume in a container
    podman run -d --name postgres \
      -v app_data:/var/lib/postgresql/data \
      -e POSTGRES_PASSWORD=securepassword \
      -p 5432:5432 \
      docker.io/library/postgres:16
    
    # List and inspect volumes
    podman volume ls
    podman volume inspect app_data

    💡 Tip

    Named volumes persist data independently of container lifecycle. Use bind mounts (-v /host/path:/container/path) when you need direct access to files on the host filesystem.

    4

    Networking

    Rootless Networking with slirp4netns

    In rootless mode, ports above 1024 work without special configuration. For ports below 1024 (80, 443), use a reverse proxy or allow unprivileged port binding.

    Option 1: Reverse Proxy (Recommended)

    Nginx reverse proxy setup
    sudo apt install -y nginx
    
    sudo tee /etc/nginx/sites-available/podman-proxy << 'EOF'
    server {
        listen 80;
        server_name yourdomain.com;
    
        location / {
            proxy_pass http://127.0.0.1:8080;
            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;
        }
    }
    EOF
    
    sudo ln -s /etc/nginx/sites-available/podman-proxy /etc/nginx/sites-enabled/
    sudo nginx -t && sudo systemctl reload nginx

    Option 2: Allow Unprivileged Port Binding

    Sysctl configuration
    sudo sysctl net.ipv4.ip_unprivileged_port_start=80
    echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee -a /etc/sysctl.conf

    Container-to-Container Networking

    Custom networks with DNS
    # Create a custom network
    podman network create app_network
    
    # Run containers on the same network
    podman run -d --name db --network app_network \
      -e POSTGRES_PASSWORD=secret postgres:16
    
    podman run -d --name app --network app_network \
      -e DATABASE_URL=postgresql://postgres:secret@db:5432/postgres \
      -p 8080:3000 myapp:latest

    Containers on the same Podman network can resolve each other by container name.

    5

    Working with Pods

    Pods group multiple containers sharing the same network namespace — ideal for tightly coupled services.

    Create and manage pods
    # Create a pod with port mappings
    podman pod create --name webapp \
      -p 8080:80 \
      -p 5432:5432
    
    # Add containers to the pod
    podman run -d --pod webapp --name webapp-nginx nginx:latest
    podman run -d --pod webapp --name webapp-db \
      -e POSTGRES_PASSWORD=secret postgres:16
    
    # View pod status
    podman pod ps
    podman pod inspect webapp
    
    # Stop/start entire pod
    podman pod stop webapp
    podman pod start webapp

    📌 Note

    All containers in a pod share localhost. Nginx can reach Postgres at localhost:5432 without any network configuration.

    Using Podman Compose

    Example deploying a WordPress stack:

    docker-compose.yml
    version: '3.8'
    
    services:
      db:
        image: docker.io/library/mariadb:11
        restart: always
        environment:
          MYSQL_ROOT_PASSWORD: rootpassword
          MYSQL_DATABASE: wordpress
          MYSQL_USER: wpuser
          MYSQL_PASSWORD: wppassword
        volumes:
          - db_data:/var/lib/mysql
    
      wordpress:
        image: docker.io/library/wordpress:latest
        restart: always
        ports:
          - '8080:80'
        environment:
          WORDPRESS_DB_HOST: db:3306
          WORDPRESS_DB_USER: wpuser
          WORDPRESS_DB_PASSWORD: wppassword
          WORDPRESS_DB_NAME: wordpress
        volumes:
          - wp_data:/var/www/html
        depends_on:
          - db
    
    volumes:
      db_data:
      wp_data:
    Launch with Podman Compose
    mkdir -p ~/wordpress && cd ~/wordpress
    # (save the above as docker-compose.yml)
    podman-compose up -d
    podman-compose ps
    6

    Systemd Integration

    Podman integrates natively with systemd for automatic container startup on boot — one of its strongest advantages over Docker.

    Generate and enable systemd service
    # Generate a user-level systemd service
    mkdir -p ~/.config/systemd/user
    
    podman generate systemd --name webserver --new --files
    mv container-webserver.service ~/.config/systemd/user/
    
    # Reload and enable
    systemctl --user daemon-reload
    systemctl --user enable container-webserver.service
    systemctl --user start container-webserver.service
    
    # Check status
    systemctl --user status container-webserver.service

    Enable Linger for Boot Persistence

    User-level systemd services only run when the user is logged in. Enable linger for boot persistence:

    Enable linger
    sudo loginctl enable-linger poduser

    Generate Systemd for Pods

    Pod systemd units
    podman generate systemd --name webapp --new --files
    mv pod-webapp.service container-webapp-*.service \
      ~/.config/systemd/user/
    systemctl --user daemon-reload
    systemctl --user enable pod-webapp.service

    💡 Tip

    Use the --new flag when generating systemd units. This creates fresh containers on each start rather than restarting stopped ones, ensuring clean state.

    7

    Automatic Image Updates

    Configure auto-updates
    # Run a container with the auto-update label
    podman run -d --name webserver \
      --label io.containers.autoupdate=registry \
      -p 8080:80 docker.io/library/nginx:latest
    
    # Generate systemd unit (required for auto-update)
    podman generate systemd --name webserver --new --files
    mv container-webserver.service ~/.config/systemd/user/
    systemctl --user daemon-reload
    systemctl --user enable container-webserver.service
    
    # Enable the auto-update timer
    systemctl --user enable podman-auto-update.timer
    systemctl --user start podman-auto-update.timer
    
    # Check what would be updated (dry run)
    podman auto-update --dry-run
    8

    Building Custom Images

    Podman uses Buildah under the hood and is fully compatible with standard Dockerfiles.

    Containerfile (Node.js example)
    FROM docker.io/library/node:20-slim
    
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci --only=production
    COPY . .
    
    EXPOSE 3000
    USER node
    CMD ["node", "server.js"]
    Build and run
    # Build the image
    podman build -t myapp:latest .
    
    # Run the custom image
    podman run -d --name myapp -p 3000:3000 myapp:latest

    📌 Note

    Podman supports both Containerfile and Dockerfile naming. Using Containerfile is the OCI-standard convention.

    9

    Security Hardening

    Rootless Security Best Practices

    • Always run containers as a non-root user (rootless mode is the default)
    • Use read-only root filesystems where possible: --read-only
    • Drop all capabilities and add only what's needed: --cap-drop=ALL --cap-add=NET_BIND_SERVICE
    • Set memory and CPU limits to prevent resource exhaustion
    • Use --security-opt=no-new-privileges to prevent privilege escalation
    Example: Hardened container
    podman run -d --name secure-nginx \
      --read-only \
      --cap-drop=ALL \
      --cap-add=NET_BIND_SERVICE \
      --security-opt=no-new-privileges \
      --memory=256m \
      --cpus=0.5 \
      --tmpfs /var/cache/nginx:rw,size=64m \
      --tmpfs /var/run:rw,size=1m \
      -p 8080:80 \
      nginx:latest

    Firewall Rules for Containers

    UFW rules
    sudo ufw allow 8080/tcp comment 'Podman web service'
    sudo ufw allow 443/tcp comment 'HTTPS via reverse proxy'
    sudo ufw reload
    10

    Resource Optimization

    Recommended Resource Limits by Plan

    RamNode PlanRAMMax ContainersPer-Container Limit
    2 GB VPS2 GB3–4 lightweight256–512 MB each
    4 GB VPS4 GB6–8 mixed512 MB–1 GB each
    8 GB VPS8 GB12–16512 MB–2 GB each
    16 GB+ VPS16+ GB20+As needed

    Storage Cleanup

    Reclaim disk space
    # Remove stopped containers, unused images, and build cache
    podman system prune -a --volumes
    
    # Check disk usage
    podman system df
    
    # Set up automatic cleanup via cron
    (crontab -l 2>/dev/null; echo '0 3 * * 0 podman system prune -a -f --volumes') | crontab -
    11

    Monitoring & Logging

    Real-time monitoring
    # Live resource usage for all containers
    podman stats --all
    
    # View container logs with timestamps
    podman logs -t --since 1h webserver
    
    # Follow logs in real time
    podman logs -f webserver

    Health Checks

    Container health checks
    podman run -d --name webserver \
      --health-cmd='curl -f http://localhost/ || exit 1' \
      --health-interval=30s \
      --health-retries=3 \
      --health-timeout=5s \
      -p 8080:80 nginx:latest
    
    # Check health status
    podman healthcheck run webserver
    podman inspect --format='{{.State.Health.Status}}' webserver
    12

    Backup & Recovery

    Export and save containers/images
    # Export a container filesystem
    podman export webserver > webserver-backup.tar
    
    # Commit a running container to an image
    podman commit webserver webserver-snapshot:$(date +%Y%m%d)
    
    # Save an image to a tar archive
    podman save -o myapp-backup.tar myapp:latest
    
    # Restore from archive
    podman load -i myapp-backup.tar

    Volume Backups

    Backup and restore named volumes
    # Backup a named volume
    podman run --rm \
      -v app_data:/source:ro \
      -v $(pwd):/backup \
      docker.io/library/alpine \
      tar czf /backup/app_data-$(date +%Y%m%d).tar.gz -C /source .
    
    # Restore a volume
    podman run --rm \
      -v app_data:/target \
      -v $(pwd):/backup:ro \
      docker.io/library/alpine \
      tar xzf /backup/app_data-20250210.tar.gz -C /target

    💡 Tip

    Automate volume backups with cron jobs and consider syncing backup archives to offsite storage using tools like rclone or restic.

    Troubleshooting

    IssueSolution
    CNI config validation errorsudo apt install containernetworking-plugins
    Permission denied on ports < 1024sudo sysctl net.ipv4.ip_unprivileged_port_start=80
    Subuid/subgid mapping errorssudo usermod --add-subuids 100000-165535 $USER
    Container DNS resolution failsCheck /etc/resolv.conf and ensure slirp4netns is installed
    Docker Hub rate limitspodman login docker.io or use alternative registries
    Containers not starting after rebootsudo loginctl enable-linger $USER
    Storage driver errorspodman system reset (deletes all containers/images)

    🎉 Next Steps

    • Set up TLS termination with Caddy or Nginx for HTTPS
    • Explore Podman Quadlet for declarative container management with systemd
    • Deploy a container registry on your RamNode VPS for private image hosting
    • Implement centralized logging with Loki or a self-hosted Grafana stack
    • Scale to multi-node deployments using Podman with Kubernetes YAML support