Hướng dẫn tạo một DB server databasse để Demo hoặc làm môi trường lab dạy SQL trong 2 phút

Tạo DB server

Mình có dạy cho 1 số bạn học làm ứng dụng Python, và có bài học về SQL. Tuy nhiên học viên của mình có một số bạn khá nhỏ, vẫn còn đang THCS hoặc THPT, nên khá băn khoăn về việc làm sao cho các bạn có môi trường thực hành. Nếu yêu cầu các bạn cài đặt vào máy thì cũng khá khó khăn, chưa kể máy các bạn dùng có thể không đáp ứng cấu hình. Mình muốn có 1 giải pháp để truy cập vào browser là có Workbench để chạy lệnh SQL. Và đây là kết quả như mình mong đợi:

Mình host server trên Vultr, và dĩ nhiên server này không chỉ phục vụ mỗi công việc dạy SQL, mà chạy cả tỉ tác vụ khác của mình ( :D ) nên mình sử dụng Docker compose để:

  • Chạy MySQL server

  • Chạy DBeaver để tạo Workbench

Mình sử dụng nginx để làm proxy cho ứng dụng.

Domain thì mình quản lý trên Cloudflare DNS, đã có sẵn SSL cert miễn phí. Nếu bạn quản lý ở nơi khác, chẳng hạn Amazon Route 53 thì có thể sử dụng ACM để có cert SSL, cũng miễn phí luôn.

Đây là file docker-compose mình đã sử dụng:

 version: '3.8'
services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: rootpwd
      MYSQL_DATABASE: dev
      MYSQL_USER: dev_user
      MYSQL_PASSWORD: devpwd
      MYSQL_ALLOW_PUBLIC_KEY_RETRIEVAL: "true"
    command: --default-authentication-plugin=mysql_native_password
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - app-network

  dbeaver:
    image: dbeaver/cloudbeaver:latest
    environment:
      CB_ADMIN_NAME: "cbadmin"
      CB_ADMIN_PASSWORD: "cbadminpwd1A"
    expose:
      - 8978
    volumes:
      - dbeaver_workspace:/opt/cloudbeaver/workspace
    networks:
      - app-network

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  mysql_data:
  dbeaver_workspace:

Ở đây mình đang cài đặt MySQL, tuy nhiên nếu bạn thích thì có thể sửa docker compose file để cài thêm MongoDB và PostgreSQL luôn.

file nginx.conf:

server {
    listen 80;
    server_name vultr-server.devopslearning.work;

    location / {
        proxy_pass http://dbeaver:8978;
        proxy_redirect off;
        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_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        client_max_body_size 100m;
    }

    location /api/ {
        proxy_pass http://dbeaver:8978/api/;
        proxy_redirect off;
        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_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    location /ws/ {
        proxy_pass http://dbeaver:8978/ws/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    location /static/ {
        proxy_pass http://dbeaver:8978/static/;
    }
}

Lệnh chạy:

docker-compose up -d

Sau đó bạn truy cập vào, thực hiện setup DBeaver, rồi đăng nhập DBeaver, rồi thực hiện tạo connection mới và truy vấn dữ liệu:

Ngoài hỗ trợ tạo môi trường lab cho học viên, thì giải pháp này cũng có thể được sử dụng trong các trường hợp Demo sản phẩm, nếu bạn thấy hữu ích thì có thể tham khảo!

Bonus: Dựng Coder IDE

Ngoài việc sử dụng SQL online, mình cùng muốn tạo IDE online nữa, để tránh các vấn đề khác. Do đó mình còn cài đặt thêm Coder (https://coder.com/)

Mình tạo 1 file setup như sau:

#!/bin/bash

log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1"
}

check_error() {
    if [ $? -ne 0 ]; then
        log "ERROR: $1"
        exit 1
    fi
}

log "Checking and cleaning up existing services..."
# Dừng các container hiện tại nếu có
docker compose down 2>/dev/null
# Kiểm tra và dừng process đang dùng port 8082
sudo fuser -k 8082/tcp 2>/dev/null

log "Setting up directories..."
cd ~/mysql-training
rm -rf ~/coder-docker
mkdir -p ~/coder-docker/{config,data,workspace-data}
cd ~/coder-docker
check_error "Failed to create directories"

log "Setting up permissions..."
chown -R 1000:988 config data workspace-data
chmod -R 755 config data workspace-data
check_error "Failed to set permissions"

log "Creating docker-compose.yml..."
cat << 'EOF' > docker-compose.yml
services:
  coder:
    image: ghcr.io/coder/coder:latest
    restart: unless-stopped
    user: "1000:988"
    group_add:
      - "988"
    environment:
      CODER_ACCESS_URL: https://ide.devopslearning.work
      CODER_CONFIG_DIR: /config
      CODER_DATA_DIR: /data
      CODER_ADDRESS: 0.0.0.0:3000
    ports:
      - "3000:3000"
    volumes:
      - ./config:/config
      - ./data:/data
      - /var/run/docker.sock:/var/run/docker.sock
      - ./workspace-data:/home/coder/workspace
    networks:
      - mysql-training_app-network

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "8082:80"
    volumes:
      - ../mysql-training/nginx.conf:/etc/nginx/conf.d/default.conf:ro
    networks:
      - mysql-training_app-network
    depends_on:
      - coder

networks:
  mysql-training_app-network:
    external: true
EOF
check_error "Failed to create docker-compose.yml"

# Cập nhật nginx.conf trong mysql-training nếu chưa có cấu hình cho Coder
log "Updating nginx configuration..."
cd ~/mysql-training
if ! grep -q "ide.devopslearning.work" nginx.conf; then
    cat << 'EOF' >> nginx.conf

# Coder IDE Configuration
server {
    listen 80;
    server_name ide.devopslearning.work;
    resolver 127.0.0.11 valid=5s;
    set $upstream http://coder:3000;
    location / {
        proxy_pass $upstream;
        proxy_redirect off;
        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_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        client_max_body_size 100m;
    }
}
EOF
    check_error "Failed to update nginx.conf"
    docker compose restart nginx
fi

log "Starting Coder services..."
cd ~/coder-docker
docker compose up -d
check_error "Failed to start containers"

log "Checking container status..."
docker compose ps
log "Setup completed successfully!"

log "You can now access:"
log "- Coder IDE at: https://ide.devopslearning.work"
log "To check logs: docker compose logs -f"

Sau đó:

chmod +x setup.sh
./setup.sh

Mình tạo bản ghi A trên Cloudflare trỏ đến địa chỉ IP public của server, với name là ide.devopslearning.work

Lúc này nginx.conf của mình sẽ như sau:

# DBeaver Configuration
server {
    listen 80;
    server_name vultr-server.devopslearning.work;

    location / {
        proxy_pass http://dbeaver:8978;
        proxy_redirect off;
        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_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        client_max_body_size 100m;
    }

    location /api/ {
        proxy_pass http://dbeaver:8978/api/;
        proxy_redirect off;
        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_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    location /ws/ {
        proxy_pass http://dbeaver:8978/ws/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    location /static/ {
        proxy_pass http://dbeaver:8978/static/;
    }
}

# Coder IDE Configuration
server {
    listen 80;
    server_name ide.devopslearning.work;

    resolver 127.0.0.11 valid=5s;
    set $upstream http://coder:3000;

    location / {
        proxy_pass $upstream;
        proxy_redirect off;
        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_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        client_max_body_size 100m;
    }
}

Và docker compose của coder sau khi chạy file setup.sh:

services:
  coder:
    image: ghcr.io/coder/coder:latest
    restart: unless-stopped
    user: "1000:988"  # Thay đổi UID và GID cho phù hợp
    group_add:
      - "988"  # Thêm group "docker" (GID 988) vào container
    environment:
      CODER_ACCESS_URL: https://ide.devopslearning.work
      CODER_CONFIG_DIR: /config
      CODER_DATA_DIR: /data
      CODER_ADDRESS: 0.0.0.0:3000
    ports:
      - "3000:3000"
    volumes:
      - ./config:/config
      - ./data:/data
      - /var/run/docker.sock:/var/run/docker.sock
      - ./workspace-data:/home/coder/workspace
    networks:
      - mysql-training_app-network
  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "8082:80"
    volumes:
      - ../mysql-training/nginx.conf:/etc/nginx/conf.d/default.conf:ro
    networks:
      - mysql-training_app-network
    depends_on:
      - coder
networks:
  mysql-training_app-network:
    external: true

Sau khi truy cập được vào coder, thì coder sẽ yêu cầu bạn tạo user.

Bước tiếp theo là tạo template. Mình dùng luôn template Docker của Coder, tuy nhiên có sửa lại 1 chút xíu:

build/Dockerfile

FROM ubuntu

RUN apt-get update --allow-insecure-repositories \
    && DEBIAN_FRONTEND=noninteractive apt-get install -y \
    curl \
    git \
    sudo \
    vim \
    wget \
    python3 \
    python3-pip \
    python3-dev \
    libpq-dev \
    default-libmysqlclient-dev \
    pkg-config \
    gcc \
    build-essential

RUN apt-get install -y \
    python3-flask \
    python3-flask-sqlalchemy \
    python3-psycopg2 \
    python3-mysqldb

RUN rm -rf /var/lib/apt/lists/*

ARG USER=coder
RUN useradd --groups sudo --no-create-home --shell /bin/bash ${USER} \
    && echo "${USER} ALL=(ALL) NOPASSWD:ALL" >/etc/sudoers.d/${USER} \
    && chmod 0440 /etc/sudoers.d/${USER}
USER ${USER}
WORKDIR /home/${USER}

và file terraform: main.tf

terraform {
  required_providers {
    coder = {
      source = "coder/coder"
    }
    docker = {
      source = "kreuzwerker/docker"
    }
  }
}

locals {
  username = data.coder_workspace_owner.me.name
}

variable "docker_socket" {
  default     = ""
  description = "(Optional) Docker socket URI"
  type        = string
}

provider "docker" {
  # Defaulting to null if the variable is an empty string lets us have an optional variable without having to set our own default
  host = var.docker_socket != "" ? var.docker_socket : null
}

data "coder_provisioner" "me" {}
data "coder_workspace" "me" {}
data "coder_workspace_owner" "me" {}

resource "coder_agent" "main" {
  arch           = data.coder_provisioner.me.arch
  os             = "linux"
  startup_script = <<-EOT
    set -e

    # Prepare user home with default files on first start.
    if [ ! -f ~/.init_done ]; then
      cp -rT /etc/skel ~
      touch ~/.init_done
    fi

    # Install the latest code-server.
    # Append "--version x.x.x" to install a specific version of code-server.
    curl -fsSL https://code-server.dev/install.sh | sh -s -- --method=standalone --prefix=/tmp/code-server

    # Start code-server in the background.
    /tmp/code-server/bin/code-server --auth none --port 13337 >/tmp/code-server.log 2>&1 &
  EOT

  # These environment variables allow you to make Git commits right away after creating a
  # workspace. Note that they take precedence over configuration defined in ~/.gitconfig!
  # You can remove this block if you'd prefer to configure Git manually or using
  # dotfiles. (see docs/dotfiles.md)
  env = {
    GIT_AUTHOR_NAME     = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
    GIT_AUTHOR_EMAIL    = "${data.coder_workspace_owner.me.email}"
    GIT_COMMITTER_NAME  = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
    GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
  }

  # The following metadata blocks are optional. They are used to display
  # information about your workspace in the dashboard. You can remove them
  # if you don't want to display any information.
  # For basic resources, you can use the `coder stat` command.
  # If you need more control, you can write your own script.
  metadata {
    display_name = "CPU Usage"
    key          = "0_cpu_usage"
    script       = "coder stat cpu"
    interval     = 10
    timeout      = 1
  }

  metadata {
    display_name = "RAM Usage"
    key          = "1_ram_usage"
    script       = "coder stat mem"
    interval     = 10
    timeout      = 1
  }

  metadata {
    display_name = "Home Disk"
    key          = "3_home_disk"
    script       = "coder stat disk --path $${HOME}"
    interval     = 60
    timeout      = 1
  }

  metadata {
    display_name = "CPU Usage (Host)"
    key          = "4_cpu_usage_host"
    script       = "coder stat cpu --host"
    interval     = 10
    timeout      = 1
  }

  metadata {
    display_name = "Memory Usage (Host)"
    key          = "5_mem_usage_host"
    script       = "coder stat mem --host"
    interval     = 10
    timeout      = 1
  }

  metadata {
    display_name = "Load Average (Host)"
    key          = "6_load_host"
    # get load avg scaled by number of cores
    script   = <<EOT
      echo "`cat /proc/loadavg | awk '{ print $1 }'` `nproc`" | awk '{ printf "%0.2f", $1/$2 }'
    EOT
    interval = 60
    timeout  = 1
  }

  metadata {
    display_name = "Swap Usage (Host)"
    key          = "7_swap_host"
    script       = <<EOT
      free -b | awk '/^Swap/ { printf("%.1f/%.1f", $3/1024.0/1024.0/1024.0, $2/1024.0/1024.0/1024.0) }'
    EOT
    interval     = 10
    timeout      = 1
  }
}

resource "coder_app" "code-server" {
  agent_id     = coder_agent.main.id
  slug         = "code-server"
  display_name = "code-server"
  url          = "http://localhost:13337/?folder=/home/${local.username}"
  icon         = "/icon/code.svg"
  subdomain    = false
  share        = "owner"

  healthcheck {
    url       = "http://localhost:13337/healthz"
    interval  = 5
    threshold = 6
  }
}

resource "docker_volume" "home_volume" {
  name = "coder-${data.coder_workspace.me.id}-home"
  # Protect the volume from being deleted due to changes in attributes.
  lifecycle {
    ignore_changes = all
  }
  # Add labels in Docker to keep track of orphan resources.
  labels {
    label = "coder.owner"
    value = data.coder_workspace_owner.me.name
  }
  labels {
    label = "coder.owner_id"
    value = data.coder_workspace_owner.me.id
  }
  labels {
    label = "coder.workspace_id"
    value = data.coder_workspace.me.id
  }
  # This field becomes outdated if the workspace is renamed but can
  # be useful for debugging or cleaning out dangling volumes.
  labels {
    label = "coder.workspace_name_at_creation"
    value = data.coder_workspace.me.name
  }
}

resource "docker_image" "main" {
  name = "coder-${data.coder_workspace.me.id}"
  build {
    context = "./build"
    build_args = {
      USER = local.username
    }
  }
  triggers = {
    dir_sha1 = sha1(join("", [for f in fileset(path.module, "build/*") : filesha1(f)]))
  }
}

resource "docker_container" "workspace" {
  count = data.coder_workspace.me.start_count
  image = docker_image.main.name
  # Uses lower() to avoid Docker restriction on container names.
  name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
  # Hostname makes the shell more user friendly: coder@my-workspace:~$
  hostname = data.coder_workspace.me.name
  # Use the docker gateway if the access URL is 127.0.0.1
  entrypoint = ["sh", "-c", replace(coder_agent.main.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")]
  env        = ["CODER_AGENT_TOKEN=${coder_agent.main.token}"]
  host {
    host = "host.docker.internal"
    ip   = "host-gateway"
  }
  volumes {
    container_path = "/home/${local.username}"
    volume_name    = docker_volume.home_volume.name
    read_only      = false
  }

  # Add labels in Docker to keep track of orphan resources.
  labels {
    label = "coder.owner"
    value = data.coder_workspace_owner.me.name
  }
  labels {
    label = "coder.owner_id"
    value = data.coder_workspace_owner.me.id
  }
  labels {
    label = "coder.workspace_id"
    value = data.coder_workspace.me.id
  }
  labels {
    label = "coder.workspace_name"
    value = data.coder_workspace.me.name
  }

  # Connect the workspace container to the Docker network
  networks_advanced {
    name = docker_network.coder_network.name
  }
}

resource "docker_container" "postgres" {
  image = "postgres:latest"
  name  = "coder-postgres-${data.coder_workspace.me.id}"
  volumes {
    host_path = "${path.cwd}/postgres-data"
    container_path = "/var/lib/postgresql/data"
  }
  env = [
    "POSTGRES_PASSWORD=postgres",
  ]
  networks_advanced {
    name = docker_network.coder_network.name
  }
}

resource "docker_container" "mysql" {
  image = "mysql:latest"
  name  = "coder-mysql-${data.coder_workspace.me.id}"
  volumes {
    host_path = "${path.cwd}/mysql-data"
    container_path = "/var/lib/mysql"
  }
  env = [
    "MYSQL_ROOT_PASSWORD=mysql",
  ]
  networks_advanced {
    name = docker_network.coder_network.name
  }
}

resource "docker_network" "coder_network" {
  name = "coder-network-${data.coder_workspace.me.id}"
}

Chon edit template hiện tại, thay code, và nhấn build, sau khi build thành công thì publish version, nhớ chọn Promote to active version

Sau khi mình có template, thì sẽ tạo workspace

Cuối cùng, mình tạo 1 app flask cơ bản để testing xem app flask trong workspace chạy OK chưa. Mình tạo app.py:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "Hello, World!"

@app.route("/test")
def test():
    return "Test route is working!"

if __name__ == "__main__":
    app.run(debug=True)

Sản phẩm: