Ansible + Docker: Automate Docker Container Deployment บน VPS

community.docker collection ให้ modules สำหรับ manage Docker containers, images, networks และ volumes โดยตรงจาก Ansible playbook — แทนที่การรัน docker run หรือ docker-compose ด้วยตนเอง ทำให้ deployment เป็น idempotent และ repeatable

บทความนี้อธิบายการ install community.docker collection, manage containers ด้วย docker_container module, จัดการ images และ networks, ใช้ Docker Compose ผ่าน docker_compose_v2 module และ pattern สำหรับ deploy multi-container applications บน VPS จริง

ติดตั้งและ Prerequisites

community.docker collection ต้องการ Docker SDK สำหรับ Python บน control node และ target host ที่มี Docker Engine ทำงานอยู่

# ติดตั้ง community.docker collection
ansible-galaxy collection install community.docker:3.10.0

# ติดตั้ง Docker SDK บน control node (Python dependency)
pip install docker

# requirements.yml
---
collections:
  - name: community.docker
    version: "3.10.0"

# ตรวจสอบ Docker ทำงานบน target host
- name: Check Docker is running
  ansible.builtin.service:
    name: docker
    state: started
    enabled: true

docker_container — จัดการ Containers

Module หลักสำหรับ manage Docker containers — สร้าง, start, stop, remove และ update container configuration ด้วย idempotency

# รัน Nginx container พื้นฐาน
- name: Start Nginx container
  community.docker.docker_container:
    name: mynginx
    image: nginx:1.25
    state: started
    restart_policy: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /etc/nginx/conf.d:/etc/nginx/conf.d:ro
      - /var/www/html:/usr/share/nginx/html:ro

# Container states ที่ใช้บ่อย:
# started  — ถ้าไม่มีให้สร้าง, ถ้าหยุดอยู่ให้ start
# present  — สร้างแต่ไม่ start
# stopped  — หยุด container ที่ทำงานอยู่
# absent   — ลบ container (หยุดก่อนถ้าทำงานอยู่)
# restarted — restart container

# ลบ container
- name: Remove old container
  community.docker.docker_container:
    name: mynginx
    state: absent
# Container พร้อม environment variables และ resource limits
- name: Deploy application container
  community.docker.docker_container:
    name: myapp
    image: "myregistry.example.com/myapp:{{ app_version }}"
    state: started
    restart_policy: unless-stopped
    env:
      DATABASE_URL: "postgresql://{{ db_user }}:{{ db_pass }}@postgres:5432/myapp"
      REDIS_URL: "redis://redis:6379/0"
      SECRET_KEY: "{{ app_secret_key }}"
      DEBUG: "false"
    ports:
      - "8000:8000"
    networks:
      - name: app_network
    memory: "512m"
    cpus: 0.5
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    log_driver: json-file
    log_options:
      max-size: "10m"
      max-file: "3"

docker_image — จัดการ Images

Pull images จาก registry, build จาก Dockerfile, tag และลบ images เก่าที่ไม่ใช้แล้ว

# Pull image จาก Docker Hub
- name: Pull latest Postgres image
  community.docker.docker_image:
    name: postgres
    tag: "16"
    source: pull

# Pull จาก private registry
- name: Pull application image
  community.docker.docker_image:
    name: "registry.example.com/myapp"
    tag: "{{ app_version }}"
    source: pull
    force_source: true  # pull แม้ tag นั้นมีอยู่แล้ว

# Login private registry ก่อน pull
- name: Login to private registry
  community.docker.docker_login:
    registry_url: registry.example.com
    username: "{{ registry_user }}"
    password: "{{ registry_password }}"

# Build image จาก Dockerfile
- name: Build custom image
  community.docker.docker_image:
    name: myapp
    tag: "{{ app_version }}"
    source: build
    build:
      path: /opt/myapp
      dockerfile: Dockerfile
      args:
        BUILD_DATE: "{{ ansible_date_time.iso8601 }}"
        VERSION: "{{ app_version }}"
    push: true

# ลบ image เก่า
- name: Remove old images
  community.docker.docker_image:
    name: myapp
    tag: "{{ old_version }}"
    state: absent

docker_network และ docker_volume

สร้าง isolated networks สำหรับ container communication และ volumes สำหรับ persistent data ก่อน deploy containers ที่ใช้งาน

# สร้าง custom bridge network
- name: Create application network
  community.docker.docker_network:
    name: app_network
    driver: bridge
    ipam_config:
      - subnet: "172.20.0.0/16"
        gateway: "172.20.0.1"

# สร้าง volumes สำหรับ persistent data
- name: Create database volume
  community.docker.docker_volume:
    name: postgres_data
    state: present

- name: Create app uploads volume
  community.docker.docker_volume:
    name: app_uploads
    state: present

# ใช้ network และ volume ใน container
- name: Start PostgreSQL with persistent storage
  community.docker.docker_container:
    name: postgres
    image: postgres:16
    state: started
    restart_policy: always
    env:
      POSTGRES_DB: myapp
      POSTGRES_USER: "{{ db_user }}"
      POSTGRES_PASSWORD: "{{ db_password }}"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - name: app_network

docker_compose_v2 — Multi-Container Deployment

docker_compose_v2 module ใช้ Docker Compose V2 (docker compose plugin) เพื่อ manage multi-container applications — อ่าน compose file และ sync state กับที่กำหนด

# deploy จาก docker-compose.yml ที่มีอยู่แล้ว
- name: Deploy application stack
  community.docker.docker_compose_v2:
    project_src: /opt/myapp
    state: present

# เทียบเท่า: docker compose up -d

# ระบุ compose file เฉพาะ
- name: Deploy with override file
  community.docker.docker_compose_v2:
    project_src: /opt/myapp
    files:
      - docker-compose.yml
      - docker-compose.prod.yml
    state: present

# Pull images ก่อน deploy
- name: Update and deploy
  community.docker.docker_compose_v2:
    project_src: /opt/myapp
    pull: always
    state: present

# หยุด stack (ไม่ลบ volumes)
- name: Stop application stack
  community.docker.docker_compose_v2:
    project_src: /opt/myapp
    state: stopped

# ลบ stack พร้อม volumes
- name: Remove application stack
  community.docker.docker_compose_v2:
    project_src: /opt/myapp
    remove_volumes: true
    state: absent

Playbook ครบวงจร: Deploy Web Stack บน VPS

ตัวอย่าง playbook ที่ deploy web application stack (Nginx + App + PostgreSQL + Redis) บน VPS ตั้งแต่ต้นจนจบ

---
# deploy_stack.yml — deploy containerized web application
- name: Deploy web application stack
  hosts: appservers
  become: true
  vars:
    app_version: "1.2.0"
    app_dir: /opt/myapp

  tasks:
    # 1. ติดตั้ง Docker
    - name: Install Docker prerequisites
      ansible.builtin.apt:
        name:
          - docker.io
          - docker-compose-plugin
          - python3-docker
        state: present
        update_cache: true

    - name: Start Docker service
      ansible.builtin.systemd:
        name: docker
        state: started
        enabled: true

    # 2. สร้าง infrastructure
    - name: Create app network
      community.docker.docker_network:
        name: app_network
        state: present

    - name: Create persistent volumes
      community.docker.docker_volume:
        name: "{{ item }}"
        state: present
      loop:
        - postgres_data
        - redis_data
        - app_uploads

    # 3. Deploy databases
    - name: Start PostgreSQL
      community.docker.docker_container:
        name: postgres
        image: postgres:16
        state: started
        restart_policy: always
        env:
          POSTGRES_DB: myapp
          POSTGRES_USER: "{{ vault_db_user }}"
          POSTGRES_PASSWORD: "{{ vault_db_password }}"
        volumes:
          - postgres_data:/var/lib/postgresql/data
        networks:
          - name: app_network

    - name: Start Redis
      community.docker.docker_container:
        name: redis
        image: redis:7-alpine
        state: started
        restart_policy: always
        volumes:
          - redis_data:/data
        networks:
          - name: app_network

    # 4. Copy application files
    - name: Create app directory
      ansible.builtin.file:
        path: "{{ app_dir }}"
        state: directory
        mode: "0755"

    - name: Upload compose file
      ansible.builtin.template:
        src: docker-compose.yml.j2
        dest: "{{ app_dir }}/docker-compose.yml"
        mode: "0644"

    # 5. Pull และ deploy application
    - name: Pull application image
      community.docker.docker_image:
        name: "registry.example.com/myapp"
        tag: "{{ app_version }}"
        source: pull
        force_source: true

    - name: Deploy application container
      community.docker.docker_container:
        name: myapp
        image: "registry.example.com/myapp:{{ app_version }}"
        state: started
        restart_policy: unless-stopped
        env:
          DATABASE_URL: "postgresql://{{ vault_db_user }}:{{ vault_db_password }}@postgres:5432/myapp"
          REDIS_URL: "redis://redis:6379/0"
          SECRET_KEY: "{{ vault_secret_key }}"
        networks:
          - name: app_network
        volumes:
          - app_uploads:/app/uploads

    # 6. Deploy Nginx reverse proxy
    - name: Copy Nginx config
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/conf.d/myapp.conf

    - name: Start Nginx container
      community.docker.docker_container:
        name: nginx
        image: nginx:1.25
        state: started
        restart_policy: always
        ports:
          - "80:80"
          - "443:443"
        volumes:
          - /etc/nginx/conf.d:/etc/nginx/conf.d:ro
          - /etc/ssl:/etc/ssl:ro
        networks:
          - name: app_network

Rolling Update Pattern

Update container โดยไม่ downtime — pull image ใหม่ แล้ว recreate container ด้วย image ล่าสุด Ansible จัดการ sequence ให้อัตโนมัติ

---
# rolling_update.yml
- name: Rolling update application
  hosts: appservers
  become: true
  serial: 1  # อัพเดตทีละ 1 host
  vars:
    app_version: "{{ new_version }}"

  tasks:
    - name: Pull new image
      community.docker.docker_image:
        name: "registry.example.com/myapp"
        tag: "{{ app_version }}"
        source: pull
        force_source: true

    - name: Stop old container gracefully
      community.docker.docker_container:
        name: myapp
        state: stopped

    - name: Start new container
      community.docker.docker_container:
        name: myapp
        image: "registry.example.com/myapp:{{ app_version }}"
        state: started
        restart_policy: unless-stopped
        # ... env, networks, volumes เหมือนเดิม

    - name: Wait for container to be healthy
      community.docker.docker_container_info:
        name: myapp
      register: container_info
      until: container_info.container.State.Health.Status == "healthy"
      retries: 10
      delay: 10

สรุป

community.docker collection ทำให้ Docker container management เป็น idempotent เหมือน infrastructure resource อื่น × — ใช้ docker_container สำหรับ lifecycle management, docker_image สำหรับ image operations, docker_network และ docker_volume สำหรับ infrastructure setup และ docker_compose_v2 สำหรับ multi-container stacks

ข้อดีหลักคือ playbook ที่รัน 2 ครั้งจะได้ผลเหมือนกัน ต่างจากการรัน docker run ด้วยตนเองที่ต้องตรวจสอบ state ก่อนทุกครั้ง — เหมาะสำหรับ automated deployment pipeline ใน production