0基础学Linux建站【马年祝福docker实操】
过年时如何让亲戚朋友点击 www. 你的名字.com 访问你搭建的抢红包网站?这篇文章将从0开始教会你如何搭建
本文章面向Linux纯小白,一点基础没有系列
功能简述:
①用户点击网站链接后,通过填写名字进行抢红包,产生0.01~888.88 随机金额,及主语录 如:马到功成等等,领取后进行排名
②设有 一等奖 666.66 和 特等奖 888.88 ,抽到时会产生特殊场景特效
demo演示:马年红包祝福
效果演示:
搭建视频:
哔哩哔哩:bilibili.com/video/BV1Au…
6.2 和 第七大步 目前看视频就行,我出完视频🐎上更新文章
前置准备
视频:哔哩哔哩:如何搭建一个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、构建与启动
- 编写 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%)
修改指南:如果你想改概率,只需要修改 if 和 elif 后面的判断数字(数字代表区间上限)