0基础学Linux建站【马年祝福docker实操】

0 阅读14分钟

0基础学Linux建站【马年祝福docker实操】

过年时如何让亲戚朋友点击 www. 你的名字.com 访问你搭建的抢红包网站?这篇文章将从0开始教会你如何搭建

本文章面向Linux纯小白,一点基础没有系列

功能简述:

①用户点击网站链接后,通过填写名字进行抢红包,产生0.01~888.88 随机金额,及主语录 如:马到功成等等,领取后进行排名

②设有 一等奖 666.66 和 特等奖 888.88 ,抽到时会产生特殊场景特效

demo演示马年红包祝福

效果演示:

微信图片_20260217032650.png

搭建视频:

哔哩哔哩:bilibili.com/video/BV1Au…

6.2 和 第七大步 目前看视频就行,我出完视频🐎上更新文章

前置准备

文章:【建站前置准备工作,连接VPS】

视频:哔哩哔哩:如何搭建一个docker项目?搭建网站前我们需要准备哪些工具?

一、加SWAP(新手忽略)

注意:此步骤新手可直接跳过,直接进行第二步

若服务器内存 ≤ 1G 可添加 swap虚拟内存,一般为内存的 1-2 倍即可

设置 SWAP 可以用脚本:

wget -O box.sh https://raw.githubusercontent.com/BlueSkyXN/SKY-BOX/main/box.sh && chmod +x box.sh && clear && ./box.sh

填写18后,选择1,填写2048或者1024,然后回车


二、更新工具

2.1、切换到 root 用户
sudo -i
2.2、升级 packages
apt update -y
2.3、安装常用的工具
apt install wget curl sudo vim git -y

三、安装 Docker 环境(非大陆)

此步为非大陆vps安装docker的步骤, 大陆服务器的 三、四 步骤请划至整篇文章最下面的补充部分 补充

3.1、安装
wget -qO- get.docker.com | bash
3.2、查看 docker 版本
docker -v  
3.3、设置开机自动启动
systemctl enable docker  

四、安装 Docker-compose(非大陆)

4.1、安装 compose 插件
apt install docker-compose-plugin -y
4.2、查看版本
docker compose version  #查看 docker compose 版本
4.3、修改 Docker 配置(可选,新手忽略并跳过此步)

增加一段自定义内网 IPv6 地址,开启容器的 IPv6 功能,以及限制日志文件大小,防止 Docker 日志塞满硬盘

cat > /etc/docker/daemon.json <<EOF
{
    "log-driver": "json-file",
    "log-opts": {
        "max-size": "20m",
        "max-file": "3"
    },
    "ipv6": true,
    "fixed-cidr-v6": "fd00:dead:beef:c0::/80",
    "experimental":true,
    "ip6tables":true
}
EOF
4.4、重启docker
systemctl restart docker

五、安装项目(马年红包)

5.1、创建目录

mkdir -p /root/data/docker_data/horse_real_server/templates

5.2、进入目录

cd /root/data/docker_data/horse_real_server

5.3、创建网络环境(关键)

为了防止出现 network not found 报错,我们需要先手动建立一个专用网络。

docker network create halo_network

5.4、编写后端核心 (app.py)

逻辑改动说明:

  • 排行榜逻辑:机器人(马斯克等)的时间戳我设为了 9999999999(未来时间)。真实用户中奖时记录的是 time.time()(当前时间)。SQL排序规则为 ORDER BY max_money DESC, achieve_time ASC(金额大的排前,金额一样时间早的排前)。因此,只要用户抽中 888.88,时间肯定比机器人早,A排第一,B排第二,机器人被挤到后面,后抽到相同数字的用户B排在先抽的用户A后面
  • 风控逻辑logs 表,每次请求前查询过去 60秒 和 600秒 内的记录数。
cat > app.py << 'EOF'
from flask import Flask, render_template, request, jsonify
import sqlite3
import time
import random
import os

app = Flask(__name__)
DB_FILE = '/data/horse.db'

