Instant Messaging -Anwendungen, einschließlich Server, Verwaltung und Client
Es wurde bereitgestellt und gestartet, willkommen, um die Kunde und die Managementseite zu erleben
Bitte ändern Sie nicht die Standardrolle und die Berechtigungen nach Belieben. Bitte fühlen Sie sich ein wenig liebevoll und erfinden Sie keine unzivilisierten Namen.
Verwenden der Egg -Framework der Serverseite des IM -Dienstes
Seit der Entwicklung des mobilen Internets wurden Instant Messaging -Dienste von WeChat in alle Ecke unseres Lebens integriert und spielen auch in einigen Unternehmen eine wichtige Rolle. Unser Unternehmen nutzte Instant Messaging -Dienste, aber viele maßgeschneiderte Anforderungen können nicht erreicht werden. Daher wurde beschlossen, einen Instant Messaging -Microservice zu entwickeln, der den individuellen Anforderungen intern erfüllt.
Das socket.io -Framework wurde verwendet, weil das Backend zu dieser Zeit keine Menschen hatte. Nachdem ich einige Beispiele gelesen hatte, fand ich es wirklich bequem, sie auf der gesamten Plattform zu verwenden und zu unterstützen. Daher wurde dieser Microservice im Front-End-Team implementiert, und der Effekt ist derzeit ziemlich gut.
Die Community hat derzeit weniger oder zu einfache Inhalte in diesem Bereich (es gibt nur einen öffentlichen Chatraum). Darüber hinaus ist es sehr unangenehm, während des Geschäftsentwicklungsprozesses PM zu sein. Daher möchte ich mich von einigen einzigartigen Geschäftssachen lösen und eine einfache und vollständige IM -Anwendung erkennen, die Unternehmensgeschäfte nicht mit einfachen Funktionen wie Server, Management und Client mischt. Das Ziel der Nachahmung des Kunden ist WeChat, weil ich damit sehr vertraut bin und nicht zu viel über das Produkt nachdenken muss. Darüber hinaus sind die Leute, die es ausprobieren, sehr vertraut damit und benötigen nicht zu viele Kommunikationskosten.
Um eine vollständige Reihe von Instant -Messaging -Diensten zu entwickeln, sind folgende Teile erforderlich:
Geboren für Rahmenbedingungen und Anwendungen auf Unternehmensebene
Der Grund, warum ich mich als Unterstützung von Alibabas Ei.js-Framework entschieden habe, ist, dass sie in groß angelegten Implementierung und Sicherheit in der IT eine gute Arbeit geleistet haben. Der Grund, warum sie sich nicht für Nest entschieden haben, ist, dass es problematisch ist, socket.io zu integrieren. ORM verwendet ein Folge und die Datenbank ist MySQL. Ich habe es schon einmal benutzt, daher ist es weniger schwierig, loszulegen.
Out-of-the-Box Front-End/Design-Lösung
Der Grund für die Auswahl von Ant Design Pro ist, dass ich mit Vue Family Eimer vertraut bin. Ich möchte diese Gelegenheit nutzen, um mich mit dem Entwicklungsprozess des gesamten React -Ökosystems vertraut zu machen und die wesentlichen Unterschiede und unterschiedlichen Wege der beiden wichtigsten Entwicklungsrahmen in China zu spüren. Ant Design Pro wird seit mehreren Jahren veröffentlicht und hat in der Tat Effizienzverbesserungen für kleine und mittelgroße Unternehmen gebracht, was genau für meine Bedürfnisse geeignet ist.
Standard -Tools, die von Vue.js entwickelt wurden
Verwenden Sie @Vue/CLI, um einen IM-Service-Client, ein mobiles H5-Projekt zu erstellen, und im UI-Framework verwendet Youzan Vant, das meine Open-Source-Komponente Vue-Page-Stack und die bessere Croll von Mr. Huang integriert, um die grundlegenden Funktionen von IM zu verwirklichen
Als Front-End-Ingenieur müssen die meisten täglichen Aufgaben nicht über Entitätsbeziehungen nachdenken. Basierend auf meiner tatsächlichen Erfahrung kann das Verständnis von Unternehmensbeziehungen uns jedoch helfen, Geschäftsmodelle besser zu verstehen. Die Verbesserung des Produkt- und Geschäftsverständnisses ist für uns von großer Hilfe. Während der Nachfrageüberprüfung finden wir viele unlogische Aspekte (warum müssen wir uns erneut über den Produktmanager beschweren). Wenn wir es zu diesem Zeitpunkt vorschlagen können, werden wir die Initiative ergreifen, um eine wiederholte Entwicklung im nachfolgenden Prozess zu vermeiden, und gleichzeitig können wir eine relativ gute Interaktion mit den Schülern auf der Produktseite (und nicht Konfrontation) bilden. Hier sind einige der wichtigsten Beziehungen der Entität:
Aus der obigen Abbildung können wir sehen, dass der Benutzer der Kern des gesamten Beziehungsdiagramms ist. Das Folgende ist die Beziehung zwischen verschiedenen Einheiten:
Das Folgende ist eine detaillierte Einführung in Sitzungen, Rollen und Berechtigungen:
Wenn Sie eine Instant -Messaging -Anwendung ausfüllen, müssen Sie das Gespräch als erstes berücksichtigen, das Konversationsfenster in unserem WeChat ist. Es erfordert große Anstrengung, über die Beziehung zwischen Gesprächen und Nachrichten, Benutzern und Gruppen nachzudenken und schließlich die folgende grundlegende Beziehung zu bilden:
Mit anderen Worten, der Benutzer hat keine direkte Beziehung zu der Sitzung und kann die Sitzung nur über den entsprechenden Einzelchat- und Gruppenchat des Benutzers erhalten. Dies kann die folgenden Vorteile haben:
Um ein flexibles, universelles und bequemes Berechtigungsmanagementsystem zu entwerfen, übernimmt dieses System die RBAC-Steuerung (rollenbasierte Zugriffskontrolle), um eine allgemeine Plattform "Benutzerrollenberechtigungen" für die Bequemlichkeit der späteren Erweiterung zu entwerfen.
RBAC (rollenbasierte Zugriffskontrolle) bezieht sich darauf, dass der Benutzer mit Berechtigungen durch Rollen verbunden ist. Das heißt, ein Benutzer hat mehrere Rollen, und jede Rolle hat mehrere Berechtigungen (natürlich kombinieren Sie keine widersprüchlichen Rollen und Berechtigungen zusammen). Auf diese Weise wird ein Autorisierungsmodell für "Benutzer-Rollen-Permissions" konstruiert. In diesem Modell gibt es im Allgemeinen eine viel-zu-Viele-Beziehung zwischen Benutzern und Rollen sowie zwischen Rollen und Berechtigungen.
Dieses System verfügt über die Standardrollen des Administrators, des allgemeinen Benutzers, des verbotenen Benutzers und des verbotenen Benutzers, und verschiedene Berechtigungen werden unterschiedlichen Rollen zugewiesen. Daher ist es notwendig, eine einheitliche Authentifizierungsverarbeitung (durch Middleware) für die Schnittstellenrouting wie Verwaltung und Sprache durchzuführen. Die spezifischen Methoden und Methoden werden im Back-End-Projekt ausführlich erläutert. Dieses System verwendet vorübergehend die Methode der vordefinierten Rollen und Berechtigungen. Wenn Sie in Zukunft erweitert werden möchten, können Sie Rollen und Berechtigungen bearbeiten.
Ich habe die Verwaltungsseite von WeChat noch nie gesehen, aber Sie können sich vorstellen, dass Administratoren Benutzerrollen und -berechtigungen konfigurieren und den Gruppenstatus bearbeiten können:
Nach der Registrierung und Anmeldung können Sie Freunde hinzufügen und sich normalen Gruppen anschließen, persönliche grundlegende Informationen und Prozessanwendungen ändern.
Sich nicht anmelden
Zum Beispiel: Jetzt gibt es eine neue Version des persönlichen Zentrums, die online getestet werden muss. Erstellen Sie zunächst eine neue Rolle "Test Personal Center" und wenden Sie dann der Rolle entsprechende Berechtigungen zu. Gruppen Sie dann gewöhnliche Benutzer und wählen Sie einige Personen aus, um diese Rolle zu konfigurieren, damit Sie sie testen können.
Sprechen wir über das Kernkommunikationsprinzip von Instant Messaging -Diensten. Wie allgemeine HTTP -Dienste gibt es einen Server und einen Client für die Kommunikation, aber das detaillierte Protokoll und die Verarbeitungsmethoden sind unterschiedlich.
Aus historischen Gründen ist das Mainstream -HTTP -Protokoll jetzt ein staatenloser Protokoll (HTTP2 wird vorerst nicht weit verbreitet). Im Allgemeinen initiiert der Kunde die Anfrage aktiv und antwortet dann darauf. Um zu erkennen, dass der Server Informationen an den Client drückt, muss das Front-End das Back-End aktiv abfragen. Diese Methode ist ineffizient und fehleranfällig. Dies geschieht in der Tat auf unserer Management -Homepage bereits (5s einmal).
Um diesen serverseitigen Bedarf zu erreichen, um Informationen aktiv zu überschreiten, begann HTML5, ein Protokoll für die Voll-Duplex-Kommunikation auf einer einzelnen TCP-Verbindung, nämlich WebSocket, bereitzustellen. WebSocket erleichtert den Datenaustausch zwischen Clients und Servern und ermöglicht dem Server, Daten aktiv an Clients zu übertragen. Das WebSocket -Protokoll wurde 2008 geboren und wurde 2011 zu einem internationalen Standard. Derzeit haben die meisten Browser es bereits unterstützt.
Die Verwendung von Websocket ist recht einfach:
var ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
ws.close();
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
Mit dem WebSocket -Protokoll hat der Server Waffen erweitert, um Informationen aktiv zu überschreiten. Gibt es also eine Möglichkeit, mit alten und neuen Browsern kompatibel zu sein? Tatsächlich denken viele Menschen daran, und die Antwort lautet socket.io
socket.io socket.io fasst die WebSocket -Schnittstelle weiter zusammen und kann automatisch auf die Verwendung von Umfragen in alten Browsern für die Kommunikation wechseln (unsere Benutzer werden nicht wahrnehmen), wodurch ein einheitlicher Satz von Schnittstellen bildet und die Entwicklungslast erheblich verringert wird. Es hat hauptsächlich die folgenden Vorteile:
Dies ist die Homepage von Socket.io
Der schnellste und zuverlässigste Instant-Messaging-Motor (mit dem schnellsten und zuverlässigsten Echtzeitmotor)
Es ist wirklich einfach zu bedienen:
var io = require('socket.io')(80);
var cfg = require('./config.json');
var tw = require('node-tweet-stream')(cfg);
tw.track('socket.io');
tw.track('javascript');
tw.on('tweet', function(tweet){
io.emit('tweet', tweet);
});
Konzentrieren wir uns auf mehrere wichtige Punkte im Server -Seitenprojekt. Der größte Teil des Inhalts muss auf der offiziellen Ei -Website angezeigt werden.
Verwenden Sie das Gerüst npm init egg --type=simple to Initialisierung des Serverprojekts, installieren Sie MySQL (My IS Version 8.0), konfigurieren
// 目录结构说明
├── package.json // 项目信息
├── app.js // 启动文件,其中有一些钩子函数
├── app
| ├── router.js // 路由
│ ├── controller
│ ├── service
│ ├── middleware // 中间件
│ ├── model // 实体模型
│ └── io // socket.io 相关
│ ├── controller
│ └── middleware // io独有的中间件
├── config // 配置文件
| ├── plugin.js // 插件配置文件
| └── config.default.js // 默认的配置文件
├── logs // server运行期间产生的log文件
└── public // 静态文件和上传文件目录
Der Router wird hauptsächlich verwendet, um die Korrespondenz zwischen der Anforderungs -URL und dem Controller zu beschreiben, der die Ausführungsaktion, dh app/router ausdrücklich ausführt
app/middleware/auth.jsapp/middleware/admin.js befinden Da dieses System als Administratoren und allgemeine Kommunikationsbenutzer unterschiedliche Rollen spielt, ist für die Schnittstelle von Management und Kommunikation eine einheitliche Authentifizierungsverarbeitung erforderlich.
Beispielsweise möchte ich die Verwaltungs -Seitenroute /v1/admin/... , ich möchte allen Routen in dieser Serie Administratorauthentifizierung hinzufügen. Zu diesem Zeitpunkt können Sie Middleware verwenden, um sich zu authentifizieren. Das Folgende ist ein spezifisches Beispiel für die Verwendung von Middleware im Admin -Router.
// middware
module.exports = () => {
return async function admin(ctx, next) {
let { session } = ctx;
// 判断admin权限
if (session.user && session.user.rights.some(right => right.keyName === 'admin')) {
await next();
} else {
ctx.redirect('/login');
}
};
};
// router
const admin = app.middleware.admin();
router.get('/api/v1/admin/rights', admin, controller.v1.admin.rightsIndex);
Die verwendete Folge+MySQL-Kombination und Ei verfügt auch über eine Folgen von Plug-Ins. Sequelize ist ein ORM, das in der Knotenumgebung verwendet wird, die Postgres, MySQL, Mariadb, SQLite und Microsoft SQL Server unterstützt, was sehr bequem zu verwenden ist. Sie müssen zuerst die direkte Beziehung zwischen dem Modell und dem Modell definieren. Nachdem die Beziehung hergestellt wurde, können Sie einige voreingestellte Methoden anwenden.
Die grundlegenden Informationen des Modells sind einfacher zu verarbeiten. Was beachtet werden muss, ist das Beziehungsdesign zwischen Unternehmen, dh assoziierten. Das Folgende ist die Beziehung Beschreibung des Benutzers
// User.js
module.exports = app => {
const { STRING } = app.Sequelize;
const User = app.model.define('user', {
provider: {
type: STRING
},
username: {
type: STRING,
unique: 'username'
},
password: {
type: STRING
}
});
User.associate = function() {
// One-To-One associations
app.model.User.hasOne(app.model.UserInfo);
// One-To-Many associations
app.model.User.hasMany(app.model.Apply);
// Many-To-Many associations
app.model.User.belongsToMany(app.model.Group, { through: 'user_group' });
app.model.User.belongsToMany(app.model.Role, { through: 'user_role' });
};
return User;
};
Beispielsweise ist die Beziehung zwischen Benutzer und UserInfo eine Eins-zu-Eins-Beziehung. Nachdem es definiert ist, können wir beim Erstellen eines neuen Benutzers user.setUserInfo(userInfo) verwenden. Wenn Sie die grundlegenden Informationen dieses Benutzers erhalten möchten, können Sie auch user.getUserInfo() verwenden.
Die Beziehung zwischen Benutzer und Anwendung ist eins zu viele, dh ein Benutzer kann mehreren Anwendungen entsprechen, und derzeit werden nur Freunde und Gruppenanwendungen angewendet:
Beim Hinzufügen einer Anwendung können Sie user.addApply(apply) verwenden, und wenn Sie sie erhalten, können Sie sie wie folgt verwenden:
const result = await ctx.model.Apply.findAndCountAll({
where: {
userId: ctx.session.user.id,
hasHandled: false
}
});
Die Beziehung zwischen Benutzer und Gruppe ist viel zu viel, dh ein Benutzer kann mehreren Gruppen entsprechen, und eine Gruppe kann mehreren Benutzern entsprechen. Auf diese Weise ermittelt Folgene eine Zwischentabelle user_group, um diese Beziehung zu erreichen.
Normalerweise benutze ich das:
group.addUser(user); // 建立群组和用户的关系
user.getGroups(); // 获取用户的群组信息
Das Ei liefert das Plug-In Eggocket.io. Sie müssen das Plug-in in config/plugin.js öffnen, nachdem Sie Ei-socket.io installiert haben. IO hat seine eigene Middleware und Controller.
Die Route von IO unterscheidet sich von der allgemeinen HTTP -Anfragen. Beachten Sie, dass die Route hier nicht mit Middleware verarbeitet werden kann (ich habe es nicht gelungen), daher habe ich die Verbotverarbeitung im Controller abgeschlossen.
// 加入群
io.of('/').route('/v1/im/join', app.io.controller.im.join);
// 发送消息
io.of('/').route('/v1/im/new-message', app.io.controller.im.newMessage);
// 查询消息
io.of('/').route('/v1/im/get-messages', app.io.controller.im.getMessages);
Hinweis: Ich betrachte sowohl die Gruppen- als auch die Freundesbeziehung als Raum (dh eine Sitzung), damit ich Nachrichten direkt an die ROMM senden kann, und jeder im Inneren kann sie empfangen.
Es gibt zwei Standard -Middleware, eine ist die Verbindungsmiddelware, die beim Verbinden und Trennen von Middleware bezeichnet wird und zur Überprüfung des Anmeldestatus und der Prozessgeschäftslogik verwendet wird. Das andere ist die Paket -Middleware, die jedes Mal gesendet wird, wenn eine Nachricht gesendet wird, mit der das Protokoll drucken wird
Da die Anrufgenehmigung voreingestellt ist, wird sie im Controller verarbeitet
// 对用户发言的权限进行判断
if (!ctx.session.user.rights.some(right => right.keyName === 'speak')) {
return;
}
Die Chats sind in einzelne Chats und Gruppen -Chats unterteilt. Die Chat -Informationen enthalten vorübergehend allgemeine Text, Bilder, Videos und Positionierungsnachrichten, die gemäß dem Unternehmen in Bestellungen oder Produkte erweitert werden können.
Das Strukturdesign von Nachrichten bezieht sich auf das Design mehrerer Dienste von Drittanbietern und wurde auch in Kombination mit der Situation dieses Projekts selbst angepasst. Es kann nach Belieben erweitert werden, und die folgende Erklärung wird abgegeben:
const Message = app.model.define('message', {
/**
* 消息类型:
* 0:单聊
* 1:群聊
*/
type: {
type: STRING
},
// 消息体
body: {
type: JSON
},
fromId: { type: INTEGER },
toId: { type: INTEGER }
});
Der Körper speichert den Nachrichtenkörper, mit dem verschiedene Nachrichtenformate mit JSON gespeichert werden:
// 文本消息
{
"type": "txt",
"msg":"哈哈哈" //消息内容
}
// 图片消息
{
"type": "img",
"url": "http://nimtest.nos.netease.com/cbc500e8-e19c-4b0f-834b-c32d4dc1075e",
"ext":"jpg",
"w":360, //宽
"h":480, //高
"size": 388245
}
// 视频消息
{
"type": 'video',
"url": "http://nimtest.nos.netease.com/cbc500e8-e19c-4b0f-834b-c32d4dc1075e",
"ext":"mp4",
"w":360, //宽
"h":480, //高
"size": 388245
}
// 地理位置消息
{
"type": "loc",
"title":"中国 浙江省 杭州市 网商路 599号", //地理位置title
"lng":120.1908686708565, // 经度
"lat":30.18704515647036 // 纬度
}
Derzeit gibt es nur einen, der das Token von Baidu aktualisieren soll. Hier ist es ganz einfach, beziehen Sie sich einfach auf die offizielle Dokumentation.
Intelligente Einheit Dialoganpassung und Serviceplattform
Das ist ziemlich interessant. Sie können einen neuen Roboter erstellen und entsprechende Fähigkeiten unter https://ai.baidu.com/ hinzufügen. Ich chatte hier und es gibt kluge Q & As usw.
Wenn Sie nicht starten möchten, können Sie ctx.service.baidu.getToken();
Zunächst müssen Sie es in der Konfigurationsdatei konfigurieren. Ich habe die Dateigröße hier beschränkt und das iOS-Videodateiformat mit dem Cross-Site-Videodatei:
config.multipart = {
mode: 'file',
fileSize: '3mb',
fileExtensions: ['.mov']
};
Eine einheitliche Schnittstelle wird verwendet, um Datei -Uploads zu verarbeiten. Das Kernproblem ist das Schreiben von Dateien und Dateien sind die Dateiliste, die vom vorderen Ende übertragen wird
for (const file of ctx.request.files) {
// 生成文件路径,注意upload文件路径需要存在
const filePath = `./public/upload/${
Date.now() + Math.floor(Math.random() * 100000).toString() + '.' + file.filename.split('.').pop()
}`;
const reader = fs.createReadStream(file.filepath); // 创建可读流
const upStream = fs.createWriteStream(filePath); // 创建可写流
reader.pipe(upStream); // 可读流通过管道写入可写流
data.push({
url: filePath.slice(1)
});
}
Ich speichere /public/upload/ im Serververzeichnis. Dieses Verzeichnis erfordert eine statische Dateikonfiguration:
config.static = {
prefix: '/public/',
dir: path.join(appInfo.baseDir, 'public')
};
Das offizielle Eierdokument dieses Kapitels besteht darin, Sie zu töten. Es gibt nichts Beispiele, Sie müssen den Quellcode lesen. Es ist so schrecklich. Ich habe es lange studiert, bevor ich herausgefunden habe, was los ist.
Da ich das Kontokennwortkennwort anmeldung freier steuern möchte, verwende ich Passport nicht für das Kontokennwortanmeldung, sondern verwende die Authentifizierung und Sitzung der normalen Schnittstelle.
Das Folgende ist eine detaillierte Beschreibung des Anmeldungsprozesses bei der Verwendung einer Plattform von Drittanbietern (ich habe Github ausgewählt):
Öffnen Sie das Plug-In:
// config/plugin.js
module.exports.passport = {
enable: true,
package: 'egg-passport',
};
module.exports.passportGithub = {
enable: true,
package: 'egg-passport-github',
};
// config.default.js
config.passportGithub = {
key: 'your_clientID',
secret: 'your_clientSecret',
callbackURL: 'http://localhost:3000/api/v1/passport/github/callback' // 注意这里非常的关键,这里需要和你在github上面设置的Authorization callback URL一致
};
this.app.passport.verify(verify);
const github = app.passport.authenticate('github', { successRedirect: '/' }); // successRedirect就是最后校验完毕后前端会跳转的路由,我这里直接跳转到主页了
router.get('/v1/passport/github', github);
router.get('/v1/passport/github/callback', github);
/v1/passport/github im vorderen Ende und initiieren Sie die GitHub -Autorisierung für diese Anwendung. Nach dem Erfolg wird GitHub zu http://localhost:3000/v1/passport/github/callback?code=12313123123 gehen. Unser GitHubPassport-Plug-In erhält die Informationen des Benutzers auf GitHub. Nachdem wir die detaillierten Informationen erhalten haben, müssen wir die Benutzerinformationen in app/passport/verify.js überprüfen und den Benutzerinformationen unserer eigenen Plattform in Verbindung bringen und der Sitzung auch einen Wert zuweisen // verify.js
module.exports = async (ctx, githubUser) => {
const { service } = ctx;
const { provider, name, photo, displayName } = githubUser;
ctx.logger.info('githubUser', { provider, name, photo, displayName });
let user = await ctx.model.User.findOne({
where: {
username: name
}
});
if (!user) {
user = await ctx.model.User.create({
provider,
username: name
});
const userInfo = await ctx.model.UserInfo.create({
nickname: displayName,
photo
});
const role = await ctx.model.Role.findOne({
where: {
keyName: 'user'
}
});
user.setUserInfo(userInfo);
user.addRole(role);
await user.save();
}
const { rights, roles } = await service.user.getUserAttribute(user.id);
// 权限判断
if (!rights.some(item => item.keyName === 'login')) {
ctx.body = {
statusCode: '1',
errorMessage: '不具备登录权限'
};
return;
}
ctx.session.user = {
id: user.id,
roles,
rights
};
return githubUser;
};
Achten Sie auf den obigen Code. Wenn es sich um die erste Autorisierung handelt, wird der Benutzer erstellt. Wenn es sich um die zweite Autorisierung handelt, wurde der Benutzer erstellt.
Wenn das System bereitgestellt oder ausgeführt wird, müssen einige Daten und Tabellen voreingestellt sein, und die Codes befinden sich in app.js und app/service/startup.js
Die Logik ist, dass nach dem Start des Projekts das Modell verwendet wird, um die Tabellenstruktur in die Datenbank zu synchronisieren und dann einige grundlegende Daten zu erstellen:
Nach Abschluss der oben genannten wurden die anfänglichen Daten abgeschlossen und können normal arbeiten
Ich kaufte den Server CentOS auf Tencent Cloud, den Domänennamen, den ich auf Alibaba Cloud gekauft habe, den Knoten (12.18.2), Nginx und MySQL8.0 und startete direkt auf CentOS. Das Front-End verwendet Nginx für Reverse Proxy. Aufgrund begrenzter Serverressourcen gibt es keine Automatisierungswerkzeuge Jenkins und Docker, was beim Aktualisieren zu manuellen Vorgängen führt.