前言
计划给学校搭建一个 OJ,选择了 vfleaking 的 Universal Online Judge。但是 UOJ 社区版安装后总是报错 ”Judgement Failed“,开了一个刚装好系统的一个 GCP 服务器也是有问题,无奈只能跑去折腾官方版。
文章内容过多,建议利用左侧目录进行导航。
安装 UOJ 官方版 Docker 镜像
如果不开梯子会遇到各种问题比如 Docker 无法安装,无法拉取 ubuntu 镜像,无法连接 Node.js NPM、PHP Composer 等等……
安装 Docker
这一步需要在终端执行下面的指令:
sudo su # 切换到 root 账户环境
curl -fsSL https://get.docker.com -o get-docker.sh # 下载 Docker 安装脚本
sudo sh ./get-docker.sh --dry-run # 运行安装脚本
Bash一般情况下,Docker Compose 将作为 Docker 的插件一并安装。
构建镜像
使用 Git 将官方版镜像克隆到本地:
git clone https://github.com/vfleaking/uoj.git
Bash使用 cd
命令进入 uoj
目录,查看、修改 docker-compose.yml
:
version: '3'
services:
all:
build: .
container_name: uoj_all
restart: always
stdin_open: true
tty: true
cap_add:
- SYS_PTRACE
volumes:
- mysql:/var/lib/mysql
- data_main:/var/uoj_data
- data_copy:/var/uoj_data_copy
- web:/opt/uoj/web
- var_log:/var/log
- judger:/opt/uoj/judger
- svn:/var/svn
ports:
- "80:80" # UOJ 主端口,冒号左边的端口号也可以改成其他未被占用的端口
- "888:888" # phpMyAdmin 端口,同理也可以修改
- "3690:3690" # SVN 端口,不建议修改
volumes:
mysql:
data_main:
data_copy:
web:
var_log:
judger:
svn:
YAML然后在当前目录下执行
docker compose up
YAML不一开始就使用后台运行方式的原因是在第一次启动时如果出现问题查看日志可以快速定位问题。
此时 Docker 会进行 UOJ 镜像的构建,这个过程依服务器性能和网络速度大概需要 10 - 20 分钟。
当最后提示
[+] Running 2/2
✔ Volume "uoj_web" Created
✔ Container uoj_all Created
Attaching to uoj_all
uoj_all | * Starting NTP server ntpd [ OK ]
uoj_all | * Starting MySQL database server mysqld [ OK ]
......
即代表构建完成,此时按下 Ctrl+C 退出容器,再使用:
docker compose up -d
Bash即可将 UOJ 置于后台运行。
配置 UOJ
使用以下命令进入 容器 终端:
docker attach uoj_all
Bash在容器中执行 exit
即可返回 宿主机 终端。
修改域名、端口
在 容器 终端执行:
cd /opt/uoj
vim web/app/.config.php
Bash进行修改:
<?php
return [
'database' => [
'database' => 'app_uoj233',
'username' => 'root',
'password' => 'xxxxxxxxxxxxxxxxxxx', # 这里应该已经生成了默认密码,记录下来
'host' => '127.0.0.1'
],
'web' => [
'main' => [
'protocol' => 'http', # HTTP 协议,可选 http / https
'host' => 'local_uoj.ac', # 域名
'port' => 80 # 端口号,如果想要网址隐藏端口号的话 http 使用 80,https 使用 443
],
'blog' => [
'protocol' => 'http',
'host' => 'blog.local_uoj.ac',
'port' => 80
],
'domain' => null
],
'security' => [
'user' => [
'client_salt' => 'salt0'
],
'cookie' => [
'checksum_salt' => ['salt1', 'salt2', 'salt3']
......
PHP这时可以通过 域名:端口号 的方式尝试访问 UOJ,如果显示正常则证明没有问题。
注册账号
在 UOJ 上注册 root 账号,注册的第一个账号将自动赋予超级管理员。
安装 phpMyAdmin
phpMyAdmin 将用于管理 UOJ 上的 MySQL 数据库。
在 容器 终端上执行:
cd /var/www
wget https://files.phpmyadmin.net/phpMyAdmin/5.2.1/phpMyAdmin-5.2.1-all-languages.zip # 下载
unzip phpMyAdmin-5.2.1-all-languages.zip -d ./ # 解压
mv ./phpMyAdmin-5.2.1-all-languages ./phpMyAdmin # 重命名
Bash随后转到 /etc/apache2/sites-available
文件夹,创建 001-phpmy.conf
:
<VirutalHost *:888>
DocumentRoot /var/www/phpMyAdmin
</VirutalHost>
XML然后编辑 /etc/apache2/ports.conf
,在 Listen 80
后面新建一行插入:
Listen 888
Plaintext建立链接并重启 Apache2:
ln -s /etc/apache2/sites-available/001-phpmy.conf /etc/apache2/sites-enabled
service apache2 restart
Bash切回到 /var/www/phpMyAdmin 目录,运行以下指令创建并编辑 phpMyAdmin 默认配置:
cp config.sample.inc.php config.inc.php
vim config.inc.php
Bash进行编辑:
/**
* First server
*/
$i++;
/* Authentication type */
$cfg['Servers'][$i]['auth_type'] = 'cookie';
/* Server parameters */
$cfg['Servers'][$i]['host'] = '127.0.0.1'; // 修改
$cfg['Servers'][$i]['compress'] = false;
$cfg['Servers'][$i]['AllowNoPassword'] = false;
/**
* phpMyAdmin configuration storage settings.
*/
/* User used to manipulate with storage */
PHP随后执行 mysql --password app_uoj233
进入 MySQL 客户端进行用户表的修改:
Enter password: <数据库密码>
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 328
Server version: 8.0.39-0ubuntu0.20.04.1 (Ubuntu)
Copyright (c) 2000, 2024, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> UPDATE mysql.user SET host = '%' WHERE user = 'root';
Query OK, 1 row affected (0.07 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.09 sec)
mysql> quit
Bye
Plaintext这时,你应当能通过之前在 docker-compose 中设定的对外端口访问到 phpMyAdmin 并进行登录了。
修改博客为单域名模式
UOJ 官方版和社区版差异已经比较大,因此链接内容可能如今不适合官方版 UOJ,仅供参考。
行号可能会有改变,善用查找功能。
在 容器 内目录 /opt/uoj/web 下修改下面这些文件:
【覆盖】app/controllers/subdomain/blog/route.php
修改博客路由
<?php
call_user_func(function () {
Route::pattern('blog_username', '[a-zA-Z0-9_\-]{1,20}');
$prefix = '/blogof/{blog_username}';
Route::group([
'domain' => UOJConfig::$data['web']['main']['host'],
'protocol' => UOJConfig::$data['web']['main']['protocol'],
'onload' => function() {
UOJContext::setupBlog();
}
], function() use ($prefix) {
Route::any("$prefix/", '/subdomain/blog/index.php');
Route::any("$prefix/archive", '/subdomain/blog/archive.php');
Route::any("$prefix/aboutme", '/subdomain/blog/aboutme.php');
Route::any("$prefix/click-zan", '/click_zan.php');
Route::any("$prefix/blog/{id}", '/subdomain/blog/blog.php');
Route::any("$prefix/slide/{id}", '/subdomain/blog/slide.php');
Route::any("$prefix/blog/(?:{id}|new)/write", '/subdomain/blog/blog_write.php?type=B');
Route::any("$prefix/slide/(?:{id}|new)/write", '/subdomain/blog/blog_write.php?type=S');
Route::any("$prefix/blog/{id}/delete", '/subdomain/blog/blog_delete.php');
Route::any("$prefix/blog/{id}/content.md", '/download.php?type=blog-md');
Route::any("$prefix/slide/{id}/content.yaml", '/download.php?type=slide-yaml');
}
);
});
PHP【修改】app/models/Route.php
加几个 rtrim
protected static function addRoute($methods, $uri, $action) {
if (is_string($methods)) {
$methods = [$methods];
}
$cur = [];
$cur['methods'] = $methods;
$cur['uri'] = rtrim($uri, '/');
if($cur['uri'] == '') {
$cur['uri'] = "/";
}
$cur['action'] = $action;
$cur = array_merge(self::getGroup(), $cur);
self::$routes[] = $cur;
return $cur;
}
PHP if (isset($route['domain'])) {
$domain_pat = strtr($route['domain'], $rep_arr);
if (!preg_match('/^'.$domain_pat.'$/', rtrim(UOJContext::requestDomain(), '/'), $domain_matches)) {
return false;
}
$matches = array_merge($matches, $domain_matches);
}
PHP【修改】app/models/HTML.php
前端链接
public static function blog_url($username, $uri, array $cfg = []) {
$cfg += [
'escape' => true
];
$protocol = HTML::protocol('main');
$url = UOJConfig::$data['web']['main']['protocol'].'://'.UOJConfig::$data['web']['main']['host'].$port.'/blogof/'.blog_name_encode($username);
if (HTML::port('blog') != HTML::standard_port($protocol)) {
$url .= ':'.HTML::port('blog');
}
$url .= $uri;
$url = rtrim($url, '/');
if ($cfg['escape']) {
$url = HTML::escape($url);
}
return $url;
}
PHP【修改】app/controllers/subdomain/blog/archive.php
按钮
<?php echoUOJPageHeader('日志') ?>
<div class="row">
<div class="col-md-3">
<?php if (UOJUserBlog::userCanManage(Auth::user())): ?>
<div class="btn-group btn-group-justified">
<a href="<?=HTML::blog_url(UOJUserBlog::id(), '/blog/new/write')?>" class="btn btn-primary"><span class="glyphicon glyphicon-edit"></span> 写新博客</a>
<a href="<?=HTML::blog_url(UOJUserBlog::id(), '/slide/new/write')?>" class="btn btn-primary"><span class="glyphicon glyphicon-edit"></span> 写新幻灯片</a>
</div>
<?php endif ?>
PHP【修改】app/controllers/blogs.php
还是按钮
<?php echoUOJPageHeader(UOJLocale::get('blogs')) ?>
<?php if (Auth::check()): ?>
<div class="pull-right">
<a href="<?= HTML::blog_url(Auth::id(), '/') ?>" class="btn btn-info btn-sm">我的博客首页</a>
<a href="<?= HTML::blog_url(Auth::id(), '/blog/new/write') ?>" class="btn btn-info btn-sm">写新博客</a>
</div>
<?php endif ?>
<h3>博客总览</h3>
<?php
echoLongTable(
PHP【修改】app/views/blog-nav.php
导航栏链接
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="<?= HTML::blog_url(UOJUserBlog::id(), '/') ?>"><?= UOJUserBlog::id() ?></a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="<?= HTML::blog_url(UOJUserBlog::id(), '/archive') ?>">日志</a></li>
<li><a href="<?= HTML::blog_url(UOJUserBlog::id(), '/aboutme') ?>">关于我</a></li>
<li><a href="<?= HTML::url('/') ?>">DYYZOJ</a></li>
</ul>
</div><!--/.nav-collapse -->
</div>
</div>
PHP【覆盖】app/views/blog-preview.php
预览界面,修改的太多了,直接覆盖吧
<?php
if ($is_preview) {
$readmore_pos = strpos($blog->content['content'], '<!-- readmore -->');
if ($readmore_pos !== false) {
$content = substr($blog->content['content'], 0, $readmore_pos).'<p><a href="'.HTML::blog_url(UOJUserBlog::id(), '/blog/').info['id'].'">阅读更多……</a></p>';
} else {
$content = $blog->content['content'];
}
} else {
$content = $blog->content['content'];
}
$extra_text = $blog->info['is_hidden'] ? '<span class="text-muted">[已隐藏]</span> ' : '';
$blog_type = $blog->info['type'] == 'B' ? 'blog' : 'slide';
?>
<h2><?= $extra_text ?><a class="header-a" href="<?= HTML::blog_url(UOJUserBlog::id(), '/blog/'.$blog['id']) ?>"><?= $blog->info['title'] ?></a></h2>
<div><?= $blog->info['post_time'] ?> <strong>By</strong> <?= getUserLink($blog->info['poster']) ?></div>
<?php if (!$show_title_only): ?>
<div class="panel panel-default">
<div class="panel-body">
<?php if ($blog->isTypeB()): ?>
<article class="uoj-article"><?= $content ?></article>
<?php elseif ($blog->isTypeS()): ?>
<article class="uoj-article">
<div class="embed-responsive embed-responsive-16by9">
<iframe class="embed-responsive-item" src="<?= HTML::blog_url(UOJUserBlog::id(), '/slide/'.$blog['id']) ?>"></iframe>
</div>
<div class="text-right top-buffer-sm">
<a class="btn btn-default btn-md" href="<?= HTML::blog_url(UOJUserBlog::id(), '/slide/'.$blog['id']) ?>"><span class="glyphicon glyphicon-fullscreen"></span> 全屏</a>
</div>
</article>
<?php endif ?>
</div>
<div class="panel-footer text-right">
<ul class="list-inline bot-buffer-no">
<li>
<?php foreach ($blog->tags as $tag): ?>
<?php echoBlogTag($tag) ?>
<?php endforeach ?>
</li>
<?php if ($is_preview): ?>
<li><a href="<?= HTML::blog_url(UOJUserBlog::id(), '/blog/'.$blog['id']) ?>">阅读全文</a></li>
<?php endif ?>
<?php if (Auth::check() && (isSuperUser(Auth::user()) || Auth::id() == $blog->info['poster'])): ?>
<li><a href="<?=HTML::blog_url(UOJUserBlog::id(), '/'.$blog_type.'/'.$blog['id'].'/write')?>">修改</a></li>
<li><a href="<?=HTML::blog_url(UOJUserBlog::id(), '/blog/'.$blog['id'].'/delete')?>">删除</a></li>
<?php endif ?>
<li><?= ClickZans::getBlock('B', $blog->info['id'], $blog->info['zan']) ?></li>
</ul>
</div>
</div>
<?php endif ?>
PHP导出镜像
刚才我们所做的所有修改都只是在容器生命周期内生效,一旦停止容器即丢弃,因此我们需要导出自定义镜像。
在 宿主机 终端上运行:
docker commit uoj_all 新镜像名字
Bash后面即可使用 docker run
运行新镜像。
还可以推送到阿里云私有仓库(个人免费):https://help.aliyun.com/zh/ack/create-an-application-by-using-a-private-image-repository。