# --- 1. 数据库初始化 ---
def init_db():
    if not os.path.exists('/data'):
        os.makedirs('/data')
    with sqlite3.connect(DB_FILE) as conn:
        # 用户表: 名字, 最高金额, 达成最高金额的时间戳
        conn.execute('''CREATE TABLE IF NOT EXISTS users 
                     (name TEXT PRIMARY KEY, max_money REAL, achieve_time REAL)''')
  
        # 日志表: 用于限频 (名字, IP, 时间戳)
        conn.execute('''CREATE TABLE IF NOT EXISTS logs 
                     (name TEXT, ip TEXT, time REAL)''')
  
        # 索引优化查询速度
        conn.execute("CREATE INDEX IF NOT EXISTS idx_logs_time ON logs(time)")

        # 预埋机器人 (时间戳设为 9999999999,确保真实用户中奖后永远排在机器人前面)
        cursor = conn.cursor()
        cursor.execute("SELECT count(*) FROM users")
        if cursor.fetchone()[0] == 0:
            future_time = 9999999999
            conn.execute("INSERT INTO users VALUES (?, ?, ?)", ('马斯克', 888.88, future_time))
            conn.execute("INSERT INTO users VALUES (?, ?, ?)", ('雷军', 666.66, future_time))
            conn.execute("INSERT INTO users VALUES (?, ?, ?)", ('库克', 66.66, future_time))

# --- 2. 祝福语词库 (50条) ---
BLESSINGS = [
    "马到成功", "龙马精神", "一马当先", "万事奔腾如意", "马年大吉", 
    "骏马迎春福满门", "奋蹄新程好运来", "前程似锦马飞扬", "天天好运马上来", "事业腾达马蹄急",
    "财源滚滚马上有", "平安顺遂一路驰", "新岁如骏马奔腾", "马年好运不断", "福气追着你跑",
    "心想事成马常在", "奋发有为迎新年", "步步高升马上赢", "喜气扬鞭过新春", "梦想驰骋新一年",
    "工作顺利不停步", "学业进步似飞马", "天天好心情常在", "阖家欢乐万事兴", "马蹄声声报喜来",
    "好运策马到你家", "马跃新程添光彩", "幸福一路相伴行", "万事顺心奔向前", "健康快乐常相随",
    "天天好运不打烊", "乘风策马赴山海", "马年更上一层楼", "奔赴热爱不止步", "春风得意马蹄轻",
    "红运当头马上发", "福星高照伴你行", "一路繁花马上开", "新年奋进勇争先", "乘势而上开新局",
    "所到之处皆顺利", "骏马踏春好运来", "天天好事奔你来", "事业爱情双丰收", "马不停蹄创佳绩",
    "新岁平安喜乐多", "心怀热爱向远方", "马年幸福满人间", "日子越过越红火", "前路光明任驰骋"
]

# --- 3. 概率算法 (0.2% / 1% / 18.8% / 80%) ---
def get_lucky_result():
    rand = random.uniform(0, 100)
    blessing = random.choice(BLESSINGS)
  
    if rand <= 0.2:
        return 888.88, blessing, 3  # Level 3: 特等奖
    elif rand <= 1.2:
        return 666.66, blessing, 2  # Level 2: 一等奖 (0.2+1.0)
    elif rand <= 20.0:
        money = round(random.uniform(200.01, 600.00), 2)
        return money, blessing, 1   # Level 1: 大额 (1.2+18.8)
    else:
        money = round(random.uniform(0.01, 200.00), 2)
        return money, blessing, 0   # Level 0: 普通

# --- 4. 风控检查 ---
def check_limit(conn, name, ip):
    now = time.time()
    cursor = conn.cursor()
  
    # 清理过期日志 (保留最近10分钟)
    # conn.execute("DELETE FROM logs WHERE time < ?", (now - 600,))
  
    # 规则1: 用户名限制 (1分钟20次, 10分钟100次)
    cursor.execute("SELECT count(*) FROM logs WHERE name=? AND time > ?", (name, now - 60))
    if cursor.fetchone()[0] >= 20: return "手速太快!用户名 1分钟限额已满"
  
    cursor.execute("SELECT count(*) FROM logs WHERE name=? AND time > ?", (name, now - 600))
    if cursor.fetchone()[0] >= 100: return "休息一下!用户名 10分钟限额已满"

    # 规则2: IP限制 (1分钟30次, 10分钟150次)
    cursor.execute("SELECT count(*) FROM logs WHERE ip=? AND time > ?", (ip, now - 60))
    if cursor.fetchone()[0] >= 30: return "该IP请求过于频繁,请稍后再试"
  
    cursor.execute("SELECT count(*) FROM logs WHERE ip=? AND time > ?", (ip, now - 600))
    if cursor.fetchone()[0] >= 150: return "该IP已被暂时限制"
  
    return None

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/api/draw', methods=['POST'])
def draw():
    name = request.json.get('name', '').strip()[:8]
    if not name: return jsonify({'error': '名字不能为空'}), 400
  
    # 获取真实IP (即使在Docker后)
    ip = request.headers.get('X-Forwarded-For', request.remote_addr)
  
    with sqlite3.connect(DB_FILE) as conn:
        # 1. 风控检查
        error = check_limit(conn, name, ip)
        if error: return jsonify({'error': error}), 429
  
        # 2. 记录日志
        now = time.time()
        conn.execute("INSERT INTO logs VALUES (?, ?, ?)", (name, ip, now))
  
        # 3. 抽奖
        money, blessing, level = get_lucky_result()
  
        # 4. 更新排行榜数据
        # 查旧数据
        cursor = conn.execute("SELECT max_money FROM users WHERE name=?", (name,))
        row = cursor.fetchone()
  
        if not row:
            # 新用户:插入
            conn.execute("INSERT INTO users VALUES (?, ?, ?)", (name, money, now))
            new_max = money
        else:
            # 老用户:只有金额 更大 时才更新 max_money 和 achieve_time
            # 如果金额一样(比如都是888.88),不更新时间,保留最早的那次,确保排名不掉
            old_max = row[0]
            if money > old_max:
                conn.execute("UPDATE users SET max_money=?, achieve_time=? WHERE name=?", (money, now, name))
                new_max = money
            else:
                new_max = old_max
  
        # 5. 计算剩余次数 (为了前端显示,查一下1分钟内的次数)
        cursor.execute("SELECT count(*) FROM logs WHERE name=? AND time > ?", (name, now - 60))
        used_1m = cursor.fetchone()[0]
  
        return jsonify({
            'money': money,
            'blessing': blessing,
            'level': level,
            'left': 20 - used_1m
        })

@app.route('/api/leaderboard')
def leaderboard():
    with sqlite3.connect(DB_FILE) as conn:
        # 排序核心逻辑:金额降序(DESC),时间升序(ASC)
        # 这样同分情况下,先达成的人排前面
        cursor = conn.execute("SELECT name, max_money FROM users ORDER BY max_money DESC, achieve_time ASC LIMIT 100")
        return jsonify([{'name': r[0], 'money': r[1]} for r in cursor.fetchall()])

if __name__ == '__main__':
    init_db()
    app.run(host='0.0.0.0', port=80)
EOF

5.5、编写前端 (templates/index.html)

.level-3 (特等奖) 和 .level-2 (一等奖) 的 CSS 特殊动画。特等奖会触发全屏红光闪烁和卡片剧烈抖动。

cat > templates/index.html << 'EOF'
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <title>2026 赛博马年·至尊版</title>
    <style>
        :root { --primary: #e60044; --gold: #f9d423; --bg: #0f0f1a; }
        body { margin: 0; background: var(--bg); font-family: sans-serif; height: 100vh; color: #fff; overflow: hidden; display: flex; flex-direction: column; align-items: center; justify-content: center; }
  
        /* 基础UI */
        .panel { background: rgba(30,30,30,0.95); border: 2px solid var(--primary); padding: 20px; border-radius: 12px; text-align: center; width: 85%; max-width: 320px; z-index: 10; }
        input { background: transparent; border: none; border-bottom: 2px solid var(--gold); color: #fff; font-size: 1.2rem; text-align: center; width: 100%; padding: 8px; margin: 15px 0; outline: none; }
        button { background: var(--primary); color: #fff; border: none; padding: 10px 25px; border-radius: 20px; font-weight: bold; font-size: 1rem; }

        /* 卡片容器 */
        .card-box { perspective: 1000px; margin: 20px 0; cursor: pointer; position: relative; z-index: 5; }
        .card { width: 220px; height: 300px; position: relative; transform-style: preserve-3d; transition: transform 0.6s; }
        .card.flipped { transform: rotateY(180deg); }
        .face { position: absolute; width: 100%; height: 100%; backface-visibility: hidden; border-radius: 15px; display: flex; flex-direction: column; align-items: center; justify-content: center; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
        .front { background: linear-gradient(135deg, #d9004c, #800020); border: 2px solid var(--gold); }
        .back { transform: rotateY(180deg); background: #fff; color: #333; border: 4px solid var(--gold); }

        /* --- 特效等级 CSS (核心修复点) --- */
  
        /* Level 2: 一等奖 (666) - 紫色光晕 */
        .level-2 .back { background: linear-gradient(to bottom, #fff, #e6e6fa); border-color: #a020f0; box-shadow: 0 0 30px #a020f0; }
        .level-2 h2 { color: #a020f0 !important; transform: scale(1.2); }

        /* Level 3: 特等奖 (888) - 极度夸张 */
        @keyframes super-shake { 
            0% { transform: translate(1px, 1px) rotate(0deg); } 
            10% { transform: translate(-1px, -2px) rotate(-1deg); } 
            20% { transform: translate(-3px, 0px) rotate(1deg); } 
            30% { transform: translate(3px, 2px) rotate(0deg); } 
            40% { transform: translate(1px, -1px) rotate(1deg); } 
            50% { transform: translate(-1px, 2px) rotate(-1deg); } 
            60% { transform: translate(-3px, 1px) rotate(0deg); } 
            70% { transform: translate(3px, 1px) rotate(-1deg); } 
            80% { transform: translate(-1px, -1px) rotate(1deg); } 
            90% { transform: translate(1px, 2px) rotate(0deg); } 
            100% { transform: translate(1px, -2px) rotate(-1deg); } 
        }
        @keyframes bg-flash { 0% { background: var(--bg); } 50% { background: #550000; } 100% { background: var(--bg); } }
  
        /* 这里的修复点:动画不再给 .card,而是给 .card-box (level-3本身) */
        .level-3-body { animation: bg-flash 0.5s infinite; }
        .level-3 { animation: super-shake 0.5s infinite; } 
  
        /* 背面样式保持夸张 */
        .level-3 .back { background: linear-gradient(135deg, #ffd700, #ff8c00); border: 5px solid #fff; box-shadow: 0 0 50px #ffd700; }
        .level-3 h2 { color: #d9004c !important; font-size: 3.5rem !important; text-shadow: 2px 2px 0 #fff; }

        /* 排行榜 */
        .lb-box { width: 90%; max-width: 400px; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 8px; height: 25vh; overflow-y: auto; font-size: 0.9rem; }
        .row { display: flex; justify-content: space-between; padding: 6px; border-bottom: 1px solid rgba(255,255,255,0.1); }
    </style>
</head>
<body>
    <div id="login-div" class="panel">
        <h2 style="color:var(--gold)">🐎 2026 试手气</h2>
        <input type="text" id="username" placeholder="输入你的大名" maxlength="8">
        <button onclick="start()">开始抽奖</button>
    </div>

    <div id="game-div" style="display:none; width:100%; flex-direction:column; align-items:center;">
        <div style="width:100%; max-width:400px; display:flex; justify-content:space-between; padding:10px; font-size:0.9rem;">
            <span>👤 <span id="u-name"></span></span>
            <span>1分钟额度: <span id="u-left" style="color:var(--gold)">20</span>/20</span>
        </div>

        <div class="card-box" id="card-box" onclick="draw()">
            <div class="card" id="card">
                <div class="face front">
                    <div style="font-size:60px">🧧</div>
                    <div>点我暴富</div>
                </div>
                <div class="face back">
                    <h2 style="color:#d9004c; font-size:2.5rem; margin:10px 0;">¥<span id="res-money">0.00</span></h2>
                    <p id="res-txt" style="color:#333; font-size:0.8rem; padding:0 10px;"></p>
                    <button onclick="event.stopPropagation(); reset()">收下</button>
                </div>
            </div>
        </div>

        <div class="lb-box">
            <div style="text-align:center; opacity:0.7; margin-bottom:5px;">🏆 实时排行榜 (同分先得)</div>
            <div id="lb-list"></div>
        </div>
    </div>

<script>
let user = '';
function start() {
    user = document.getElementById('username').value;
    if(!user) return alert('名字不能为空');
    document.getElementById('login-div').style.display = 'none';
    document.getElementById('game-div').style.display = 'flex';
    document.getElementById('u-name').innerText = user;
    loadLB();
}

async function draw() {
    const card = document.getElementById('card');
    const box = document.getElementById('card-box');
    if(card.classList.contains('flipped')) return;

    try {
        const res = await fetch('/api/draw', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({name: user})
        });
        const data = await res.json();

        if(res.status === 429) return alert(data.error); 
  
        document.getElementById('res-money').innerText = data.money;
        document.getElementById('res-txt').innerText = data.blessing;
        document.getElementById('u-left').innerText = data.left;

        // 特效逻辑
        box.className = 'card-box'; 
        document.body.className = ''; 
        if(data.level === 3) {
            box.classList.add('level-3');
            document.body.classList.add('level-3-body');
        } else if(data.level === 2) {
            box.classList.add('level-2');
        }

        // 强行延时一小会儿翻转,视觉更佳(可选,不加也行)
        requestAnimationFrame(() => {
            card.classList.add('flipped');
        });
  
        loadLB();
    } catch(e) { alert("网络请求失败"); }
}

function reset() {
    document.getElementById('card').classList.remove('flipped');
    // 延迟移除特效,防止瞬间消失太突兀
    setTimeout(() => {
        document.getElementById('card-box').className = 'card-box';
        document.body.className = '';
    }, 300);
}

async function loadLB() {
    try {
        const res = await fetch('/api/leaderboard');
        const list = await res.json();
        let html = '';
        list.forEach((u, i) => {
            let icon = i<3 ? ['🥇','🥈','🥉'][i] : (i+1)+'.';
            html += `<div class="row"><span>${icon} ${u.name}</span><span>¥${u.money}</span></div>`;
        });
        document.getElementById('lb-list').innerHTML = html;
    } catch(e) {}
}
setInterval(loadLB, 5000);
</script>
</body>
</html>
EOF

5.6、构建与启动

  1. 编写 Dockerfile
cat > Dockerfile << 'EOF'
FROM python:3.9-slim
WORKDIR /app
RUN pip install flask -i https://pypi.tuna.tsinghua.edu.cn/simple
COPY app.py .
COPY templates ./templates
EXPOSE 80
CMD ["python", "app.py"]
EOF

2. 编写 docker-compose.yml

cat > docker-compose.yml << 'EOF'
services:
  horse-server:
    build: .
    container_name: horse_real_server
    restart: always
    ports:
      - "8888:80"
    volumes:
      - ./data:/data
    networks:
      - halo_network

networks:
  halo_network:
    external: true
EOF

3. 启动

docker compose up -d --build

5.7、完成

此时打开浏览器,访问 http://你的vps的ip:8888


到这搭建网站和访问所有步骤已经完成,下面 六、七 俩章节为进阶操作


六、域名访问

通过ip+端口让别人访问,总担心隐私泄露?可以通过反向代理后让别人直接通过你的域名访问你的网站

比如我的名字叫rckin我就可以购买一个 rckin.com 的域名,别人点击一下就能访问了,也不用担心ip暴露

6.1、购买域名

推荐前往namesilo 购买,点击链接购买我可以拿到一点分成,感谢各位小伙伴的支持

购买教程:点击链接滑动到视频第3步即可:哔哩哔哩YouTube

6.2、域名托管到cloud flare

等待更新ing(看视频)

cloud flare注册官网:dash.cloudflare.com/sign-up

namesilo主页:NS主页

6.3、安装 NPM

购买完域名和让域名在全球传播之后,我们终于可以回到Ubuntu了

现在开始安装 Nginx Proxy Manager 进行反向代理

6.3.1、创建文件夹目录
mkdir -p /root/data/docker_data/npm
6.3.2、进入文件夹
cd /root/data/docker_data/npm
6.3.3、配置
nano docker-compose.yml
6.3.4、nano输入:
services:
  app:
    image: jc21/nginx-proxy-manager:2.11.3 #如需升级,请手动修改版本号并重新 docker compose pull
    restart: unless-stopped
    ports:
      - '80:80'  # 保持默认即可,不建议修改左侧的80
      - '81:81'  # 冒号左边可以改成自己服务器未被占用的端口
      - '443:443' # 保持默认即可,不建议修改左侧的443
    volumes:
      - ./data:/data # 冒号左边可以改路径,现在是表示把数据存放在在当前文件夹下的 data 文件夹中
      - ./letsencrypt:/etc/letsencrypt  # 冒号左边可以改路径,现在是表示把数据存放在在当前文件夹下的 letsencrypt 文件夹中

💡 Nano 编辑器小贴士:

  • 粘贴内容: 在终端点击鼠标右键,或使用快捷键 Shift + Insert
  • 保存修改:Ctrl + O,然后按 Enter 确认文件名。
  • 退出编辑器:Ctrl + X
  • 注意: 如果你对文件做了改动,退出时会询问是否保存,输入 y (Yes) 即可。
6.3.5、配置启动
docker compose up -d
6.3.6、浏览器访问

此时我们就可以通过浏览器 http://改为你的vps的ip:81 访问 NPM 面板了

⚠️ 补充(小白暂时别看这句话):生产环境建议不要长期对公网暴露 81 管理端口,域名配置完成后可关闭,可将'81:81'改为'127.0.0.1:81:81',后续访问NPM可通过走 SSH 隧道:ssh -L 81:127.0.0.1:81 root@你的VPS_IP,然后再浏览器http://localhost:81


七、开始反向代理

通过 http://你的vps的ip:81 访问你的NPM面板后

7.1、登录

默认登录账号密码:

默认Email:       admin@example.com    #记得改
默认Password:    changeme             #记得改

7.2、添加代理主机

7.3、添加域名,开启ip代理

7.4、启动https协议,添加证书,搞定!

最后祝大家马到成功,新春快乐,我们下期再见。

八、补充1:大陆VPS装docker

大陆服务器由于网络原因不能直接访问docker网站,所以我们可以去国内镜像网站拉取docker和compose

大陆服务器1.3,1.4步骤不一样,且要多一步 开通防火墙,国内大厂阿里云,腾讯云等等都自带防火墙

8.1、打开提供商控制台


8.2、找到防火墙设置

必须开放端口:

80

443

81(如未关闭)

8888(本篇项目默认端口)


8.3、安装 Docker 环境(大陆服务器)

8.3.1、安装脚本
bash <(curl -sSL https://linuxmirrors.cn/docker.sh)
8.3.2、设置开机启动
systemctl enable docker

8.4、安装 Docker-compose大陆服务器)

apt install docker-compose-plugin -y

8.5、后续步骤与上面相同

回到总文章上面的 第五步、安装项目(马年红包) 即可,(大陆/非大陆后续步骤一样)


九、补充2:爆率修改

9.1、进入项目目录

cd /root/data/docker_data/horse_real_server

9.2、编辑 app.py

使用 nano 编辑器打开后端逻辑文件:

nano app.py

9.3、找到并修改 get_lucky_result 函数

向下滚动找到 def get_lucky_result(): 这一块。

原来的逻辑是:

  • rand <= 0.2: 特等奖 (0.2%)
  • rand <= 1.2: 一等奖 (0.2% + 1% = 1.2%)
  • rand <= 20.0: 大额 (1.2% + 18.8% = 20%)

修改指南:如果你想改概率,只需要修改 ifelif 后面的判断数字(数字代表区间上限