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
Table of contents
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: