使用RK3588搭建个人轻NAS记录


1.硬件平台

这里使用radxa的ROCK5B+作为服务器本地,内存8GB,七百多购入,焊了一颗64GB的闪迪的EMMC,这个板子有俩2280的M.2接口,我装了旧笔记本的硬盘,两根共1TB。风扇用的淘宝零度JM科技的风扇,好用。

image

这个板子的外壳,我用的官方的,但是官方的外壳不好用,整个就是一个铁棺材,不透风,上面那点透风口和风扇风向不匹配,不散热,跑图片整理识别的时候,CPU吃满,散热压不住,一直70°,建议还是用开源的3D打印的,散热好些。而且官方这个外壳,内存卡插进去就拔不出来了(尽管我不用),然后MIPI接口全堵住(尽管我不用),电源口的Type-C和DP接口的Type-C容易搞混,槽点满满。

拉跨玩意

2.系统部署

安装系统

  • 下载官方的Debian12镜像文件,通过balenaEtcher下载到SD卡,从SD卡中启动,然后使用dd命令将系统拷贝到EMMC或者NVME中
  • 下载官方的Debian12镜像文件,通过Type-C连接到PC,使用瑞芯微的官方工具,线刷系统到EMMC中,并烧录SPI Flash,直接到位。
  • 官方教程

驱动风扇

官方系统的风扇控制不太好用,可以重新整一下风扇驱动,可以重新加载dto文件,也可以使用别人整好的开源工具,这里使用GitHub上的这个软件,Fan-control,直接安装下载,修改配置文件:

wget https://github.com/pymumu/fan-control-rock5b/releases/download/1.2.1/fan-control-rock5b.1.2.1.arm64.deb
dpkg -i fan-control-rock5b.*.arm64.deb
systemctl enable fan-control
systemctl start fan-control

配置文件:/etc/fan-control.json

{
  "pwmchip": -1,
  "gpio": 0,
  "pwm-period": 10000,
  "temp-map": [
    {
      "temp": 30,
      "duty": 30,
      "duration": 30
    },
    {
      "temp": 40,
      "duty": 45,
      "duration": 30
    },
    {
      "temp": 50,
      "duty": 55,
      "duration": 30
    },
    {
      "temp": 60,
      "duty": 70,
      "duration": 30
    },
    {
      "temp": 70,
      "duty": 90,
      "duration": 120
    },
    {
      "temp": 75,
      "duty": 100,
      "duration": 180
    }
  ]
}

安装CasaOS

命令行执行
wget -qO- https://get.casaos.io | sudo bash
安装完成:
image

直接浏览器访问板卡IP地址即可进入控制台,然后注册用户输入密码即可。

image

我这里已经安装了很多Docker软件,刚进入游戏应该只有App Store和Files。

内网穿透

用的ddnsto,免费,无需多言。

在官网控制台注册,获取Tokens,官网

命令行运行,绑定Token,在官网就能看到设备在线了,免费的,每七天登陆一下续杯即可。
sh -c "$(curl -sSL http://fw.koolcenter.com/binary/ddnsto/linux/install_ddnsto_linux.sh)"

插件使用

在AppStore中添加更多软件源:

image

自用图床搭建

这里主要是用的RustFS,兼容S3协议,用nginx反代,用内网透传映射端口,实现图床功能。

  • 在AppStore中搜索RustFS,如果添加了bigbear的第三方软件源,是可以搜索到的,点击安装。

  • 打开设置,修改网络端口和WebUI的端口,这个Docker有俩端口要设置,一个是访问存储的端口,我这里设置为9000,一个是Console控制台,也就是WebUI的,我这里设置为9001,然后还需要设置下面的环境变量的值,才能通过网络访问。
    image

  • 在设置界面,还有俩环境变量的值,RUSTFS_ACCESS_KEY和RUSTFS_SECRET_KEY,可以理解为这个就是登陆的用户名和密码,也是管理员,打开WebUI,使用这俩值登录。
    image

  • 创建存储桶
    image

  • 编辑存储桶访问策略为公有
    image

  • 可以在存储桶中上传一张图片,然后访问地址是http://IP:9000/test/image.png,注意这里端口是另一个,不是WebUI的端口,后续可以通过Nginx做代理,然后用DDNSTO内网透传绑定代理的端口,实现远程访问。

  • 该存储桶,有两种上传图片的方式,查看官方文档,有很详细的操作教程:
    1. 使用API上传和下载资源,我没有使用这种方式,我用这个RustFS,就是奔着S3协议兼容来的。
    image

    1. 使用S3协议上传和下载,这里根据你使用的场景,有多种工具可以支持,我这里使用Obsidian的插件S3 Image Uploader,可以支持内网透传后的EndPoint,支持度很高,好用。
      image
      另外,官方还提供了很多其他S3协议的Demo,也可以说是SDK,我这里测试使用Python+Boto3实现了基本的上传功能,局域网内通过内网IP上传很轻松,没有问题,使用内网透传的域名的话,就会出现SDK上传成功,返回URL正常,但通过curl -I 图片链接 测试就是404,而且存储桶内上传是失败的,没有图片,这个概率很大,十次大概有8次,使用内网就没事。
      image
  • 使用python + Boto3实现的图片上传功能,S3协议需要的访问信息,在config.yaml中定义。

    #!/usr/bin/env python3
    """
    S3图片上传脚本
    读取config.yaml中的S3配置,上传本地图片到S3服务器,返回图片链接。
    模块化设计,可单独测试运行,也可被其他脚本调用。
    """
    
    import yaml
    import boto3
    from botocore.client import Config
    import os
    import sys
    import mimetypes
    import argparse
    from pathlib import Path
    from typing import Optional, Dict, Any
    import logging
    import time
    
    logging.basicConfig(level=logging.WARNING)
    logger = logging.getLogger(__name__)
    
    
    class S3Uploader:
        """S3上传器类,封装boto3操作"""
    
        def __init__(self, config_path: str = "config.yaml"):
            """
            初始化S3上传器,从配置文件加载配置
    
            :param config_path: 配置文件路径,默认为config.yaml
            """
            self.config = self.load_config(config_path)
            self.s3_client = self.create_s3_client()
    
        def load_config(self, config_path: str) -> Dict[str, Any]:
            """
            加载YAML配置文件
    
            :param config_path: 配置文件路径
            :return: 配置字典
            """
            try:
                with open(config_path, 'r', encoding='utf-8') as f:
                    config = yaml.safe_load(f)
            except FileNotFoundError:
                logger.error(f"配置文件 {config_path} 不存在")
                sys.exit(1)
            except yaml.YAMLError as e:
                logger.error(f"配置文件解析错误: {e}")
                sys.exit(1)
    
            # 提取S3配置部分
            s3_config = config.get('s3', {})
            required_keys = ['endpoint', 'access_key', 'secret_key', 'bucket']
            missing = [k for k in required_keys if k not in s3_config]
            if missing:
                logger.error(f"配置文件中缺少必要的S3配置项: {missing}")
                sys.exit(1)
    
            # 处理前缀(可选)
            s3_config['prefix'] = s3_config.get('prefix', '').strip('/')
            # 区域(可选)
            s3_config['region'] = s3_config.get('region', 'auto')
            # 路径样式(可选,默认为True,除非端点是Amazon AWS)
            s3_config['force_path_style'] = s3_config.get('force_path_style', None)
            # 自定义图片URL路径(可选)
            s3_config['image_url_path'] = s3_config.get('image_url_path', '')
            return s3_config
    
        def create_s3_client(self):
            """
            创建S3客户端,支持自定义端点
            """
            endpoint = self.config['endpoint']
            access_key = self.config['access_key']
            secret_key = self.config['secret_key']
            region = self.config['region']
    
            # 判断是否使用路径样式:优先使用配置,否则根据端点推断
            if self.config.get('force_path_style') is not None:
                force_path_style = self.config['force_path_style']
            else:
                # 如果端点包含.amazonaws.com,则使用虚拟主机样式,否则使用路径样式
                force_path_style = not ('amazonaws.com' in endpoint)
            
            # 配置boto3使用自定义端点
            session = boto3.session.Session()
            s3_client = session.client(
                's3',
                endpoint_url=endpoint,
                aws_access_key_id=access_key,
                aws_secret_access_key=secret_key,
                region_name=region,
                config=Config(
                    signature_version='s3v4',
                    s3={'addressing_style': 'path' if force_path_style else 'auto'}
                ),
            )
            return s3_client
    
        def upload_image(
            self,
            local_path: str,
            target_dir: str = "",
            custom_filename: Optional[str] = None
        ) -> str:
            """
            上传本地图片到S3,返回可访问的URL
    
            :param local_path: 本地图片文件路径
            :param target_dir: 目标目录(在S3存储桶中的路径),默认为空
            :param custom_filename: 自定义文件名,如果为None则使用本地文件名
            :return: 图片的公开访问URL
            """
            if not os.path.isfile(local_path):
                raise FileNotFoundError(f"文件不存在: {local_path}")
    
            # 确定S3对象键
            if custom_filename:
                filename = custom_filename
            else:
                filename = os.path.basename(local_path)
    
            # 拼接前缀和目标目录
            prefix = self.config['prefix']
            target_dir = target_dir.strip('/')
            if prefix and target_dir:
                object_key = f"{prefix}/{target_dir}/{filename}"
            elif prefix:
                object_key = f"{prefix}/{filename}"
            elif target_dir:
                object_key = f"{target_dir}/{filename}"
            else:
                object_key = filename
    
            # 猜测内容类型
            content_type, _ = mimetypes.guess_type(local_path)
            if content_type is None:
                content_type = 'application/octet-stream'
    
            bucket = self.config['bucket']
    
            logger.info(f"上传 {local_path}{bucket}/{object_key}")
            
            # 整体重试配置:上传 + 验证
            max_total_retries = 3
            verify_retry_count = 3
            verify_retry_delay = 5  # 秒
            
            for total_attempt in range(1, max_total_retries + 1):
                # 尝试上传
                try:
                    self.s3_client.upload_file(
                        local_path,
                        bucket,
                        object_key,
                        ExtraArgs={'ContentType': content_type}
                    )
                except Exception as e:
                    logger.warning(f"上传失败,第{total_attempt}次尝试: {e}")
                    if total_attempt < max_total_retries:
                        wait_time = 2 ** total_attempt
                        logger.info(f"等待 {wait_time} 秒后重试上传...")
                        time.sleep(wait_time)
                        continue
                    else:
                        logger.error(f"上传失败,已达到最大重试次数")
                        raise
                
                # 上传成功,尝试验证
                verified = False
                for verify_attempt in range(1, verify_retry_count + 1):
                    try:
                        self.s3_client.head_object(Bucket=bucket, Key=object_key)
                        verified = True
                        logger.info(f"验证成功(第{verify_attempt}次尝试)")
                        break
                    except Exception as e:
                        logger.warning(f"验证失败,第{verify_attempt}次尝试: {e}")
                        if verify_attempt < verify_retry_count:
                            logger.info(f"等待 {verify_retry_delay} 秒后重试验证...")
                            time.sleep(verify_retry_delay)
                        else:
                            logger.warning(f"验证失败,已达到最大重试次数,可能对象尚未立即可用")
                            # 继续,可能仍可生成URL
                            break
                
                if verified:
                    logger.info(f"上传并验证成功")
                    break
                else:
                    # 验证失败,可能是上传不完整,重试上传
                    logger.warning(f"验证未通过,将重试上传(第{total_attempt}次)")
                    if total_attempt < max_total_retries:
                        time.sleep(3)
                        continue
                    else:
                        logger.warning(f"已达到最大重试次数,但仍未通过验证,但可能已上传成功,继续生成URL")
                        break
            
            # 构建访问URL(假设为公开可访问)
            image_url_path = self.config.get('image_url_path')
            if image_url_path:
                # 使用自定义图片URL路径
                if not image_url_path.endswith('/'):
                    image_url_path += '/'
                url = f"{image_url_path}{object_key}"
            else:
                # 根据路径样式决定URL格式
                force_path_style = self.config.get('force_path_style')
                if force_path_style is None:
                    # 根据端点推断
                    force_path_style = not ('amazonaws.com' in self.config['endpoint'])
                endpoint = self.config['endpoint'].rstrip('/')
                if force_path_style:
                    # 路径样式:endpoint/bucket/object_key
                    url = f"{endpoint}/{bucket}/{object_key}"
                else:
                    # 虚拟主机样式:bucket.endpoint/object_key
                    from urllib.parse import urlparse, urlunparse
                    parsed = urlparse(endpoint)
                    # 将 bucket 插入到主机名前
                    new_netloc = f"{bucket}.{parsed.netloc}"
                    new_parsed = parsed._replace(netloc=new_netloc)
                    base_url = urlunparse(new_parsed)
                    url = f"{base_url}/{object_key}"
            logger.info(f"上传成功,URL: {url}")
            return url
    
        def list_buckets(self):
            """列出所有存储桶,用于测试连接"""
            try:
                response = self.s3_client.list_buckets()
                buckets = [b['Name'] for b in response['Buckets']]
                logger.info(f"可用存储桶: {buckets}")
                return buckets
            except Exception as e:
                logger.error(f"列出存储桶失败: {e}")
                return []
    
    
    def main():
        """命令行入口点"""
        parser = argparse.ArgumentParser(description='上传本地图片到S3服务器')
        parser.add_argument('image', help='本地图片文件路径')
        parser.add_argument('-d', '--directory', default='', help='S3目标目录(可选)')
        parser.add_argument('-c', '--config', default='config.yaml', help='配置文件路径(默认为config.yaml)')
        parser.add_argument('-o', '--output', help='自定义文件名(可选)')
        args = parser.parse_args()
    
        uploader = S3Uploader(args.config)
        # 可选:测试连接
        # uploader.list_buckets()
    
        try:
            url = uploader.upload_image(args.image, args.directory, args.output)
            print(url)
        except Exception as e:
            logger.error(f"上传过程中出错: {e}")
            sys.exit(1)
    
    
    if __name__ == '__main__':
        main()       
  • 到这里,图床的基本功能就已经实现了,已经可以在内网环境中使用了,但如果要给Blog或其他外网环境下使用的话,还需要实现内网透传和Nginx代理

  • 在AppStore中下载Nginx代理,实际上也可以在命令行终端下载,不需要使用WebUI的话。

    # 更新软件源
    sudo apt update
    
    # 安装 Nginx
    sudo apt install -y nginx
    
    # 备份原始配置文件
    sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
    
    # 将当前目录下的 nginx.conf 导入到系统目录
    sudo cp nginx.conf /etc/nginx/nginx.conf
    
    # 检查 Nginx 配置语法
    sudo nginx -t
    
    # 启动 Nginx
    sudo systemctl start nginx
    
    # 重新加载配置(配置正确后执行)
    sudo systemctl reload nginx
    
    # 设置开机自启
    sudo systemctl enable nginx
    
    # 查看运行状态
    sudo systemctl status nginx
  • 我这里用的Nginx.conf配置,是抄RustFS官方文档的,要多看文档
    image

    load_module /usr/lib/nginx/modules/ngx_stream_module.so;
    
    worker_processes auto;
    
    events {
        worker_connections 1024;
        accept_mutex on;
    }
    
    http {
        include       mime.types;
        default_type  application/octet-stream;
        # ---------- upstream 定义 ----------
        upstream rustfs {
            least_conn;
            server 127.0.0.1:9000;
        }
        upstream rustfs-console {
            least_conn;
            server 127.0.0.1:9001;
        }
        # ---------- RustFS 代理 ----------
        server {
            listen       9004;
            listen       [::]:9004;
            server_name  YOUR_DOMAIN;
            # 防盗链:只允许指定域名访问
            valid_referers none blocked sparkle-now.cn *.sparkle-now.cn;
            location / {
                if ($invalid_referer) {
                    return 403;
                }
                # 允许上传大文件
                client_max_body_size 0;
                # 禁用缓冲
                proxy_buffering off;
                proxy_request_buffering off;
                proxy_set_header Host $http_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_connect_timeout 300;
                proxy_http_version 1.1;
                proxy_set_header Connection "";
                chunked_transfer_encoding off;
                # WebSocket 支持
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
                proxy_pass http://rustfs;
            }
        }
        # ---------- RustFS 控制台代理 ----------
        server {
            listen       9005;
            listen       [::]:9005;
            server_name  YOUR_DOMAIN;
            location / {
                # 建议控制台加访问限制(IP 或 HTTP Auth),这里简单示例允许全部内网访问
                allow 127.0.0.1;
                allow 192.168.0.0/16;
                deny all;
                client_max_body_size 0;
                proxy_buffering off;
                proxy_request_buffering off;
                proxy_set_header Host $http_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_connect_timeout 300;
                proxy_http_version 1.1;
                proxy_set_header Connection "";
                chunked_transfer_encoding off;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
                proxy_pass http://rustfs-console;
            }
        }
    }
  • 然后使用内网透传绑定Nginx代理后的端口即可

其他插件

其他这些插件,都是娱乐用,AppStore有不少有意思的插件,可以部署着玩玩,后续有其他功能部署,也会在这里更新使用过程。


下班……


文章作者: biubiu选手
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 biubiu选手 !
评论
 本篇
使用RK3588搭建个人轻NAS记录 使用RK3588搭建个人轻NAS记录
记录使用RADXA ROCK5B+搭建个人服务器的过程和相关软件,主要是RK3588+Debian12+CasaOS+各种Docker容器,简单快捷,够自己用目前。
2026-02-08
下一篇 
嵌入式软件优化记录 嵌入式软件优化记录
记录嵌入式开发过程中一些小技巧和坑,包括内核、编译器、内存等相关的零碎的知识点,记录下来,方便开发时查找和提醒,提高工程效率和代码运行速度。
2026-02-06
  目录