<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://www.completenoobs.com/noobs/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=AwesomO</id>
	<title>CompleteNoobs - User contributions [en]</title>
	<link rel="self" type="application/atom+xml" href="https://www.completenoobs.com/noobs/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=AwesomO"/>
	<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/Special:Contributions/AwesomO"/>
	<updated>2026-04-30T01:12:41Z</updated>
	<subtitle>User contributions</subtitle>
	<generator>MediaWiki 1.43.1</generator>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Delegate_Hive_Power_(HP)_to_Another_Account&amp;diff=748</id>
		<title>Delegate Hive Power (HP) to Another Account</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Delegate_Hive_Power_(HP)_to_Another_Account&amp;diff=748"/>
		<updated>2026-04-27T11:02:17Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot;= Delegate Hive Power (HP) to Another Account =  &amp;#039;&amp;#039;&amp;#039;CompleteNoobs Walkthrough&amp;#039;&amp;#039;&amp;#039;   &amp;#039;&amp;#039;How to lend your Hive Power (HP) to another account safely and easily&amp;#039;&amp;#039;  == What is HP Delegation? == Hive Power (HP) is your staked HIVE. When you &amp;#039;&amp;#039;&amp;#039;delegate&amp;#039;&amp;#039;&amp;#039; HP to another account, you are temporarily lending them your voting power and Resource Credits (RC).    - You still &amp;#039;&amp;#039;&amp;#039;own&amp;#039;&amp;#039;&amp;#039; the HP 100%. - The other person can use the voting power to upvote posts. - You can take it back when...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Delegate Hive Power (HP) to Another Account =&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;CompleteNoobs Walkthrough&#039;&#039;&#039;  &lt;br /&gt;
&#039;&#039;How to lend your Hive Power (HP) to another account safely and easily&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== What is HP Delegation? ==&lt;br /&gt;
Hive Power (HP) is your staked HIVE. When you &#039;&#039;&#039;delegate&#039;&#039;&#039; HP to another account, you are temporarily lending them your voting power and Resource Credits (RC).  &lt;br /&gt;
&lt;br /&gt;
- You still &#039;&#039;&#039;own&#039;&#039;&#039; the HP 100%.&lt;br /&gt;
- The other person can use the voting power to upvote posts.&lt;br /&gt;
- You can take it back whenever you want (undelegate).&lt;br /&gt;
- This is very common on Hive to support projects, friends, curators, or to rent voting power.&lt;br /&gt;
&lt;br /&gt;
== Why Would You Do This? ==&lt;br /&gt;
- Help a friend or project get more voting power&lt;br /&gt;
- Support a curation team or witness&lt;br /&gt;
- Earn a small fee by renting your HP (some people do this)&lt;br /&gt;
- Move voting power between your own accounts&lt;br /&gt;
&lt;br /&gt;
== Important Warnings (Read This First) ==&lt;br /&gt;
&#039;&#039;&#039;⚠️ Only delegate what you are comfortable lending.&#039;&#039;&#039;  &lt;br /&gt;
&#039;&#039;&#039;⚠️ Never share your private keys or posting key.&#039;&#039;&#039;  &lt;br /&gt;
&#039;&#039;&#039;⚠️ Always double-check the &amp;quot;To&amp;quot; account name before confirming.&#039;&#039;&#039;  &lt;br /&gt;
&#039;&#039;&#039;⚠️ Start with a small test amount (e.g. 10 HP) the first time.&#039;&#039;&#039;  &lt;br /&gt;
&#039;&#039;&#039;⚠️ Use trusted interfaces only (PeakD or Hive Keychain recommended).&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
- A Hive account with some Hive Power (HP)&lt;br /&gt;
- Either:&lt;br /&gt;
  - Hive Keychain browser extension (recommended), &#039;&#039;&#039;or&#039;&#039;&#039;&lt;br /&gt;
  - Access to PeakD.com&lt;br /&gt;
&lt;br /&gt;
== Method 1: Easiest – Using PeakD.com (Web Interface) ==&lt;br /&gt;
&lt;br /&gt;
=== Step 1: Go to PeakD ===&lt;br /&gt;
# Open your browser and go to: [https://peakd.com/ https://peakd.com/]&lt;br /&gt;
# Click &#039;&#039;&#039;Login&#039;&#039;&#039; (top right) and log in with &#039;&#039;&#039;Hive Keychain&#039;&#039;&#039; (easiest) or your posting/active key.&lt;br /&gt;
&lt;br /&gt;
=== Step 2: Open Your Wallet ===&lt;br /&gt;
# Click on your username (top right) → &#039;&#039;&#039;Wallet&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Step 3: Start the Delegation ===&lt;br /&gt;
# In the wallet, look for the big blue button that says &#039;&#039;&#039;Delegate HP&#039;&#039;&#039; (or &amp;quot;Power Delegation&amp;quot;).&lt;br /&gt;
# If you don&#039;t see it immediately, scroll down to the &#039;&#039;&#039;&amp;quot;Delegations&amp;quot;&#039;&#039;&#039; section and click &#039;&#039;&#039;Delegate&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== Step 4: Fill in the Details ===&lt;br /&gt;
# In the popup/window that appears:&lt;br /&gt;
#* &#039;&#039;&#039;To:&#039;&#039;&#039; → Type the exact Hive username you want to delegate to (e.g. &amp;lt;code&amp;gt;@completenoobs&amp;lt;/code&amp;gt;)&lt;br /&gt;
#* &#039;&#039;&#039;Amount:&#039;&#039;&#039; → Enter how much HP you want to delegate (you can use decimals, e.g. &amp;lt;code&amp;gt;50.123&amp;lt;/code&amp;gt;)&lt;br /&gt;
#* (Optional) Add a memo if you want&lt;br /&gt;
&lt;br /&gt;
=== Step 5: Confirm and Send ===&lt;br /&gt;
# Review everything carefully.&lt;br /&gt;
# Click &#039;&#039;&#039;Delegate&#039;&#039;&#039; or &#039;&#039;&#039;Confirm&#039;&#039;&#039;.&lt;br /&gt;
# Approve the transaction in Hive Keychain (or enter your active key if using manual login).&lt;br /&gt;
# You should see a green success message.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Done!&#039;&#039;&#039; The HP is now delegated.&lt;br /&gt;
&lt;br /&gt;
== Method 2: Fastest – Using Hive Keychain Extension (Recommended) ==&lt;br /&gt;
&lt;br /&gt;
This is the quickest way once you have the extension installed.&lt;br /&gt;
&lt;br /&gt;
=== Step 1: Install Hive Keychain (if you haven&#039;t) ===&lt;br /&gt;
# Go to the Chrome Web Store (or Firefox Add-ons) and search for &#039;&#039;&#039;&amp;quot;Hive Keychain&amp;quot;&#039;&#039;&#039;.&lt;br /&gt;
# Install it and set it up with your Hive account.&lt;br /&gt;
&lt;br /&gt;
=== Step 2: Open Keychain ===&lt;br /&gt;
# Click the little Hive Keychain icon in your browser toolbar.&lt;br /&gt;
# Make sure you are logged into the account that has the HP.&lt;br /&gt;
&lt;br /&gt;
=== Step 3: Choose Delegate ===&lt;br /&gt;
# In the Keychain popup, click the &#039;&#039;&#039;Delegate&#039;&#039;&#039; button (it usually has a little arrow icon).&lt;br /&gt;
&lt;br /&gt;
=== Step 4: Enter Details ===&lt;br /&gt;
# Fill in:&lt;br /&gt;
#* &#039;&#039;&#039;Delegate to:&#039;&#039;&#039; → The username (without the @)&lt;br /&gt;
#* &#039;&#039;&#039;Amount:&#039;&#039;&#039; → How much HP to send&lt;br /&gt;
# Click &#039;&#039;&#039;Next&#039;&#039;&#039; → Review → &#039;&#039;&#039;Confirm&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Done!&#039;&#039;&#039; Super fast.&lt;br /&gt;
&lt;br /&gt;
== How to Check Your Current Delegations ==&lt;br /&gt;
&lt;br /&gt;
=== On PeakD ===&lt;br /&gt;
# Go to your Wallet → scroll down to the &#039;&#039;&#039;Delegations&#039;&#039;&#039; tab.&lt;br /&gt;
# You will see two sections:&lt;br /&gt;
#* &#039;&#039;&#039;Outgoing&#039;&#039;&#039; (what you have delegated to others)&lt;br /&gt;
#* &#039;&#039;&#039;Incoming&#039;&#039;&#039; (what others have delegated to you)&lt;br /&gt;
&lt;br /&gt;
=== On Hive Keychain ===&lt;br /&gt;
# Click the Keychain icon → &#039;&#039;&#039;Delegations&#039;&#039;&#039; tab.&lt;br /&gt;
&lt;br /&gt;
== How to Undelegate (Take Your HP Back) ==&lt;br /&gt;
&lt;br /&gt;
The process is almost identical to delegating:&lt;br /&gt;
&lt;br /&gt;
1. Go back to PeakD Wallet or Hive Keychain.&lt;br /&gt;
2. Find the delegation you want to cancel.&lt;br /&gt;
3. Click &#039;&#039;&#039;Undelegate&#039;&#039;&#039; (or set the amount to 0).&lt;br /&gt;
4. Confirm the transaction.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note:&#039;&#039;&#039; On Hive, undelegation usually returns the power within a few minutes to a few hours (much faster than the old Steem 5-day cooldown).&lt;br /&gt;
&lt;br /&gt;
== Advanced: Using the Command Line (CLI) ==&lt;br /&gt;
&lt;br /&gt;
Only do this if you are comfortable with the terminal.&lt;br /&gt;
&lt;br /&gt;
Using &amp;lt;code&amp;gt;beem&amp;lt;/code&amp;gt; Python library (example):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
beempy delegate @youraccount @targetaccount 100 HP&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Or using the official hived CLI (more advanced).&lt;br /&gt;
== Common Questions ==&lt;br /&gt;
* &#039;&#039;&#039;Q: Can I delegate all my HP?&#039;&#039;&#039;&lt;br /&gt;
A: Yes, but leave a little for yourself so you can still make transactions (you need some RC).&lt;br /&gt;
* &#039;&#039;&#039;Q: Do I still earn rewards on delegated HP?&#039;&#039;&#039;&lt;br /&gt;
A: Yes — you still own the HP and continue to receive the 10% annual inflation reward on it. Only the &#039;&#039;&#039;voting power&#039;&#039;&#039; is lent.&lt;br /&gt;
* &#039;&#039;&#039;Q: Is there a fee?&#039;&#039;&#039;&lt;br /&gt;
A: No delegation fee on Hive (only tiny blockchain resource cost).&lt;br /&gt;
*&#039;&#039;&#039;Q: Can the person I delegate to steal my HP?&#039;&#039;&#039;&lt;br /&gt;
A: No. They can only use the voting power. You always keep ownership.&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Main_Page&amp;diff=747</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Main_Page&amp;diff=747"/>
		<updated>2026-04-27T01:31:37Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* In Concept development Mode - a wiki you can download */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
=In Concept development Mode - a wiki you can download=&lt;br /&gt;
&lt;br /&gt;
*  [https://www.completenoobs.com/noobs/V4call-v0.11| cnoobs.com on hold while working on v4call]&lt;br /&gt;
*  https://github.com/CompleteNoobs/v4call&lt;br /&gt;
&lt;br /&gt;
= CompleteNoobs: A Downloadable Wiki for Reproducible Computer Tutorials =&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;CompleteNoobs&#039;&#039;&#039; is a open-source wiki offering free, reproducible computer science tutorials. &lt;br /&gt;
&lt;br /&gt;
* [[Ubuntu2404_Install_Docker_and_Docker_Compose| Download the wiki as a &#039;&#039;&#039;Docker image&#039;&#039;&#039; to run locally on your computer or fork it to contribute and share knowledge.]]&lt;br /&gt;
&lt;br /&gt;
== About CompleteNoobs ==&lt;br /&gt;
&lt;br /&gt;
Our mission is to provide accessible, libre-licensed resources for hobbyists, sysadmins, students, teachers, and computer science enthusiasts. All content is available under a &#039;&#039;&#039;Creative Commons BY-NC-SA&#039;&#039;&#039; license, ensuring the freedoms to:&lt;br /&gt;
* Read&lt;br /&gt;
* Edit/Modify&lt;br /&gt;
* Copy&lt;br /&gt;
* Share freely&lt;br /&gt;
&lt;br /&gt;
Download the wiki as an XML file at [https://xml.completenoobs.com xml.completenoobs.com] or access data-heavy content (images, videos, audio) via &#039;&#039;&#039;IPFS&#039;&#039;&#039; hashes.&lt;br /&gt;
&lt;br /&gt;
== Get Started ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Download the Wiki&#039;&#039;&#039;: Run CompleteNoobs locally using our [[Local_CompleteNoobs_Wiki|Manually install guides or Docker image]].&lt;br /&gt;
* &#039;&#039;&#039;Host Your Own&#039;&#039;&#039;: Set up your own MediaWiki instance with our guide: [[Host_Your_Own_Mediawiki_Online|Host Your Own MediaWiki Online]].&lt;br /&gt;
&lt;br /&gt;
== CompleteNoobs Blockchain Project ==&lt;br /&gt;
&lt;br /&gt;
As Noobs we want to learn more about bitcoin, without losing any real bitcoin&lt;br /&gt;
&lt;br /&gt;
First we are learning how to fork bitcoin version 0.14.3, best way to learn is by tinkering with it.&lt;br /&gt;
* [[N33Bcoin| n33b.com]]&lt;br /&gt;
&lt;br /&gt;
== Essential Links ==&lt;br /&gt;
&lt;br /&gt;
* [[Main_Index|Main Index Page]]&lt;br /&gt;
* [[Special:AllPages|All Pages]]&lt;br /&gt;
* [[Wiki_Basic_Syntax|Wiki Basic Syntax]]&lt;br /&gt;
* [[COMPLETENOOBS_FUNDING|Support Us]]&lt;br /&gt;
&lt;br /&gt;
== Licenses ==&lt;br /&gt;
[[LICENCE_HEADERS]]&lt;br /&gt;
All content is libre-licensed for free copying, modification, and distribution. Add a license header to your contributions using:&lt;br /&gt;
&amp;lt;pre&amp;gt;{{:LICENCE_HEADER_CC0}}&amp;lt;/pre&amp;gt;&lt;br /&gt;
{{:LICENCE_HEADER_CC0}}&lt;br /&gt;
&lt;br /&gt;
== Disclaimer ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;&#039;&#039;DISCLAIMER:&#039;&#039;&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
Content on CompleteNoobs is for informational and educational purposes only. We make no warranties about accuracy or reliability. Use at your own risk. Links to external sites are not endorsements, and we are not responsible for their content or availability. The site may be temporarily unavailable due to technical issues beyond our control.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Special:ContributionScores/10/5}}&lt;br /&gt;
{{Special:PopularPages/10}}&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Main_Page&amp;diff=746</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Main_Page&amp;diff=746"/>
		<updated>2026-04-27T01:11:48Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* In Concept development Mode - a wiki you can download */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
=In Concept development Mode - a wiki you can download=&lt;br /&gt;
&lt;br /&gt;
*  [https://www.completenoobs.com/noobs/V4call-v0.11| cnoobs.com on hold while working on v4call]&lt;br /&gt;
&lt;br /&gt;
= CompleteNoobs: A Downloadable Wiki for Reproducible Computer Tutorials =&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;CompleteNoobs&#039;&#039;&#039; is a open-source wiki offering free, reproducible computer science tutorials. &lt;br /&gt;
&lt;br /&gt;
* [[Ubuntu2404_Install_Docker_and_Docker_Compose| Download the wiki as a &#039;&#039;&#039;Docker image&#039;&#039;&#039; to run locally on your computer or fork it to contribute and share knowledge.]]&lt;br /&gt;
&lt;br /&gt;
== About CompleteNoobs ==&lt;br /&gt;
&lt;br /&gt;
Our mission is to provide accessible, libre-licensed resources for hobbyists, sysadmins, students, teachers, and computer science enthusiasts. All content is available under a &#039;&#039;&#039;Creative Commons BY-NC-SA&#039;&#039;&#039; license, ensuring the freedoms to:&lt;br /&gt;
* Read&lt;br /&gt;
* Edit/Modify&lt;br /&gt;
* Copy&lt;br /&gt;
* Share freely&lt;br /&gt;
&lt;br /&gt;
Download the wiki as an XML file at [https://xml.completenoobs.com xml.completenoobs.com] or access data-heavy content (images, videos, audio) via &#039;&#039;&#039;IPFS&#039;&#039;&#039; hashes.&lt;br /&gt;
&lt;br /&gt;
== Get Started ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Download the Wiki&#039;&#039;&#039;: Run CompleteNoobs locally using our [[Local_CompleteNoobs_Wiki|Manually install guides or Docker image]].&lt;br /&gt;
* &#039;&#039;&#039;Host Your Own&#039;&#039;&#039;: Set up your own MediaWiki instance with our guide: [[Host_Your_Own_Mediawiki_Online|Host Your Own MediaWiki Online]].&lt;br /&gt;
&lt;br /&gt;
== CompleteNoobs Blockchain Project ==&lt;br /&gt;
&lt;br /&gt;
As Noobs we want to learn more about bitcoin, without losing any real bitcoin&lt;br /&gt;
&lt;br /&gt;
First we are learning how to fork bitcoin version 0.14.3, best way to learn is by tinkering with it.&lt;br /&gt;
* [[N33Bcoin| n33b.com]]&lt;br /&gt;
&lt;br /&gt;
== Essential Links ==&lt;br /&gt;
&lt;br /&gt;
* [[Main_Index|Main Index Page]]&lt;br /&gt;
* [[Special:AllPages|All Pages]]&lt;br /&gt;
* [[Wiki_Basic_Syntax|Wiki Basic Syntax]]&lt;br /&gt;
* [[COMPLETENOOBS_FUNDING|Support Us]]&lt;br /&gt;
&lt;br /&gt;
== Licenses ==&lt;br /&gt;
[[LICENCE_HEADERS]]&lt;br /&gt;
All content is libre-licensed for free copying, modification, and distribution. Add a license header to your contributions using:&lt;br /&gt;
&amp;lt;pre&amp;gt;{{:LICENCE_HEADER_CC0}}&amp;lt;/pre&amp;gt;&lt;br /&gt;
{{:LICENCE_HEADER_CC0}}&lt;br /&gt;
&lt;br /&gt;
== Disclaimer ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;&#039;&#039;DISCLAIMER:&#039;&#039;&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
Content on CompleteNoobs is for informational and educational purposes only. We make no warranties about accuracy or reliability. Use at your own risk. Links to external sites are not endorsements, and we are not responsible for their content or availability. The site may be temporarily unavailable due to technical issues beyond our control.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Special:ContributionScores/10/5}}&lt;br /&gt;
{{Special:PopularPages/10}}&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=HTML_Expanding_info_box&amp;diff=745</id>
		<title>HTML Expanding info box</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=HTML_Expanding_info_box&amp;diff=745"/>
		<updated>2026-04-27T01:08:48Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* Full Example Page */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== How to Add an Expanding Info Box (Simple Guide)==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Feature&lt;br /&gt;
! Value&lt;br /&gt;
|-&lt;br /&gt;
| Method&lt;br /&gt;
| &amp;amp;lt;details&amp;amp;gt; + &amp;amp;lt;summary&amp;amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| JavaScript&lt;br /&gt;
| Not needed&lt;br /&gt;
|-&lt;br /&gt;
| Mobile Support&lt;br /&gt;
| Yes&lt;br /&gt;
|-&lt;br /&gt;
| Difficulty&lt;br /&gt;
| Very Easy&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Basic Example===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;details&amp;gt;&lt;br /&gt;
  &amp;lt;summary&amp;gt;What you will like&amp;lt;/summary&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;div&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;This content is hidden until clicked.&amp;lt;/p&amp;gt;&lt;br /&gt;
 &amp;lt;code&amp;gt;print &amp;quot;hello content&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/details&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Optional CSS Styling===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
body {&lt;br /&gt;
  font-family: sans-serif;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
details {&lt;br /&gt;
  border: 1px solid #aaa;&lt;br /&gt;
  margin: 20px 0;&lt;br /&gt;
  padding: 5px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
summary {&lt;br /&gt;
  font-weight: bold;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Dark Mode (Optional)===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
@media (prefers-color-scheme: dark) {&lt;br /&gt;
  details {&lt;br /&gt;
    background: #222;&lt;br /&gt;
    color: #ddd;&lt;br /&gt;
    border: 1px solid #555;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  summary {&lt;br /&gt;
    color: #fff;&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Full Example Page===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;title&amp;gt;Example&amp;lt;/title&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;style&amp;gt;&lt;br /&gt;
    body {&lt;br /&gt;
      font-family: sans-serif;&lt;br /&gt;
      max-width: 800px;&lt;br /&gt;
      margin: 40px auto;&lt;br /&gt;
      padding: 20px;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    details {&lt;br /&gt;
      border: 1px solid #aaa;&lt;br /&gt;
      margin: 20px 0;&lt;br /&gt;
      padding: 5px;&lt;br /&gt;
      background: #f9f9f9;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    summary {&lt;br /&gt;
      font-weight: bold;&lt;br /&gt;
      cursor: pointer;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /* Dark Mode */&lt;br /&gt;
    @media (prefers-color-scheme: dark) {&lt;br /&gt;
      body {&lt;br /&gt;
        background: #111;&lt;br /&gt;
        color: #ddd;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      details {&lt;br /&gt;
        background: #222;&lt;br /&gt;
        border: 1px solid #555;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      summary {&lt;br /&gt;
        color: #fff;&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
  &amp;lt;/style&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;h1&amp;gt;Example Page&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;details&amp;gt;&lt;br /&gt;
  &amp;lt;summary&amp;gt;What you will like&amp;lt;/summary&amp;gt;&lt;br /&gt;
  &amp;lt;div&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;Hidden content here...&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;code&amp;gt;print &amp;quot;hello content&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/details&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- bug hunt --&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Another example:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;title&amp;gt;completenoobs.com&amp;lt;/title&amp;gt;&lt;br /&gt;
  &amp;lt;style&amp;gt;&lt;br /&gt;
    body { font-family: system-ui, -apple-system, sans-serif; max-width: 800px; margin: 40px auto; padding: 20px; line-height: 1.6; }&lt;br /&gt;
    &lt;br /&gt;
    details {&lt;br /&gt;
      border: 1px solid #ddd;&lt;br /&gt;
      border-radius: 8px;&lt;br /&gt;
      margin: 25px 0;&lt;br /&gt;
      background: #f9f9f9;&lt;br /&gt;
    }&lt;br /&gt;
    summary {&lt;br /&gt;
      padding: 15px 20px;&lt;br /&gt;
      font-weight: bold;&lt;br /&gt;
      cursor: pointer;&lt;br /&gt;
      background: #f0f0f0;&lt;br /&gt;
      border-radius: 8px 8px 0 0;&lt;br /&gt;
    }&lt;br /&gt;
    .mw-collapsible-content { padding: 20px; }&lt;br /&gt;
&lt;br /&gt;
    /* Dark Mode */&lt;br /&gt;
    @media (prefers-color-scheme: dark) {&lt;br /&gt;
      details { border: 1px solid #555; background: #1e1e1e; color: #ddd; }&lt;br /&gt;
      summary { background: #2a2a2a; color: #eee; }&lt;br /&gt;
      .mw-collapsible-content { color: #ccc; }&lt;br /&gt;
    }&lt;br /&gt;
  &amp;lt;/style&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;h1&amp;gt;Welcome to completenoobs.com&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;details&amp;gt;&lt;br /&gt;
    &amp;lt;summary&amp;gt;What you will like&amp;lt;/summary&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
      &amp;lt;p&amp;gt;Put all your hidden content here...&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;code&amp;gt;print &amp;quot;hello content&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
  &amp;lt;/details&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Tips===&lt;br /&gt;
&lt;br /&gt;
* To open by default: &amp;lt;code&amp;gt;&amp;amp;lt;details open&amp;amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
* Duplicate the block to create more sections&lt;br /&gt;
* Works without CSS (just looks basic)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
The CSS explained:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;What is CSS?&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
CSS is what makes things look nice.&lt;br /&gt;
&lt;br /&gt;
* HTML = the structure (the box)&lt;br /&gt;
* CSS = the style (colors, spacing, fonts)&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;The Page (body)&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
body {&lt;br /&gt;
  font-family: sans-serif;&lt;br /&gt;
  max-width: 800px;&lt;br /&gt;
  margin: 40px auto;&lt;br /&gt;
  padding: 20px;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This controls the whole page:&lt;br /&gt;
&lt;br /&gt;
* Changes the font&lt;br /&gt;
* Keeps the page centered&lt;br /&gt;
* Adds space so things are easy to read&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;The Box (details)&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
details {&lt;br /&gt;
  border: 1px solid #aaa;&lt;br /&gt;
  margin: 20px 0;&lt;br /&gt;
  padding: 5px;&lt;br /&gt;
  background: #f9f9f9;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the expandable box:&lt;br /&gt;
&lt;br /&gt;
* Border = thin line around it&lt;br /&gt;
* Margin = space outside the box&lt;br /&gt;
* Padding = space inside the box&lt;br /&gt;
* Background = box color&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;The Clickable Title (summary)&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
summary {&lt;br /&gt;
  font-weight: bold;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is what you click:&lt;br /&gt;
&lt;br /&gt;
* Bold text&lt;br /&gt;
* Mouse turns into a hand&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dark Mode (automatic)&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
@media (prefers-color-scheme: dark) {&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This means:&lt;br /&gt;
&lt;br /&gt;
&amp;quot;If the user is using dark mode, use different colors&amp;quot;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dark Mode Colours&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
body {&lt;br /&gt;
  background: #111;&lt;br /&gt;
  color: #ddd;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
details {&lt;br /&gt;
  background: #222;&lt;br /&gt;
  border: 1px solid #555;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
summary {&lt;br /&gt;
  color: #fff;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This makes:&lt;br /&gt;
&lt;br /&gt;
* Dark background&lt;br /&gt;
* Light text&lt;br /&gt;
* Darker boxes&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;How to Change Colours&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* Darker background → change &amp;lt;code&amp;gt;#111&amp;lt;/code&amp;gt;&lt;br /&gt;
* Lighter text → change &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt;&lt;br /&gt;
* Box color → change &amp;lt;code&amp;gt;#222&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Examples:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;#000&amp;lt;/code&amp;gt; = pure black&lt;br /&gt;
* &amp;lt;code&amp;gt;#fff&amp;lt;/code&amp;gt; = pure white&lt;br /&gt;
* &amp;lt;code&amp;gt;#1a1a1a&amp;lt;/code&amp;gt; = softer black&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Force Dark Mode (always on)&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Remove the &amp;lt;code&amp;gt;@media&amp;lt;/code&amp;gt; line and just use:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
body {&lt;br /&gt;
  background: #111;&lt;br /&gt;
  color: #ddd;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Super Simple Summary&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* CSS = makes things look nice&lt;br /&gt;
* details = the box&lt;br /&gt;
* summary = the clickable title&lt;br /&gt;
* dark mode = automatic color switch&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;!-- AI&#039;s used in creating page, grok and chatgpt 27-04-2026 --&amp;gt;&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=HTML_Expanding_info_box&amp;diff=744</id>
		<title>HTML Expanding info box</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=HTML_Expanding_info_box&amp;diff=744"/>
		<updated>2026-04-27T01:02:11Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot;== How to Add an Expanding Info Box (Simple Guide)==  {| class=&amp;quot;wikitable&amp;quot; |- ! Feature ! Value |- | Method | &amp;amp;lt;details&amp;amp;gt; + &amp;amp;lt;summary&amp;amp;gt; |- | JavaScript | Not needed |- | Mobile Support | Yes |- | Difficulty | Very Easy |}  === Basic Example===  &amp;lt;pre&amp;gt; &amp;lt;details&amp;gt;   &amp;lt;summary&amp;gt;What you will like&amp;lt;/summary&amp;gt;    &amp;lt;div&amp;gt;     &amp;lt;p&amp;gt;This content is hidden until clicked.&amp;lt;/p&amp;gt;  &amp;lt;code&amp;gt;print &amp;quot;hello content&amp;quot;&amp;lt;/code&amp;gt;   &amp;lt;/div&amp;gt;  &amp;lt;/details&amp;gt; &amp;lt;/pre&amp;gt;  === Optional CSS Styling===  &amp;lt;pre&amp;gt; body {...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== How to Add an Expanding Info Box (Simple Guide)==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Feature&lt;br /&gt;
! Value&lt;br /&gt;
|-&lt;br /&gt;
| Method&lt;br /&gt;
| &amp;amp;lt;details&amp;amp;gt; + &amp;amp;lt;summary&amp;amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| JavaScript&lt;br /&gt;
| Not needed&lt;br /&gt;
|-&lt;br /&gt;
| Mobile Support&lt;br /&gt;
| Yes&lt;br /&gt;
|-&lt;br /&gt;
| Difficulty&lt;br /&gt;
| Very Easy&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Basic Example===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;details&amp;gt;&lt;br /&gt;
  &amp;lt;summary&amp;gt;What you will like&amp;lt;/summary&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;div&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;This content is hidden until clicked.&amp;lt;/p&amp;gt;&lt;br /&gt;
 &amp;lt;code&amp;gt;print &amp;quot;hello content&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/details&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Optional CSS Styling===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
body {&lt;br /&gt;
  font-family: sans-serif;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
details {&lt;br /&gt;
  border: 1px solid #aaa;&lt;br /&gt;
  margin: 20px 0;&lt;br /&gt;
  padding: 5px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
summary {&lt;br /&gt;
  font-weight: bold;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Dark Mode (Optional)===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
@media (prefers-color-scheme: dark) {&lt;br /&gt;
  details {&lt;br /&gt;
    background: #222;&lt;br /&gt;
    color: #ddd;&lt;br /&gt;
    border: 1px solid #555;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  summary {&lt;br /&gt;
    color: #fff;&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Full Example Page===&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;title&amp;gt;Example&amp;lt;/title&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;style&amp;gt;&lt;br /&gt;
    body {&lt;br /&gt;
      font-family: sans-serif;&lt;br /&gt;
      max-width: 800px;&lt;br /&gt;
      margin: 40px auto;&lt;br /&gt;
      padding: 20px;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    details {&lt;br /&gt;
      border: 1px solid #aaa;&lt;br /&gt;
      margin: 20px 0;&lt;br /&gt;
      padding: 5px;&lt;br /&gt;
      background: #f9f9f9;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    summary {&lt;br /&gt;
      font-weight: bold;&lt;br /&gt;
      cursor: pointer;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /* Dark Mode */&lt;br /&gt;
    @media (prefers-color-scheme: dark) {&lt;br /&gt;
      body {&lt;br /&gt;
        background: #111;&lt;br /&gt;
        color: #ddd;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      details {&lt;br /&gt;
        background: #222;&lt;br /&gt;
        border: 1px solid #555;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      summary {&lt;br /&gt;
        color: #fff;&lt;br /&gt;
      }&lt;br /&gt;
    }&lt;br /&gt;
  &amp;lt;/style&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;h1&amp;gt;Example Page&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;details&amp;gt;&lt;br /&gt;
  &amp;lt;summary&amp;gt;What you will like&amp;lt;/summary&amp;gt;&lt;br /&gt;
  &amp;lt;div&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;Hidden content here...&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;code&amp;gt;print &amp;quot;hello content&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/details&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Tips===&lt;br /&gt;
&lt;br /&gt;
* To open by default: &amp;lt;code&amp;gt;&amp;amp;lt;details open&amp;amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
* Duplicate the block to create more sections&lt;br /&gt;
* Works without CSS (just looks basic)&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
The CSS explained:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;What is CSS?&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
CSS is what makes things look nice.&lt;br /&gt;
&lt;br /&gt;
* HTML = the structure (the box)&lt;br /&gt;
* CSS = the style (colors, spacing, fonts)&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;The Page (body)&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
body {&lt;br /&gt;
  font-family: sans-serif;&lt;br /&gt;
  max-width: 800px;&lt;br /&gt;
  margin: 40px auto;&lt;br /&gt;
  padding: 20px;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This controls the whole page:&lt;br /&gt;
&lt;br /&gt;
* Changes the font&lt;br /&gt;
* Keeps the page centered&lt;br /&gt;
* Adds space so things are easy to read&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;The Box (details)&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
details {&lt;br /&gt;
  border: 1px solid #aaa;&lt;br /&gt;
  margin: 20px 0;&lt;br /&gt;
  padding: 5px;&lt;br /&gt;
  background: #f9f9f9;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the expandable box:&lt;br /&gt;
&lt;br /&gt;
* Border = thin line around it&lt;br /&gt;
* Margin = space outside the box&lt;br /&gt;
* Padding = space inside the box&lt;br /&gt;
* Background = box color&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;The Clickable Title (summary)&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
summary {&lt;br /&gt;
  font-weight: bold;&lt;br /&gt;
  cursor: pointer;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is what you click:&lt;br /&gt;
&lt;br /&gt;
* Bold text&lt;br /&gt;
* Mouse turns into a hand&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dark Mode (automatic)&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
@media (prefers-color-scheme: dark) {&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This means:&lt;br /&gt;
&lt;br /&gt;
&amp;quot;If the user is using dark mode, use different colors&amp;quot;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Dark Mode Colours&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
body {&lt;br /&gt;
  background: #111;&lt;br /&gt;
  color: #ddd;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
details {&lt;br /&gt;
  background: #222;&lt;br /&gt;
  border: 1px solid #555;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
summary {&lt;br /&gt;
  color: #fff;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This makes:&lt;br /&gt;
&lt;br /&gt;
* Dark background&lt;br /&gt;
* Light text&lt;br /&gt;
* Darker boxes&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;How to Change Colours&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* Darker background → change &amp;lt;code&amp;gt;#111&amp;lt;/code&amp;gt;&lt;br /&gt;
* Lighter text → change &amp;lt;code&amp;gt;#ddd&amp;lt;/code&amp;gt;&lt;br /&gt;
* Box color → change &amp;lt;code&amp;gt;#222&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Examples:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;#000&amp;lt;/code&amp;gt; = pure black&lt;br /&gt;
* &amp;lt;code&amp;gt;#fff&amp;lt;/code&amp;gt; = pure white&lt;br /&gt;
* &amp;lt;code&amp;gt;#1a1a1a&amp;lt;/code&amp;gt; = softer black&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Force Dark Mode (always on)&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Remove the &amp;lt;code&amp;gt;@media&amp;lt;/code&amp;gt; line and just use:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
body {&lt;br /&gt;
  background: #111;&lt;br /&gt;
  color: #ddd;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Super Simple Summary&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* CSS = makes things look nice&lt;br /&gt;
* details = the box&lt;br /&gt;
* summary = the clickable title&lt;br /&gt;
* dark mode = automatic color switch&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;!-- AI&#039;s used in creating page, grok and chatgpt 27-04-2026 --&amp;gt;&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=V4call-v0.11&amp;diff=743</id>
		<title>V4call-v0.11</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=V4call-v0.11&amp;diff=743"/>
		<updated>2026-04-25T01:49:08Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot;{{:LICENCE_HEADER_MIT}}   = v4call — Deploy Your Own Server on Ubuntu 24.04 with Docker =  From CompleteNoobs  This guide walks through deploying your own v4call server from scratch on a fresh Vultr Ubuntu 24.04 VPS — from first login to a working HTTPS video/audio calling service on your own domain, optionally federated with other v4call servers.  v4call is an open-source, decentralised video and audio calling platform that uses Hive blockchain for identity and HBD...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{:LICENCE_HEADER_MIT}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= v4call — Deploy Your Own Server on Ubuntu 24.04 with Docker =&lt;br /&gt;
&lt;br /&gt;
From CompleteNoobs&lt;br /&gt;
&lt;br /&gt;
This guide walks through deploying your own v4call server from scratch on a fresh Vultr Ubuntu 24.04 VPS — from first login to a working HTTPS video/audio calling service on your own domain, optionally federated with other v4call servers.&lt;br /&gt;
&lt;br /&gt;
v4call is an open-source, decentralised video and audio calling platform that uses Hive blockchain for identity and HBD micropayments. It supports custom Hive-Engine token payments, encrypted direct messages with persistent chat history, voice-only and video calls, and a free-market platform fee system. Fork it, run your own server, keep all your platform fees, join the federation.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Source code&#039;&#039;&#039;: [https://github.com/CompleteNoobs/v4call https://github.com/CompleteNoobs/v4call]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;End result&#039;&#039;&#039;: A working v4call server at &amp;lt;code&amp;gt;https://call.yourdomain.com&amp;lt;/code&amp;gt; that you can log into with a Hive account, optionally connected to other v4call servers via federation.&lt;br /&gt;
&lt;br /&gt;
== Contents ==&lt;br /&gt;
&lt;br /&gt;
* [[#What_You_Need|1 What You Need]]&lt;br /&gt;
* [[#Step_1:_Create_Your_Vultr_VPS|2 Step 1: Create Your Vultr VPS]]&lt;br /&gt;
* [[#Step_2:_Point_Your_Domain_at_the_VPS|3 Step 2: Point Your Domain at the VPS]]&lt;br /&gt;
* [[#Step_3:_Log_into_Your_VPS|4 Step 3: Log into Your VPS]]&lt;br /&gt;
* [[#Step_4:_Update_the_Server|5 Step 4: Update the Server]]&lt;br /&gt;
* [[#Step_5:_Install_Docker|6 Step 5: Install Docker]]&lt;br /&gt;
* [[#Step_6:_Install_Git|7 Step 6: Install Git]]&lt;br /&gt;
* [[#Step_7:_Fork_and_Clone_the_Code|8 Step 7: Fork and Clone the Code]]&lt;br /&gt;
* [[#Step_8:_Configure_Your_Server_(.env_file)|9 Step 8: Configure Your Server (.env file)]]&lt;br /&gt;
* [[#Step_9:_Configure_Nginx_—_HTTP_Only_First|10 Step 9: Configure Nginx — HTTP Only First]]&lt;br /&gt;
* [[#Step_10:_Create_Data_Directories_and_Fix_Permissions|11 Step 10: Create Data Directories and Fix Permissions]]&lt;br /&gt;
* [[#Step_11:_Build_and_Start_the_Server|12 Step 11: Build and Start the Server]]&lt;br /&gt;
* [[#Step_12:_Get_Your_SSL_Certificate|13 Step 12: Get Your SSL Certificate]]&lt;br /&gt;
* [[#Step_13:_Enable_HTTPS_in_Nginx|14 Step 13: Enable HTTPS in Nginx]]&lt;br /&gt;
* [[#Step_14:_Set_Up_SSL_Auto-Renewal|15 Step 14: Set Up SSL Auto-Renewal]]&lt;br /&gt;
* [[#Step_15:_Test_Everything_is_Working|16 Step 15: Test Everything is Working]]&lt;br /&gt;
* [[#Step_16:_Set_Up_Your_Call_Rates_on_Hive|17 Step 16: Set Up Your Call Rates on Hive]]&lt;br /&gt;
* [[#Step_17:_Federation_Setup_(Optional)|18 Step 17: Federation Setup (Optional)]]&lt;br /&gt;
* [[#Feature_Guide:_What_Your_Server_Can_Do|19 Feature Guide: What Your Server Can Do]]&lt;br /&gt;
* [[#Admin_Configuration_Reference|20 Admin Configuration Reference]]&lt;br /&gt;
* [[#Updating_Your_Server|21 Updating Your Server]]&lt;br /&gt;
* [[#Common_Problems_and_Fixes|22 Common Problems and Fixes]]&lt;br /&gt;
* [[#Quick_Reference|23 Quick Reference]]&lt;br /&gt;
&lt;br /&gt;
== What You Need ==&lt;br /&gt;
&lt;br /&gt;
Before starting, make sure you have the following:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;A Vultr account&#039;&#039;&#039; — sign up at [https://vultr.com vultr.com] - please use are [https://www.vultr.com/?ref=7704739 Vultr Referral link] to help us cover server costs.&lt;br /&gt;
* &#039;&#039;&#039;A domain name&#039;&#039;&#039; with DNS access — e.g. &amp;lt;code&amp;gt;call.yourdomain.com&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;Two Hive accounts&#039;&#039;&#039; — one for your server identity (receives platform fees), one for escrow (holds caller funds during calls). Create free accounts at [https://signup.hive.io signup.hive.io]&lt;br /&gt;
* &#039;&#039;&#039;Hive Keychain browser extension&#039;&#039;&#039; — for login and payments. Install from [https://hive-keychain.com hive-keychain.com]&lt;br /&gt;
* &#039;&#039;&#039;A GitHub account&#039;&#039;&#039; — free at [https://github.com github.com]. You will fork the v4call project.&lt;br /&gt;
* &#039;&#039;&#039;A terminal&#039;&#039;&#039; — Mac: Terminal app. Windows: PowerShell or PuTTY.&lt;br /&gt;
&lt;br /&gt;
You do not need to know how to code. Every command can be copy-pasted exactly as shown.&lt;br /&gt;
&lt;br /&gt;
== Step 1: Create Your Vultr VPS ==&lt;br /&gt;
&lt;br /&gt;
# Log into [https://my.vultr.com my.vultr.com]&lt;br /&gt;
# Click &#039;&#039;&#039;Deploy New Server&#039;&#039;&#039;&lt;br /&gt;
# Choose &#039;&#039;&#039;Cloud Compute — Shared CPU&#039;&#039;&#039;&lt;br /&gt;
# Choose a location close to you&lt;br /&gt;
# Choose &#039;&#039;&#039;Ubuntu 24.04 LTS x64&#039;&#039;&#039;&lt;br /&gt;
# Choose the &#039;&#039;&#039;$6/month&#039;&#039;&#039; plan (1 CPU, 1GB RAM)&lt;br /&gt;
# Set Server Hostname to something like &amp;lt;code&amp;gt;v4call-server&amp;lt;/code&amp;gt;&lt;br /&gt;
# Click &#039;&#039;&#039;Deploy Now&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Wait ~60 seconds for it to start. Click the server to find:&lt;br /&gt;
* &#039;&#039;&#039;IP Address&#039;&#039;&#039; — looks like &amp;lt;code&amp;gt;123.456.789.012&amp;lt;/code&amp;gt; — write it down&lt;br /&gt;
* &#039;&#039;&#039;Password&#039;&#039;&#039; — click the eye icon — write it down&lt;br /&gt;
&lt;br /&gt;
== Step 2: Point Your Domain at the VPS ==&lt;br /&gt;
&lt;br /&gt;
Log into your DNS provider and add an A record:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Field !! Value&lt;br /&gt;
|-&lt;br /&gt;
| Type || A&lt;br /&gt;
|-&lt;br /&gt;
| Name || &amp;lt;code&amp;gt;call&amp;lt;/code&amp;gt; (or &amp;lt;code&amp;gt;@&amp;lt;/code&amp;gt; for root domain)&lt;br /&gt;
|-&lt;br /&gt;
| Value || Your VPS IP address&lt;br /&gt;
|-&lt;br /&gt;
| TTL || 300&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
DNS takes a few minutes to propagate. Check from your computer later:&lt;br /&gt;
&lt;br /&gt;
 nslookup call.yourdomain.com&lt;br /&gt;
&lt;br /&gt;
Must show your VPS IP before Step 12. You can continue with all other steps while waiting.&lt;br /&gt;
&lt;br /&gt;
== Step 3: Log into Your VPS ==&lt;br /&gt;
&lt;br /&gt;
Open a terminal on your computer:&lt;br /&gt;
&lt;br /&gt;
 ssh root@YOUR_SERVER_IP&lt;br /&gt;
* NOTE: you can also do &amp;lt;code&amp;gt;root@your-domain.com&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Type &amp;lt;code&amp;gt;yes&amp;lt;/code&amp;gt; when asked about the fingerprint, then paste the password from Vultr (right-click to paste in most terminals).&lt;br /&gt;
&lt;br /&gt;
When you see &amp;lt;code&amp;gt;root@v4call-server:~#&amp;lt;/code&amp;gt; you are in.&lt;br /&gt;
&lt;br /&gt;
== Step 4: Update the Server ==&lt;br /&gt;
&lt;br /&gt;
 apt update &amp;amp;&amp;amp; apt upgrade -y&lt;br /&gt;
&lt;br /&gt;
Wait for it to complete (1-2 minutes).&lt;br /&gt;
&lt;br /&gt;
== Step 5: Install Docker ==&lt;br /&gt;
&lt;br /&gt;
Install Docker using the official installer script:&lt;br /&gt;
&lt;br /&gt;
 curl -fsSL https://get.docker.com | sh&lt;br /&gt;
&lt;br /&gt;
Verify:&lt;br /&gt;
&lt;br /&gt;
 docker --version&lt;br /&gt;
&lt;br /&gt;
Should show &amp;lt;code&amp;gt;Docker version 26.x.x&amp;lt;/code&amp;gt; or similar.&lt;br /&gt;
&lt;br /&gt;
Install Docker Compose plugin:&lt;br /&gt;
&lt;br /&gt;
 apt install -y docker-compose-plugin&lt;br /&gt;
&lt;br /&gt;
Verify:&lt;br /&gt;
&lt;br /&gt;
 docker compose version&lt;br /&gt;
&lt;br /&gt;
Should show &amp;lt;code&amp;gt;Docker Compose version v2.x.x&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Step 6: Install Git ==&lt;br /&gt;
&lt;br /&gt;
 apt install -y git&lt;br /&gt;
 git --version&lt;br /&gt;
&lt;br /&gt;
== Step 7: Fork and Clone the Code ==&lt;br /&gt;
&lt;br /&gt;
=== Clone onto your VPS ===&lt;br /&gt;
&lt;br /&gt;
 cd /opt&lt;br /&gt;
 git clone https://github.com/CompleteNoobs/v4call.git&lt;br /&gt;
 cd v4call&lt;br /&gt;
&lt;br /&gt;
==== Fork on GitHub - Optional ====&lt;br /&gt;
&lt;br /&gt;
Forking gives you your own copy of the code that you can customise — change the name, branding, fees — without affecting the original project.&lt;br /&gt;
&lt;br /&gt;
# Go to [https://github.com/CompleteNoobs/v4call https://github.com/CompleteNoobs/v4call]&lt;br /&gt;
# Click the &#039;&#039;&#039;Fork&#039;&#039;&#039; button (top right of the page)&lt;br /&gt;
# Select your GitHub account as the destination&lt;br /&gt;
# Click &#039;&#039;&#039;Create fork&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
You now have your own copy at &amp;lt;code&amp;gt;https://github.com/YOURUSERNAME/v4call&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Clone onto your VPS ====&lt;br /&gt;
&lt;br /&gt;
 cd /opt&lt;br /&gt;
 git clone https://github.com/YOURUSERNAME/v4call.git&lt;br /&gt;
 cd v4call&lt;br /&gt;
&lt;br /&gt;
List the files to confirm:&lt;br /&gt;
&lt;br /&gt;
 ls&lt;br /&gt;
&lt;br /&gt;
You should see: &amp;lt;code&amp;gt;server.js  public/  Dockerfile  docker-compose.yml  nginx/  package.json  .env.example  README.md&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Worth knowing about the &amp;lt;code&amp;gt;public/&amp;lt;/code&amp;gt; folder&#039;&#039;&#039; — alongside the main UI (&amp;lt;code&amp;gt;index.html&amp;lt;/code&amp;gt;) you&#039;ll find:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;rate-editor.html&amp;lt;/code&amp;gt; — generates your rates post for Hive&lt;br /&gt;
* &amp;lt;code&amp;gt;server-sign.html&amp;lt;/code&amp;gt; — generates the signed federation verify file (used in [[#Step_17:_Federation_Setup_(Optional)|Federation Setup]] below)&lt;br /&gt;
* &amp;lt;code&amp;gt;server-announce.html&amp;lt;/code&amp;gt; — publishes your server to the on-chain federation directory&lt;br /&gt;
* &amp;lt;code&amp;gt;admin-peers.html&amp;lt;/code&amp;gt; — manages your federation peer list&lt;br /&gt;
* &amp;lt;code&amp;gt;info.html&amp;lt;/code&amp;gt; — public landing page for visitors who haven&#039;t logged in&lt;br /&gt;
* &amp;lt;code&amp;gt;.well-known/v4call-server.json&amp;lt;/code&amp;gt; — placeholder; each operator overwrites this with their own signed file in [[#Step_17:_Federation_Setup_(Optional)|Federation Setup]]&lt;br /&gt;
&lt;br /&gt;
== Step 8: Configure Your Server (.env file) ==&lt;br /&gt;
&lt;br /&gt;
All settings live in a single &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; file. Copy the template:&lt;br /&gt;
&lt;br /&gt;
 cp .env.example .env&lt;br /&gt;
 nano .env&lt;br /&gt;
&lt;br /&gt;
Fill in your values:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# ── Server Identity ──────────────────────────────────────&lt;br /&gt;
SERVER_NAME=yourcallapp&lt;br /&gt;
SERVER_DOMAIN=call.yourdomain.com&lt;br /&gt;
SERVER_HIVE_ACCOUNT=yourhiveaccount&lt;br /&gt;
ESCROW_ACCOUNT=yourescrowaccount&lt;br /&gt;
V4CALL_ESCROW_KEY=5Kxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&lt;br /&gt;
# Secret key to access /admin/* endpoints.&lt;br /&gt;
# Choose a long random string — treat it like a password.&lt;br /&gt;
# Example: openssl rand -hex 32&lt;br /&gt;
ADMIN_KEY=make-up-a-long-random-string-at-least-20-characters&lt;br /&gt;
&lt;br /&gt;
# ── Hive Blockchain ──────────────────────────────────────&lt;br /&gt;
CHAIN=hive&lt;br /&gt;
HIVE_API=&lt;br /&gt;
&lt;br /&gt;
# ── Platform Fee ─────────────────────────────────────────&lt;br /&gt;
# Percentage your server takes from each paid call/DM (10 = 10%)&lt;br /&gt;
# This is the MINIMUM fee — users whose rates post sets a lower&lt;br /&gt;
# platform fee will be rejected. Users who set a higher fee&lt;br /&gt;
# get the best price (your server&#039;s rate, not their higher number).&lt;br /&gt;
DEFAULT_PLATFORM_FEE=10&lt;br /&gt;
&lt;br /&gt;
# ── Network ──────────────────────────────────────────────&lt;br /&gt;
PORT=3000&lt;br /&gt;
BIND_HOST=127.0.0.1&lt;br /&gt;
&lt;br /&gt;
# ── Chat Storage ─────────────────────────────────────────&lt;br /&gt;
# How many days to keep stored DMs before automatic cleanup&lt;br /&gt;
DM_RETENTION_DAYS=33&lt;br /&gt;
# How many days to keep stored room messages before cleanup&lt;br /&gt;
ROOM_RETENTION_DAYS=33&lt;br /&gt;
# How many recent DMs per conversation to show on login (0 = off)&lt;br /&gt;
DM_PREVIEW_COUNT=1&lt;br /&gt;
&lt;br /&gt;
# ── Call Behaviour (advanced — defaults are fine) ────────&lt;br /&gt;
# CALL_COOLDOWN_MS=30000&lt;br /&gt;
# MAX_CALL_DURATION_MIN=120&lt;br /&gt;
# PAYMENT_VERIFY_RETRIES=3&lt;br /&gt;
# PAYMENT_VERIFY_DELAY_MS=5000&lt;br /&gt;
&lt;br /&gt;
# ── Federation (optional) ────────────────────────────────&lt;br /&gt;
# Comma-separated list of peer v4call server federation WebSocket URLs.&lt;br /&gt;
# Leave blank for standalone mode (no federation).&lt;br /&gt;
# Servers listed here are auto-approved on startup. Discovered peers&lt;br /&gt;
# (via Hive directory) need manual approval at /admin-peers.html.&lt;br /&gt;
# Example two-server setup:&lt;br /&gt;
#   On call.completenoobs.com → FEDERATION_PEERS=wss://hive-book.com/federation&lt;br /&gt;
#   On hive-book.com           → FEDERATION_PEERS=wss://call.completenoobs.com/federation&lt;br /&gt;
FEDERATION_PEERS=&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Key points:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;V4CALL_ESCROW_KEY&amp;lt;/code&amp;gt; — must be the &#039;&#039;&#039;active&#039;&#039;&#039; private key for your escrow account. Find it in your Hive wallet → Keys &amp;amp; Permissions → Active. Starts with &amp;lt;code&amp;gt;5K&amp;lt;/code&amp;gt;. &#039;&#039;&#039;Never share this.&#039;&#039;&#039;&lt;br /&gt;
* &amp;lt;code&amp;gt;ADMIN_KEY&amp;lt;/code&amp;gt; — invent a secret password for accessing admin tools&lt;br /&gt;
* &amp;lt;code&amp;gt;HIVE_API&amp;lt;/code&amp;gt; — leave blank to use all built-in Hive nodes automatically&lt;br /&gt;
* &amp;lt;code&amp;gt;DEFAULT_PLATFORM_FEE&amp;lt;/code&amp;gt; — your server&#039;s minimum platform fee percentage. See [[#Platform_Fee_System|Platform Fee System]] below for how this works.&lt;br /&gt;
* &amp;lt;code&amp;gt;DM_RETENTION_DAYS&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;ROOM_RETENTION_DAYS&amp;lt;/code&amp;gt; — how long chat messages are kept in the database. A cleanup job runs every hour and deletes anything older. Set to &amp;lt;code&amp;gt;0&amp;lt;/code&amp;gt; to keep messages indefinitely (not recommended).&lt;br /&gt;
* &amp;lt;code&amp;gt;DM_PREVIEW_COUNT&amp;lt;/code&amp;gt; — when a user logs in, this many recent DMs per conversation are loaded into the lobby chat so they can see previews. Set to &amp;lt;code&amp;gt;0&amp;lt;/code&amp;gt; to disable previews (users still get an unread count alert).&lt;br /&gt;
* &amp;lt;code&amp;gt;FEDERATION_PEERS&amp;lt;/code&amp;gt; — leave blank for now. You can add peers later (see [[#Step_17:_Federation_Setup_(Optional)|Federation Setup]]).&lt;br /&gt;
&lt;br /&gt;
Save: &#039;&#039;&#039;Ctrl+X&#039;&#039;&#039; → &#039;&#039;&#039;Y&#039;&#039;&#039; → &#039;&#039;&#039;Enter&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Step 9: Configure Nginx — HTTP Only First ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;This step is critical.&#039;&#039;&#039; A very common mistake is putting the HTTPS/SSL config in Nginx before getting the certificate. Nginx tries to load the certificate at startup — if the file doesn&#039;t exist yet, Nginx crashes in a restart loop. Certbot then cannot serve the challenge because Nginx is down. Result: no certificate, stuck in a loop.&lt;br /&gt;
&lt;br /&gt;
The fix: always start with HTTP only, get the certificate, then add HTTPS.&lt;br /&gt;
&lt;br /&gt;
Edit the Nginx config:&lt;br /&gt;
&lt;br /&gt;
 nano /opt/v4call/nginx/v4call.conf&lt;br /&gt;
&lt;br /&gt;
Delete everything and replace with this HTTP-only config:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
server {&lt;br /&gt;
    listen 80;&lt;br /&gt;
    server_name call.yourdomain.com www.call.yourdomain.com;&lt;br /&gt;
&lt;br /&gt;
    # Certbot challenge path — do not remove this block&lt;br /&gt;
    location /.well-known/acme-challenge/ {&lt;br /&gt;
        root /var/www/certbot;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # Proxy all other requests to the v4call app&lt;br /&gt;
    location / {&lt;br /&gt;
        proxy_pass         http://app:3000;&lt;br /&gt;
        proxy_http_version 1.1;&lt;br /&gt;
        proxy_set_header   Upgrade $http_upgrade;&lt;br /&gt;
        proxy_set_header   Connection &amp;quot;upgrade&amp;quot;;&lt;br /&gt;
        proxy_set_header   Host $host;&lt;br /&gt;
        proxy_set_header   X-Real-IP $remote_addr;&lt;br /&gt;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;&lt;br /&gt;
        proxy_set_header   X-Forwarded-Proto $scheme;&lt;br /&gt;
        proxy_read_timeout 86400;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Replace &amp;lt;code&amp;gt;call.yourdomain.com&amp;lt;/code&amp;gt; with your actual domain in both places.&lt;br /&gt;
&lt;br /&gt;
Save: &#039;&#039;&#039;Ctrl+X&#039;&#039;&#039; → &#039;&#039;&#039;Y&#039;&#039;&#039; → &#039;&#039;&#039;Enter&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Step 10: Create Data Directories and Fix Permissions ==&lt;br /&gt;
&lt;br /&gt;
Create the folders Docker uses for persistent data:&lt;br /&gt;
&lt;br /&gt;
 mkdir -p /opt/v4call/data/logs  \&lt;br /&gt;
 mkdir -p /opt/v4call/data/certbot/conf  \&lt;br /&gt;
 mkdir -p /opt/v4call/data/certbot/www/.well-known/acme-challenge  \&lt;br /&gt;
 mkdir -p /opt/v4call/data/certbot/logs&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Fix permissions — do not skip this.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The v4call app runs inside Docker as user &amp;lt;code&amp;gt;node&amp;lt;/code&amp;gt; (UID 1000). On the host, these directories are created by root, so the container cannot write to them. This causes a &amp;lt;code&amp;gt;SQLITE_CANTOPEN&amp;lt;/code&amp;gt; error that crashes the app.&lt;br /&gt;
&lt;br /&gt;
Fix the logs directory for the app:&lt;br /&gt;
&lt;br /&gt;
 chown -R 1000:1000 /opt/v4call/data/logs&lt;br /&gt;
&lt;br /&gt;
Certbot runs as root so its directories stay root-owned:&lt;br /&gt;
&lt;br /&gt;
 chown -R root:root /opt/v4call/data/certbot&lt;br /&gt;
 chmod -R 755 /opt/v4call/data/certbot&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note:&#039;&#039;&#039; The app creates two separate SQLite databases in the logs directory:&lt;br /&gt;
* &amp;lt;code&amp;gt;v4call-ledger.db&amp;lt;/code&amp;gt; — payment records (calls, ring fees, payouts). Only the server writes to this.&lt;br /&gt;
* &amp;lt;code&amp;gt;v4call-chat.db&amp;lt;/code&amp;gt; — stored DMs and room messages. Separate from the ledger for security — if a bug in chat storage were exploited, the payment ledger remains untouched.&lt;br /&gt;
&lt;br /&gt;
Federation also persists state in this directory:&lt;br /&gt;
* &amp;lt;code&amp;gt;approved-peers.json&amp;lt;/code&amp;gt; — list of federation peer domains you&#039;ve approved (created on first approval; survives container restarts).&lt;br /&gt;
&lt;br /&gt;
== Step 11: Build and Start the Server ==&lt;br /&gt;
&lt;br /&gt;
 cd /opt/v4call&lt;br /&gt;
 docker compose up -d --build&lt;br /&gt;
&lt;br /&gt;
The first build downloads dependencies and takes 2-4 minutes. Check the status:&lt;br /&gt;
&lt;br /&gt;
 docker compose ps&lt;br /&gt;
&lt;br /&gt;
You should see:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
NAME              STATUS&lt;br /&gt;
v4call-app        Up (healthy)&lt;br /&gt;
v4call-nginx      Up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Check the app started correctly:&lt;br /&gt;
&lt;br /&gt;
 docker compose logs app&lt;br /&gt;
&lt;br /&gt;
Look for:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
[ledger] SQLite ready: /app/logs/v4call-ledger.db&lt;br /&gt;
[chat] SQLite ready: /app/logs/v4call-chat.db&lt;br /&gt;
v4call server running on 0.0.0.0:3000&lt;br /&gt;
[config] Server: yourcallapp (call.yourdomain.com)&lt;br /&gt;
[config] DM retention: 33 days | Room retention: 33 days | DM preview: 1&lt;br /&gt;
✓ Escrow key verified — matches @yourescrowaccount active key&lt;br /&gt;
✓ Escrow account @yourescrowaccount balance: 0.000 HBD&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you see &amp;lt;code&amp;gt;SqliteError: unable to open database file&amp;lt;/code&amp;gt; — run the chown command from Step 10 again then &amp;lt;code&amp;gt;docker compose restart app&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Test HTTP is working:&lt;br /&gt;
&lt;br /&gt;
 curl http://call.yourdomain.com/debug-state&lt;br /&gt;
&lt;br /&gt;
Should return: &amp;lt;code&amp;gt;{&amp;quot;lobbyUsers&amp;quot;:[],&amp;quot;rooms&amp;quot;:[]}&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you can see the site in a browser over HTTP at this point — everything is working and ready for the certificate.&lt;br /&gt;
&lt;br /&gt;
== Step 12: Get Your SSL Certificate ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Before running certbot&#039;&#039;&#039;, verify DNS and the challenge path are both working:&lt;br /&gt;
&lt;br /&gt;
 # Check DNS points to this server&lt;br /&gt;
 nslookup call.yourdomain.com&lt;br /&gt;
&lt;br /&gt;
 # Test the challenge path&lt;br /&gt;
 echo &amp;quot;test&amp;quot; &amp;gt; /opt/v4call/data/certbot/www/.well-known/acme-challenge/testfile&lt;br /&gt;
 curl http://call.yourdomain.com/.well-known/acme-challenge/testfile&lt;br /&gt;
&lt;br /&gt;
The curl command must return &amp;lt;code&amp;gt;test&amp;lt;/code&amp;gt;. If it does not, Nginx is not running — check &amp;lt;code&amp;gt;docker compose ps&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;docker compose logs nginx&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
When both work, get the certificate. The &amp;lt;code&amp;gt;--entrypoint certbot&amp;lt;/code&amp;gt; flag is required — without it, Docker runs the container&#039;s default renewal loop instead of the certonly command:&lt;br /&gt;
&lt;br /&gt;
 docker compose run --rm \&lt;br /&gt;
   --entrypoint certbot \&lt;br /&gt;
   certbot certonly \&lt;br /&gt;
   --webroot \&lt;br /&gt;
   -w /var/www/certbot \&lt;br /&gt;
   -d call.yourdomain.com \&lt;br /&gt;
   --email your@email.com \&lt;br /&gt;
   --agree-tos \&lt;br /&gt;
   --no-eff-email&lt;br /&gt;
&lt;br /&gt;
Replace &amp;lt;code&amp;gt;call.yourdomain.com&amp;lt;/code&amp;gt; with your domain and &amp;lt;code&amp;gt;your@email.com&amp;lt;/code&amp;gt; with your email.&lt;br /&gt;
&lt;br /&gt;
Success looks like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Requesting a certificate for call.yourdomain.com&lt;br /&gt;
&lt;br /&gt;
Successfully received certificate.&lt;br /&gt;
Certificate is saved at: /etc/letsencrypt/live/call.yourdomain.com/fullchain.pem&lt;br /&gt;
Key is saved at:         /etc/letsencrypt/live/call.yourdomain.com/privkey.pem&lt;br /&gt;
This certificate expires on 2026-07-09.&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Verify the files exist on the host:&lt;br /&gt;
&lt;br /&gt;
 ls /opt/v4call/data/certbot/conf/live/call.yourdomain.com/&lt;br /&gt;
&lt;br /&gt;
Should show: &amp;lt;code&amp;gt;cert.pem  chain.pem  fullchain.pem  privkey.pem  README&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 13: Enable HTTPS in Nginx ==&lt;br /&gt;
&lt;br /&gt;
Now the certificate exists, update Nginx to serve HTTPS.&lt;br /&gt;
&lt;br /&gt;
 nano /opt/v4call/nginx/v4call.conf&lt;br /&gt;
&lt;br /&gt;
Replace everything with the full HTTPS config:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# HTTP — only used for certbot challenge and redirect to HTTPS&lt;br /&gt;
server {&lt;br /&gt;
    listen 80;&lt;br /&gt;
    server_name call.yourdomain.com www.call.yourdomain.com;&lt;br /&gt;
&lt;br /&gt;
    location /.well-known/acme-challenge/ {&lt;br /&gt;
        root /var/www/certbot;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    location / {&lt;br /&gt;
        return 301 https://$host$request_uri;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# HTTPS — main app&lt;br /&gt;
server {&lt;br /&gt;
    listen 443 ssl;&lt;br /&gt;
    server_name call.yourdomain.com www.call.yourdomain.com;&lt;br /&gt;
&lt;br /&gt;
    # CHANGE PATH: ssl certs path uses your domain — find with: ls data/certbot/conf/live/&lt;br /&gt;
    ssl_certificate     /etc/letsencrypt/live/call.yourdomain.com/fullchain.pem;&lt;br /&gt;
    ssl_certificate_key /etc/letsencrypt/live/call.yourdomain.com/privkey.pem;&lt;br /&gt;
&lt;br /&gt;
    ssl_protocols TLSv1.2 TLSv1.3;&lt;br /&gt;
    ssl_prefer_server_ciphers off;&lt;br /&gt;
&lt;br /&gt;
    add_header Strict-Transport-Security &amp;quot;max-age=63072000&amp;quot; always;&lt;br /&gt;
    add_header X-Frame-Options DENY;&lt;br /&gt;
    add_header X-Content-Type-Options nosniff;&lt;br /&gt;
&lt;br /&gt;
    # When user cancels the basic-auth login prompt, send them here&lt;br /&gt;
    error_page 401 /info.html;&lt;br /&gt;
&lt;br /&gt;
    # info.html is served directly by Nginx — no auth, no proxy&lt;br /&gt;
    location = /info.html {&lt;br /&gt;
        root /usr/share/nginx/html;&lt;br /&gt;
        auth_basic off;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # WebSocket — main client traffic&lt;br /&gt;
    location /socket.io/ {&lt;br /&gt;
        # Uncomment below to require username/password to enter site (testing only).&lt;br /&gt;
        # See &amp;quot;Optional: Password Protect Your Server During Testing&amp;quot; section for setup.&lt;br /&gt;
        #auth_basic           &amp;quot;v4call — Private Testing&amp;quot;;&lt;br /&gt;
        #auth_basic_user_file /etc/nginx/.htpasswd;&lt;br /&gt;
&lt;br /&gt;
        proxy_pass         http://app:3000;&lt;br /&gt;
        proxy_http_version 1.1;&lt;br /&gt;
        proxy_set_header   Upgrade $http_upgrade;&lt;br /&gt;
        proxy_set_header   Connection &amp;quot;upgrade&amp;quot;;&lt;br /&gt;
        proxy_set_header   Host $host;&lt;br /&gt;
        proxy_set_header   X-Real-IP $remote_addr;&lt;br /&gt;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;&lt;br /&gt;
        proxy_set_header   X-Forwarded-Proto $scheme;&lt;br /&gt;
        proxy_cache_bypass $http_upgrade;&lt;br /&gt;
        proxy_read_timeout 86400;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # Federation WebSocket — server-to-server, NEVER auth-protected.&lt;br /&gt;
    # Peer v4call servers connect here to exchange presence/DMs/call invites.&lt;br /&gt;
    # Trust validation happens inside the Node app (signed verify.json check&lt;br /&gt;
    # + operator approval via /admin-peers.html). Do not add basic auth here —&lt;br /&gt;
    # it would block all federation.&lt;br /&gt;
    location /federation {&lt;br /&gt;
        proxy_pass         http://app:3000;&lt;br /&gt;
        proxy_http_version 1.1;&lt;br /&gt;
        proxy_set_header   Upgrade $http_upgrade;&lt;br /&gt;
        proxy_set_header   Connection &amp;quot;upgrade&amp;quot;;&lt;br /&gt;
        proxy_set_header   Host $host;&lt;br /&gt;
        proxy_set_header   X-Real-IP $remote_addr;&lt;br /&gt;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;&lt;br /&gt;
        proxy_set_header   X-Forwarded-Proto $scheme;&lt;br /&gt;
        proxy_read_timeout 86400;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # Everything else — main HTTP traffic&lt;br /&gt;
    location / {&lt;br /&gt;
        # Uncomment below to require username/password to enter site (testing only).&lt;br /&gt;
        #auth_basic           &amp;quot;v4call — Private Testing&amp;quot;;&lt;br /&gt;
        #auth_basic_user_file /etc/nginx/.htpasswd;&lt;br /&gt;
&lt;br /&gt;
        proxy_pass         http://app:3000;&lt;br /&gt;
        proxy_http_version 1.1;&lt;br /&gt;
        proxy_set_header   Host $host;&lt;br /&gt;
        proxy_set_header   X-Real-IP $remote_addr;&lt;br /&gt;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;&lt;br /&gt;
        proxy_set_header   X-Forwarded-Proto $scheme;&lt;br /&gt;
        proxy_read_timeout 300;&lt;br /&gt;
        proxy_send_timeout 300;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # Logs failed and successful login attempts when .htpasswd is enabled&lt;br /&gt;
    error_log /var/log/nginx/error.log warn;&lt;br /&gt;
    access_log /var/log/nginx/access.log;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* REPLACE &amp;lt;code&amp;gt;call.yourdomain.com&amp;lt;/code&amp;gt; with your domain throughout (it appears 4 times — server_name lines and ssl_certificate paths).&lt;br /&gt;
* The &amp;lt;code&amp;gt;auth_basic&amp;lt;/code&amp;gt; lines are &#039;&#039;&#039;commented out by default&#039;&#039;&#039; so federation and normal users work out-of-box. To enable password protection for private testing, see [[#Optional:_Password_Protect_Your_Server_During_Testing|the Optional section]] below — it shows how to uncomment them and create users.&lt;br /&gt;
* &#039;&#039;&#039;Never&#039;&#039;&#039; add &amp;lt;code&amp;gt;auth_basic&amp;lt;/code&amp;gt; to the &amp;lt;code&amp;gt;/federation&amp;lt;/code&amp;gt; block — it would block all peer v4call servers from connecting to you.&lt;br /&gt;
&lt;br /&gt;
Save and restart Nginx:&lt;br /&gt;
&lt;br /&gt;
 docker compose restart nginx&lt;br /&gt;
&lt;br /&gt;
Check the logs — you should see &#039;&#039;&#039;no&#039;&#039;&#039; &amp;lt;code&amp;gt;[emerg]&amp;lt;/code&amp;gt; errors:&lt;br /&gt;
&lt;br /&gt;
 docker compose logs nginx&lt;br /&gt;
&lt;br /&gt;
The last lines should show:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
nginx/1.x.x ...&lt;br /&gt;
start worker processes&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* If you see an error such as:&lt;br /&gt;
&amp;lt;pre&amp;gt;[emerg] 1#1: cannot load certificate &amp;quot;/etc/letsencrypt/live/call.yourdomain.com/fullchain.pem&amp;quot;: BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/call.yourdomain.com/fullchain.pem, r) error:10000080:BIO routines::no such file)&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Make sure you edited &amp;lt;code&amp;gt;nginx/v4call.conf&amp;lt;/code&amp;gt; correctly.&lt;br /&gt;
* To find the path for your SSL certs look in path &amp;lt;code&amp;gt;data/certbot/conf/live/&amp;lt;/code&amp;gt;&lt;br /&gt;
* There are 4 lines to check in &amp;lt;code&amp;gt;nginx/v4call.conf&amp;lt;/code&amp;gt;:&lt;br /&gt;
** &amp;lt;code&amp;gt;server_name call.yourdomain.com www.call.yourdomain.com;&amp;lt;/code&amp;gt; (HTTP block)&lt;br /&gt;
** &amp;lt;code&amp;gt;server_name call.yourdomain.com www.call.yourdomain.com;&amp;lt;/code&amp;gt; (HTTPS block)&lt;br /&gt;
** &amp;lt;code&amp;gt;ssl_certificate     /etc/letsencrypt/live/call.yourdomain.com/fullchain.pem;&amp;lt;/code&amp;gt;&lt;br /&gt;
** &amp;lt;code&amp;gt;ssl_certificate_key /etc/letsencrypt/live/call.yourdomain.com/privkey.pem;&amp;lt;/code&amp;gt;&lt;br /&gt;
* When done restart nginx with: &amp;lt;code&amp;gt;docker compose restart nginx&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 14: Set Up SSL Auto-Renewal ==&lt;br /&gt;
&lt;br /&gt;
Let&#039;s Encrypt certificates expire after 90 days. A cron job renews them automatically.&lt;br /&gt;
&lt;br /&gt;
 crontab -e&lt;br /&gt;
&lt;br /&gt;
Add this line at the bottom:&lt;br /&gt;
&lt;br /&gt;
 0 3 * * * cd /opt/v4call &amp;amp;&amp;amp; docker compose run --rm --entrypoint certbot certbot renew --quiet &amp;amp;&amp;amp; docker compose exec nginx nginx -s reload&lt;br /&gt;
&lt;br /&gt;
Save and exit. This runs at 3am every day, renews if the cert is close to expiry, and reloads Nginx to pick up the new certificate.&lt;br /&gt;
&lt;br /&gt;
== Step 15: Test Everything is Working ==&lt;br /&gt;
&lt;br /&gt;
Test HTTPS:&lt;br /&gt;
&lt;br /&gt;
 curl https://call.yourdomain.com/debug-state&lt;br /&gt;
&lt;br /&gt;
Should return: &amp;lt;code&amp;gt;{&amp;quot;lobbyUsers&amp;quot;:[],&amp;quot;rooms&amp;quot;:[]}&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Test admin access:&lt;br /&gt;
&lt;br /&gt;
 curl &amp;quot;https://call.yourdomain.com/admin/balance?key=YOUR_ADMIN_KEY&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Should return your escrow account balance.&lt;br /&gt;
&lt;br /&gt;
Open your browser and go to &amp;lt;code&amp;gt;https://call.yourdomain.com&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You should see the v4call login page with:&lt;br /&gt;
* A padlock icon in the browser address bar&lt;br /&gt;
* A green &#039;&#039;&#039;⚡ Sign in with Keychain&#039;&#039;&#039; button (if Hive Keychain is installed)&lt;br /&gt;
* A manual posting key login option below it&lt;br /&gt;
* A &#039;&#039;&#039;📖 New here? Learn the v4call basics →&#039;&#039;&#039; link at the bottom&lt;br /&gt;
&lt;br /&gt;
Log in with Hive Keychain or a posting key to confirm the login flow works. 🎉&lt;br /&gt;
&lt;br /&gt;
== Step 16: Set Up Your Call Rates on Hive ==&lt;br /&gt;
&lt;br /&gt;
For callers to be charged when they ring you, publish your rates on the Hive blockchain:&lt;br /&gt;
&lt;br /&gt;
# Make sure Hive Keychain is installed in your browser&lt;br /&gt;
# Go to &amp;lt;code&amp;gt;https://call.yourdomain.com/rate-editor.html&amp;lt;/code&amp;gt;&lt;br /&gt;
# Enter your Hive username&lt;br /&gt;
# Set your rates — ring fee, connect fee, duration rate per hour, minimum credit deposit&lt;br /&gt;
# Set &amp;lt;code&amp;gt;PLATFORM-FEE&amp;lt;/code&amp;gt; to at least your server&#039;s &amp;lt;code&amp;gt;DEFAULT_PLATFORM_FEE&amp;lt;/code&amp;gt; percentage (e.g. &amp;lt;code&amp;gt;10&amp;lt;/code&amp;gt; for 10%). If you set it lower, paid contacts to your account will be rejected on this server.&lt;br /&gt;
# Optionally add custom token sections (e.g. &amp;lt;code&amp;gt;[TOKEN:CNOOBS]&amp;lt;/code&amp;gt;) to offer discounted rates for callers who hold your token&lt;br /&gt;
# Click &#039;&#039;&#039;Generate&#039;&#039;&#039; to preview the rates block&lt;br /&gt;
# Click &#039;&#039;&#039;Post to Hive&#039;&#039;&#039; — Keychain will ask you to approve the post&lt;br /&gt;
&lt;br /&gt;
This creates a post titled &amp;lt;code&amp;gt;v4call-rates&amp;lt;/code&amp;gt; on your Hive blog. Your server reads this post automatically.&lt;br /&gt;
&lt;br /&gt;
To verify your server read the rates correctly:&lt;br /&gt;
&lt;br /&gt;
 https://call.yourdomain.com/debug-rates/yourusername&lt;br /&gt;
&lt;br /&gt;
To test with a specific caller (checks their token balances too):&lt;br /&gt;
&lt;br /&gt;
 https://call.yourdomain.com/debug-rates/yourusername?caller=theirusername&amp;amp;type=voice&lt;br /&gt;
&lt;br /&gt;
You should see your rates as JSON, including which currency and rates apply for that caller.&lt;br /&gt;
&lt;br /&gt;
== Step 17: Federation Setup (Optional) ==&lt;br /&gt;
&lt;br /&gt;
If you want your server to talk to other v4call servers — so users on different servers can call/DM each other — set up federation. This is optional. A standalone server works fine without it.&lt;br /&gt;
&lt;br /&gt;
Federation has two trust steps:&lt;br /&gt;
&lt;br /&gt;
# &#039;&#039;&#039;Domain proof&#039;&#039;&#039; — you publish a signed JSON file at &amp;lt;code&amp;gt;https://yourdomain.com/.well-known/v4call-server.json&amp;lt;/code&amp;gt; proving that your Hive account controls this domain&lt;br /&gt;
# &#039;&#039;&#039;Directory listing&#039;&#039;&#039; — you publish a Hive post tagged &amp;lt;code&amp;gt;v4call-server&amp;lt;/code&amp;gt; announcing your server exists, so other operators can discover you&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important escrow rule&#039;&#039;&#039;: the &amp;lt;code&amp;gt;ESCROW_ACCOUNT&amp;lt;/code&amp;gt; in your &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; must match the &amp;lt;code&amp;gt;ESCROW:&amp;lt;/code&amp;gt; field in your users&#039; rates posts. The active key for that escrow has to live on &#039;&#039;&#039;this&#039;&#039;&#039; server. If a user&#039;s rates post points at a different escrow than what your server holds the key for, paid calls/DMs to that user will fail (your server can&#039;t disburse from an escrow it doesn&#039;t control).&lt;br /&gt;
&lt;br /&gt;
=== Step 17a: Generate and Host the Verify File ===&lt;br /&gt;
&lt;br /&gt;
# Open &amp;lt;code&amp;gt;https://yourdomain.com/server-sign.html&amp;lt;/code&amp;gt; in a browser with Hive Keychain installed&lt;br /&gt;
# Fill in: Hive account (same as &amp;lt;code&amp;gt;SERVER_HIVE_ACCOUNT&amp;lt;/code&amp;gt;), domain, escrow account, fee account, federation WS URL (&amp;lt;code&amp;gt;wss://yourdomain.com/federation&amp;lt;/code&amp;gt;)&lt;br /&gt;
# Click &#039;&#039;&#039;⚡ Sign with Hive Keychain&#039;&#039;&#039;&lt;br /&gt;
# Click &#039;&#039;&#039;⬇ Download&#039;&#039;&#039; — saves &amp;lt;code&amp;gt;v4call-server.json&amp;lt;/code&amp;gt;&lt;br /&gt;
# Upload to your server, replacing the placeholder file. From your local machine:&lt;br /&gt;
   scp v4call-server.json root@yourvps:/opt/v4call/public/.well-known/&lt;br /&gt;
&lt;br /&gt;
(Or use any other transfer method you prefer — SFTP client, paste via SSH, etc.)&lt;br /&gt;
&lt;br /&gt;
Verify it&#039;s served:&lt;br /&gt;
 curl https://yourdomain.com/.well-known/v4call-server.json&lt;br /&gt;
&lt;br /&gt;
You should see your signed JSON. If you see &amp;lt;code&amp;gt;404&amp;lt;/code&amp;gt;, check the file is at exactly &amp;lt;code&amp;gt;public/.well-known/v4call-server.json&amp;lt;/code&amp;gt; — the directory uses a &#039;&#039;&#039;hyphen&#039;&#039;&#039;, not an underscore (RFC 8615), and the filename must be exactly &amp;lt;code&amp;gt;v4call-server.json&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Rebuild so the file is baked into the Docker image:&lt;br /&gt;
 docker compose down &amp;amp;&amp;amp; docker compose build --no-cache &amp;amp;&amp;amp; docker compose up -d&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Tip&#039;&#039;&#039;: commit the generated file to your git repo so future fresh installs include it automatically. It contains only public data — safe to commit. The placeholder shipped in the repo is overwritten by your file.&lt;br /&gt;
&lt;br /&gt;
=== Step 17b: Announce on Hive ===&lt;br /&gt;
&lt;br /&gt;
For other operators to discover you, publish a Hive post advertising your server:&lt;br /&gt;
&lt;br /&gt;
# After signing in Step 17a, click the &#039;&#039;&#039;📡 Announce on Hive →&#039;&#039;&#039; link on the signer output card (auto-fills the announce page)&lt;br /&gt;
# Or open &amp;lt;code&amp;gt;https://yourdomain.com/server-announce.html&amp;lt;/code&amp;gt; directly&lt;br /&gt;
# Confirm the prefilled values&lt;br /&gt;
# Click &#039;&#039;&#039;📡 Post to Hive&#039;&#039;&#039; — Keychain will broadcast the post&lt;br /&gt;
&lt;br /&gt;
The post lives forever on Hive. If your config changes, just post a new one — the most recent post per Hive account wins. No need to delete old posts (Hive posts are effectively permanent anyway).&lt;br /&gt;
&lt;br /&gt;
=== Step 17c: Connect to Known Peers (Manual Mode) ===&lt;br /&gt;
&lt;br /&gt;
Skip the discovery step and go straight to a known peer by adding their WebSocket URL to &amp;lt;code&amp;gt;FEDERATION_PEERS&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt;:&lt;br /&gt;
 FEDERATION_PEERS=wss://otherserver.com/federation&lt;br /&gt;
&lt;br /&gt;
Both servers must list each other. Rebuild after editing &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt;:&lt;br /&gt;
 docker compose down &amp;amp;&amp;amp; docker compose build --no-cache &amp;amp;&amp;amp; docker compose up -d&lt;br /&gt;
&lt;br /&gt;
Servers in &amp;lt;code&amp;gt;FEDERATION_PEERS&amp;lt;/code&amp;gt; are auto-approved on startup — they bypass the manual approval step.&lt;br /&gt;
&lt;br /&gt;
=== Step 17d: Discover and Approve Peers (Auto Mode) ===&lt;br /&gt;
&lt;br /&gt;
Your server scans Hive for &amp;lt;code&amp;gt;v4call-server&amp;lt;/code&amp;gt; posts on startup and every 2 hours. Discovered peers are NOT auto-connected — you must approve each one.&lt;br /&gt;
&lt;br /&gt;
# Open &amp;lt;code&amp;gt;https://yourdomain.com/admin-peers.html&amp;lt;/code&amp;gt;&lt;br /&gt;
# Paste your &amp;lt;code&amp;gt;ADMIN_KEY&amp;lt;/code&amp;gt; (from &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt;) and click &#039;&#039;&#039;🔄 Load / Refresh&#039;&#039;&#039;&lt;br /&gt;
# Each discovered peer shows a card with verification status, escrow, fee account, post age&lt;br /&gt;
# Click &#039;&#039;&#039;✓ Approve&#039;&#039;&#039; to add them to your federation&lt;br /&gt;
# Click &#039;&#039;&#039;✗ Revoke&#039;&#039;&#039; to remove an approved peer&lt;br /&gt;
&lt;br /&gt;
Approvals persist to &amp;lt;code&amp;gt;/app/logs/approved-peers.json&amp;lt;/code&amp;gt; so they survive container restarts.&lt;br /&gt;
&lt;br /&gt;
=== Federation Health Check ===&lt;br /&gt;
&lt;br /&gt;
Watch the federation logs:&lt;br /&gt;
 docker compose logs -f app | grep -E &amp;quot;\[federation\]|\[discovery\]|\[peers\]&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Healthy startup looks like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
[federation] Approved peers: peer-domain.com&lt;br /&gt;
[federation] Connecting to wss://peer-domain.com/federation...&lt;br /&gt;
[federation] Outbound connected: wss://peer-domain.com/federation&lt;br /&gt;
[federation] ✓ Peer verified: @peer-domain.com (signer: @peer-account, escrow: @peer-escrow)&lt;br /&gt;
[discovery] Hive returned 2 post(s) under v4call-server tag&lt;br /&gt;
[discovery] Scan complete — 2 v4call-server post(s), 2 verified&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
When a user logs in on either server, you should see:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
[federation] → user-online @username → 1 peer(s)         (on their home server)&lt;br /&gt;
[federation] ← user-online @username@theirhome.com       (on your server)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Feature Guide: What Your Server Can Do ==&lt;br /&gt;
&lt;br /&gt;
This section explains all the features available in v4call and how they work. No code changes needed — everything described here is built in and ready to use.&lt;br /&gt;
&lt;br /&gt;
=== Login Options ===&lt;br /&gt;
&lt;br /&gt;
v4call supports two ways to sign in:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hive Keychain&#039;&#039;&#039; (recommended) — click the green &#039;&#039;&#039;⚡ Sign in with Keychain&#039;&#039;&#039; button. Keychain signs a challenge to prove your identity. No key paste needed. After login, a 🔑 panel appears in the lobby where you can optionally enter your posting key to unlock encrypted messaging (Keychain cannot expose private keys, so encryption needs the key entered once per session).&lt;br /&gt;
* &#039;&#039;&#039;Manual posting key&#039;&#039;&#039; — paste your Hive posting private key (starts with &amp;lt;code&amp;gt;5J&amp;lt;/code&amp;gt;) directly. The key stays in browser session memory only — never sent to the server.&lt;br /&gt;
&lt;br /&gt;
Both methods verify your identity against the Hive blockchain.&lt;br /&gt;
&lt;br /&gt;
=== Voice and Video Calls ===&lt;br /&gt;
&lt;br /&gt;
Each online user in the lobby shows three action buttons:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;📞 Green phone icon&#039;&#039;&#039; — start a &#039;&#039;&#039;voice-only&#039;&#039;&#039; call (audio, no camera)&lt;br /&gt;
* &#039;&#039;&#039;🎥 Blue camera icon&#039;&#039;&#039; — start a &#039;&#039;&#039;video&#039;&#039;&#039; call (audio + camera)&lt;br /&gt;
* &#039;&#039;&#039;💬 Purple chat bubble icon&#039;&#039;&#039; — open the &#039;&#039;&#039;DM panel&#039;&#039;&#039; to send a direct message&lt;br /&gt;
&lt;br /&gt;
Voice calls request microphone only — no camera permission prompt. Video calls request both. The caller and callee can have different call types — the type is set by whoever initiates the call.&lt;br /&gt;
&lt;br /&gt;
Separate rates can be set for voice and video calls in the rates post (voice is typically cheaper).&lt;br /&gt;
&lt;br /&gt;
Federated users (on a different v4call server) appear in the lobby with a small server-domain badge under their username. The call/DM buttons work the same — federation routing is transparent.&lt;br /&gt;
&lt;br /&gt;
=== Direct Messages (DMs) ===&lt;br /&gt;
&lt;br /&gt;
Click the purple 💬 button next to any online user to open the DM panel. DMs are end-to-end encrypted using Hive posting keys — the server stores only ciphertext it cannot read.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Chat storage:&#039;&#039;&#039; DMs are stored on the server in an encrypted database (&amp;lt;code&amp;gt;v4call-chat.db&amp;lt;/code&amp;gt;) for up to &amp;lt;code&amp;gt;DM_RETENTION_DAYS&amp;lt;/code&amp;gt; (default: 33 days). Both sender and recipient get their own encrypted copy stored, so both can retrieve their history later.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Unread alerts:&#039;&#039;&#039; When you log in, if you have unread DMs, a popup appears showing how many messages from how many users. Click a username in the popup to open their DM history.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;DM previews:&#039;&#039;&#039; The last &amp;lt;code&amp;gt;DM_PREVIEW_COUNT&amp;lt;/code&amp;gt; messages per conversation are loaded into the lobby chat on login, so you can see recent activity at a glance. Set to &amp;lt;code&amp;gt;0&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; to disable.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Full history:&#039;&#039;&#039; Click the DM button for any user to load the complete conversation history, shown between &amp;quot;— DM history —&amp;quot; dividers.&lt;br /&gt;
&lt;br /&gt;
=== Rooms ===&lt;br /&gt;
&lt;br /&gt;
Users can create private rooms by selecting users in the lobby (toggle switch) and clicking &#039;&#039;&#039;Create &amp;amp; Invite&#039;&#039;&#039;. Rooms support encrypted messaging, video, and voice.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Room history:&#039;&#039;&#039; When a new user joins a room, they see past messages — broadcasts in full, encrypted messages only if they were addressed to them. A &amp;quot;— earlier messages —&amp;quot; divider separates history from live messages.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Ephemeral rooms:&#039;&#039;&#039; A warning banner at the top of every room says: &amp;quot;⚠ Room is ephemeral — if all users leave, the room and its history are deleted. New members can only read messages encrypted to their key.&amp;quot; When the last person leaves a room, all stored messages for that room are deleted from the database.&lt;br /&gt;
&lt;br /&gt;
=== Custom Token Payments (Hive-Engine) ===&lt;br /&gt;
&lt;br /&gt;
v4call supports payment in any Hive-Engine token, not just HBD. This is configured per-user in their rates post using &amp;lt;code&amp;gt;[TOKEN:SYMBOL]&amp;lt;/code&amp;gt; sections.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;How it works:&#039;&#039;&#039;&lt;br /&gt;
# A user (e.g. @cnoobz) creates a custom token on Hive-Engine (e.g. CNOOBS)&lt;br /&gt;
# In their rates post, they add a &amp;lt;code&amp;gt;[TOKEN:CNOOBS]&amp;lt;/code&amp;gt; section with lower rates than their default HBD rates&lt;br /&gt;
# When a caller who holds CNOOBS contacts @cnoobz, the server detects the token balance and offers the token rates&lt;br /&gt;
# If the caller holds multiple qualifying tokens, &#039;&#039;&#039;all options are shown&#039;&#039;&#039; in a currency picker — the caller chooses which to pay with&lt;br /&gt;
# The payment goes through Hive Keychain as a &amp;lt;code&amp;gt;custom_json&amp;lt;/code&amp;gt; Hive-Engine transfer&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;For the escrow account:&#039;&#039;&#039; Your escrow account needs to hold some of each custom token that users on your server accept. Token payouts (to the callee) and refunds (to the caller) are sent from the escrow account&#039;s token balance, just like HBD.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Payment picker:&#039;&#039;&#039; When multiple payment options exist (e.g. CNOOBS at 1 per message, HBD at 100000 per message), the payment modal shows clickable currency buttons so the caller can see all rates and choose the best option.&lt;br /&gt;
&lt;br /&gt;
=== Platform Fee System ===&lt;br /&gt;
&lt;br /&gt;
The platform fee is how your server earns revenue from paid calls and DMs.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;How it works:&#039;&#039;&#039;&lt;br /&gt;
* Your server&#039;s &amp;lt;code&amp;gt;DEFAULT_PLATFORM_FEE&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; is the &#039;&#039;&#039;minimum&#039;&#039;&#039; percentage your server accepts (e.g. &amp;lt;code&amp;gt;10&amp;lt;/code&amp;gt; = 10%)&lt;br /&gt;
* Each user sets &amp;lt;code&amp;gt;PLATFORM-FEE&amp;lt;/code&amp;gt; in their Hive rates post — this is the maximum fee they are willing to pay to a server&lt;br /&gt;
* If the user&#039;s posted fee is &#039;&#039;&#039;lower&#039;&#039;&#039; than your server&#039;s minimum → &#039;&#039;&#039;rejected&#039;&#039;&#039;. The caller sees a message explaining the mismatch, and the callee is told to raise their fee.&lt;br /&gt;
* If the user&#039;s posted fee &#039;&#039;&#039;meets or exceeds&#039;&#039;&#039; your server&#039;s minimum → &#039;&#039;&#039;accepted&#039;&#039;&#039;, and &#039;&#039;&#039;the server charges its own rate&#039;&#039;&#039; (the minimum), not the user&#039;s higher number. The callee gets the best price.&lt;br /&gt;
* If the user has &#039;&#039;&#039;no &amp;lt;code&amp;gt;PLATFORM-FEE&amp;lt;/code&amp;gt; line&#039;&#039;&#039; in their rates post → the server&#039;s default is used automatically. No mismatch.&lt;br /&gt;
* &#039;&#039;&#039;Free contacts&#039;&#039;&#039; (no payment involved) are never affected by fee enforcement.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Why this matters for federation:&#039;&#039;&#039; Different servers can set different platform fees. Users can shop around — pick a server with a fee they find agreeable. This creates a free market for server operators.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Example:&#039;&#039;&#039;&lt;br /&gt;
* Your server: &amp;lt;code&amp;gt;DEFAULT_PLATFORM_FEE=3&amp;lt;/code&amp;gt; (3%)&lt;br /&gt;
* @alice posts: &amp;lt;code&amp;gt;PLATFORM-FEE: 5%&amp;lt;/code&amp;gt; → accepted, server charges 3% (best price for alice)&lt;br /&gt;
* @bob posts: &amp;lt;code&amp;gt;PLATFORM-FEE: 1%&amp;lt;/code&amp;gt; → rejected, bob needs to raise to at least 3%&lt;br /&gt;
* @charlie has no fee line → defaults to 3%, automatically accepted&lt;br /&gt;
&lt;br /&gt;
=== Federation ===&lt;br /&gt;
&lt;br /&gt;
Federation lets users on different v4call servers see, call, and DM each other. Each server independently:&lt;br /&gt;
&lt;br /&gt;
* Holds its own escrow account (the active key never leaves the server it belongs to)&lt;br /&gt;
* Verifies and disburses payments through its own escrow&lt;br /&gt;
* Takes the platform fee for calls/DMs received by its own users&lt;br /&gt;
&lt;br /&gt;
Cross-server payments work like this: caller pays the callee&#039;s escrow on Hive (read from the callee&#039;s rates post), the callee&#039;s server verifies the payment on-chain and forwards a notification, both servers exchange call/DM signalling, and the callee&#039;s server disburses callee-net + platform-fee from its own escrow at call end (with refunds going cross-server back to the caller as a normal Hive transfer).&lt;br /&gt;
&lt;br /&gt;
WebRTC media stays peer-to-peer — neither server sees the audio/video.&lt;br /&gt;
&lt;br /&gt;
See [[#Step_17:_Federation_Setup_(Optional)|Step 17: Federation Setup]] for the setup steps.&lt;br /&gt;
&lt;br /&gt;
== Admin Configuration Reference ==&lt;br /&gt;
&lt;br /&gt;
All settings are in the &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; file. After changing any value, rebuild:&lt;br /&gt;
&lt;br /&gt;
 docker compose down &amp;amp;&amp;amp; docker compose build --no-cache &amp;amp;&amp;amp; docker compose up -d&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Variable !! Default !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;SERVER_NAME&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;v4call&amp;lt;/code&amp;gt; || Display name for your server&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;SERVER_DOMAIN&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;v4call.com&amp;lt;/code&amp;gt; || Your server&#039;s domain&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;SERVER_HIVE_ACCOUNT&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;v4call&amp;lt;/code&amp;gt; || Hive account that receives platform fees (and signs your federation verify file)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ESCROW_ACCOUNT&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;v4call-escrow&amp;lt;/code&amp;gt; || Hive account that holds funds during calls. The active key for this account must live on &#039;&#039;&#039;this&#039;&#039;&#039; server.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;V4CALL_ESCROW_KEY&amp;lt;/code&amp;gt; || &#039;&#039;(none)&#039;&#039; || Active private key for the escrow account. &#039;&#039;&#039;Required.&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ADMIN_KEY&amp;lt;/code&amp;gt; || &#039;&#039;(none)&#039;&#039; || Password for admin endpoints (&amp;lt;code&amp;gt;/admin/balance&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;/admin/ledger&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;/admin/peers&amp;lt;/code&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;DEFAULT_PLATFORM_FEE&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;10&amp;lt;/code&amp;gt; || Server&#039;s minimum platform fee percentage&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;FEDERATION_PEERS&amp;lt;/code&amp;gt; || &#039;&#039;(blank)&#039;&#039; || Comma-separated peer WebSocket URLs (e.g. &amp;lt;code&amp;gt;wss://peer.com/federation&amp;lt;/code&amp;gt;). Listed peers are auto-approved on startup. Blank = standalone mode.&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;DM_RETENTION_DAYS&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;33&amp;lt;/code&amp;gt; || Days to keep stored DMs before cleanup&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ROOM_RETENTION_DAYS&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;33&amp;lt;/code&amp;gt; || Days to keep stored room messages before cleanup&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;DM_PREVIEW_COUNT&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;1&amp;lt;/code&amp;gt; || Recent DMs per conversation shown on login (0 = off)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;HIVE_API&amp;lt;/code&amp;gt; || &#039;&#039;(blank)&#039;&#039; || Override primary Hive API node. Blank = auto-select from built-in list&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MAX_CALL_DURATION_MIN&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;120&amp;lt;/code&amp;gt; || Maximum call length in minutes before auto-disconnect&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CALL_COOLDOWN_MS&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;30000&amp;lt;/code&amp;gt; || Milliseconds between call attempts to same user&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;PAYMENT_VERIFY_RETRIES&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;3&amp;lt;/code&amp;gt; || Number of attempts to verify a blockchain payment&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;PAYMENT_VERIFY_DELAY_MS&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;5000&amp;lt;/code&amp;gt; || Delay between verification retry attempts&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Debug &amp;amp; Admin Endpoints ===&lt;br /&gt;
&lt;br /&gt;
These are useful for testing without making actual calls:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;/debug-state&amp;lt;/code&amp;gt; — shows current lobby users and active rooms (no auth required)&lt;br /&gt;
* &amp;lt;code&amp;gt;/debug-rates/USERNAME&amp;lt;/code&amp;gt; — shows parsed rates for a user&lt;br /&gt;
* &amp;lt;code&amp;gt;/debug-rates/USERNAME?caller=CALLER&amp;amp;type=voice&amp;lt;/code&amp;gt; — shows what rates a specific caller would receive (checks token balances too)&lt;br /&gt;
* &amp;lt;code&amp;gt;/admin/balance?key=YOUR_ADMIN_KEY&amp;lt;/code&amp;gt; — shows escrow account HBD balance&lt;br /&gt;
* &amp;lt;code&amp;gt;/admin/ledger?key=YOUR_ADMIN_KEY&amp;lt;/code&amp;gt; — shows recent payment records&lt;br /&gt;
* &amp;lt;code&amp;gt;/admin/peers?key=YOUR_ADMIN_KEY&amp;lt;/code&amp;gt; — JSON list of discovered + approved federation peers&lt;br /&gt;
* &amp;lt;code&amp;gt;/admin/peers/approve?key=YOUR_ADMIN_KEY&amp;amp;domain=peer.com&amp;lt;/code&amp;gt; (POST) — approve a discovered peer&lt;br /&gt;
* &amp;lt;code&amp;gt;/admin/peers/revoke?key=YOUR_ADMIN_KEY&amp;amp;domain=peer.com&amp;lt;/code&amp;gt; (POST) — revoke approval&lt;br /&gt;
* &amp;lt;code&amp;gt;/admin/peers/rescan?key=YOUR_ADMIN_KEY&amp;lt;/code&amp;gt; (POST) — force a Hive directory rescan&lt;br /&gt;
&lt;br /&gt;
=== Operator Tool Pages ===&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;/rate-editor.html&amp;lt;/code&amp;gt; — generate your &amp;lt;code&amp;gt;v4call-rates&amp;lt;/code&amp;gt; Hive post&lt;br /&gt;
* &amp;lt;code&amp;gt;/server-sign.html&amp;lt;/code&amp;gt; — generate your signed federation verify file&lt;br /&gt;
* &amp;lt;code&amp;gt;/server-announce.html&amp;lt;/code&amp;gt; — publish your server announcement on Hive&lt;br /&gt;
* &amp;lt;code&amp;gt;/admin-peers.html&amp;lt;/code&amp;gt; — UI for federation peer approve/revoke/rescan&lt;br /&gt;
* &amp;lt;code&amp;gt;/info.html&amp;lt;/code&amp;gt; — public landing page (shown to users who hit the basic-auth login cancel)&lt;br /&gt;
&lt;br /&gt;
== Updating Your Server ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important:&#039;&#039;&#039; Always use &amp;lt;code&amp;gt;docker compose down&amp;lt;/code&amp;gt; before rebuilding. Without this step, Docker may reuse the old container even after a rebuild, and your changes will not take effect.&lt;br /&gt;
&lt;br /&gt;
 cd /opt/v4call&lt;br /&gt;
 docker compose down&lt;br /&gt;
 docker compose build --no-cache&lt;br /&gt;
 docker compose up -d&lt;br /&gt;
&lt;br /&gt;
Your data, SQLite databases and &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; config are preserved — they live in &amp;lt;code&amp;gt;data/logs/&amp;lt;/code&amp;gt; which is a mounted volume.&lt;br /&gt;
&lt;br /&gt;
To pull updates from GitHub and deploy:&lt;br /&gt;
&lt;br /&gt;
 cd /opt/v4call&lt;br /&gt;
 git pull&lt;br /&gt;
 docker compose down&lt;br /&gt;
 docker compose build --no-cache&lt;br /&gt;
 docker compose up -d&lt;br /&gt;
&lt;br /&gt;
To push your own customisations to GitHub:&lt;br /&gt;
&lt;br /&gt;
 # On your local computer after making changes:&lt;br /&gt;
 git add .&lt;br /&gt;
 git commit -m &amp;quot;describe what you changed&amp;quot;&lt;br /&gt;
 git push&lt;br /&gt;
&lt;br /&gt;
 # On the VPS:&lt;br /&gt;
 cd /opt/v4call&lt;br /&gt;
 git pull&lt;br /&gt;
 docker compose down&lt;br /&gt;
 docker compose build --no-cache&lt;br /&gt;
 docker compose up -d&lt;br /&gt;
&lt;br /&gt;
== Common Problems and Fixes ==&lt;br /&gt;
&lt;br /&gt;
=== Changes not showing after rebuild ===&lt;br /&gt;
&lt;br /&gt;
If you edited &amp;lt;code&amp;gt;server.js&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;index.html&amp;lt;/code&amp;gt; but changes are not visible, you probably forgot to bring Docker down first. &amp;lt;code&amp;gt;docker compose restart&amp;lt;/code&amp;gt; and even &amp;lt;code&amp;gt;docker compose up -d --build&amp;lt;/code&amp;gt; can reuse old containers.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Fix:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
 docker compose down&lt;br /&gt;
 docker compose build --no-cache&lt;br /&gt;
 docker compose up -d&lt;br /&gt;
&lt;br /&gt;
=== SqliteError: unable to open database file ===&lt;br /&gt;
&lt;br /&gt;
The app container runs as UID 1000 but the logs directory was created by root. Fix:&lt;br /&gt;
&lt;br /&gt;
 chown -R 1000:1000 /opt/v4call/data/logs&lt;br /&gt;
 docker compose restart app&lt;br /&gt;
&lt;br /&gt;
=== Certbot says &amp;quot;No renewals were attempted&amp;quot; ===&lt;br /&gt;
&lt;br /&gt;
The certbot container&#039;s default behaviour is a renewal loop. Your &amp;lt;code&amp;gt;certonly&amp;lt;/code&amp;gt; command is being ignored. Always use &amp;lt;code&amp;gt;--entrypoint certbot&amp;lt;/code&amp;gt; to override it:&lt;br /&gt;
&lt;br /&gt;
 docker compose run --rm --entrypoint certbot certbot certonly ...&lt;br /&gt;
&lt;br /&gt;
Without &amp;lt;code&amp;gt;--entrypoint certbot&amp;lt;/code&amp;gt; the container runs its renewal script instead of your command.&lt;br /&gt;
&lt;br /&gt;
=== Nginx crashes with &amp;quot;cannot load certificate: No such file&amp;quot; ===&lt;br /&gt;
&lt;br /&gt;
You added the HTTPS server block before getting the certificate. Nginx reads all server blocks at startup — if the cert file doesn&#039;t exist, the entire process fails.&lt;br /&gt;
&lt;br /&gt;
Fix: revert nginx config to HTTP-only (Step 9), restart Nginx, get the certificate (Step 12), then re-add HTTPS (Step 13).&lt;br /&gt;
&lt;br /&gt;
=== Webroot challenge test returns nothing ===&lt;br /&gt;
&lt;br /&gt;
Nginx is not running. Check:&lt;br /&gt;
&lt;br /&gt;
 docker compose ps&lt;br /&gt;
 docker compose logs nginx&lt;br /&gt;
&lt;br /&gt;
If Nginx is restarting — it has the HTTPS config with a missing cert file. Use the HTTP-only config.&lt;br /&gt;
&lt;br /&gt;
=== &amp;quot;Escrow key does NOT match&amp;quot; warning on startup ===&lt;br /&gt;
&lt;br /&gt;
The key in &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; is the wrong type. You need the &#039;&#039;&#039;active&#039;&#039;&#039; private key, not the posting or owner key. Find it in your Hive wallet → Keys &amp;amp; Permissions → Active. It starts with &amp;lt;code&amp;gt;5K&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Site unreachable on port 443 ===&lt;br /&gt;
&lt;br /&gt;
Check that the certificate was issued:&lt;br /&gt;
&lt;br /&gt;
 ls /opt/v4call/data/certbot/conf/live/&lt;br /&gt;
&lt;br /&gt;
Should show a folder with your domain name. If empty — go back to Step 12.&lt;br /&gt;
&lt;br /&gt;
=== npm ci error during Docker build ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;npm ci&amp;lt;/code&amp;gt; command requires a &amp;lt;code&amp;gt;package-lock.json&amp;lt;/code&amp;gt; file. The project uses &amp;lt;code&amp;gt;npm install&amp;lt;/code&amp;gt; instead. If you see this error, check your &amp;lt;code&amp;gt;Dockerfile&amp;lt;/code&amp;gt; — it should say &amp;lt;code&amp;gt;npm install&amp;lt;/code&amp;gt; not &amp;lt;code&amp;gt;npm ci&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Custom token payments not working ===&lt;br /&gt;
&lt;br /&gt;
If token rates are detected but payments fail:&lt;br /&gt;
* Check the escrow account holds the token — send some tokens to your escrow account on Hive-Engine&lt;br /&gt;
* Check the token symbol matches exactly (case-sensitive) between the rates post and Hive-Engine&lt;br /&gt;
* Check the server logs: &amp;lt;code&amp;gt;docker compose logs app | grep -i &amp;quot;token\|cnoobs\|escrow-token&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
* The Hive-Engine API endpoint must be &amp;lt;code&amp;gt;https://api.hive-engine.com/rpc/contracts&amp;lt;/code&amp;gt; — this is built into the code&lt;br /&gt;
&lt;br /&gt;
=== [encrypted — unlock with 🔑 key panel to read] ===&lt;br /&gt;
&lt;br /&gt;
You logged in with Hive Keychain. Keychain does not expose private keys, so encrypted messages cannot be decrypted without your posting key. Enter your posting key in the 🔑 panel at the bottom of the online users list. The key stays in browser session memory only — it is needed once per session.&lt;br /&gt;
&lt;br /&gt;
=== Federation peer verification fails with HTTP 404 ===&lt;br /&gt;
&lt;br /&gt;
Logs show:&lt;br /&gt;
 [federation] ✗ Peer verification failed for X: Cannot fetch verify.json: HTTP 404&lt;br /&gt;
&lt;br /&gt;
The peer&#039;s &amp;lt;code&amp;gt;/.well-known/v4call-server.json&amp;lt;/code&amp;gt; file isn&#039;t being served. Common causes:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Wrong directory name&#039;&#039;&#039; — must be &amp;lt;code&amp;gt;.well-known&amp;lt;/code&amp;gt; with a hyphen, not &amp;lt;code&amp;gt;.well_known&amp;lt;/code&amp;gt; with an underscore (RFC 8615).&lt;br /&gt;
* &#039;&#039;&#039;Wrong filename&#039;&#039;&#039; — must be &amp;lt;code&amp;gt;v4call-server.json&amp;lt;/code&amp;gt;, not &amp;lt;code&amp;gt;verify.json&amp;lt;/code&amp;gt; or anything else. The verifier code looks for this exact name.&lt;br /&gt;
* &#039;&#039;&#039;Not in the Docker image&#039;&#039;&#039; — file lives at &amp;lt;code&amp;gt;public/.well-known/v4call-server.json&amp;lt;/code&amp;gt; in the repo and is copied into the image at build time. After placing the file, rebuild: &amp;lt;code&amp;gt;docker compose down &amp;amp;&amp;amp; docker compose build --no-cache &amp;amp;&amp;amp; docker compose up -d&amp;lt;/code&amp;gt;.&lt;br /&gt;
* &#039;&#039;&#039;Placeholder not replaced&#039;&#039;&#039; — the repo ships with a tiny placeholder. You must overwrite it with your own signed file from &amp;lt;code&amp;gt;/server-sign.html&amp;lt;/code&amp;gt; before federation will work.&lt;br /&gt;
&lt;br /&gt;
Test from outside the container:&lt;br /&gt;
 curl https://yourdomain.com/.well-known/v4call-server.json&lt;br /&gt;
&lt;br /&gt;
Should return your signed JSON, not &amp;lt;code&amp;gt;404&amp;lt;/code&amp;gt; or the placeholder.&lt;br /&gt;
&lt;br /&gt;
=== Federation connection flapping (rapid disconnect/reconnect loop) ===&lt;br /&gt;
&lt;br /&gt;
If logs show repeated &amp;lt;code&amp;gt;Outbound connected&amp;lt;/code&amp;gt; followed by &amp;lt;code&amp;gt;Disconnected — retry in 2s&amp;lt;/code&amp;gt; with no successful handshake, check verify.json:&lt;br /&gt;
&lt;br /&gt;
* Is the file present at the path above?&lt;br /&gt;
* Does its &amp;lt;code&amp;gt;signature&amp;lt;/code&amp;gt; match the Hive account&#039;s posting key? Test by pasting the JSON into the &#039;&#039;&#039;Verify&#039;&#039;&#039; panel on &amp;lt;code&amp;gt;/server-sign.html&amp;lt;/code&amp;gt; — should say &amp;quot;✓ Signature valid&amp;quot;.&lt;br /&gt;
* Does &amp;lt;code&amp;gt;hive_account&amp;lt;/code&amp;gt; in the JSON match &amp;lt;code&amp;gt;SERVER_HIVE_ACCOUNT&amp;lt;/code&amp;gt; in the peer&#039;s .env? They must match.&lt;br /&gt;
&lt;br /&gt;
=== Federation peer connects but my user can&#039;t be seen on the other side ===&lt;br /&gt;
&lt;br /&gt;
If two servers connect successfully but a user on Server A doesn&#039;t appear in Server B&#039;s lobby, check:&lt;br /&gt;
&lt;br /&gt;
* Server A logs: &amp;lt;code&amp;gt;[federation] → user-online @username → 1 peer(s)&amp;lt;/code&amp;gt; — confirms A broadcast the presence&lt;br /&gt;
* Server B logs: &amp;lt;code&amp;gt;[federation] ← user-online @username@A.com&amp;lt;/code&amp;gt; — confirms B received it&lt;br /&gt;
&lt;br /&gt;
If A logs the broadcast but B never received it, the federation socket might be one-way. Check both servers have approved each other (visible at &amp;lt;code&amp;gt;/admin-peers.html&amp;lt;/code&amp;gt;).&lt;br /&gt;
&lt;br /&gt;
=== Federated paid call/DM fails silently after payment ===&lt;br /&gt;
&lt;br /&gt;
Most common cause: the user&#039;s &amp;lt;code&amp;gt;v4call-rates&amp;lt;/code&amp;gt; post declares an &amp;lt;code&amp;gt;ESCROW:&amp;lt;/code&amp;gt; account whose active key isn&#039;t on their home server. The caller&#039;s payment lands in that escrow correctly, but no server can disburse from it.&lt;br /&gt;
&lt;br /&gt;
Fix: have the user re-post their rates with an &amp;lt;code&amp;gt;ESCROW:&amp;lt;/code&amp;gt; matching their home server&#039;s &amp;lt;code&amp;gt;ESCROW_ACCOUNT&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt;. Or migrate that escrow account&#039;s active key to the right server.&lt;br /&gt;
&lt;br /&gt;
The federation handshake also surfaces this as a clear error message via the escrow-mismatch guard — check the caller-side server logs for it.&lt;br /&gt;
&lt;br /&gt;
=== Discovery scanner finds 0 posts ===&lt;br /&gt;
&lt;br /&gt;
If &amp;lt;code&amp;gt;/admin-peers.html&amp;lt;/code&amp;gt; shows &amp;quot;No v4call-server posts discovered yet&amp;quot; even after rescanning:&lt;br /&gt;
&lt;br /&gt;
* Click &#039;&#039;&#039;🔍 Rescan Hive now&#039;&#039;&#039; and watch logs:  &amp;lt;code&amp;gt;docker compose logs -f app | grep discovery&amp;lt;/code&amp;gt;&lt;br /&gt;
* Expected: &amp;lt;code&amp;gt;[discovery] Hive returned N post(s) under v4call-server tag&amp;lt;/code&amp;gt;&lt;br /&gt;
* If N = 0, no Hive posts exist with that tag yet — publish your own via &amp;lt;code&amp;gt;/server-announce.html&amp;lt;/code&amp;gt;&lt;br /&gt;
* If you see &amp;lt;code&amp;gt;[discovery] No response from Hive tag query&amp;lt;/code&amp;gt;, the Hive API is unreachable from inside the container. Test container DNS / outbound HTTPS by hitting any external HTTPS URL from the container.&lt;br /&gt;
&lt;br /&gt;
== Quick Reference ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Command !! What it does&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose ps&amp;lt;/code&amp;gt; || Show status of all containers&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose logs app&amp;lt;/code&amp;gt; || Show app logs&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose logs nginx&amp;lt;/code&amp;gt; || Show Nginx logs&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose logs -f app&amp;lt;/code&amp;gt; || Watch live logs (Ctrl+C to stop)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose down&amp;lt;/code&amp;gt; || Stop everything (&#039;&#039;&#039;always do this before rebuilding&#039;&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose build --no-cache&amp;lt;/code&amp;gt; || Rebuild without using cached layers&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose up -d&amp;lt;/code&amp;gt; || Start everything in background&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose down &amp;amp;&amp;amp; docker compose build --no-cache &amp;amp;&amp;amp; docker compose up -d&amp;lt;/code&amp;gt; || Full rebuild cycle (use after any code change)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose restart nginx&amp;lt;/code&amp;gt; || Restart Nginx after config-only changes (no rebuild needed)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;chown -R 1000:1000 /opt/v4call/data/logs&amp;lt;/code&amp;gt; || Fix SQLite write permissions&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose run --rm --entrypoint certbot certbot certificates&amp;lt;/code&amp;gt; || List SSL certificates&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;curl http://yourdomain.com/.well-known/acme-challenge/testfile&amp;lt;/code&amp;gt; || Test certbot webroot works&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose logs app &amp;amp;#124; grep -i &amp;quot;token\|escrow&amp;quot;&amp;lt;/code&amp;gt; || Check token payment logs&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose logs -f app &amp;amp;#124; grep -E &amp;quot;\[federation\]&amp;amp;#124;\[discovery\]&amp;quot;&amp;lt;/code&amp;gt; || Watch federation activity&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;curl https://yourdomain.com/.well-known/v4call-server.json&amp;lt;/code&amp;gt; || Check verify file is served&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;https://yourdomain.com/server-sign.html&amp;lt;/code&amp;gt; || Generate signed federation verify file&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;https://yourdomain.com/server-announce.html&amp;lt;/code&amp;gt; || Publish server to Hive directory&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;https://yourdomain.com/admin-peers.html&amp;lt;/code&amp;gt; || Manage federation peer approvals&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Optional: Password Protect Your Server During Testing ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;strong&amp;gt;Optional: Password Protect Your Server During Testing (HTTP Basic Auth)&amp;lt;/strong&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
During development and testing you may want to restrict access so only people you invite can use your server. This uses Nginx HTTP Basic Auth — a simple username and password prompt that appears before the v4call login screen.&lt;br /&gt;
&lt;br /&gt;
When a visitor cancels the login prompt they are shown a public &amp;lt;code&amp;gt;info.html&amp;lt;/code&amp;gt; page where they can read about the project and request access.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important&#039;&#039;&#039;: do NOT add basic auth to the &amp;lt;code&amp;gt;/federation&amp;lt;/code&amp;gt; location block. Federation peers cannot supply HTTP credentials and the connection will fail. The auth lines below are added only to &amp;lt;code&amp;gt;/socket.io/&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Step 1 — Install the htpasswd tool&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;apt install -y apache2-utils&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Step 2 — Create the password file and add your first user &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;htpasswd -c /opt/v4call/nginx/.htpasswd yourusername&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You will be prompted to enter and confirm a password. The &amp;lt;code&amp;gt;-c&amp;lt;/code&amp;gt; flag creates the file. Do not use &amp;lt;code&amp;gt;-c&amp;lt;/code&amp;gt; again or it will overwrite the file and delete existing users.&lt;br /&gt;
&lt;br /&gt;
To add more users later:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;htpasswd /opt/v4call/nginx/.htpasswd anotherusername&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To remove a user:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;htpasswd -D /opt/v4call/nginx/.htpasswd username&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Step 3 — Create the public info page &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;nano /opt/v4call/public/info.html&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Paste your HTML content — a page explaining the project and how to request access. This page is served publicly without a password so visitors who cancel the login prompt can still read it.&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Step 4 — Mount the files into the Nginx container &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Edit &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt; and add two lines to the nginx volumes section:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  nginx:&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ./nginx/v4call.conf:/etc/nginx/conf.d/default.conf:ro&lt;br /&gt;
      - ./nginx/.htpasswd:/etc/nginx/.htpasswd:ro          # add this&lt;br /&gt;
      - ./public/info.html:/usr/share/nginx/html/info.html:ro  # add this&lt;br /&gt;
      - ./data/certbot/conf:/etc/letsencrypt:ro&lt;br /&gt;
      - ./data/certbot/www:/var/www/certbot:ro&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Step 5 — Enable auth in your Nginx config &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The HTTPS config from Step 13 already includes the &amp;lt;code&amp;gt;auth_basic&amp;lt;/code&amp;gt; lines, just commented out. Open &amp;lt;code&amp;gt;nginx/v4call.conf&amp;lt;/code&amp;gt; and uncomment them in BOTH the &amp;lt;code&amp;gt;/socket.io/&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt; location blocks (4 lines total, two pairs):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
        auth_basic           &amp;quot;v4call — Private Testing&amp;quot;;&lt;br /&gt;
        auth_basic_user_file /etc/nginx/.htpasswd;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Do not&#039;&#039;&#039; uncomment auth in the &amp;lt;code&amp;gt;/federation&amp;lt;/code&amp;gt; block — leave it as-is.&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Step 6 — Recreate the Nginx container to pick up the new volume mounts &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important&#039;&#039;&#039;: &amp;lt;code&amp;gt;docker compose restart nginx&amp;lt;/code&amp;gt; is not enough — it reuses the old container and ignores new volume mounts. You must recreate it:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker compose down &amp;amp;&amp;amp; docker compose up -d&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
After this, verify the files are mounted inside the container:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker compose exec nginx ls /usr/share/nginx/html/&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You should see &amp;lt;code&amp;gt;info.html&amp;lt;/code&amp;gt; listed alongside &amp;lt;code&amp;gt;50x.html&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;index.html&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Step 7 — Test it works &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;curl -o /dev/null -s -w &amp;quot;%{http_code}&amp;quot; https://call.yourdomain.com/&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Should return &amp;lt;code&amp;gt;401&amp;lt;/code&amp;gt; (login required).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;curl -o /dev/null -s -w &amp;quot;%{http_code}&amp;quot; https://call.yourdomain.com/info.html&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Should return &amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt; (public, no auth needed).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;curl -u youruser:yourpassword -o /dev/null -s -w &amp;quot;%{http_code}&amp;quot; https://call.yourdomain.com/&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Should return &amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt; (correct credentials accepted).&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Managing users without restarting &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
After adding or removing users from the &amp;lt;code&amp;gt;.htpasswd&amp;lt;/code&amp;gt; file, reload Nginx config without downtime:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker compose exec nginx nginx -s reload&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
No restart needed — Nginx re-reads the &amp;lt;code&amp;gt;.htpasswd&amp;lt;/code&amp;gt; file on every request anyway.&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Keep .htpasswd out of GitHub &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The password file should never be committed to your repository. It&#039;s already in the project&#039;s &amp;lt;code&amp;gt;.gitignore&amp;lt;/code&amp;gt;, but if you forked, double-check:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;echo &amp;quot;nginx/.htpasswd&amp;quot; &amp;gt;&amp;gt; /opt/v4call/.gitignore&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Removing auth when you go public &amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
When you are ready to open your server to everyone, simply re-comment the &amp;lt;code&amp;gt;auth_basic&amp;lt;/code&amp;gt; lines from &amp;lt;code&amp;gt;nginx/v4call.conf&amp;lt;/code&amp;gt; and reload:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker compose exec nginx nginx -s reload&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
No other changes needed.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;How to check if anyone tried to log in (failed attempts) or successfully logged in with .htpasswd on Nginx in Docker&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important Tip:&#039;&#039;&#039; Always run these commands in the same folder where your &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt; file is located. If you are in the wrong directory the commands will not find your container and nothing will show up.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&#039;&#039; Basic Commands &#039;&#039;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
; &#039;&#039;&#039;See everything live (good while testing)&#039;&#039;&#039;&lt;br /&gt;
: &amp;lt;code&amp;gt;docker compose logs -f nginx&amp;lt;/code&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
: &amp;lt;code&amp;gt;-f&amp;lt;/code&amp;gt; means &amp;quot;follow/live&amp;quot; – new log lines appear automatically. Remove &amp;lt;code&amp;gt;-f&amp;lt;/code&amp;gt; if you only want to read the current logs once and stop.&lt;br /&gt;
&lt;br /&gt;
; &#039;&#039;&#039;Best command to watch failed login attempts (people guessing passwords)&#039;&#039;&#039;&lt;br /&gt;
: &amp;lt;code&amp;gt;docker compose logs -f nginx | grep -E &amp;quot;mismatch|not found|401&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
; &#039;&#039;&#039;See who successfully logged in&#039;&#039;&#039;&lt;br /&gt;
: &amp;lt;code&amp;gt;docker compose logs nginx | grep &amp;quot;remote_user&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
; &#039;&#039;&#039;Combined view – most useful for daily checking (failed + successful)&#039;&#039;&#039;&lt;br /&gt;
: &amp;lt;code&amp;gt;docker compose logs -f --tail=100 nginx | grep -E &amp;quot;(mismatch|not found|remote_user|401)&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&#039;&#039; What the Logs Look Like &#039;&#039;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Failed attempt (wrong password):&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;2026/04/17 01:23:45 [error] ... user &amp;quot;admin&amp;quot;: password mismatch, client: 185.123.45.67, ...&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Failed attempt (wrong username):&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;2026/04/17 01:24:12 [error] ... user &amp;quot;hacker123&amp;quot; was not found in &amp;quot;/etc/nginx/.htpasswd&amp;quot;, client: 45.67.89.10, ...&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Successful login:&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;... &amp;quot;GET /protected/ HTTP/1.1&amp;quot; 200 ... remote_user: &amp;quot;myuser&amp;quot; ...&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&#039;&#039; How to Customise These Commands &#039;&#039;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* Change &amp;lt;code&amp;gt;nginx&amp;lt;/code&amp;gt; to the exact name of your service if it is different in docker-compose.yml.&lt;br /&gt;
* Remove &amp;lt;code&amp;gt;-f&amp;lt;/code&amp;gt; to read the full log once without live following.&lt;br /&gt;
* Change &amp;lt;code&amp;gt;--tail=100&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;--tail=500&amp;lt;/code&amp;gt; (or any number) to show more or fewer old lines.&lt;br /&gt;
* Add or remove words in the &amp;lt;code&amp;gt;grep&amp;lt;/code&amp;gt; part to filter differently.&lt;br /&gt;
  Examples:&lt;br /&gt;
  * Only failed attempts: &amp;lt;code&amp;gt;grep -E &amp;quot;mismatch|not found&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
  * Only 401 errors: &amp;lt;code&amp;gt;grep &amp;quot; 401 &amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
  * Everything auth related: &amp;lt;code&amp;gt;grep -E &amp;quot;(auth|password|mismatch|not found|remote_user)&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&#039;&#039;Quick Copy-Paste Commands &#039;&#039;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
# Watch failed guesses live&lt;br /&gt;
docker compose logs -f nginx | grep -E &amp;quot;mismatch|not found|401&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Check successful logins&lt;br /&gt;
docker compose logs nginx | grep &amp;quot;remote_user&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Combined quick check (recommended)&lt;br /&gt;
docker compose logs -f --tail=100 nginx | grep -E &amp;quot;(mismatch|not found|remote_user|401)&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Docker]]&lt;br /&gt;
[[Category:Ubuntu]]&lt;br /&gt;
[[Category:Hive]]&lt;br /&gt;
[[Category:v4call]]&lt;br /&gt;
[[Category:WebRTC]]&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Basic_Html_JavaScript_POPUP_warning_Accept&amp;diff=741</id>
		<title>Basic Html JavaScript POPUP warning Accept</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Basic_Html_JavaScript_POPUP_warning_Accept&amp;diff=741"/>
		<updated>2026-04-19T19:24:55Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot;3 parts  ==Part one== * This is placed in the &amp;lt;head&amp;gt; &amp;lt;/head&amp;gt; at the top of index.html contains css &amp;lt;pre&amp;gt; &amp;lt;style&amp;gt;   #overlay {     position: fixed; top: 0; left: 0; width: 100%; height: 100%;     background: rgba(0,0,0,0.85); color: white; z-index: 10000;     display: flex; align-items: center; justify-content: center; text-align: center;   }   .popup-box { background: #222; padding: 30px; border-radius: 10px; border: 1px solid #444; }   button { padding: 10px 20px; curso...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;3 parts&lt;br /&gt;
&lt;br /&gt;
==Part one==&lt;br /&gt;
* This is placed in the &amp;lt;head&amp;gt; &amp;lt;/head&amp;gt; at the top of index.html contains css&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;style&amp;gt;&lt;br /&gt;
  #overlay {&lt;br /&gt;
    position: fixed; top: 0; left: 0; width: 100%; height: 100%;&lt;br /&gt;
    background: rgba(0,0,0,0.85); color: white; z-index: 10000;&lt;br /&gt;
    display: flex; align-items: center; justify-content: center; text-align: center;&lt;br /&gt;
  }&lt;br /&gt;
  .popup-box { background: #222; padding: 30px; border-radius: 10px; border: 1px solid #444; }&lt;br /&gt;
  button { padding: 10px 20px; cursor: pointer; background: #007bff; color: white; border: none; border-radius: 5px; }&lt;br /&gt;
  .hidden { display: none !important; }&lt;br /&gt;
&amp;lt;/style&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==part two==&lt;br /&gt;
* This is placed in/at the top of the &amp;lt;body&amp;gt; &amp;lt;/body&amp;gt; section, contains message.&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!-- pop up cookies and other 2/3--&amp;gt;&lt;br /&gt;
&amp;lt;div id=&amp;quot;overlay&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div class=&amp;quot;popup-box&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;h2&amp;gt;Welcome to Complete Noobs!&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;We use cookies to track traffic. Are you 21 or older?&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;By accepting you are entering at your own risk&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;button onclick=&amp;quot;acceptAndHide()&amp;quot;&amp;gt;Yes, I Accept &amp;amp; I&#039;m Over 21 &amp;amp; am aware you are noobs and are still learning i enter at my own risk&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;→ &amp;lt;a href=&amp;quot;https://www.google.com&amp;quot; &amp;gt;No Thank You - take me to Google&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==part 3==&lt;br /&gt;
* This is placed in/at the bottom of the &amp;lt;body&amp;gt; &amp;lt;/body&amp;gt; section, contains js script.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!-- pop up cookies and other  3/3--&amp;gt;&lt;br /&gt;
&amp;lt;script&amp;gt;&lt;br /&gt;
  function acceptAndHide() {&lt;br /&gt;
    // Save the choice in the browser&#039;s &amp;quot;localStorage&amp;quot; so it doesn&#039;t pop up again&lt;br /&gt;
    localStorage.setItem(&#039;gate_passed&#039;, &#039;true&#039;);&lt;br /&gt;
    document.getElementById(&#039;overlay&#039;).classList.add(&#039;hidden&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // On page load, check if they already clicked &amp;quot;Yes&amp;quot;&lt;br /&gt;
  window.onload = function() {&lt;br /&gt;
    if (localStorage.getItem(&#039;gate_passed&#039;) === &#039;true&#039;) {&lt;br /&gt;
      document.getElementById(&#039;overlay&#039;).classList.add(&#039;hidden&#039;);&lt;br /&gt;
    }&lt;br /&gt;
  };&lt;br /&gt;
&amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==use case==&lt;br /&gt;
* Sample use case&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;My Projects - Status Update&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;style&amp;gt;&lt;br /&gt;
        body {&lt;br /&gt;
            font-family: Arial, Helvetica, sans-serif;&lt;br /&gt;
            max-width: 800px;&lt;br /&gt;
            margin: 40px auto;&lt;br /&gt;
            padding: 20px;&lt;br /&gt;
            line-height: 1.6;&lt;br /&gt;
            color: #333;&lt;br /&gt;
        }&lt;br /&gt;
        h1 {&lt;br /&gt;
            text-align: center;&lt;br /&gt;
            color: #222;&lt;br /&gt;
        }&lt;br /&gt;
        h2 {&lt;br /&gt;
            color: #444;&lt;br /&gt;
            border-bottom: 2px solid #eee;&lt;br /&gt;
            padding-bottom: 8px;&lt;br /&gt;
        }&lt;br /&gt;
        a {&lt;br /&gt;
            color: #0066cc;&lt;br /&gt;
            text-decoration: none;&lt;br /&gt;
        }&lt;br /&gt;
        a:hover {&lt;br /&gt;
            text-decoration: underline;&lt;br /&gt;
        }&lt;br /&gt;
        .status {&lt;br /&gt;
            font-style: italic;&lt;br /&gt;
            color: #555;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/style&amp;gt;&lt;br /&gt;
&amp;lt;!-- pop up cookies and other 1/3--&amp;gt;&lt;br /&gt;
&amp;lt;style&amp;gt;&lt;br /&gt;
  #overlay {&lt;br /&gt;
    position: fixed; top: 0; left: 0; width: 100%; height: 100%;&lt;br /&gt;
    background: rgba(0,0,0,0.85); color: white; z-index: 10000;&lt;br /&gt;
    display: flex; align-items: center; justify-content: center; text-align: center;&lt;br /&gt;
  }&lt;br /&gt;
  .popup-box { background: #222; padding: 30px; border-radius: 10px; border: 1px solid #444; }&lt;br /&gt;
  button { padding: 10px 20px; cursor: pointer; background: #007bff; color: white; border: none; border-radius: 5px; }&lt;br /&gt;
  .hidden { display: none !important; }&lt;br /&gt;
&amp;lt;/style&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;!--24-06-25 adding google analytic  --&amp;gt;&lt;br /&gt;
&amp;lt;!-- Google tag (gtag.js) --&amp;gt;&lt;br /&gt;
&amp;lt;script async src=&amp;quot;https://www.googletagmanager.com/gtag/js?id=G-YXYQE65XY1&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;script&amp;gt;&lt;br /&gt;
  window.dataLayer = window.dataLayer || [];&lt;br /&gt;
  function gtag(){dataLayer.push(arguments);}&lt;br /&gt;
  gtag(&#039;js&#039;, new Date());&lt;br /&gt;
&lt;br /&gt;
  gtag(&#039;config&#039;, &#039;G-YXYQE65XY1&#039;);&lt;br /&gt;
&amp;lt;/script&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
&amp;lt;!-- pop up cookies and other 2/3--&amp;gt;&lt;br /&gt;
&amp;lt;div id=&amp;quot;overlay&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div class=&amp;quot;popup-box&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;h2&amp;gt;Welcome to Complete Noobs!&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;We use cookies to track traffic. Are you 21 or older?&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;By accepting you are entering at your own risk&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;button onclick=&amp;quot;acceptAndHide()&amp;quot;&amp;gt;Yes, I Accept &amp;amp; I&#039;m Over 21 &amp;amp; am aware you are noobs and are still learning i enter at my own risk&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;→ &amp;lt;a href=&amp;quot;https://www.google.com&amp;quot; &amp;gt;No Thank You - take me to Google&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h1&amp;gt;Project Status&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;CompleteNoobs&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;p class=&amp;quot;status&amp;quot;&amp;gt;Currently on hold due to lack of free time.&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;It will be relaunched as &amp;lt;strong&amp;gt;cnoobs.com&amp;lt;/strong&amp;gt; running inside a Docker container.&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;Future plans include a possible soft-fork of MediaWiki that allows Hive accounts holding a certain amount of CNOOBS coins to post and edit pages. &lt;br /&gt;
       I&#039;m also interested in integrating the Hive rewards system for content creators and exploring reward splits between users.&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;Additional custom features being considered:&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        &amp;lt;li&amp;gt;Custom tags for embedding IPFS content to keep the wiki database smaller and more portable:&amp;lt;/li&amp;gt;&lt;br /&gt;
        &amp;lt;ul&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;code&amp;gt;&amp;amp;lt;ipfs_video&amp;amp;gt;ipfs_addr&amp;amp;lt;/ipfs_video&amp;amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;code&amp;gt;&amp;amp;lt;ipfs_pic&amp;amp;gt;ipfs_addr&amp;amp;lt;/ipfs_pic&amp;amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;code&amp;gt;&amp;amp;lt;ipfs_audio&amp;amp;gt;ipfs_addr&amp;amp;lt;/ipfs_audio&amp;amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;code&amp;gt;&amp;amp;lt;ipfs_file&amp;amp;gt;ipfs_addr&amp;amp;lt;/ipfs_file&amp;amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
        &amp;lt;/ul&amp;gt;&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;The old draft remains available but is unmaintained:&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;→ &amp;lt;a href=&amp;quot;https://www.completenoobs.com/noobs/Main_Page&amp;quot; target=&amp;quot;_blank&amp;quot;&amp;gt;CompleteNoobs.com (old wiki)&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;→ &amp;lt;a href=&amp;quot;https://xml.completenoobs.com&amp;quot; target=&amp;quot;_blank&amp;quot;&amp;gt;xml.CompleteNoobs.com (old xml downloads)&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;n33b.com&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;p class=&amp;quot;status&amp;quot;&amp;gt;Currently on hold.&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;It will &amp;lt;strong&amp;gt;not&amp;lt;/strong&amp;gt; become a coin project. It will return to its original purpose: a purely educational site for people who like to learn by tinkering and hands-on experimentation.&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;v4call.com&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;→ &amp;lt;a href=&amp;quot;https://v4call.com&amp;quot; target=&amp;quot;_blank&amp;quot;&amp;gt;v4call.com&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p class=&amp;quot;status&amp;quot;&amp;gt;idk — tinker gonna tinker 😄&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;hr&amp;gt;&lt;br /&gt;
    &amp;lt;p style=&amp;quot;text-align:center; color:#777; font-size:0.9em;&amp;quot;&amp;gt;&lt;br /&gt;
        Last updated: April 2026&lt;br /&gt;
    &amp;lt;/p&amp;gt;&lt;br /&gt;
&amp;lt;!-- pop up cookies and other  3/3--&amp;gt;&lt;br /&gt;
&amp;lt;script&amp;gt;&lt;br /&gt;
  function acceptAndHide() {&lt;br /&gt;
    // Save the choice in the browser&#039;s &amp;quot;localStorage&amp;quot; so it doesn&#039;t pop up again&lt;br /&gt;
    localStorage.setItem(&#039;gate_passed&#039;, &#039;true&#039;);&lt;br /&gt;
    document.getElementById(&#039;overlay&#039;).classList.add(&#039;hidden&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // On page load, check if they already clicked &amp;quot;Yes&amp;quot;&lt;br /&gt;
  window.onload = function() {&lt;br /&gt;
    if (localStorage.getItem(&#039;gate_passed&#039;) === &#039;true&#039;) {&lt;br /&gt;
      document.getElementById(&#039;overlay&#039;).classList.add(&#039;hidden&#039;);&lt;br /&gt;
    }&lt;br /&gt;
  };&lt;br /&gt;
&amp;lt;/script&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=V4CALL&amp;diff=740</id>
		<title>V4CALL</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=V4CALL&amp;diff=740"/>
		<updated>2026-04-19T14:17:40Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot;{{:LICENCE_HEADER_MIT}}   = v4call — Deploy Your Own Server on Ubuntu 24.04 with Docker =  From CompleteNoobs  This guide walks through deploying your own v4call server from scratch on a fresh Vultr Ubuntu 24.04 VPS — from first login to a working HTTPS video/audio calling service on your own domain.  v4call is an open-source, decentralised video and audio calling platform that uses Hive blockchain for identity and HBD micropayments. It supports custom Hive-Engine to...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{:LICENCE_HEADER_MIT}}&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
= v4call — Deploy Your Own Server on Ubuntu 24.04 with Docker =&lt;br /&gt;
&lt;br /&gt;
From CompleteNoobs&lt;br /&gt;
&lt;br /&gt;
This guide walks through deploying your own v4call server from scratch on a fresh Vultr Ubuntu 24.04 VPS — from first login to a working HTTPS video/audio calling service on your own domain.&lt;br /&gt;
&lt;br /&gt;
v4call is an open-source, decentralised video and audio calling platform that uses Hive blockchain for identity and HBD micropayments. It supports custom Hive-Engine token payments, encrypted direct messages with persistent chat history, voice-only and video calls, and a free-market platform fee system. Fork it, run your own server, keep all your platform fees, join the federation, federation coming (unknown time) kismet.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Source code&#039;&#039;&#039;: [https://github.com/CompleteNoobs/v4call https://github.com/CompleteNoobs/v4call]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;End result&#039;&#039;&#039;: A working v4call server at &amp;lt;code&amp;gt;https://call.yourdomain.com&amp;lt;/code&amp;gt; that you can log into with a Hive account.&lt;br /&gt;
&lt;br /&gt;
== Contents ==&lt;br /&gt;
&lt;br /&gt;
* [[#What_You_Need|1 What You Need]]&lt;br /&gt;
* [[#Step_1:_Create_Your_Vultr_VPS|2 Step 1: Create Your Vultr VPS]]&lt;br /&gt;
* [[#Step_2:_Point_Your_Domain_at_the_VPS|3 Step 2: Point Your Domain at the VPS]]&lt;br /&gt;
* [[#Step_3:_Log_into_Your_VPS|4 Step 3: Log into Your VPS]]&lt;br /&gt;
* [[#Step_4:_Update_the_Server|5 Step 4: Update the Server]]&lt;br /&gt;
* [[#Step_5:_Install_Docker|6 Step 5: Install Docker]]&lt;br /&gt;
* [[#Step_6:_Install_Git|7 Step 6: Install Git]]&lt;br /&gt;
* [[#Step_7:_Fork_and_Clone_the_Code|8 Step 7: Fork and Clone the Code]]&lt;br /&gt;
* [[#Step_8:_Configure_Your_Server_(.env_file)|9 Step 8: Configure Your Server (.env file)]]&lt;br /&gt;
* [[#Step_9:_Configure_Nginx_—_HTTP_Only_First|10 Step 9: Configure Nginx — HTTP Only First]]&lt;br /&gt;
* [[#Step_10:_Create_Data_Directories_and_Fix_Permissions|11 Step 10: Create Data Directories and Fix Permissions]]&lt;br /&gt;
* [[#Step_11:_Build_and_Start_the_Server|12 Step 11: Build and Start the Server]]&lt;br /&gt;
* [[#Step_12:_Get_Your_SSL_Certificate|13 Step 12: Get Your SSL Certificate]]&lt;br /&gt;
* [[#Step_13:_Enable_HTTPS_in_Nginx|14 Step 13: Enable HTTPS in Nginx]]&lt;br /&gt;
* [[#Step_14:_Set_Up_SSL_Auto-Renewal|15 Step 14: Set Up SSL Auto-Renewal]]&lt;br /&gt;
* [[#Step_15:_Test_Everything_is_Working|16 Step 15: Test Everything is Working]]&lt;br /&gt;
* [[#Step_16:_Set_Up_Your_Call_Rates_on_Hive|17 Step 16: Set Up Your Call Rates on Hive]]&lt;br /&gt;
* [[#Feature_Guide:_What_Your_Server_Can_Do|18 Feature Guide: What Your Server Can Do]]&lt;br /&gt;
* [[#Admin_Configuration_Reference|19 Admin Configuration Reference]]&lt;br /&gt;
* [[#Updating_Your_Server|20 Updating Your Server]]&lt;br /&gt;
* [[#Common_Problems_and_Fixes|21 Common Problems and Fixes]]&lt;br /&gt;
* [[#Quick_Reference|22 Quick Reference]]&lt;br /&gt;
&lt;br /&gt;
== What You Need ==&lt;br /&gt;
&lt;br /&gt;
Before starting, make sure you have the following:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;A Vultr account&#039;&#039;&#039; — sign up at [https://vultr.com vultr.com] - please use are [https://www.vultr.com/?ref=7704739 Vultr Referral link] to help us cover server costs.&lt;br /&gt;
* &#039;&#039;&#039;A domain name&#039;&#039;&#039; with DNS access — e.g. &amp;lt;code&amp;gt;call.yourdomain.com&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;Two Hive accounts&#039;&#039;&#039; — one for your server identity (receives platform fees), one for escrow (holds caller funds during calls). Create free accounts at [https://signup.hive.io signup.hive.io]&lt;br /&gt;
* &#039;&#039;&#039;Hive Keychain browser extension&#039;&#039;&#039; — for login and payments. Install from [https://hive-keychain.com hive-keychain.com]&lt;br /&gt;
* &#039;&#039;&#039;A GitHub account&#039;&#039;&#039; — free at [https://github.com github.com]. You will fork the v4call project.&lt;br /&gt;
* &#039;&#039;&#039;A terminal&#039;&#039;&#039; — Mac: Terminal app. Windows: PowerShell or PuTTY.&lt;br /&gt;
&lt;br /&gt;
You do not need to know how to code. Every command can be copy-pasted exactly as shown.&lt;br /&gt;
&lt;br /&gt;
== Step 1: Create Your Vultr VPS ==&lt;br /&gt;
&lt;br /&gt;
# Log into [https://my.vultr.com my.vultr.com]&lt;br /&gt;
# Click &#039;&#039;&#039;Deploy New Server&#039;&#039;&#039;&lt;br /&gt;
# Choose &#039;&#039;&#039;Cloud Compute — Shared CPU&#039;&#039;&#039;&lt;br /&gt;
# Choose a location close to you&lt;br /&gt;
# Choose &#039;&#039;&#039;Ubuntu 24.04 LTS x64&#039;&#039;&#039;&lt;br /&gt;
# Choose the &#039;&#039;&#039;$6/month&#039;&#039;&#039; plan (1 CPU, 1GB RAM)&lt;br /&gt;
# Set Server Hostname to something like &amp;lt;code&amp;gt;v4call-server&amp;lt;/code&amp;gt;&lt;br /&gt;
# Click &#039;&#039;&#039;Deploy Now&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
Wait ~60 seconds for it to start. Click the server to find:&lt;br /&gt;
* &#039;&#039;&#039;IP Address&#039;&#039;&#039; — looks like &amp;lt;code&amp;gt;123.456.789.012&amp;lt;/code&amp;gt; — write it down&lt;br /&gt;
* &#039;&#039;&#039;Password&#039;&#039;&#039; — click the eye icon — write it down&lt;br /&gt;
&lt;br /&gt;
== Step 2: Point Your Domain at the VPS ==&lt;br /&gt;
&lt;br /&gt;
Log into your DNS provider and add an A record:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Field !! Value&lt;br /&gt;
|-&lt;br /&gt;
| Type || A&lt;br /&gt;
|-&lt;br /&gt;
| Name || &amp;lt;code&amp;gt;call&amp;lt;/code&amp;gt; (or &amp;lt;code&amp;gt;@&amp;lt;/code&amp;gt; for root domain)&lt;br /&gt;
|-&lt;br /&gt;
| Value || Your VPS IP address&lt;br /&gt;
|-&lt;br /&gt;
| TTL || 300&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
DNS takes a few minutes to propagate. Check from your computer later:&lt;br /&gt;
&lt;br /&gt;
 nslookup call.yourdomain.com&lt;br /&gt;
&lt;br /&gt;
Must show your VPS IP before Step 12. You can continue with all other steps while waiting.&lt;br /&gt;
&lt;br /&gt;
== Step 3: Log into Your VPS ==&lt;br /&gt;
&lt;br /&gt;
Open a terminal on your computer:&lt;br /&gt;
&lt;br /&gt;
 ssh root@YOUR_SERVER_IP&lt;br /&gt;
&lt;br /&gt;
Type &amp;lt;code&amp;gt;yes&amp;lt;/code&amp;gt; when asked about the fingerprint, then paste the password from Vultr (right-click to paste in most terminals).&lt;br /&gt;
&lt;br /&gt;
When you see &amp;lt;code&amp;gt;root@v4call-server:~#&amp;lt;/code&amp;gt; you are in.&lt;br /&gt;
&lt;br /&gt;
== Step 4: Update the Server ==&lt;br /&gt;
&lt;br /&gt;
 apt update &amp;amp;&amp;amp; apt upgrade -y&lt;br /&gt;
&lt;br /&gt;
Wait for it to complete (1-2 minutes).&lt;br /&gt;
&lt;br /&gt;
== Step 5: Install Docker ==&lt;br /&gt;
&lt;br /&gt;
Install Docker using the official installer script:&lt;br /&gt;
&lt;br /&gt;
 curl -fsSL https://get.docker.com | sh&lt;br /&gt;
&lt;br /&gt;
Verify:&lt;br /&gt;
&lt;br /&gt;
 docker --version&lt;br /&gt;
&lt;br /&gt;
Should show &amp;lt;code&amp;gt;Docker version 26.x.x&amp;lt;/code&amp;gt; or similar.&lt;br /&gt;
&lt;br /&gt;
Install Docker Compose plugin:&lt;br /&gt;
&lt;br /&gt;
 apt install -y docker-compose-plugin&lt;br /&gt;
&lt;br /&gt;
Verify:&lt;br /&gt;
&lt;br /&gt;
 docker compose version&lt;br /&gt;
&lt;br /&gt;
Should show &amp;lt;code&amp;gt;Docker Compose version v2.x.x&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
== Step 6: Install Git ==&lt;br /&gt;
&lt;br /&gt;
 apt install -y git&lt;br /&gt;
 git --version&lt;br /&gt;
&lt;br /&gt;
== Step 7: Download and/or Fork and Clone the Code ==&lt;br /&gt;
&lt;br /&gt;
=== Clone onto your VPS ===&lt;br /&gt;
&lt;br /&gt;
 cd /opt&lt;br /&gt;
 git clone https://github.com/CompleteNoobs/v4call.git&lt;br /&gt;
 cd v4call&lt;br /&gt;
&lt;br /&gt;
==== Fork on GitHub - Optional ====&lt;br /&gt;
&lt;br /&gt;
Forking gives you your own copy of the code that you can customise — change the name, branding, fees — without affecting the original project.&lt;br /&gt;
&lt;br /&gt;
# Go to [https://github.com/CompleteNoobs/v4call https://github.com/CompleteNoobs/v4call]&lt;br /&gt;
# Click the &#039;&#039;&#039;Fork&#039;&#039;&#039; button (top right of the page)&lt;br /&gt;
# Select your GitHub account as the destination&lt;br /&gt;
# Click &#039;&#039;&#039;Create fork&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
You now have your own copy at &amp;lt;code&amp;gt;https://github.com/YOURUSERNAME/v4call&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Clone onto your VPS ====&lt;br /&gt;
&lt;br /&gt;
 cd /opt&lt;br /&gt;
 git clone https://github.com/YOURUSERNAME/v4call.git&lt;br /&gt;
 cd v4call&lt;br /&gt;
&lt;br /&gt;
List the files to confirm:&lt;br /&gt;
&lt;br /&gt;
 ls&lt;br /&gt;
&lt;br /&gt;
You should see: &amp;lt;code&amp;gt;server.js  public/  Dockerfile  docker-compose.yml  nginx/  package.json  .env.example&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 8: Configure Your Server (.env file) ==&lt;br /&gt;
&lt;br /&gt;
All settings live in a single &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; file. Copy the template:&lt;br /&gt;
&lt;br /&gt;
 cp .env.example .env&lt;br /&gt;
 nano .env&lt;br /&gt;
&lt;br /&gt;
Fill in your values:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# ── Server Identity ──────────────────────────────────────&lt;br /&gt;
SERVER_NAME=yourcallapp&lt;br /&gt;
SERVER_DOMAIN=call.yourdomain.com&lt;br /&gt;
SERVER_HIVE_ACCOUNT=yourhiveaccount&lt;br /&gt;
ESCROW_ACCOUNT=yourescrowaccount&lt;br /&gt;
V4CALL_ESCROW_KEY=5Kxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&lt;br /&gt;
# Secret key to access /admin/ledger and /admin/balance endpoints.&lt;br /&gt;
# Choose a long random string — treat it like a password.&lt;br /&gt;
# Example: openssl rand -hex 32&lt;br /&gt;
ADMIN_KEY=make-up-a-long-random-string-at-least-20-characters&lt;br /&gt;
&lt;br /&gt;
# ── Hive Blockchain ──────────────────────────────────────&lt;br /&gt;
CHAIN=hive&lt;br /&gt;
HIVE_API=&lt;br /&gt;
&lt;br /&gt;
# ── Platform Fee ─────────────────────────────────────────&lt;br /&gt;
# Percentage your server takes from each paid call/DM (10 = 10%)&lt;br /&gt;
# This is the MINIMUM fee — users whose rates post sets a lower&lt;br /&gt;
# platform fee will be rejected. Users who set a higher fee&lt;br /&gt;
# get the best price (your server&#039;s rate, not their higher number).&lt;br /&gt;
DEFAULT_PLATFORM_FEE=10&lt;br /&gt;
&lt;br /&gt;
# ── Network ──────────────────────────────────────────────&lt;br /&gt;
PORT=3000&lt;br /&gt;
BIND_HOST=127.0.0.1&lt;br /&gt;
&lt;br /&gt;
# ── Chat Storage ─────────────────────────────────────────&lt;br /&gt;
# How many days to keep stored DMs before automatic cleanup&lt;br /&gt;
DM_RETENTION_DAYS=33&lt;br /&gt;
# How many days to keep stored room messages before cleanup&lt;br /&gt;
ROOM_RETENTION_DAYS=33&lt;br /&gt;
# How many recent DMs per conversation to show on login (0 = off)&lt;br /&gt;
DM_PREVIEW_COUNT=1&lt;br /&gt;
&lt;br /&gt;
# ── Call Behaviour (advanced — defaults are fine) ────────&lt;br /&gt;
# CALL_COOLDOWN_MS=30000&lt;br /&gt;
# MAX_CALL_DURATION_MIN=120&lt;br /&gt;
# PAYMENT_VERIFY_RETRIES=3&lt;br /&gt;
# PAYMENT_VERIFY_DELAY_MS=5000&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Key points:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;V4CALL_ESCROW_KEY&amp;lt;/code&amp;gt; — must be the &#039;&#039;&#039;active&#039;&#039;&#039; private key for your escrow account. Find it in your Hive wallet → Keys &amp;amp; Permissions → Active. Starts with &amp;lt;code&amp;gt;5K&amp;lt;/code&amp;gt;. &#039;&#039;&#039;Never share this.&#039;&#039;&#039;&lt;br /&gt;
* &amp;lt;code&amp;gt;ADMIN_KEY&amp;lt;/code&amp;gt; — invent a secret password for accessing admin tools&lt;br /&gt;
* &amp;lt;code&amp;gt;HIVE_API&amp;lt;/code&amp;gt; — leave blank to use all built-in Hive nodes automatically&lt;br /&gt;
* &amp;lt;code&amp;gt;DEFAULT_PLATFORM_FEE&amp;lt;/code&amp;gt; — your server&#039;s minimum platform fee percentage. See [[#Platform_Fee_System|Platform Fee System]] below for how this works.&lt;br /&gt;
* &amp;lt;code&amp;gt;DM_RETENTION_DAYS&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;ROOM_RETENTION_DAYS&amp;lt;/code&amp;gt; — how long chat messages are kept in the database. A cleanup job runs every hour and deletes anything older. Set to &amp;lt;code&amp;gt;0&amp;lt;/code&amp;gt; to keep messages indefinitely (not recommended).&lt;br /&gt;
* &amp;lt;code&amp;gt;DM_PREVIEW_COUNT&amp;lt;/code&amp;gt; — when a user logs in, this many recent DMs per conversation are loaded into the lobby chat so they can see previews. Set to &amp;lt;code&amp;gt;0&amp;lt;/code&amp;gt; to disable previews (users still get an unread count alert).&lt;br /&gt;
&lt;br /&gt;
Save: &#039;&#039;&#039;Ctrl+X&#039;&#039;&#039; → &#039;&#039;&#039;Y&#039;&#039;&#039; → &#039;&#039;&#039;Enter&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Step 9: Configure Nginx — HTTP Only First ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;This step is critical.&#039;&#039;&#039; A very common mistake is putting the HTTPS/SSL config in Nginx before getting the certificate. Nginx tries to load the certificate at startup — if the file doesn&#039;t exist yet, Nginx crashes in a restart loop. Certbot then cannot serve the challenge because Nginx is down. Result: no certificate, stuck in a loop.&lt;br /&gt;
&lt;br /&gt;
The fix: always start with HTTP only, get the certificate, then add HTTPS.&lt;br /&gt;
&lt;br /&gt;
Edit the Nginx config:&lt;br /&gt;
&lt;br /&gt;
 nano /opt/v4call/nginx/v4call.conf&lt;br /&gt;
&lt;br /&gt;
Delete everything and replace with this HTTP-only config:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
server {&lt;br /&gt;
    listen 80;&lt;br /&gt;
    server_name call.yourdomain.com www.call.yourdomain.com;&lt;br /&gt;
&lt;br /&gt;
    # Certbot challenge path — do not remove this block&lt;br /&gt;
    location /.well-known/acme-challenge/ {&lt;br /&gt;
        root /var/www/certbot;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # Proxy all other requests to the v4call app&lt;br /&gt;
    location / {&lt;br /&gt;
        proxy_pass         http://app:3000;&lt;br /&gt;
        proxy_http_version 1.1;&lt;br /&gt;
        proxy_set_header   Upgrade $http_upgrade;&lt;br /&gt;
        proxy_set_header   Connection &amp;quot;upgrade&amp;quot;;&lt;br /&gt;
        proxy_set_header   Host $host;&lt;br /&gt;
        proxy_set_header   X-Real-IP $remote_addr;&lt;br /&gt;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;&lt;br /&gt;
        proxy_set_header   X-Forwarded-Proto $scheme;&lt;br /&gt;
        proxy_read_timeout 86400;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Replace &amp;lt;code&amp;gt;call.yourdomain.com&amp;lt;/code&amp;gt; with your actual domain in both places.&lt;br /&gt;
&lt;br /&gt;
Save: &#039;&#039;&#039;Ctrl+X&#039;&#039;&#039; → &#039;&#039;&#039;Y&#039;&#039;&#039; → &#039;&#039;&#039;Enter&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Step 10: Create Data Directories and Fix Permissions ==&lt;br /&gt;
&lt;br /&gt;
Create the folders Docker uses for persistent data:&lt;br /&gt;
&lt;br /&gt;
 mkdir -p /opt/v4call/data/logs  \&lt;br /&gt;
 mkdir -p /opt/v4call/data/certbot/conf  \&lt;br /&gt;
 mkdir -p /opt/v4call/data/certbot/www/.well-known/acme-challenge  \&lt;br /&gt;
 mkdir -p /opt/v4call/data/certbot/logs&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Fix permissions — do not skip this.&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
The v4call app runs inside Docker as user &amp;lt;code&amp;gt;node&amp;lt;/code&amp;gt; (UID 1000). On the host, these directories are created by root, so the container cannot write to them. This causes a &amp;lt;code&amp;gt;SQLITE_CANTOPEN&amp;lt;/code&amp;gt; error that crashes the app.&lt;br /&gt;
&lt;br /&gt;
Fix the logs directory for the app:&lt;br /&gt;
&lt;br /&gt;
 chown -R 1000:1000 /opt/v4call/data/logs&lt;br /&gt;
&lt;br /&gt;
Certbot runs as root so its directories stay root-owned:&lt;br /&gt;
&lt;br /&gt;
 chown -R root:root /opt/v4call/data/certbot&lt;br /&gt;
 chmod -R 755 /opt/v4call/data/certbot&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note:&#039;&#039;&#039; The app creates two separate SQLite databases in the logs directory:&lt;br /&gt;
* &amp;lt;code&amp;gt;v4call-ledger.db&amp;lt;/code&amp;gt; — payment records (calls, ring fees, payouts). Only the server writes to this.&lt;br /&gt;
* &amp;lt;code&amp;gt;v4call-chat.db&amp;lt;/code&amp;gt; — stored DMs and room messages. Separate from the ledger for security — if a bug in chat storage were exploited, the payment ledger remains untouched.&lt;br /&gt;
&lt;br /&gt;
== Step 11: Build and Start the Server ==&lt;br /&gt;
&lt;br /&gt;
 cd /opt/v4call&lt;br /&gt;
 docker compose up -d --build&lt;br /&gt;
&lt;br /&gt;
The first build downloads dependencies and takes 2-4 minutes. Check the status:&lt;br /&gt;
&lt;br /&gt;
 docker compose ps&lt;br /&gt;
&lt;br /&gt;
You should see:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
NAME              STATUS&lt;br /&gt;
v4call-app        Up (healthy)&lt;br /&gt;
v4call-nginx      Up&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Check the app started correctly:&lt;br /&gt;
&lt;br /&gt;
 docker compose logs app&lt;br /&gt;
&lt;br /&gt;
Look for:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
[ledger] SQLite ready: /app/logs/v4call-ledger.db&lt;br /&gt;
[chat] SQLite ready: /app/logs/v4call-chat.db&lt;br /&gt;
v4call server running on 0.0.0.0:3000&lt;br /&gt;
[config] Server: yourcallapp (call.yourdomain.com)&lt;br /&gt;
[config] DM retention: 33 days | Room retention: 33 days | DM preview: 1&lt;br /&gt;
✓ Escrow key verified — matches @yourescrowaccount active key&lt;br /&gt;
✓ Escrow account @yourescrowaccount balance: 0.000 HBD&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you see &amp;lt;code&amp;gt;SqliteError: unable to open database file&amp;lt;/code&amp;gt; — run the chown command from Step 10 again then &amp;lt;code&amp;gt;docker compose restart app&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Test HTTP is working:&lt;br /&gt;
&lt;br /&gt;
 curl http://call.yourdomain.com/debug-state&lt;br /&gt;
&lt;br /&gt;
Should return: &amp;lt;code&amp;gt;{&amp;quot;lobbyUsers&amp;quot;:[],&amp;quot;rooms&amp;quot;:[]}&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If you can see the site in a browser over HTTP at this point — everything is working and ready for the certificate.&lt;br /&gt;
&lt;br /&gt;
== Step 12: Get Your SSL Certificate ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Before running certbot&#039;&#039;&#039;, verify DNS and the challenge path are both working:&lt;br /&gt;
&lt;br /&gt;
 # Check DNS points to this server&lt;br /&gt;
 nslookup call.yourdomain.com&lt;br /&gt;
&lt;br /&gt;
 # Test the challenge path&lt;br /&gt;
 echo &amp;quot;test&amp;quot; &amp;gt; /opt/v4call/data/certbot/www/.well-known/acme-challenge/testfile&lt;br /&gt;
 curl http://call.yourdomain.com/.well-known/acme-challenge/testfile&lt;br /&gt;
&lt;br /&gt;
The curl command must return &amp;lt;code&amp;gt;test&amp;lt;/code&amp;gt;. If it does not, Nginx is not running — check &amp;lt;code&amp;gt;docker compose ps&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;docker compose logs nginx&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
When both work, get the certificate. The &amp;lt;code&amp;gt;--entrypoint certbot&amp;lt;/code&amp;gt; flag is required — without it, Docker runs the container&#039;s default renewal loop instead of the certonly command:&lt;br /&gt;
&lt;br /&gt;
 docker compose run --rm \&lt;br /&gt;
   --entrypoint certbot \&lt;br /&gt;
   certbot certonly \&lt;br /&gt;
   --webroot \&lt;br /&gt;
   -w /var/www/certbot \&lt;br /&gt;
   -d call.yourdomain.com \&lt;br /&gt;
   --email your@email.com \&lt;br /&gt;
   --agree-tos \&lt;br /&gt;
   --no-eff-email&lt;br /&gt;
&lt;br /&gt;
Replace &amp;lt;code&amp;gt;call.yourdomain.com&amp;lt;/code&amp;gt; with your domain and &amp;lt;code&amp;gt;your@email.com&amp;lt;/code&amp;gt; with your email.&lt;br /&gt;
&lt;br /&gt;
Success looks like:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Requesting a certificate for call.yourdomain.com&lt;br /&gt;
&lt;br /&gt;
Successfully received certificate.&lt;br /&gt;
Certificate is saved at: /etc/letsencrypt/live/call.yourdomain.com/fullchain.pem&lt;br /&gt;
Key is saved at:         /etc/letsencrypt/live/call.yourdomain.com/privkey.pem&lt;br /&gt;
This certificate expires on 2026-07-09.&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Verify the files exist on the host:&lt;br /&gt;
&lt;br /&gt;
 ls /opt/v4call/data/certbot/conf/live/call.yourdomain.com/&lt;br /&gt;
&lt;br /&gt;
Should show: &amp;lt;code&amp;gt;cert.pem  chain.pem  fullchain.pem  privkey.pem  README&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 13: Enable HTTPS in Nginx ==&lt;br /&gt;
&lt;br /&gt;
Now the certificate exists, update Nginx to serve HTTPS.&lt;br /&gt;
&lt;br /&gt;
 nano /opt/v4call/nginx/v4call.conf&lt;br /&gt;
&lt;br /&gt;
Replace everything with the full HTTPS config:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
# HTTP — only used for certbot challenge and redirect to HTTPS&lt;br /&gt;
server {&lt;br /&gt;
    listen 80;&lt;br /&gt;
    server_name v4call.com v4call.com;&lt;br /&gt;
&lt;br /&gt;
    location /.well-known/acme-challenge/ {&lt;br /&gt;
        root /var/www/certbot;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    location / {&lt;br /&gt;
        return 301 https://$host$request_uri;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# HTTPS — auth lives here&lt;br /&gt;
server {&lt;br /&gt;
    listen 443 ssl;&lt;br /&gt;
    server_name v4call.com v4call.com;&lt;br /&gt;
# CHANGE PATH: for ssl certifcates change v4call.com to your path: which you can find at &amp;quot;ls data/certbot/conf/live/&amp;quot;&lt;br /&gt;
    ssl_certificate     /etc/letsencrypt/live/v4call.com/fullchain.pem;&lt;br /&gt;
    ssl_certificate_key /etc/letsencrypt/live/v4call.com/privkey.pem;&lt;br /&gt;
&lt;br /&gt;
    ssl_protocols TLSv1.2 TLSv1.3;&lt;br /&gt;
    ssl_prefer_server_ciphers off;&lt;br /&gt;
&lt;br /&gt;
    add_header Strict-Transport-Security &amp;quot;max-age=63072000&amp;quot; always;&lt;br /&gt;
    add_header X-Frame-Options DENY;&lt;br /&gt;
    add_header X-Content-Type-Options nosniff;&lt;br /&gt;
&lt;br /&gt;
    # When user cancels the login prompt, send them here&lt;br /&gt;
    error_page 401 /info.html;&lt;br /&gt;
&lt;br /&gt;
    # info.html is served directly by Nginx — no auth, no proxy&lt;br /&gt;
    location = /info.html {&lt;br /&gt;
        root /usr/share/nginx/html;&lt;br /&gt;
        auth_basic off;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # WebSocket — requires auth&lt;br /&gt;
    location /socket.io/ {&lt;br /&gt;
# uncomment to require username and password to enter site - useful for testing&lt;br /&gt;
#        auth_basic           &amp;quot;v4call — Private Testing&amp;quot;;&lt;br /&gt;
#        auth_basic_user_file /etc/nginx/.htpasswd;&lt;br /&gt;
&lt;br /&gt;
        proxy_pass         http://app:3000;&lt;br /&gt;
        proxy_http_version 1.1;&lt;br /&gt;
        proxy_set_header   Upgrade $http_upgrade;&lt;br /&gt;
        proxy_set_header   Connection &amp;quot;upgrade&amp;quot;;&lt;br /&gt;
        proxy_set_header   Host $host;&lt;br /&gt;
        proxy_set_header   X-Real-IP $remote_addr;&lt;br /&gt;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;&lt;br /&gt;
        proxy_set_header   X-Forwarded-Proto $scheme;&lt;br /&gt;
        proxy_cache_bypass $http_upgrade;&lt;br /&gt;
        proxy_read_timeout 86400;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # Everything else — requires auth, proxied to app&lt;br /&gt;
    location / {&lt;br /&gt;
# uncomment to require username and password to enter site - useful for testing&lt;br /&gt;
#        auth_basic           &amp;quot;v4call — Private Testing&amp;quot;;&lt;br /&gt;
#        auth_basic_user_file /etc/nginx/.htpasswd;&lt;br /&gt;
&lt;br /&gt;
        proxy_pass         http://app:3000;&lt;br /&gt;
        proxy_http_version 1.1;&lt;br /&gt;
        proxy_set_header   Host $host;&lt;br /&gt;
        proxy_set_header   X-Real-IP $remote_addr;&lt;br /&gt;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;&lt;br /&gt;
        proxy_set_header   X-Forwarded-Proto $scheme;&lt;br /&gt;
        proxy_read_timeout 300;&lt;br /&gt;
        proxy_send_timeout 300;&lt;br /&gt;
    }&lt;br /&gt;
    # This will log correct and incorrect attempts logging in, if .htpasswd used&lt;br /&gt;
    error_log /var/log/nginx/error.log warn;&lt;br /&gt;
    access_log /var/log/nginx/access.log;&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* REPLACE &amp;lt;code&amp;gt;call.yourdomain.com&amp;lt;/code&amp;gt; with your domain throughout (it appears 5 times).&lt;br /&gt;
* CHANGE PATH: for ssl certifcates change v4call.com to your path: which you can find at &amp;lt;code&amp;gt;ls data/certbot/conf/live/&amp;lt;/code&amp;gt; if not sure.&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    ssl_certificate     /etc/letsencrypt/live/v4call.com/fullchain.pem;&lt;br /&gt;
    ssl_certificate_key /etc/letsencrypt/live/v4call.com/privkey.pem;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Save and restart Nginx:&lt;br /&gt;
&lt;br /&gt;
 docker compose restart nginx&lt;br /&gt;
&lt;br /&gt;
Check the logs — you should see &#039;&#039;&#039;no&#039;&#039;&#039; &amp;lt;code&amp;gt;[emerg]&amp;lt;/code&amp;gt; errors:&lt;br /&gt;
&lt;br /&gt;
 docker compose logs nginx&lt;br /&gt;
&lt;br /&gt;
The last lines should show:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
nginx/1.x.x ...&lt;br /&gt;
start worker processes&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* If you see an error such as:&lt;br /&gt;
&amp;lt;pre&amp;gt;[emerg] 1#1: cannot load certificate &amp;quot;/etc/letsencrypt/live/call.yourdomain.com/fullchain.pem&amp;quot;: BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory:calling fopen(/etc/letsencrypt/live/call.yourdomain.com/fullchain.pem, r) error:10000080:BIO routines::no such file)&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Make sure you edited &amp;lt;code&amp;gt;nginx/v4call.conf&amp;lt;/code&amp;gt; correctly.&lt;br /&gt;
* To find the path for your SSL certs look in path &amp;lt;code&amp;gt;data/certbot/conf/live/&amp;lt;/code&amp;gt;&lt;br /&gt;
* There are 4 lines to check in &amp;lt;code&amp;gt;nginx/v4call.conf&amp;lt;/code&amp;gt;:&lt;br /&gt;
** Line 4: &amp;lt;code&amp;gt;server_name call.yourdomain.com www.call.yourdomain.com;&amp;lt;/code&amp;gt;&lt;br /&gt;
** Line 18: &amp;lt;code&amp;gt;server_name call.yourdomain.com www.call.yourdomain.com;&amp;lt;/code&amp;gt;&lt;br /&gt;
** Line 20: &amp;lt;code&amp;gt;ssl_certificate     /etc/letsencrypt/live/call.yourdomain.com/fullchain.pem;&amp;lt;/code&amp;gt;&lt;br /&gt;
** Line 21: &amp;lt;code&amp;gt;ssl_certificate_key /etc/letsencrypt/live/call.yourdomain.com/privkey.pem;&amp;lt;/code&amp;gt;&lt;br /&gt;
* When done restart nginx with: &amp;lt;code&amp;gt;docker compose restart nginx&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 14: Set Up SSL Auto-Renewal ==&lt;br /&gt;
&lt;br /&gt;
Let&#039;s Encrypt certificates expire after 90 days. A cron job renews them automatically.&lt;br /&gt;
&lt;br /&gt;
 crontab -e&lt;br /&gt;
&lt;br /&gt;
Add this line at the bottom:&lt;br /&gt;
&lt;br /&gt;
 0 3 * * * cd /opt/v4call &amp;amp;&amp;amp; docker compose run --rm --entrypoint certbot certbot renew --quiet &amp;amp;&amp;amp; docker compose exec nginx nginx -s reload&lt;br /&gt;
&lt;br /&gt;
Save and exit. This runs at 3am every day, renews if the cert is close to expiry, and reloads Nginx to pick up the new certificate.&lt;br /&gt;
&lt;br /&gt;
== Step 15: Test Everything is Working ==&lt;br /&gt;
&lt;br /&gt;
Test HTTPS:&lt;br /&gt;
&lt;br /&gt;
 curl https://call.yourdomain.com/debug-state&lt;br /&gt;
&lt;br /&gt;
Should return: &amp;lt;code&amp;gt;{&amp;quot;lobbyUsers&amp;quot;:[],&amp;quot;rooms&amp;quot;:[]}&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Test admin access:&lt;br /&gt;
&lt;br /&gt;
 curl &amp;quot;https://call.yourdomain.com/admin/balance?key=YOUR_ADMIN_KEY&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Should return your escrow account balance.&lt;br /&gt;
&lt;br /&gt;
Open your browser and go to &amp;lt;code&amp;gt;https://call.yourdomain.com&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You should see the v4call login page with:&lt;br /&gt;
* A padlock icon in the browser address bar&lt;br /&gt;
* A green &#039;&#039;&#039;⚡ Sign in with Keychain&#039;&#039;&#039; button (if Hive Keychain is installed)&lt;br /&gt;
* A manual posting key login option below it&lt;br /&gt;
* A &#039;&#039;&#039;📖 New here? Learn the v4call basics →&#039;&#039;&#039; link at the bottom&lt;br /&gt;
&lt;br /&gt;
Log in with Hive Keychain or a posting key to confirm the login flow works. 🎉&lt;br /&gt;
&lt;br /&gt;
== Step 16: Set Up Your Call Rates on Hive ==&lt;br /&gt;
&lt;br /&gt;
For callers to be charged when they ring you, publish your rates on the Hive blockchain:&lt;br /&gt;
&lt;br /&gt;
# Make sure Hive Keychain is installed in your browser&lt;br /&gt;
# Go to &amp;lt;code&amp;gt;https://call.yourdomain.com/rate-editor.html&amp;lt;/code&amp;gt;&lt;br /&gt;
# Enter your Hive username&lt;br /&gt;
# Set your rates — ring fee, connect fee, duration rate per hour, minimum credit deposit&lt;br /&gt;
# Set &amp;lt;code&amp;gt;PLATFORM-FEE&amp;lt;/code&amp;gt; to at least your server&#039;s &amp;lt;code&amp;gt;DEFAULT_PLATFORM_FEE&amp;lt;/code&amp;gt; percentage (e.g. &amp;lt;code&amp;gt;10&amp;lt;/code&amp;gt; for 10%). If you set it lower, paid contacts to your account will be rejected on this server.&lt;br /&gt;
# Optionally add custom token sections (e.g. &amp;lt;code&amp;gt;[TOKEN:CNOOBS]&amp;lt;/code&amp;gt;) to offer discounted rates for callers who hold your token&lt;br /&gt;
# Click &#039;&#039;&#039;Generate&#039;&#039;&#039; to preview the rates block&lt;br /&gt;
# Click &#039;&#039;&#039;Post to Hive&#039;&#039;&#039; — Keychain will ask you to approve the post&lt;br /&gt;
&lt;br /&gt;
This creates a post titled &amp;lt;code&amp;gt;v4call-rates&amp;lt;/code&amp;gt; on your Hive blog. Your server reads this post automatically.&lt;br /&gt;
&lt;br /&gt;
To verify your server read the rates correctly:&lt;br /&gt;
&lt;br /&gt;
 https://call.yourdomain.com/debug-rates/yourusername&lt;br /&gt;
&lt;br /&gt;
To test with a specific caller (checks their token balances too):&lt;br /&gt;
&lt;br /&gt;
 https://call.yourdomain.com/debug-rates/yourusername?caller=theirusername&amp;amp;type=voice&lt;br /&gt;
&lt;br /&gt;
You should see your rates as JSON, including which currency and rates apply for that caller.&lt;br /&gt;
&lt;br /&gt;
== Feature Guide: What Your Server Can Do ==&lt;br /&gt;
&lt;br /&gt;
This section explains all the features available in v4call and how they work. No code changes needed — everything described here is built in and ready to use.&lt;br /&gt;
&lt;br /&gt;
=== Login Options ===&lt;br /&gt;
&lt;br /&gt;
v4call supports two ways to sign in:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Hive Keychain&#039;&#039;&#039; (recommended) — click the green &#039;&#039;&#039;⚡ Sign in with Keychain&#039;&#039;&#039; button. Keychain signs a challenge to prove your identity. No key paste needed. After login, a 🔑 panel appears in the lobby where you can optionally enter your posting key to unlock encrypted messaging (Keychain cannot expose private keys, so encryption needs the key entered once per session).&lt;br /&gt;
* &#039;&#039;&#039;Manual posting key&#039;&#039;&#039; — paste your Hive posting private key (starts with &amp;lt;code&amp;gt;5J&amp;lt;/code&amp;gt;) directly. The key stays in browser session memory only — never sent to the server.&lt;br /&gt;
&lt;br /&gt;
Both methods verify your identity against the Hive blockchain.&lt;br /&gt;
&lt;br /&gt;
=== Voice and Video Calls ===&lt;br /&gt;
&lt;br /&gt;
Each online user in the lobby shows three action buttons:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;📞 Green phone icon&#039;&#039;&#039; — start a &#039;&#039;&#039;voice-only&#039;&#039;&#039; call (audio, no camera)&lt;br /&gt;
* &#039;&#039;&#039;🎥 Blue camera icon&#039;&#039;&#039; — start a &#039;&#039;&#039;video&#039;&#039;&#039; call (audio + camera)&lt;br /&gt;
* &#039;&#039;&#039;💬 Purple chat bubble icon&#039;&#039;&#039; — open the &#039;&#039;&#039;DM panel&#039;&#039;&#039; to send a direct message&lt;br /&gt;
&lt;br /&gt;
Voice calls request microphone only — no camera permission prompt. Video calls request both. The caller and callee can have different call types — the type is set by whoever initiates the call.&lt;br /&gt;
&lt;br /&gt;
Separate rates can be set for voice and video calls in the rates post (voice is typically cheaper).&lt;br /&gt;
&lt;br /&gt;
=== Direct Messages (DMs) ===&lt;br /&gt;
&lt;br /&gt;
Click the purple 💬 button next to any online user to open the DM panel. DMs are end-to-end encrypted using Hive posting keys — the server stores only ciphertext it cannot read.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Chat storage:&#039;&#039;&#039; DMs are stored on the server in an encrypted database (&amp;lt;code&amp;gt;v4call-chat.db&amp;lt;/code&amp;gt;) for up to &amp;lt;code&amp;gt;DM_RETENTION_DAYS&amp;lt;/code&amp;gt; (default: 33 days). Both sender and recipient get their own encrypted copy stored, so both can retrieve their history later.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Unread alerts:&#039;&#039;&#039; When you log in, if you have unread DMs, a popup appears showing how many messages from how many users. Click a username in the popup to open their DM history.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;DM previews:&#039;&#039;&#039; The last &amp;lt;code&amp;gt;DM_PREVIEW_COUNT&amp;lt;/code&amp;gt; messages per conversation are loaded into the lobby chat on login, so you can see recent activity at a glance. Set to &amp;lt;code&amp;gt;0&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; to disable.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Full history:&#039;&#039;&#039; Click the DM button for any user to load the complete conversation history, shown between &amp;quot;— DM history —&amp;quot; dividers.&lt;br /&gt;
&lt;br /&gt;
=== Rooms ===&lt;br /&gt;
&lt;br /&gt;
Users can create private rooms by selecting users in the lobby (toggle switch) and clicking &#039;&#039;&#039;Create &amp;amp; Invite&#039;&#039;&#039;. Rooms support encrypted messaging, video, and voice.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Room history:&#039;&#039;&#039; When a new user joins a room, they see past messages — broadcasts in full, encrypted messages only if they were addressed to them. A &amp;quot;— earlier messages —&amp;quot; divider separates history from live messages.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Ephemeral rooms:&#039;&#039;&#039; A warning banner at the top of every room says: &amp;quot;⚠ Room is ephemeral — if all users leave, the room and its history are deleted. New members can only read messages encrypted to their key.&amp;quot; When the last person leaves a room, all stored messages for that room are deleted from the database.&lt;br /&gt;
&lt;br /&gt;
=== Custom Token Payments (Hive-Engine) ===&lt;br /&gt;
&lt;br /&gt;
v4call supports payment in any Hive-Engine token, not just HBD. This is configured per-user in their rates post using &amp;lt;code&amp;gt;[TOKEN:SYMBOL]&amp;lt;/code&amp;gt; sections.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;How it works:&#039;&#039;&#039;&lt;br /&gt;
# A user (e.g. @cnoobz) creates a custom token on Hive-Engine (e.g. CNOOBS)&lt;br /&gt;
# In their rates post, they add a &amp;lt;code&amp;gt;[TOKEN:CNOOBS]&amp;lt;/code&amp;gt; section with lower rates than their default HBD rates&lt;br /&gt;
# When a caller who holds CNOOBS contacts @cnoobz, the server detects the token balance and offers the token rates&lt;br /&gt;
# If the caller holds multiple qualifying tokens, &#039;&#039;&#039;all options are shown&#039;&#039;&#039; in a currency picker — the caller chooses which to pay with&lt;br /&gt;
# The payment goes through Hive Keychain as a &amp;lt;code&amp;gt;custom_json&amp;lt;/code&amp;gt; Hive-Engine transfer&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;For the escrow account:&#039;&#039;&#039; Your escrow account needs to hold some of each custom token that users on your server accept. Token payouts (to the callee) and refunds (to the caller) are sent from the escrow account&#039;s token balance, just like HBD.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Payment picker:&#039;&#039;&#039; When multiple payment options exist (e.g. CNOOBS at 1 per message, HBD at 100000 per message), the payment modal shows clickable currency buttons so the caller can see all rates and choose the best option.&lt;br /&gt;
&lt;br /&gt;
=== Platform Fee System ===&lt;br /&gt;
&lt;br /&gt;
The platform fee is how your server earns revenue from paid calls and DMs.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;How it works:&#039;&#039;&#039;&lt;br /&gt;
* Your server&#039;s &amp;lt;code&amp;gt;DEFAULT_PLATFORM_FEE&amp;lt;/code&amp;gt; in &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; is the &#039;&#039;&#039;minimum&#039;&#039;&#039; percentage your server accepts (e.g. &amp;lt;code&amp;gt;10&amp;lt;/code&amp;gt; = 10%)&lt;br /&gt;
* Each user sets &amp;lt;code&amp;gt;PLATFORM-FEE&amp;lt;/code&amp;gt; in their Hive rates post — this is the maximum fee they are willing to pay to a server&lt;br /&gt;
* If the user&#039;s posted fee is &#039;&#039;&#039;lower&#039;&#039;&#039; than your server&#039;s minimum → &#039;&#039;&#039;rejected&#039;&#039;&#039;. The caller sees a message explaining the mismatch, and the callee is told to raise their fee.&lt;br /&gt;
* If the user&#039;s posted fee &#039;&#039;&#039;meets or exceeds&#039;&#039;&#039; your server&#039;s minimum → &#039;&#039;&#039;accepted&#039;&#039;&#039;, and &#039;&#039;&#039;the server charges its own rate&#039;&#039;&#039; (the minimum), not the user&#039;s higher number. The callee gets the best price.&lt;br /&gt;
* If the user has &#039;&#039;&#039;no &amp;lt;code&amp;gt;PLATFORM-FEE&amp;lt;/code&amp;gt; line&#039;&#039;&#039; in their rates post → the server&#039;s default is used automatically. No mismatch.&lt;br /&gt;
* &#039;&#039;&#039;Free contacts&#039;&#039;&#039; (no payment involved) are never affected by fee enforcement.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Why this matters for federation:&#039;&#039;&#039; Different servers can set different platform fees. Users can shop around — pick a server with a fee they find agreeable. This creates a free market for server operators.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Example:&#039;&#039;&#039;&lt;br /&gt;
* Your server: &amp;lt;code&amp;gt;DEFAULT_PLATFORM_FEE=3&amp;lt;/code&amp;gt; (3%)&lt;br /&gt;
* @alice posts: &amp;lt;code&amp;gt;PLATFORM-FEE: 5%&amp;lt;/code&amp;gt; → accepted, server charges 3% (best price for alice)&lt;br /&gt;
* @bob posts: &amp;lt;code&amp;gt;PLATFORM-FEE: 1%&amp;lt;/code&amp;gt; → rejected, bob needs to raise to at least 3%&lt;br /&gt;
* @charlie has no fee line → defaults to 3%, automatically accepted&lt;br /&gt;
&lt;br /&gt;
== Admin Configuration Reference ==&lt;br /&gt;
&lt;br /&gt;
All settings are in the &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; file. After changing any value, rebuild:&lt;br /&gt;
&lt;br /&gt;
 docker compose down &amp;amp;&amp;amp; docker compose build --no-cache &amp;amp;&amp;amp; docker compose up -d&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Variable !! Default !! Description&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;SERVER_NAME&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;v4call&amp;lt;/code&amp;gt; || Display name for your server&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;SERVER_DOMAIN&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;v4call.com&amp;lt;/code&amp;gt; || Your server&#039;s domain&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;SERVER_HIVE_ACCOUNT&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;v4call&amp;lt;/code&amp;gt; || Hive account that receives platform fees&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ESCROW_ACCOUNT&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;v4call-escrow&amp;lt;/code&amp;gt; || Hive account that holds funds during calls&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;V4CALL_ESCROW_KEY&amp;lt;/code&amp;gt; || &#039;&#039;(none)&#039;&#039; || Active private key for the escrow account. &#039;&#039;&#039;Required.&#039;&#039;&#039;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ADMIN_KEY&amp;lt;/code&amp;gt; || &#039;&#039;(none)&#039;&#039; || Password for admin endpoints (&amp;lt;code&amp;gt;/admin/balance&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;/admin/ledger&amp;lt;/code&amp;gt;)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;DEFAULT_PLATFORM_FEE&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;10&amp;lt;/code&amp;gt; || Server&#039;s minimum platform fee percentage&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;DM_RETENTION_DAYS&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;33&amp;lt;/code&amp;gt; || Days to keep stored DMs before cleanup&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ROOM_RETENTION_DAYS&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;33&amp;lt;/code&amp;gt; || Days to keep stored room messages before cleanup&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;DM_PREVIEW_COUNT&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;1&amp;lt;/code&amp;gt; || Recent DMs per conversation shown on login (0 = off)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;HIVE_API&amp;lt;/code&amp;gt; || &#039;&#039;(blank)&#039;&#039; || Override primary Hive API node. Blank = auto-select from built-in list&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;MAX_CALL_DURATION_MIN&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;120&amp;lt;/code&amp;gt; || Maximum call length in minutes before auto-disconnect&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;CALL_COOLDOWN_MS&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;30000&amp;lt;/code&amp;gt; || Milliseconds between call attempts to same user&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;PAYMENT_VERIFY_RETRIES&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;3&amp;lt;/code&amp;gt; || Number of attempts to verify a blockchain payment&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;PAYMENT_VERIFY_DELAY_MS&amp;lt;/code&amp;gt; || &amp;lt;code&amp;gt;5000&amp;lt;/code&amp;gt; || Delay between verification retry attempts&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Debug Endpoints ===&lt;br /&gt;
&lt;br /&gt;
These are useful for testing without making actual calls:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;/debug-state&amp;lt;/code&amp;gt; — shows current lobby users and active rooms (no auth required)&lt;br /&gt;
* &amp;lt;code&amp;gt;/debug-rates/USERNAME&amp;lt;/code&amp;gt; — shows parsed rates for a user&lt;br /&gt;
* &amp;lt;code&amp;gt;/debug-rates/USERNAME?caller=CALLER&amp;amp;type=voice&amp;lt;/code&amp;gt; — shows what rates a specific caller would receive (checks token balances too)&lt;br /&gt;
* &amp;lt;code&amp;gt;/admin/balance?key=YOUR_ADMIN_KEY&amp;lt;/code&amp;gt; — shows escrow account HBD balance&lt;br /&gt;
* &amp;lt;code&amp;gt;/admin/ledger?key=YOUR_ADMIN_KEY&amp;lt;/code&amp;gt; — shows recent payment records&lt;br /&gt;
&lt;br /&gt;
== Updating Your Server ==&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important:&#039;&#039;&#039; Always use &amp;lt;code&amp;gt;docker compose down&amp;lt;/code&amp;gt; before rebuilding. Without this step, Docker may reuse the old container even after a rebuild, and your changes will not take effect.&lt;br /&gt;
&lt;br /&gt;
 cd /opt/v4call&lt;br /&gt;
 docker compose down&lt;br /&gt;
 docker compose build --no-cache&lt;br /&gt;
 docker compose up -d&lt;br /&gt;
&lt;br /&gt;
Your data, SQLite databases and &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; config are preserved — they live in &amp;lt;code&amp;gt;data/logs/&amp;lt;/code&amp;gt; which is a mounted volume.&lt;br /&gt;
&lt;br /&gt;
To pull updates from GitHub and deploy:&lt;br /&gt;
&lt;br /&gt;
 cd /opt/v4call&lt;br /&gt;
 git pull&lt;br /&gt;
 docker compose down&lt;br /&gt;
 docker compose build --no-cache&lt;br /&gt;
 docker compose up -d&lt;br /&gt;
&lt;br /&gt;
To push your own customisations to GitHub:&lt;br /&gt;
&lt;br /&gt;
 # On your local computer after making changes:&lt;br /&gt;
 git add .&lt;br /&gt;
 git commit -m &amp;quot;describe what you changed&amp;quot;&lt;br /&gt;
 git push&lt;br /&gt;
&lt;br /&gt;
 # On the VPS:&lt;br /&gt;
 cd /opt/v4call&lt;br /&gt;
 git pull&lt;br /&gt;
 docker compose down&lt;br /&gt;
 docker compose build --no-cache&lt;br /&gt;
 docker compose up -d&lt;br /&gt;
&lt;br /&gt;
== Common Problems and Fixes ==&lt;br /&gt;
&lt;br /&gt;
=== Changes not showing after rebuild ===&lt;br /&gt;
&lt;br /&gt;
If you edited &amp;lt;code&amp;gt;server.js&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;index.html&amp;lt;/code&amp;gt; but changes are not visible, you probably forgot to bring Docker down first. &amp;lt;code&amp;gt;docker compose restart&amp;lt;/code&amp;gt; and even &amp;lt;code&amp;gt;docker compose up -d --build&amp;lt;/code&amp;gt; can reuse old containers.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Fix:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
 docker compose down&lt;br /&gt;
 docker compose build --no-cache&lt;br /&gt;
 docker compose up -d&lt;br /&gt;
&lt;br /&gt;
=== SqliteError: unable to open database file ===&lt;br /&gt;
&lt;br /&gt;
The app container runs as UID 1000 but the logs directory was created by root. Fix:&lt;br /&gt;
&lt;br /&gt;
 chown -R 1000:1000 /opt/v4call/data/logs&lt;br /&gt;
 docker compose restart app&lt;br /&gt;
&lt;br /&gt;
=== Certbot says &amp;quot;No renewals were attempted&amp;quot; ===&lt;br /&gt;
&lt;br /&gt;
The certbot container&#039;s default behaviour is a renewal loop. Your &amp;lt;code&amp;gt;certonly&amp;lt;/code&amp;gt; command is being ignored. Always use &amp;lt;code&amp;gt;--entrypoint certbot&amp;lt;/code&amp;gt; to override it:&lt;br /&gt;
&lt;br /&gt;
 docker compose run --rm --entrypoint certbot certbot certonly ...&lt;br /&gt;
&lt;br /&gt;
Without &amp;lt;code&amp;gt;--entrypoint certbot&amp;lt;/code&amp;gt; the container runs its renewal script instead of your command.&lt;br /&gt;
&lt;br /&gt;
=== Nginx crashes with &amp;quot;cannot load certificate: No such file&amp;quot; ===&lt;br /&gt;
&lt;br /&gt;
You added the HTTPS server block before getting the certificate. Nginx reads all server blocks at startup — if the cert file doesn&#039;t exist, the entire process fails.&lt;br /&gt;
&lt;br /&gt;
Fix: revert nginx config to HTTP-only (Step 9), restart Nginx, get the certificate (Step 12), then re-add HTTPS (Step 13).&lt;br /&gt;
&lt;br /&gt;
=== Webroot challenge test returns nothing ===&lt;br /&gt;
&lt;br /&gt;
Nginx is not running. Check:&lt;br /&gt;
&lt;br /&gt;
 docker compose ps&lt;br /&gt;
 docker compose logs nginx&lt;br /&gt;
&lt;br /&gt;
If Nginx is restarting — it has the HTTPS config with a missing cert file. Use the HTTP-only config.&lt;br /&gt;
&lt;br /&gt;
=== &amp;quot;Escrow key does NOT match&amp;quot; warning on startup ===&lt;br /&gt;
&lt;br /&gt;
The key in &amp;lt;code&amp;gt;.env&amp;lt;/code&amp;gt; is the wrong type. You need the &#039;&#039;&#039;active&#039;&#039;&#039; private key, not the posting or owner key. Find it in your Hive wallet → Keys &amp;amp; Permissions → Active. It starts with &amp;lt;code&amp;gt;5K&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Site unreachable on port 443 ===&lt;br /&gt;
&lt;br /&gt;
Check that the certificate was issued:&lt;br /&gt;
&lt;br /&gt;
 ls /opt/v4call/data/certbot/conf/live/&lt;br /&gt;
&lt;br /&gt;
Should show a folder with your domain name. If empty — go back to Step 12.&lt;br /&gt;
&lt;br /&gt;
=== npm ci error during Docker build ===&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;npm ci&amp;lt;/code&amp;gt; command requires a &amp;lt;code&amp;gt;package-lock.json&amp;lt;/code&amp;gt; file. The project uses &amp;lt;code&amp;gt;npm install&amp;lt;/code&amp;gt; instead. If you see this error, check your &amp;lt;code&amp;gt;Dockerfile&amp;lt;/code&amp;gt; — it should say &amp;lt;code&amp;gt;npm install&amp;lt;/code&amp;gt; not &amp;lt;code&amp;gt;npm ci&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Custom token payments not working ===&lt;br /&gt;
&lt;br /&gt;
If token rates are detected but payments fail:&lt;br /&gt;
* Check the escrow account holds the token — send some tokens to your escrow account on Hive-Engine&lt;br /&gt;
* Check the token symbol matches exactly (case-sensitive) between the rates post and Hive-Engine&lt;br /&gt;
* Check the server logs: &amp;lt;code&amp;gt;docker compose logs app | grep -i &amp;quot;token\|cnoobs\|escrow-token&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
* The Hive-Engine API endpoint must be &amp;lt;code&amp;gt;https://api.hive-engine.com/rpc/contracts&amp;lt;/code&amp;gt; — this is built into the code&lt;br /&gt;
&lt;br /&gt;
=== [encrypted — unlock with 🔑 key panel to read] ===&lt;br /&gt;
&lt;br /&gt;
You logged in with Hive Keychain. Keychain does not expose private keys, so encrypted messages cannot be decrypted without your posting key. Enter your posting key in the 🔑 panel at the bottom of the online users list. The key stays in browser session memory only — it is needed once per session.&lt;br /&gt;
&lt;br /&gt;
== Quick Reference ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Command !! What it does&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose ps&amp;lt;/code&amp;gt; || Show status of all containers&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose logs app&amp;lt;/code&amp;gt; || Show app logs&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose logs nginx&amp;lt;/code&amp;gt; || Show Nginx logs&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose logs -f app&amp;lt;/code&amp;gt; || Watch live logs (Ctrl+C to stop)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose down&amp;lt;/code&amp;gt; || Stop everything (&#039;&#039;&#039;always do this before rebuilding&#039;&#039;&#039;)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose build --no-cache&amp;lt;/code&amp;gt; || Rebuild without using cached layers&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose up -d&amp;lt;/code&amp;gt; || Start everything in background&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose down &amp;amp;&amp;amp; docker compose build --no-cache &amp;amp;&amp;amp; docker compose up -d&amp;lt;/code&amp;gt; || Full rebuild cycle (use after any code change)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose restart nginx&amp;lt;/code&amp;gt; || Restart Nginx after config-only changes (no rebuild needed)&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;chown -R 1000:1000 /opt/v4call/data/logs&amp;lt;/code&amp;gt; || Fix SQLite write permissions&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose run --rm --entrypoint certbot certbot certificates&amp;lt;/code&amp;gt; || List SSL certificates&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;curl http://yourdomain.com/.well-known/acme-challenge/testfile&amp;lt;/code&amp;gt; || Test certbot webroot works&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;docker compose logs app &amp;amp;#124; grep -i &amp;quot;token\|escrow&amp;quot;&amp;lt;/code&amp;gt; || Check token payment logs&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Optional: Password Protect Your Server During Testing ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;strong&amp;gt;Optional: Password Protect Your Server During Testing (HTTP Basic Auth)&amp;lt;/strong&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
During development and testing you may want to restrict access so only people you invite can use your server. This uses Nginx HTTP Basic Auth — a simple username and password prompt that appears before the v4call login screen.&lt;br /&gt;
&lt;br /&gt;
When a visitor cancels the login prompt they are shown a public &amp;lt;code&amp;gt;info.html&amp;lt;/code&amp;gt; page where they can read about the project and request access.&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Step 1 — Install the htpasswd tool&amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;apt install -y apache2-utils&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Step 2 — Create the password file and add your first user &amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;htpasswd -c /opt/v4call/nginx/.htpasswd yourusername&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You will be prompted to enter and confirm a password. The &amp;lt;code&amp;gt;-c&amp;lt;/code&amp;gt; flag creates the file. Do not use &amp;lt;code&amp;gt;-c&amp;lt;/code&amp;gt; again or it will overwrite the file and delete existing users.&lt;br /&gt;
&lt;br /&gt;
To add more users later:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;htpasswd /opt/v4call/nginx/.htpasswd anotherusername&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To remove a user:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;htpasswd -D /opt/v4call/nginx/.htpasswd username&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Step 3 — Create the public info page &amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;nano /opt/v4call/public/info.html&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Paste your HTML content — a page explaining the project and how to request access. This page is served publicly without a password so visitors who cancel the login prompt can still read it.&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Step 4 — Mount the files into the Nginx container &amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
Edit &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt; and add two lines to the nginx volumes section:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
  nginx:&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ./nginx/v4call.conf:/etc/nginx/conf.d/default.conf:ro&lt;br /&gt;
      - ./nginx/.htpasswd:/etc/nginx/.htpasswd:ro          # add this&lt;br /&gt;
      - ./public/info.html:/usr/share/nginx/html/info.html:ro  # add this&lt;br /&gt;
      - ./data/certbot/conf:/etc/letsencrypt:ro&lt;br /&gt;
      - ./data/certbot/www:/var/www/certbot:ro&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Step 5 — Update your Nginx config &amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
Add auth to the &#039;&#039;&#039;HTTPS server block&#039;&#039;&#039; (not the HTTP block — users are redirected to HTTPS so auth must live there). Add these lines to both the &amp;lt;code&amp;gt;/socket.io/&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;/&amp;lt;/code&amp;gt; location blocks, and add the &amp;lt;code&amp;gt;error_page&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;info.html&amp;lt;/code&amp;gt; location blocks:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
server {&lt;br /&gt;
    listen 443 ssl;&lt;br /&gt;
    server_name call.yourdomain.com www.call.yourdomain.com;&lt;br /&gt;
&lt;br /&gt;
    # ... ssl_certificate lines stay the same ...&lt;br /&gt;
&lt;br /&gt;
    # Send cancelled logins to the public info page&lt;br /&gt;
    error_page 401 /info.html;&lt;br /&gt;
&lt;br /&gt;
    # info.html is served by Nginx directly — no auth, no proxy&lt;br /&gt;
    location = /info.html {&lt;br /&gt;
        root /usr/share/nginx/html;&lt;br /&gt;
        auth_basic off;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # WebSocket — requires auth&lt;br /&gt;
    location /socket.io/ {&lt;br /&gt;
        auth_basic           &amp;quot;v4call — Private Testing&amp;quot;;&lt;br /&gt;
        auth_basic_user_file /etc/nginx/.htpasswd;&lt;br /&gt;
        proxy_pass http://app:3000;&lt;br /&gt;
        # ... rest of proxy headers stay the same ...&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    # All other requests — requires auth&lt;br /&gt;
    location / {&lt;br /&gt;
        auth_basic           &amp;quot;v4call — Private Testing&amp;quot;;&lt;br /&gt;
        auth_basic_user_file /etc/nginx/.htpasswd;&lt;br /&gt;
        proxy_pass http://app:3000;&lt;br /&gt;
        # ... rest of proxy headers stay the same ...&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
* Note: if you want to log successful and unsuccessful login attempts, add:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
    error_log /var/log/nginx/error.log warn;&lt;br /&gt;
    access_log /var/log/nginx/access.log;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Step 6 — Recreate the Nginx container to pick up the new volume mounts &amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important&#039;&#039;&#039;: &amp;lt;code&amp;gt;docker compose restart nginx&amp;lt;/code&amp;gt; is not enough — it reuses the old container and ignores new volume mounts. You must recreate it:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker compose down &amp;amp;&amp;amp; docker compose up -d&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
After this, verify the files are mounted inside the container:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker compose exec nginx ls /usr/share/nginx/html/&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You should see &amp;lt;code&amp;gt;info.html&amp;lt;/code&amp;gt; listed alongside &amp;lt;code&amp;gt;50x.html&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;index.html&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Step 7 — Test it works &amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;curl -o /dev/null -s -w &amp;quot;%{http_code}&amp;quot; https://call.yourdomain.com/&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Should return &amp;lt;code&amp;gt;401&amp;lt;/code&amp;gt; (login required).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;curl -o /dev/null -s -w &amp;quot;%{http_code}&amp;quot; https://call.yourdomain.com/info.html&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Should return &amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt; (public, no auth needed).&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;curl -u youruser:yourpassword -o /dev/null -s -w &amp;quot;%{http_code}&amp;quot; https://call.yourdomain.com/&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Should return &amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt; (correct credentials accepted).&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Managing users without restarting &amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
After adding or removing users from the &amp;lt;code&amp;gt;.htpasswd&amp;lt;/code&amp;gt; file, reload Nginx config without downtime:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker compose exec nginx nginx -s reload&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
No restart needed — Nginx re-reads the &amp;lt;code&amp;gt;.htpasswd&amp;lt;/code&amp;gt; file on every request anyway.&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Keep .htpasswd out of GitHub &amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
The password file should never be committed to your repository. Add it to &amp;lt;code&amp;gt;.gitignore&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;echo &amp;quot;nginx/.htpasswd&amp;quot; &amp;gt;&amp;gt; /opt/v4call/.gitignore&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
*&amp;lt;b&amp;gt; Removing auth when you go public &amp;lt;/b&amp;gt; &lt;br /&gt;
&lt;br /&gt;
When you are ready to open your server to everyone, simply remove the &amp;lt;code&amp;gt;auth_basic&amp;lt;/code&amp;gt; lines from &amp;lt;code&amp;gt;nginx/v4call.conf&amp;lt;/code&amp;gt; and reload:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker compose exec nginx nginx -s reload&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
No other changes needed.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;How to check if anyone tried to log in (failed attempts) or successfully logged in with .htpasswd on Nginx in Docker&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important Tip:&#039;&#039;&#039; Always run these commands in the same folder where your &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt; file is located. If you are in the wrong directory the commands will not find your container and nothing will show up.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&#039;&#039; Basic Commands &#039;&#039;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
; &#039;&#039;&#039;See everything live (good while testing)&#039;&#039;&#039;&lt;br /&gt;
: &amp;lt;code&amp;gt;docker compose logs -f nginx&amp;lt;/code&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
: &amp;lt;code&amp;gt;-f&amp;lt;/code&amp;gt; means &amp;quot;follow/live&amp;quot; – new log lines appear automatically. Remove &amp;lt;code&amp;gt;-f&amp;lt;/code&amp;gt; if you only want to read the current logs once and stop.&lt;br /&gt;
&lt;br /&gt;
; &#039;&#039;&#039;Best command to watch failed login attempts (people guessing passwords)&#039;&#039;&#039;&lt;br /&gt;
: &amp;lt;code&amp;gt;docker compose logs -f nginx | grep -E &amp;quot;mismatch|not found|401&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
; &#039;&#039;&#039;See who successfully logged in&#039;&#039;&#039;&lt;br /&gt;
: &amp;lt;code&amp;gt;docker compose logs nginx | grep &amp;quot;remote_user&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
; &#039;&#039;&#039;Combined view – most useful for daily checking (failed + successful)&#039;&#039;&#039;&lt;br /&gt;
: &amp;lt;code&amp;gt;docker compose logs -f --tail=100 nginx | grep -E &amp;quot;(mismatch|not found|remote_user|401)&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&#039;&#039; What the Logs Look Like &#039;&#039;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Failed attempt (wrong password):&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;2026/04/17 01:23:45 [error] ... user &amp;quot;admin&amp;quot;: password mismatch, client: 185.123.45.67, ...&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Failed attempt (wrong username):&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;2026/04/17 01:24:12 [error] ... user &amp;quot;hacker123&amp;quot; was not found in &amp;quot;/etc/nginx/.htpasswd&amp;quot;, client: 45.67.89.10, ...&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Successful login:&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;pre&amp;gt;... &amp;quot;GET /protected/ HTTP/1.1&amp;quot; 200 ... remote_user: &amp;quot;myuser&amp;quot; ...&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&#039;&#039; How to Customise These Commands &#039;&#039;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
* Change &amp;lt;code&amp;gt;nginx&amp;lt;/code&amp;gt; to the exact name of your service if it is different in docker-compose.yml.&lt;br /&gt;
* Remove &amp;lt;code&amp;gt;-f&amp;lt;/code&amp;gt; to read the full log once without live following.&lt;br /&gt;
* Change &amp;lt;code&amp;gt;--tail=100&amp;lt;/code&amp;gt; to &amp;lt;code&amp;gt;--tail=500&amp;lt;/code&amp;gt; (or any number) to show more or fewer old lines.&lt;br /&gt;
* Add or remove words in the &amp;lt;code&amp;gt;grep&amp;lt;/code&amp;gt; part to filter differently.  &lt;br /&gt;
  Examples:&lt;br /&gt;
  * Only failed attempts: &amp;lt;code&amp;gt;grep -E &amp;quot;mismatch|not found&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
  * Only 401 errors: &amp;lt;code&amp;gt;grep &amp;quot; 401 &amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
  * Everything auth related: &amp;lt;code&amp;gt;grep -E &amp;quot;(auth|password|mismatch|not found|remote_user)&amp;quot;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;&#039;&#039;Quick Copy-Paste Commands &#039;&#039;&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
# Watch failed guesses live&lt;br /&gt;
docker compose logs -f nginx | grep -E &amp;quot;mismatch|not found|401&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Check successful logins&lt;br /&gt;
docker compose logs nginx | grep &amp;quot;remote_user&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Combined quick check (recommended)&lt;br /&gt;
docker compose logs -f --tail=100 nginx | grep -E &amp;quot;(mismatch|not found|remote_user|401)&amp;quot;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
[[Category:Docker]]&lt;br /&gt;
[[Category:Ubuntu]]&lt;br /&gt;
[[Category:Hive]]&lt;br /&gt;
[[Category:v4call]]&lt;br /&gt;
[[Category:WebRTC]]&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Main_Page&amp;diff=739</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Main_Page&amp;diff=739"/>
		<updated>2026-04-19T14:16:23Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* Licenses */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
=In Concept development Mode - a wiki you can download=&lt;br /&gt;
= CompleteNoobs: A Downloadable Wiki for Reproducible Computer Tutorials =&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;CompleteNoobs&#039;&#039;&#039; is a open-source wiki offering free, reproducible computer science tutorials. &lt;br /&gt;
&lt;br /&gt;
* [[Ubuntu2404_Install_Docker_and_Docker_Compose| Download the wiki as a &#039;&#039;&#039;Docker image&#039;&#039;&#039; to run locally on your computer or fork it to contribute and share knowledge.]]&lt;br /&gt;
&lt;br /&gt;
== About CompleteNoobs ==&lt;br /&gt;
&lt;br /&gt;
Our mission is to provide accessible, libre-licensed resources for hobbyists, sysadmins, students, teachers, and computer science enthusiasts. All content is available under a &#039;&#039;&#039;Creative Commons BY-NC-SA&#039;&#039;&#039; license, ensuring the freedoms to:&lt;br /&gt;
* Read&lt;br /&gt;
* Edit/Modify&lt;br /&gt;
* Copy&lt;br /&gt;
* Share freely&lt;br /&gt;
&lt;br /&gt;
Download the wiki as an XML file at [https://xml.completenoobs.com xml.completenoobs.com] or access data-heavy content (images, videos, audio) via &#039;&#039;&#039;IPFS&#039;&#039;&#039; hashes.&lt;br /&gt;
&lt;br /&gt;
== Get Started ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Download the Wiki&#039;&#039;&#039;: Run CompleteNoobs locally using our [[Local_CompleteNoobs_Wiki|Manually install guides or Docker image]].&lt;br /&gt;
* &#039;&#039;&#039;Host Your Own&#039;&#039;&#039;: Set up your own MediaWiki instance with our guide: [[Host_Your_Own_Mediawiki_Online|Host Your Own MediaWiki Online]].&lt;br /&gt;
&lt;br /&gt;
== CompleteNoobs Blockchain Project ==&lt;br /&gt;
&lt;br /&gt;
As Noobs we want to learn more about bitcoin, without losing any real bitcoin&lt;br /&gt;
&lt;br /&gt;
First we are learning how to fork bitcoin version 0.14.3, best way to learn is by tinkering with it.&lt;br /&gt;
* [[N33Bcoin| n33b.com]]&lt;br /&gt;
&lt;br /&gt;
== Essential Links ==&lt;br /&gt;
&lt;br /&gt;
* [[Main_Index|Main Index Page]]&lt;br /&gt;
* [[Special:AllPages|All Pages]]&lt;br /&gt;
* [[Wiki_Basic_Syntax|Wiki Basic Syntax]]&lt;br /&gt;
* [[COMPLETENOOBS_FUNDING|Support Us]]&lt;br /&gt;
&lt;br /&gt;
== Licenses ==&lt;br /&gt;
[[LICENCE_HEADERS]]&lt;br /&gt;
All content is libre-licensed for free copying, modification, and distribution. Add a license header to your contributions using:&lt;br /&gt;
&amp;lt;pre&amp;gt;{{:LICENCE_HEADER_CC0}}&amp;lt;/pre&amp;gt;&lt;br /&gt;
{{:LICENCE_HEADER_CC0}}&lt;br /&gt;
&lt;br /&gt;
== Disclaimer ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;&#039;&#039;DISCLAIMER:&#039;&#039;&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
Content on CompleteNoobs is for informational and educational purposes only. We make no warranties about accuracy or reliability. Use at your own risk. Links to external sites are not endorsements, and we are not responsible for their content or availability. The site may be temporarily unavailable due to technical issues beyond our control.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Special:ContributionScores/10/5}}&lt;br /&gt;
{{Special:PopularPages/10}}&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Hive_Blockchain_-_How_to_change_Active_and_Posting_keys_on_Hive&amp;diff=738</id>
		<title>Hive Blockchain - How to change Active and Posting keys on Hive</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Hive_Blockchain_-_How_to_change_Active_and_Posting_keys_on_Hive&amp;diff=738"/>
		<updated>2026-04-19T13:57:14Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot;&amp;#039;&amp;#039;&amp;#039;How to Change Your Active and Posting Keys on Hive (2026)&amp;#039;&amp;#039;&amp;#039;   To update your Active, Posting (or any) keys on Hive, you must perform a full **password reset**. This generates a new &amp;#039;&amp;#039;&amp;#039;Master Password&amp;#039;&amp;#039;&amp;#039; that automatically creates a fresh set of all keys (Owner, Active, Posting, Memo).  ==&amp;#039;&amp;#039;&amp;#039;Critical Warnings&amp;#039;&amp;#039;&amp;#039;== * If you lose the new Master Password or Owner Key, &amp;#039;&amp;#039;&amp;#039;no one&amp;#039;&amp;#039;&amp;#039; (not even Hive support) can recover your account. * &amp;#039;&amp;#039;&amp;#039;Backup everything&amp;#039;&amp;#039;&amp;#039; (especially the...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&#039;&#039;&#039;How to Change Your Active and Posting Keys on Hive (2026)&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
To update your Active, Posting (or any) keys on Hive, you must perform a full **password reset**. This generates a new &#039;&#039;&#039;Master Password&#039;&#039;&#039; that automatically creates a fresh set of all keys (Owner, Active, Posting, Memo).&lt;br /&gt;
&lt;br /&gt;
==&#039;&#039;&#039;Critical Warnings&#039;&#039;&#039;==&lt;br /&gt;
* If you lose the new Master Password or Owner Key, &#039;&#039;&#039;no one&#039;&#039;&#039; (not even Hive support) can recover your account.&lt;br /&gt;
* &#039;&#039;&#039;Backup everything&#039;&#039;&#039; (especially the new Master Password and all private keys) before clicking Update.&lt;br /&gt;
* You will need to manually add the new keys to Hive Keychain and other apps afterward.&lt;br /&gt;
&lt;br /&gt;
==&#039;&#039;&#039;Method 1: Official Hive Wallet (wallet.hive.blog)&#039;&#039;&#039;==&lt;br /&gt;
# Go to [https://wallet.hive.blog wallet.hive.blog] (or directly to &amp;lt;code&amp;gt;https://wallet.hive.blog/@yourusername/permissions&amp;lt;/code&amp;gt;).&lt;br /&gt;
# Log in using your current &#039;&#039;&#039;Owner Key&#039;&#039;&#039; or &#039;&#039;&#039;Master Password&#039;&#039;&#039;.&lt;br /&gt;
# Navigate to the &#039;&#039;&#039;Change Password&#039;&#039;&#039; section.&lt;br /&gt;
# Click &#039;&#039;&#039;&amp;quot;Click to Generate Password&amp;quot;&#039;&#039;&#039;.&lt;br /&gt;
# &#039;&#039;&#039;Save everything&#039;&#039;&#039;: Copy the new Master Password and all displayed private/public keys. Store them securely offline.&lt;br /&gt;
# Re-enter the new Master Password in the confirmation field.&lt;br /&gt;
# Check all confirmation boxes.&lt;br /&gt;
# Click &#039;&#039;&#039;Update Password&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
==&#039;&#039;&#039;Method 2: Using PeakD (often easier)&#039;&#039;&#039;==&lt;br /&gt;
# Log in to [https://peakd.com peakd.com] (usually via Hive Keychain).&lt;br /&gt;
# Go to your profile → &#039;&#039;&#039;Wallet&#039;&#039;&#039; → &#039;&#039;&#039;Keys &amp;amp; Permissions&#039;&#039;&#039;.&lt;br /&gt;
# Click the &#039;&#039;&#039;Change Password&#039;&#039;&#039; button.&lt;br /&gt;
# Authorize with your current &#039;&#039;&#039;Owner Key&#039;&#039;&#039;.&lt;br /&gt;
# Generate the new password, download/save all keys, then confirm the update.&lt;br /&gt;
&lt;br /&gt;
==&#039;&#039;&#039;After Updating&#039;&#039;&#039;==&lt;br /&gt;
* Update Hive Keychain (remove old keys and import the new ones).&lt;br /&gt;
* Update any mobile apps (Ecency, Hive Wallet, etc.).&lt;br /&gt;
* Test logging in with the new keys before deleting old backups.&lt;br /&gt;
&lt;br /&gt;
*&#039;&#039;&#039;Tip&#039;&#039;&#039;: Always keep your Owner Key extremely safe — it is the only key that can reset everything.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Expanding info box==&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;How to Change Your Active and Posting Keys on Hive (2026)&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To update your Active, Posting (or any) keys on Hive, you must perform a full **password reset**. This generates a new &#039;&#039;&#039;Master Password&#039;&#039;&#039; that automatically creates a fresh set of all keys (Owner, Active, Posting, Memo).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Critical Warnings&#039;&#039;&#039;&lt;br /&gt;
* If you lose the new Master Password or Owner Key, &#039;&#039;&#039;no one&#039;&#039;&#039; (not even Hive support) can recover your account.&lt;br /&gt;
* &#039;&#039;&#039;Backup everything&#039;&#039;&#039; (especially the new Master Password and all private keys) before clicking Update.&lt;br /&gt;
* You will need to manually add the new keys to Hive Keychain and other apps afterward.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Method 1: Official Hive Wallet (wallet.hive.blog)&#039;&#039;&#039;&lt;br /&gt;
# Go to [https://wallet.hive.blog wallet.hive.blog] (or directly to &amp;lt;code&amp;gt;https://wallet.hive.blog/@yourusername/permissions&amp;lt;/code&amp;gt;).&lt;br /&gt;
# Log in using your current &#039;&#039;&#039;Owner Key&#039;&#039;&#039; or &#039;&#039;&#039;Master Password&#039;&#039;&#039;.&lt;br /&gt;
# Navigate to the &#039;&#039;&#039;Change Password&#039;&#039;&#039; section.&lt;br /&gt;
# Click &#039;&#039;&#039;&amp;quot;Click to Generate Password&amp;quot;&#039;&#039;&#039;.&lt;br /&gt;
# &#039;&#039;&#039;Save everything&#039;&#039;&#039;: Copy the new Master Password and all displayed private/public keys. Store them securely offline.&lt;br /&gt;
# Re-enter the new Master Password in the confirmation field.&lt;br /&gt;
# Check all confirmation boxes.&lt;br /&gt;
# Click &#039;&#039;&#039;Update Password&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Method 2: Using PeakD (often easier)&#039;&#039;&#039;&lt;br /&gt;
# Log in to [https://peakd.com peakd.com] (usually via Hive Keychain).&lt;br /&gt;
# Go to your profile → &#039;&#039;&#039;Wallet&#039;&#039;&#039; → &#039;&#039;&#039;Keys &amp;amp; Permissions&#039;&#039;&#039;.&lt;br /&gt;
# Click the &#039;&#039;&#039;Change Password&#039;&#039;&#039; button.&lt;br /&gt;
# Authorize with your current &#039;&#039;&#039;Owner Key&#039;&#039;&#039;.&lt;br /&gt;
# Generate the new password, download/save all keys, then confirm the update.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;After Updating&#039;&#039;&#039;&lt;br /&gt;
* Update Hive Keychain (remove old keys and import the new ones).&lt;br /&gt;
* Update any mobile apps (Ecency, Hive Wallet, etc.).&lt;br /&gt;
* Test logging in with the new keys before deleting old backups.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Tip&#039;&#039;&#039;: Always keep your Owner Key extremely safe — it is the only key that can reset everything.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Hive_Blockchain_-_buying/exchanging_Hive_Based_Dollars_$HBD&amp;diff=737</id>
		<title>Hive Blockchain - buying/exchanging Hive Based Dollars $HBD</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Hive_Blockchain_-_buying/exchanging_Hive_Based_Dollars_$HBD&amp;diff=737"/>
		<updated>2026-04-19T12:48:22Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot;= Hive Blockchain Buying HBD =  == Prerequisites == Before buying HBD, you must have a Hive account.  It is recommended to use Hive Keychain for easy and secure access to your account.  * Create a Hive account (see: &amp;#039;&amp;#039;Hive Blockchain Create Extra Accounts&amp;#039;&amp;#039;) * Install Hive Keychain (browser extension) * Log in to your account using your keys  == Buying HIVE from an Exchange == To obtain HBD, you will first need to buy HIVE from a cryptocurrency exchange.  Example (UK): *...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Hive Blockchain Buying HBD =&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
Before buying HBD, you must have a Hive account.&lt;br /&gt;
&lt;br /&gt;
It is recommended to use Hive Keychain for easy and secure access to your account.&lt;br /&gt;
&lt;br /&gt;
* Create a Hive account (see: &#039;&#039;Hive Blockchain Create Extra Accounts&#039;&#039;)&lt;br /&gt;
* Install Hive Keychain (browser extension)&lt;br /&gt;
* Log in to your account using your keys&lt;br /&gt;
&lt;br /&gt;
== Buying HIVE from an Exchange ==&lt;br /&gt;
To obtain HBD, you will first need to buy HIVE from a cryptocurrency exchange.&lt;br /&gt;
&lt;br /&gt;
Example (UK):&lt;br /&gt;
* https://www.mexc.com/en-GB/&lt;br /&gt;
&lt;br /&gt;
Steps:&lt;br /&gt;
* Create and verify an account on the exchange&lt;br /&gt;
* Deposit funds (GBP, EUR, etc.)&lt;br /&gt;
* Buy HIVE&lt;br /&gt;
&lt;br /&gt;
== Sending HIVE to Your Hive Account ==&lt;br /&gt;
Once you have purchased HIVE:&lt;br /&gt;
&lt;br /&gt;
* Go to &#039;&#039;&#039;Withdraw&#039;&#039;&#039; on the exchange&lt;br /&gt;
* Enter your Hive username as the destination&lt;br /&gt;
* Double-check the username (transactions cannot be reversed)&lt;br /&gt;
* Send the HIVE&lt;br /&gt;
&lt;br /&gt;
After a short time, the HIVE will appear in your Hive wallet.&lt;br /&gt;
&lt;br /&gt;
== Swapping HIVE to HBD ==&lt;br /&gt;
To convert HIVE into HBD, you can use a Hive-based exchange:&lt;br /&gt;
&lt;br /&gt;
* https://hivedex.io/&lt;br /&gt;
&lt;br /&gt;
Steps:&lt;br /&gt;
* Connect your Hive account (via Hive Keychain)&lt;br /&gt;
* Select the HIVE → HBD market&lt;br /&gt;
* Enter the amount of HIVE to swap&lt;br /&gt;
* Confirm the transaction in Hive Keychain&lt;br /&gt;
&lt;br /&gt;
Once completed, your HBD balance will be updated in your wallet.&lt;br /&gt;
&lt;br /&gt;
== Notes ==&lt;br /&gt;
* Always verify URLs before logging in or signing transactions&lt;br /&gt;
* Keep your keys secure and never share them&lt;br /&gt;
* Test with a small amount first if you are new&lt;br /&gt;
&lt;br /&gt;
And that&#039;s it — you now have HBD in your Hive account.&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Hive_Blockchain_Create_Create_Extra_Accounts&amp;diff=736</id>
		<title>Hive Blockchain Create Create Extra Accounts</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Hive_Blockchain_Create_Create_Extra_Accounts&amp;diff=736"/>
		<updated>2026-04-19T12:33:01Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot;= Hive Blockchain Create Extra Accounts =  == Free accounts == This is the on-ramp for your first account.  https://signup.hive.io/  == Paid accounts ==  === Hive Keychain ===  * Download Hive Keychain (browser extension) * Log in with your &amp;#039;&amp;#039;&amp;#039;Active Key&amp;#039;&amp;#039;&amp;#039; (required to sign the ~3 HIVE account creation fee) * Click the ☰ (top left) → &amp;#039;&amp;#039;&amp;#039;Accounts&amp;#039;&amp;#039;&amp;#039; → &amp;#039;&amp;#039;&amp;#039;Create Account&amp;#039;&amp;#039;&amp;#039; * Enter a username  If the username is available, you will proceed to the next stage where you...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Hive Blockchain Create Extra Accounts =&lt;br /&gt;
&lt;br /&gt;
== Free accounts ==&lt;br /&gt;
This is the on-ramp for your first account.&lt;br /&gt;
&lt;br /&gt;
https://signup.hive.io/&lt;br /&gt;
&lt;br /&gt;
== Paid accounts ==&lt;br /&gt;
&lt;br /&gt;
=== Hive Keychain ===&lt;br /&gt;
&lt;br /&gt;
* Download Hive Keychain (browser extension)&lt;br /&gt;
* Log in with your &#039;&#039;&#039;Active Key&#039;&#039;&#039; (required to sign the ~3 HIVE account creation fee)&lt;br /&gt;
* Click the ☰ (top left) → &#039;&#039;&#039;Accounts&#039;&#039;&#039; → &#039;&#039;&#039;Create Account&#039;&#039;&#039;&lt;br /&gt;
* Enter a username&lt;br /&gt;
&lt;br /&gt;
If the username is available, you will proceed to the next stage where your key pairs are shown.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important:&#039;&#039;&#039;&lt;br /&gt;
When Hive Keychain is open on the key display screen, clicking outside the extension (e.g. to open a text editor) may close it. If this happens before saving, you will lose the generated keys and must restart the process.&lt;br /&gt;
&lt;br /&gt;
* As soon as the keys are shown:&lt;br /&gt;
** Click &#039;&#039;&#039;Copy&#039;&#039;&#039;&lt;br /&gt;
** Tick all confirmation boxes&lt;br /&gt;
** Click &#039;&#039;&#039;Copy&#039;&#039;&#039; again to be safe&lt;br /&gt;
* Click &#039;&#039;&#039;Create&#039;&#039;&#039; to finalize the account&lt;br /&gt;
* Immediately paste your keys into a text file and back them up securely&lt;br /&gt;
&lt;br /&gt;
And that&#039;s it — you have created a new Hive account.&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Hive_Blockchain_Create_Custom_Coins&amp;diff=735</id>
		<title>Hive Blockchain Create Custom Coins</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Hive_Blockchain_Create_Custom_Coins&amp;diff=735"/>
		<updated>2026-04-19T12:23:06Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{:LICENCE_HEADER_CC0}}&lt;br /&gt;
= v4call — How to Create Your Own Custom Token (e.g. CNOOBS) on Hive Engine =&lt;br /&gt;
From CompleteNoobs&lt;br /&gt;
F&lt;br /&gt;
This guide walks you through creating a custom Hive Engine token (example: &#039;&#039;&#039;CNOOBS&#039;&#039;&#039;) in under 10 minutes. No coding required.&lt;br /&gt;
&lt;br /&gt;
These tokens power the custom communication economy in v4call: you can set rates like &amp;quot;1 CNOOBS = 1 text message&amp;quot; or &amp;quot;10 CNOOBS = 1 hour video call&amp;quot;, gift them to friends/family, or let blocked users bypass your blocklist if they hold enough of your token.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Why create your own token?&#039;&#039;&#039;&lt;br /&gt;
* Full control over supply and distribution.&lt;br /&gt;
* Real utility inside v4call (and any other Hive dApp that supports Hive Engine tokens).&lt;br /&gt;
* Scarcity you decide — perfect for personal or community communication economies.&lt;br /&gt;
* Transferable, tradable, and giftable via Hive Keychain or TribalDex.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cost&#039;&#039;&#039;: Approximately 100 BEE (Hive Engine&#039;s utility token). BEE price fluctuates — check current value on TribalDex or PeakD. This is a one-time creation fee.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Source&#039;&#039;&#039;: Based on the official Hive Engine / TribalDex interface (as of 2026).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;End result&#039;&#039;&#039;: A live custom token (e.g. CNOOBS) that appears in users&#039; Hive Keychain wallets and can be used in your v4call rates post.&lt;br /&gt;
&lt;br /&gt;
== Contents ==&lt;br /&gt;
* [[#What_You_Need|1 What You Need]]&lt;br /&gt;
* [[#Step_1:_Get_Some_BEE_Tokens|2 Step 1: Get Some BEE Tokens]]&lt;br /&gt;
* [[#Step_2:_Log_In_to_TribalDex|3 Step 2: Log In to TribalDex]]&lt;br /&gt;
* [[#Step_3:_Create_Your_Token|4 Step 3: Create Your Token]]&lt;br /&gt;
* [[#Step_4:_Issue_Tokens_to_Yourself|5 Step 4: Issue Tokens to Yourself]]&lt;br /&gt;
* [[#Step_5:_Optional_-_Add_Metadata_(Logo_Description)|6 Step 5: Optional - Add Metadata (Logo &amp;amp; Description)]]&lt;br /&gt;
* [[#Step_6:_Distribute_Your_Tokens|7 Step 6: Distribute Your Tokens]]&lt;br /&gt;
* [[#Using_Your_Token_in_v4call|8 Using Your Token in v4call]]&lt;br /&gt;
* [[#Common_Problems_and_Fixes|9 Common Problems and Fixes]]&lt;br /&gt;
* [[#Quick_Reference|10 Quick Reference]]&lt;br /&gt;
&lt;br /&gt;
== What You Need ==&lt;br /&gt;
* A Hive account (e.g. @noblemage) with Hive Keychain installed and the &#039;&#039;&#039;active key&#039;&#039;&#039; available.&lt;br /&gt;
* Some BEE tokens (≈100 BEE for creation fee).&lt;br /&gt;
* A web browser.&lt;br /&gt;
&lt;br /&gt;
You do &#039;&#039;&#039;not&#039;&#039;&#039; need to run a node or write code.&lt;br /&gt;
&lt;br /&gt;
== Step 1: Get Some BEE Tokens ==&lt;br /&gt;
BEE is the &amp;quot;gas&amp;quot; token for Hive Engine actions.&lt;br /&gt;
&lt;br /&gt;
# Go to [https://tribaldex.com tribaldex.com] or any Hive Engine market (e.g. via PeakD wallet).&lt;br /&gt;
# Swap or buy BEE using HIVE or HBD (very easy with Keychain).&lt;br /&gt;
# Alternative: Many users get small amounts of BEE from community airdrops or by swapping on the built-in market.&lt;br /&gt;
&lt;br /&gt;
Make sure you have at least 110 BEE to cover the fee plus a small buffer.&lt;br /&gt;
&lt;br /&gt;
== Step 2: Log In to TribalDex ==&lt;br /&gt;
# Visit [https://tribaldex.com/tokens/create https://tribaldex.com/tokens/create]&lt;br /&gt;
# Click the login button at the top.&lt;br /&gt;
# Hive Keychain will pop up — approve the login with your &#039;&#039;&#039;active key&#039;&#039;&#039; (or posting key in some cases, but active is safest for token actions).&lt;br /&gt;
&lt;br /&gt;
You are now logged in as your Hive account.&lt;br /&gt;
&lt;br /&gt;
== Step 3: Create Your Token ==&lt;br /&gt;
On the token creation form, fill in the following fields carefully. Most cannot be changed later.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Symbol&#039;&#039;&#039; — e.g. &#039;&#039;&#039;CNOOBS&#039;&#039;&#039; (uppercase, 1–10 characters, unique)&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; — e.g. &#039;&#039;&#039;Cnoobs Coins&#039;&#039;&#039;&lt;br /&gt;
* &#039;&#039;&#039;Max Supply&#039;&#039;&#039; — Choose a number (e.g. 1,000,000). This is the absolute maximum that can ever exist. You can issue less.&lt;br /&gt;
* &#039;&#039;&#039;Precision&#039;&#039;&#039; — Usually &#039;&#039;&#039;3&#039;&#039;&#039; (allows 0.001 precision). You can only increase this later, not decrease.&lt;br /&gt;
* &#039;&#039;&#039;Website&#039;&#039;&#039; (optional) — Link to your profile or v4call server (e.g. https://call.yourdomain.com)&lt;br /&gt;
* &#039;&#039;&#039;Description&#039;&#039;&#039; (optional) — Short text like &amp;quot;Personal communication token for v4call — used for messages and calls with @cnoobz&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Double-check everything — the symbol especially cannot be changed.&lt;br /&gt;
&lt;br /&gt;
Click &#039;&#039;&#039;Create Token&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Hive Keychain will ask you to approve a custom_json transaction. Confirm it.&lt;br /&gt;
&lt;br /&gt;
Wait 3–10 seconds for confirmation on the blockchain.&lt;br /&gt;
&lt;br /&gt;
Success message should appear: your token is now created!&lt;br /&gt;
&lt;br /&gt;
== Step 4: Issue Tokens to Yourself ==&lt;br /&gt;
After creation, you usually start with zero balance.&lt;br /&gt;
&lt;br /&gt;
# Go to [https://tribaldex.com/tokens/manage https://tribaldex.com/tokens/manage]&lt;br /&gt;
# Find your new token (CNOOBS) in the list.&lt;br /&gt;
# Click the &#039;&#039;&#039;Issue&#039;&#039;&#039; button.&lt;br /&gt;
# Enter the amount you want to issue to yourself (e.g. 10000).&lt;br /&gt;
# Confirm with Keychain.&lt;br /&gt;
&lt;br /&gt;
You now hold the full issued supply.&lt;br /&gt;
&lt;br /&gt;
== Step 5: Optional — Add Metadata (Logo &amp;amp; Description) ==&lt;br /&gt;
# Still on the manage page, click the edit icon for your token.&lt;br /&gt;
# Upload a square logo image (recommended 200x200 px or larger).&lt;br /&gt;
# Improve the description and website link.&lt;br /&gt;
# Save changes (another small Keychain transaction).&lt;br /&gt;
&lt;br /&gt;
This makes your token look professional when users view it in wallets or markets.&lt;br /&gt;
&lt;br /&gt;
== Step 6: Distribute Your Tokens ==&lt;br /&gt;
* Send to friends/family via Keychain → &amp;quot;Tokens&amp;quot; tab → Transfer.&lt;br /&gt;
* Airdrop to your community.&lt;br /&gt;
* Use in v4call rates (see below).&lt;br /&gt;
* List on TribalDex market if you want people to buy/sell them.&lt;br /&gt;
&lt;br /&gt;
Tokens are fully transferable and appear instantly in recipients&#039; Hive Keychain wallets.&lt;br /&gt;
&lt;br /&gt;
== Using Your Token in v4call ==&lt;br /&gt;
Once you have your token:&lt;br /&gt;
&lt;br /&gt;
# Go to your v4call server → /rate-editor.html&lt;br /&gt;
# In the rate editor (V2 format), create a new list like [LIST:token-holders]&lt;br /&gt;
# Set TOKEN:CNOOBS&lt;br /&gt;
# Define rates, e.g.:&lt;br /&gt;
** TEXT:1 CNOOBS&lt;br /&gt;
** VOICE:RING:5 CNOOBS;CONNECT:10 CNOOBS;RATE:20 CNOOBS/hr&lt;br /&gt;
# Your server will automatically verify balances using public Hive Engine RPC when someone tries to call or message you.&lt;br /&gt;
&lt;br /&gt;
Blocked users can bypass your blocklist if you set ALLOW-IF-TOKEN:CNOOBS and they hold enough.&lt;br /&gt;
&lt;br /&gt;
== Common Problems and Fixes ==&lt;br /&gt;
=== &amp;quot;Not enough BEE&amp;quot; error ===&lt;br /&gt;
Buy or swap more BEE on TribalDex.&lt;br /&gt;
&lt;br /&gt;
=== Token symbol already taken ===&lt;br /&gt;
Choose a different symbol (add numbers or make it longer, e.g. CNOOBS2).&lt;br /&gt;
&lt;br /&gt;
=== Transaction fails ===&lt;br /&gt;
Make sure you are using the &#039;&#039;&#039;active key&#039;&#039;&#039; in Keychain for token creation/issuing. Restart Keychain if needed.&lt;br /&gt;
&lt;br /&gt;
=== Can&#039;t find my token after creation ===&lt;br /&gt;
Refresh the page or check https://hive-engine.com/tokens — it may take a few seconds to appear.&lt;br /&gt;
&lt;br /&gt;
=== Want to issue more later? ===&lt;br /&gt;
You can issue up to your max supply at any time from the manage page.&lt;br /&gt;
&lt;br /&gt;
== Quick Reference ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Action !! Where to Do It&lt;br /&gt;
|-&lt;br /&gt;
| Create token || https://tribaldex.com/tokens/create&lt;br /&gt;
|-&lt;br /&gt;
| Manage / Issue tokens || https://tribaldex.com/tokens/manage&lt;br /&gt;
|-&lt;br /&gt;
| View all tokens || https://hive-engine.com/tokens&lt;br /&gt;
|-&lt;br /&gt;
| Swap BEE || TribalDex market&lt;br /&gt;
|-&lt;br /&gt;
| Check balance in Keychain || Hive Keychain extension → Tokens tab&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Next step for v4call users&#039;&#039;&#039;: After creating your token, update your v4call-rates post using the improved rate-editor.html to include your custom token rates and blocklist bypass.&lt;br /&gt;
&lt;br /&gt;
You now have your own personal communication currency on Hive!&lt;br /&gt;
&lt;br /&gt;
[[Category:Hive]]&lt;br /&gt;
[[Category:Hive Engine]]&lt;br /&gt;
[[Category:v4call]]&lt;br /&gt;
[[Category:Token Creation]]&lt;br /&gt;
[[Category:Web3]]&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Hive_Blockchain_Create_Custom_Coins&amp;diff=734</id>
		<title>Hive Blockchain Create Custom Coins</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Hive_Blockchain_Create_Custom_Coins&amp;diff=734"/>
		<updated>2026-04-19T12:22:14Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot;{{LICENCE_CC0}} = v4call — How to Create Your Own Custom Token (e.g. CNOOBS) on Hive Engine = From CompleteNoobs F This guide walks you through creating a custom Hive Engine token (example: &amp;#039;&amp;#039;&amp;#039;CNOOBS&amp;#039;&amp;#039;&amp;#039;) in under 10 minutes. No coding required.  These tokens power the custom communication economy in v4call: you can set rates like &amp;quot;1 CNOOBS = 1 text message&amp;quot; or &amp;quot;10 CNOOBS = 1 hour video call&amp;quot;, gift them to friends/family, or let blocked users bypass your blocklist if th...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{{LICENCE_CC0}}&lt;br /&gt;
= v4call — How to Create Your Own Custom Token (e.g. CNOOBS) on Hive Engine =&lt;br /&gt;
From CompleteNoobs&lt;br /&gt;
F&lt;br /&gt;
This guide walks you through creating a custom Hive Engine token (example: &#039;&#039;&#039;CNOOBS&#039;&#039;&#039;) in under 10 minutes. No coding required.&lt;br /&gt;
&lt;br /&gt;
These tokens power the custom communication economy in v4call: you can set rates like &amp;quot;1 CNOOBS = 1 text message&amp;quot; or &amp;quot;10 CNOOBS = 1 hour video call&amp;quot;, gift them to friends/family, or let blocked users bypass your blocklist if they hold enough of your token.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Why create your own token?&#039;&#039;&#039;&lt;br /&gt;
* Full control over supply and distribution.&lt;br /&gt;
* Real utility inside v4call (and any other Hive dApp that supports Hive Engine tokens).&lt;br /&gt;
* Scarcity you decide — perfect for personal or community communication economies.&lt;br /&gt;
* Transferable, tradable, and giftable via Hive Keychain or TribalDex.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Cost&#039;&#039;&#039;: Approximately 100 BEE (Hive Engine&#039;s utility token). BEE price fluctuates — check current value on TribalDex or PeakD. This is a one-time creation fee.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Source&#039;&#039;&#039;: Based on the official Hive Engine / TribalDex interface (as of 2026).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;End result&#039;&#039;&#039;: A live custom token (e.g. CNOOBS) that appears in users&#039; Hive Keychain wallets and can be used in your v4call rates post.&lt;br /&gt;
&lt;br /&gt;
== Contents ==&lt;br /&gt;
* [[#What_You_Need|1 What You Need]]&lt;br /&gt;
* [[#Step_1:_Get_Some_BEE_Tokens|2 Step 1: Get Some BEE Tokens]]&lt;br /&gt;
* [[#Step_2:_Log_In_to_TribalDex|3 Step 2: Log In to TribalDex]]&lt;br /&gt;
* [[#Step_3:_Create_Your_Token|4 Step 3: Create Your Token]]&lt;br /&gt;
* [[#Step_4:_Issue_Tokens_to_Yourself|5 Step 4: Issue Tokens to Yourself]]&lt;br /&gt;
* [[#Step_5:_Optional_-_Add_Metadata_(Logo_Description)|6 Step 5: Optional - Add Metadata (Logo &amp;amp; Description)]]&lt;br /&gt;
* [[#Step_6:_Distribute_Your_Tokens|7 Step 6: Distribute Your Tokens]]&lt;br /&gt;
* [[#Using_Your_Token_in_v4call|8 Using Your Token in v4call]]&lt;br /&gt;
* [[#Common_Problems_and_Fixes|9 Common Problems and Fixes]]&lt;br /&gt;
* [[#Quick_Reference|10 Quick Reference]]&lt;br /&gt;
&lt;br /&gt;
== What You Need ==&lt;br /&gt;
* A Hive account (e.g. @noblemage) with Hive Keychain installed and the &#039;&#039;&#039;active key&#039;&#039;&#039; available.&lt;br /&gt;
* Some BEE tokens (≈100 BEE for creation fee).&lt;br /&gt;
* A web browser.&lt;br /&gt;
&lt;br /&gt;
You do &#039;&#039;&#039;not&#039;&#039;&#039; need to run a node or write code.&lt;br /&gt;
&lt;br /&gt;
== Step 1: Get Some BEE Tokens ==&lt;br /&gt;
BEE is the &amp;quot;gas&amp;quot; token for Hive Engine actions.&lt;br /&gt;
&lt;br /&gt;
# Go to [https://tribaldex.com tribaldex.com] or any Hive Engine market (e.g. via PeakD wallet).&lt;br /&gt;
# Swap or buy BEE using HIVE or HBD (very easy with Keychain).&lt;br /&gt;
# Alternative: Many users get small amounts of BEE from community airdrops or by swapping on the built-in market.&lt;br /&gt;
&lt;br /&gt;
Make sure you have at least 110 BEE to cover the fee plus a small buffer.&lt;br /&gt;
&lt;br /&gt;
== Step 2: Log In to TribalDex ==&lt;br /&gt;
# Visit [https://tribaldex.com/tokens/create https://tribaldex.com/tokens/create]&lt;br /&gt;
# Click the login button at the top.&lt;br /&gt;
# Hive Keychain will pop up — approve the login with your &#039;&#039;&#039;active key&#039;&#039;&#039; (or posting key in some cases, but active is safest for token actions).&lt;br /&gt;
&lt;br /&gt;
You are now logged in as your Hive account.&lt;br /&gt;
&lt;br /&gt;
== Step 3: Create Your Token ==&lt;br /&gt;
On the token creation form, fill in the following fields carefully. Most cannot be changed later.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Symbol&#039;&#039;&#039; — e.g. &#039;&#039;&#039;CNOOBS&#039;&#039;&#039; (uppercase, 1–10 characters, unique)&lt;br /&gt;
* &#039;&#039;&#039;Name&#039;&#039;&#039; — e.g. &#039;&#039;&#039;Cnoobs Coins&#039;&#039;&#039;&lt;br /&gt;
* &#039;&#039;&#039;Max Supply&#039;&#039;&#039; — Choose a number (e.g. 1,000,000). This is the absolute maximum that can ever exist. You can issue less.&lt;br /&gt;
* &#039;&#039;&#039;Precision&#039;&#039;&#039; — Usually &#039;&#039;&#039;3&#039;&#039;&#039; (allows 0.001 precision). You can only increase this later, not decrease.&lt;br /&gt;
* &#039;&#039;&#039;Website&#039;&#039;&#039; (optional) — Link to your profile or v4call server (e.g. https://call.yourdomain.com)&lt;br /&gt;
* &#039;&#039;&#039;Description&#039;&#039;&#039; (optional) — Short text like &amp;quot;Personal communication token for v4call — used for messages and calls with @cnoobz&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Double-check everything — the symbol especially cannot be changed.&lt;br /&gt;
&lt;br /&gt;
Click &#039;&#039;&#039;Create Token&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Hive Keychain will ask you to approve a custom_json transaction. Confirm it.&lt;br /&gt;
&lt;br /&gt;
Wait 3–10 seconds for confirmation on the blockchain.&lt;br /&gt;
&lt;br /&gt;
Success message should appear: your token is now created!&lt;br /&gt;
&lt;br /&gt;
== Step 4: Issue Tokens to Yourself ==&lt;br /&gt;
After creation, you usually start with zero balance.&lt;br /&gt;
&lt;br /&gt;
# Go to [https://tribaldex.com/tokens/manage https://tribaldex.com/tokens/manage]&lt;br /&gt;
# Find your new token (CNOOBS) in the list.&lt;br /&gt;
# Click the &#039;&#039;&#039;Issue&#039;&#039;&#039; button.&lt;br /&gt;
# Enter the amount you want to issue to yourself (e.g. 10000).&lt;br /&gt;
# Confirm with Keychain.&lt;br /&gt;
&lt;br /&gt;
You now hold the full issued supply.&lt;br /&gt;
&lt;br /&gt;
== Step 5: Optional — Add Metadata (Logo &amp;amp; Description) ==&lt;br /&gt;
# Still on the manage page, click the edit icon for your token.&lt;br /&gt;
# Upload a square logo image (recommended 200x200 px or larger).&lt;br /&gt;
# Improve the description and website link.&lt;br /&gt;
# Save changes (another small Keychain transaction).&lt;br /&gt;
&lt;br /&gt;
This makes your token look professional when users view it in wallets or markets.&lt;br /&gt;
&lt;br /&gt;
== Step 6: Distribute Your Tokens ==&lt;br /&gt;
* Send to friends/family via Keychain → &amp;quot;Tokens&amp;quot; tab → Transfer.&lt;br /&gt;
* Airdrop to your community.&lt;br /&gt;
* Use in v4call rates (see below).&lt;br /&gt;
* List on TribalDex market if you want people to buy/sell them.&lt;br /&gt;
&lt;br /&gt;
Tokens are fully transferable and appear instantly in recipients&#039; Hive Keychain wallets.&lt;br /&gt;
&lt;br /&gt;
== Using Your Token in v4call ==&lt;br /&gt;
Once you have your token:&lt;br /&gt;
&lt;br /&gt;
# Go to your v4call server → /rate-editor.html&lt;br /&gt;
# In the rate editor (V2 format), create a new list like [LIST:token-holders]&lt;br /&gt;
# Set TOKEN:CNOOBS&lt;br /&gt;
# Define rates, e.g.:&lt;br /&gt;
** TEXT:1 CNOOBS&lt;br /&gt;
** VOICE:RING:5 CNOOBS;CONNECT:10 CNOOBS;RATE:20 CNOOBS/hr&lt;br /&gt;
# Your server will automatically verify balances using public Hive Engine RPC when someone tries to call or message you.&lt;br /&gt;
&lt;br /&gt;
Blocked users can bypass your blocklist if you set ALLOW-IF-TOKEN:CNOOBS and they hold enough.&lt;br /&gt;
&lt;br /&gt;
== Common Problems and Fixes ==&lt;br /&gt;
=== &amp;quot;Not enough BEE&amp;quot; error ===&lt;br /&gt;
Buy or swap more BEE on TribalDex.&lt;br /&gt;
&lt;br /&gt;
=== Token symbol already taken ===&lt;br /&gt;
Choose a different symbol (add numbers or make it longer, e.g. CNOOBS2).&lt;br /&gt;
&lt;br /&gt;
=== Transaction fails ===&lt;br /&gt;
Make sure you are using the &#039;&#039;&#039;active key&#039;&#039;&#039; in Keychain for token creation/issuing. Restart Keychain if needed.&lt;br /&gt;
&lt;br /&gt;
=== Can&#039;t find my token after creation ===&lt;br /&gt;
Refresh the page or check https://hive-engine.com/tokens — it may take a few seconds to appear.&lt;br /&gt;
&lt;br /&gt;
=== Want to issue more later? ===&lt;br /&gt;
You can issue up to your max supply at any time from the manage page.&lt;br /&gt;
&lt;br /&gt;
== Quick Reference ==&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Action !! Where to Do It&lt;br /&gt;
|-&lt;br /&gt;
| Create token || https://tribaldex.com/tokens/create&lt;br /&gt;
|-&lt;br /&gt;
| Manage / Issue tokens || https://tribaldex.com/tokens/manage&lt;br /&gt;
|-&lt;br /&gt;
| View all tokens || https://hive-engine.com/tokens&lt;br /&gt;
|-&lt;br /&gt;
| Swap BEE || TribalDex market&lt;br /&gt;
|-&lt;br /&gt;
| Check balance in Keychain || Hive Keychain extension → Tokens tab&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Next step for v4call users&#039;&#039;&#039;: After creating your token, update your v4call-rates post using the improved rate-editor.html to include your custom token rates and blocklist bypass.&lt;br /&gt;
&lt;br /&gt;
You now have your own personal communication currency on Hive!&lt;br /&gt;
&lt;br /&gt;
[[Category:Hive]]&lt;br /&gt;
[[Category:Hive Engine]]&lt;br /&gt;
[[Category:v4call]]&lt;br /&gt;
[[Category:Token Creation]]&lt;br /&gt;
[[Category:Web3]]&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Html_JavaScript_Cookies_PopUP_Accept_Basic&amp;diff=731</id>
		<title>Html JavaScript Cookies PopUP Accept Basic</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Html_JavaScript_Cookies_PopUP_Accept_Basic&amp;diff=731"/>
		<updated>2026-04-16T15:14:19Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot;3 parts  ==Part one== * This is placed in the &amp;lt;head&amp;gt; &amp;lt;/head&amp;gt; at the top of index.html contains css &amp;lt;pre&amp;gt; &amp;lt;style&amp;gt;   #overlay {     position: fixed; top: 0; left: 0; width: 100%; height: 100%;     background: rgba(0,0,0,0.85); color: white; z-index: 10000;     display: flex; align-items: center; justify-content: center; text-align: center;   }   .popup-box { background: #222; padding: 30px; border-radius: 10px; border: 1px solid #444; }   button { padding: 10px 20px; curso...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;3 parts&lt;br /&gt;
&lt;br /&gt;
==Part one==&lt;br /&gt;
* This is placed in the &amp;lt;head&amp;gt; &amp;lt;/head&amp;gt; at the top of index.html contains css&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;style&amp;gt;&lt;br /&gt;
  #overlay {&lt;br /&gt;
    position: fixed; top: 0; left: 0; width: 100%; height: 100%;&lt;br /&gt;
    background: rgba(0,0,0,0.85); color: white; z-index: 10000;&lt;br /&gt;
    display: flex; align-items: center; justify-content: center; text-align: center;&lt;br /&gt;
  }&lt;br /&gt;
  .popup-box { background: #222; padding: 30px; border-radius: 10px; border: 1px solid #444; }&lt;br /&gt;
  button { padding: 10px 20px; cursor: pointer; background: #007bff; color: white; border: none; border-radius: 5px; }&lt;br /&gt;
  .hidden { display: none !important; }&lt;br /&gt;
&amp;lt;/style&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==part two==&lt;br /&gt;
* This is placed in/at the top of the &amp;lt;body&amp;gt; &amp;lt;/body&amp;gt; section, contains message.&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!-- pop up cookies and other 2/3--&amp;gt;&lt;br /&gt;
&amp;lt;div id=&amp;quot;overlay&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div class=&amp;quot;popup-box&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;h2&amp;gt;Welcome to Complete Noobs!&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;We use cookies to track traffic. Are you 21 or older?&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;By accepting you are entering at your own risk&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;button onclick=&amp;quot;acceptAndHide()&amp;quot;&amp;gt;Yes, I Accept &amp;amp; I&#039;m Over 21 &amp;amp; am aware you are noobs and are still learning i enter at my own risk&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;→ &amp;lt;a href=&amp;quot;https://www.google.com&amp;quot; &amp;gt;No Thank You - take me to Google&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==part 3==&lt;br /&gt;
* This is placed in/at the bottom of the &amp;lt;body&amp;gt; &amp;lt;/body&amp;gt; section, contains js script.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!-- pop up cookies and other  3/3--&amp;gt;&lt;br /&gt;
&amp;lt;script&amp;gt;&lt;br /&gt;
  function acceptAndHide() {&lt;br /&gt;
    // Save the choice in the browser&#039;s &amp;quot;localStorage&amp;quot; so it doesn&#039;t pop up again&lt;br /&gt;
    localStorage.setItem(&#039;gate_passed&#039;, &#039;true&#039;);&lt;br /&gt;
    document.getElementById(&#039;overlay&#039;).classList.add(&#039;hidden&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // On page load, check if they already clicked &amp;quot;Yes&amp;quot;&lt;br /&gt;
  window.onload = function() {&lt;br /&gt;
    if (localStorage.getItem(&#039;gate_passed&#039;) === &#039;true&#039;) {&lt;br /&gt;
      document.getElementById(&#039;overlay&#039;).classList.add(&#039;hidden&#039;);&lt;br /&gt;
    }&lt;br /&gt;
  };&lt;br /&gt;
&amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==use case==&lt;br /&gt;
* Sample use case&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;My Projects - Status Update&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;style&amp;gt;&lt;br /&gt;
        body {&lt;br /&gt;
            font-family: Arial, Helvetica, sans-serif;&lt;br /&gt;
            max-width: 800px;&lt;br /&gt;
            margin: 40px auto;&lt;br /&gt;
            padding: 20px;&lt;br /&gt;
            line-height: 1.6;&lt;br /&gt;
            color: #333;&lt;br /&gt;
        }&lt;br /&gt;
        h1 {&lt;br /&gt;
            text-align: center;&lt;br /&gt;
            color: #222;&lt;br /&gt;
        }&lt;br /&gt;
        h2 {&lt;br /&gt;
            color: #444;&lt;br /&gt;
            border-bottom: 2px solid #eee;&lt;br /&gt;
            padding-bottom: 8px;&lt;br /&gt;
        }&lt;br /&gt;
        a {&lt;br /&gt;
            color: #0066cc;&lt;br /&gt;
            text-decoration: none;&lt;br /&gt;
        }&lt;br /&gt;
        a:hover {&lt;br /&gt;
            text-decoration: underline;&lt;br /&gt;
        }&lt;br /&gt;
        .status {&lt;br /&gt;
            font-style: italic;&lt;br /&gt;
            color: #555;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/style&amp;gt;&lt;br /&gt;
&amp;lt;!-- pop up cookies and other 1/3--&amp;gt;&lt;br /&gt;
&amp;lt;style&amp;gt;&lt;br /&gt;
  #overlay {&lt;br /&gt;
    position: fixed; top: 0; left: 0; width: 100%; height: 100%;&lt;br /&gt;
    background: rgba(0,0,0,0.85); color: white; z-index: 10000;&lt;br /&gt;
    display: flex; align-items: center; justify-content: center; text-align: center;&lt;br /&gt;
  }&lt;br /&gt;
  .popup-box { background: #222; padding: 30px; border-radius: 10px; border: 1px solid #444; }&lt;br /&gt;
  button { padding: 10px 20px; cursor: pointer; background: #007bff; color: white; border: none; border-radius: 5px; }&lt;br /&gt;
  .hidden { display: none !important; }&lt;br /&gt;
&amp;lt;/style&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;!--24-06-25 adding google analytic  --&amp;gt;&lt;br /&gt;
&amp;lt;!-- Google tag (gtag.js) --&amp;gt;&lt;br /&gt;
&amp;lt;script async src=&amp;quot;https://www.googletagmanager.com/gtag/js?id=G-YXYQE65XY1&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;script&amp;gt;&lt;br /&gt;
  window.dataLayer = window.dataLayer || [];&lt;br /&gt;
  function gtag(){dataLayer.push(arguments);}&lt;br /&gt;
  gtag(&#039;js&#039;, new Date());&lt;br /&gt;
&lt;br /&gt;
  gtag(&#039;config&#039;, &#039;G-YXYQE65XY1&#039;);&lt;br /&gt;
&amp;lt;/script&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
&amp;lt;!-- pop up cookies and other 2/3--&amp;gt;&lt;br /&gt;
&amp;lt;div id=&amp;quot;overlay&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;div class=&amp;quot;popup-box&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;h2&amp;gt;Welcome to Complete Noobs!&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;We use cookies to track traffic. Are you 21 or older?&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;By accepting you are entering at your own risk&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;button onclick=&amp;quot;acceptAndHide()&amp;quot;&amp;gt;Yes, I Accept &amp;amp; I&#039;m Over 21 &amp;amp; am aware you are noobs and are still learning i enter at my own risk&amp;lt;/button&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;→ &amp;lt;a href=&amp;quot;https://www.google.com&amp;quot; &amp;gt;No Thank You - take me to Google&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h1&amp;gt;Project Status&amp;lt;/h1&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;CompleteNoobs&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;p class=&amp;quot;status&amp;quot;&amp;gt;Currently on hold due to lack of free time.&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;It will be relaunched as &amp;lt;strong&amp;gt;cnoobs.com&amp;lt;/strong&amp;gt; running inside a Docker container.&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;Future plans include a possible soft-fork of MediaWiki that allows Hive accounts holding a certain amount of CNOOBS coins to post and edit pages. &lt;br /&gt;
       I&#039;m also interested in integrating the Hive rewards system for content creators and exploring reward splits between users.&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;Additional custom features being considered:&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;ul&amp;gt;&lt;br /&gt;
        &amp;lt;li&amp;gt;Custom tags for embedding IPFS content to keep the wiki database smaller and more portable:&amp;lt;/li&amp;gt;&lt;br /&gt;
        &amp;lt;ul&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;code&amp;gt;&amp;amp;lt;ipfs_video&amp;amp;gt;ipfs_addr&amp;amp;lt;/ipfs_video&amp;amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;code&amp;gt;&amp;amp;lt;ipfs_pic&amp;amp;gt;ipfs_addr&amp;amp;lt;/ipfs_pic&amp;amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;code&amp;gt;&amp;amp;lt;ipfs_audio&amp;amp;gt;ipfs_addr&amp;amp;lt;/ipfs_audio&amp;amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
            &amp;lt;li&amp;gt;&amp;lt;code&amp;gt;&amp;amp;lt;ipfs_file&amp;amp;gt;ipfs_addr&amp;amp;lt;/ipfs_file&amp;amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/li&amp;gt;&lt;br /&gt;
        &amp;lt;/ul&amp;gt;&lt;br /&gt;
    &amp;lt;/ul&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;The old draft remains available but is unmaintained:&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;→ &amp;lt;a href=&amp;quot;https://www.completenoobs.com/noobs/Main_Page&amp;quot; target=&amp;quot;_blank&amp;quot;&amp;gt;CompleteNoobs.com (old wiki)&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;→ &amp;lt;a href=&amp;quot;https://xml.completenoobs.com&amp;quot; target=&amp;quot;_blank&amp;quot;&amp;gt;xml.CompleteNoobs.com (old xml downloads)&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;n33b.com&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;p class=&amp;quot;status&amp;quot;&amp;gt;Currently on hold.&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;It will &amp;lt;strong&amp;gt;not&amp;lt;/strong&amp;gt; become a coin project. It will return to its original purpose: a purely educational site for people who like to learn by tinkering and hands-on experimentation.&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;h2&amp;gt;v4call.com&amp;lt;/h2&amp;gt;&lt;br /&gt;
    &amp;lt;p&amp;gt;→ &amp;lt;a href=&amp;quot;https://v4call.com&amp;quot; target=&amp;quot;_blank&amp;quot;&amp;gt;v4call.com&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;&lt;br /&gt;
    &amp;lt;p class=&amp;quot;status&amp;quot;&amp;gt;idk — tinker gonna tinker 😄&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;hr&amp;gt;&lt;br /&gt;
    &amp;lt;p style=&amp;quot;text-align:center; color:#777; font-size:0.9em;&amp;quot;&amp;gt;&lt;br /&gt;
        Last updated: April 2026&lt;br /&gt;
    &amp;lt;/p&amp;gt;&lt;br /&gt;
&amp;lt;!-- pop up cookies and other  3/3--&amp;gt;&lt;br /&gt;
&amp;lt;script&amp;gt;&lt;br /&gt;
  function acceptAndHide() {&lt;br /&gt;
    // Save the choice in the browser&#039;s &amp;quot;localStorage&amp;quot; so it doesn&#039;t pop up again&lt;br /&gt;
    localStorage.setItem(&#039;gate_passed&#039;, &#039;true&#039;);&lt;br /&gt;
    document.getElementById(&#039;overlay&#039;).classList.add(&#039;hidden&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // On page load, check if they already clicked &amp;quot;Yes&amp;quot;&lt;br /&gt;
  window.onload = function() {&lt;br /&gt;
    if (localStorage.getItem(&#039;gate_passed&#039;) === &#039;true&#039;) {&lt;br /&gt;
      document.getElementById(&#039;overlay&#039;).classList.add(&#039;hidden&#039;);&lt;br /&gt;
    }&lt;br /&gt;
  };&lt;br /&gt;
&amp;lt;/script&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Ubuntu_24.04_WebRTC_lxc_Intro_Basics&amp;diff=725</id>
		<title>Ubuntu 24.04 WebRTC lxc Intro Basics</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Ubuntu_24.04_WebRTC_lxc_Intro_Basics&amp;diff=725"/>
		<updated>2026-03-28T11:17:43Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot;= WebRTC Video Chat on Ubuntu 24.04 with LXC — Complete Beginner&amp;#039;s Guide =  * This walk through is tested on a ubuntu-mate 24.04 host. ** Guide written with help from claude.ai for Ubuntu 24.04 LTS (Noble Numbat). Node.js 20 LTS. Last verified March 2026    This guide walks you through setting up a working browser-based WebRTC video/audio chat application. The signalling server will run inside an &amp;#039;&amp;#039;&amp;#039;LXC container&amp;#039;&amp;#039;&amp;#039; on your Ubuntu 24.04 host machine, and you will conne...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= WebRTC Video Chat on Ubuntu 24.04 with LXC — Complete Beginner&#039;s Guide =&lt;br /&gt;
&lt;br /&gt;
* This walk through is tested on a ubuntu-mate 24.04 host.&lt;br /&gt;
** Guide written with help from claude.ai for Ubuntu 24.04 LTS (Noble Numbat). Node.js 20 LTS. Last verified March 2026 &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
This guide walks you through setting up a working browser-based WebRTC video/audio chat application. The signalling server will run inside an &#039;&#039;&#039;LXC container&#039;&#039;&#039; on your Ubuntu 24.04 host machine, and you will connect to it from your browser.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== What You Will End Up With ==&lt;br /&gt;
&lt;br /&gt;
* An LXC container running Ubuntu 24.04&lt;br /&gt;
* A Node.js signalling server inside that container&lt;br /&gt;
* A static HTML/JS frontend served from the container&lt;br /&gt;
* Two browser tabs (or two devices on your network) that can video/audio chat peer-to-peer&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== How It Works (Plain English) ==&lt;br /&gt;
&lt;br /&gt;
WebRTC lets two browsers talk directly to each other for audio and video. But before they can do that, they need to &#039;&#039;&#039;find each other&#039;&#039;&#039; and &#039;&#039;&#039;agree on connection details&#039;&#039;&#039;. This is called &#039;&#039;&#039;signalling&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
Think of it like two people wanting to call each other — they first need to exchange phone numbers through a third party. The signalling server is that third party. Once the call is set up, the signalling server steps aside and the browsers talk directly.&lt;br /&gt;
&lt;br /&gt;
 [Browser A] &amp;lt;--WebSocket--&amp;gt; [Signalling Server in LXC] &amp;lt;--WebSocket--&amp;gt; [Browser B]&lt;br /&gt;
                                        |&lt;br /&gt;
                             (only used during setup)&lt;br /&gt;
                                        |&lt;br /&gt;
 [Browser A] &amp;lt;============ WebRTC peer-to-peer audio/video ============&amp;gt; [Browser B]&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Part 1: Set Up the LXC Container ==&lt;br /&gt;
&lt;br /&gt;
=== Step 1.1 — Install LXC on Your Host ===&lt;br /&gt;
&lt;br /&gt;
Open a terminal on your Ubuntu 24.04 host and run:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;b&amp;gt;NOTE: you can also install lxd with snap packages on ubuntu.&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
 sudo apt update&lt;br /&gt;
 sudo apt install lxc lxc-utils -y&lt;br /&gt;
&lt;br /&gt;
Check it installed correctly:&lt;br /&gt;
&lt;br /&gt;
 lxc-checkconfig&lt;br /&gt;
&lt;br /&gt;
You should see mostly &#039;&#039;&#039;enabled&#039;&#039;&#039; next to each item. A few &#039;&#039;&#039;missing&#039;&#039;&#039; items are normal and will not affect this guide.&lt;br /&gt;
&lt;br /&gt;
=== Step 1.2 — Create the Container ===&lt;br /&gt;
&lt;br /&gt;
Create a new Ubuntu 24.04 container called &amp;lt;code&amp;gt;webrtc&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
 sudo lxc-create -n webrtc -t download -- -d ubuntu -r noble -a amd64&lt;br /&gt;
&lt;br /&gt;
This downloads the Ubuntu 24.04 (Noble) image. It may take a minute or two.&lt;br /&gt;
&lt;br /&gt;
=== Step 1.3 — Start the Container ===&lt;br /&gt;
&lt;br /&gt;
 sudo lxc-start -n webrtc&lt;br /&gt;
&lt;br /&gt;
Check it is running:&lt;br /&gt;
&lt;br /&gt;
 sudo lxc-ls --fancy&lt;br /&gt;
&lt;br /&gt;
You should see &amp;lt;code&amp;gt;webrtc&amp;lt;/code&amp;gt; listed with state &#039;&#039;&#039;RUNNING&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== Step 1.4 — Log Into the Container ===&lt;br /&gt;
&lt;br /&gt;
 sudo lxc-attach -n webrtc&lt;br /&gt;
&lt;br /&gt;
Your prompt will change — you are now &#039;&#039;&#039;inside&#039;&#039;&#039; the container. Everything from here until told otherwise runs inside the container.&lt;br /&gt;
&lt;br /&gt;
=== Step 1.5 — Update the Container ===&lt;br /&gt;
&lt;br /&gt;
 apt update &amp;amp;&amp;amp; apt upgrade -y&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Part 2: Install Node.js Inside the Container ==&lt;br /&gt;
&lt;br /&gt;
=== Step 2.1 — Install Node.js ===&lt;br /&gt;
&lt;br /&gt;
We will use the official NodeSource repository to get a recent version of Node.js:&lt;br /&gt;
&lt;br /&gt;
 apt install -y curl&lt;br /&gt;
 curl -fsSL https://deb.nodesource.com/setup_20.x | bash -&lt;br /&gt;
 apt install -y nodejs&lt;br /&gt;
&lt;br /&gt;
Check the versions installed:&lt;br /&gt;
&lt;br /&gt;
 node -v&lt;br /&gt;
 npm -v&lt;br /&gt;
&lt;br /&gt;
You should see something like &amp;lt;code&amp;gt;v20.x.x&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;10.x.x&amp;lt;/code&amp;gt;. Any version of Node 18 or higher is fine.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Part 3: Create the Signalling Server ==&lt;br /&gt;
&lt;br /&gt;
=== Step 3.1 — Create a Project Folder ===&lt;br /&gt;
&lt;br /&gt;
 mkdir /opt/webrtc&lt;br /&gt;
 cd /opt/webrtc&lt;br /&gt;
&lt;br /&gt;
=== Step 3.2 — Initialise the Node Project ===&lt;br /&gt;
&lt;br /&gt;
 npm init -y&lt;br /&gt;
&lt;br /&gt;
This creates a &amp;lt;code&amp;gt;package.json&amp;lt;/code&amp;gt; file. The &amp;lt;code&amp;gt;-y&amp;lt;/code&amp;gt; flag just accepts all the defaults.&lt;br /&gt;
&lt;br /&gt;
=== Step 3.3 — Install Dependencies ===&lt;br /&gt;
&lt;br /&gt;
We need two packages:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;express&#039;&#039;&#039; — a simple web server to serve the HTML frontend&lt;br /&gt;
* &#039;&#039;&#039;socket.io&#039;&#039;&#039; — handles WebSocket connections for signalling&lt;br /&gt;
&lt;br /&gt;
 npm install express socket.io&lt;br /&gt;
&lt;br /&gt;
=== Step 3.4 — Create the Server File ===&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;b&amp;gt;NOTE: &amp;lt;code&amp;gt;nano&amp;lt;/code&amp;gt; will need to be installed in the container with &amp;lt;code&amp;gt;apt install nano -y&amp;lt;/code&amp;gt;&amp;lt;/b&amp;gt;&lt;br /&gt;
Create the file:&lt;br /&gt;
&lt;br /&gt;
 nano server.js&lt;br /&gt;
&lt;br /&gt;
Paste in the following code exactly. Use &#039;&#039;&#039;Ctrl+Shift+V&#039;&#039;&#039; to paste in the terminal:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;javascript&amp;quot;&amp;gt;&lt;br /&gt;
const express = require(&#039;express&#039;);&lt;br /&gt;
const http = require(&#039;http&#039;);&lt;br /&gt;
const { Server } = require(&#039;socket.io&#039;);&lt;br /&gt;
const path = require(&#039;path&#039;);&lt;br /&gt;
&lt;br /&gt;
const app = express();&lt;br /&gt;
const server = http.createServer(app);&lt;br /&gt;
const io = new Server(server);&lt;br /&gt;
&lt;br /&gt;
// Serve the frontend HTML file&lt;br /&gt;
app.use(express.static(path.join(__dirname, &#039;public&#039;)));&lt;br /&gt;
&lt;br /&gt;
// Keep track of who is in each room&lt;br /&gt;
const rooms = {};&lt;br /&gt;
&lt;br /&gt;
io.on(&#039;connection&#039;, (socket) =&amp;gt; {&lt;br /&gt;
  console.log(&#039;A user connected:&#039;, socket.id);&lt;br /&gt;
&lt;br /&gt;
  // User wants to join a room&lt;br /&gt;
  socket.on(&#039;join&#039;, (room) =&amp;gt; {&lt;br /&gt;
    socket.join(room);&lt;br /&gt;
&lt;br /&gt;
    if (!rooms[room]) {&lt;br /&gt;
      rooms[room] = [];&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Tell the new user who is already in the room&lt;br /&gt;
    socket.emit(&#039;room-users&#039;, rooms[room]);&lt;br /&gt;
&lt;br /&gt;
    // Add this user to the room list&lt;br /&gt;
    rooms[room].push(socket.id);&lt;br /&gt;
&lt;br /&gt;
    console.log(`${socket.id} joined room: ${room}`);&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Relay a WebRTC offer to a specific user&lt;br /&gt;
  socket.on(&#039;offer&#039;, ({ to, offer }) =&amp;gt; {&lt;br /&gt;
    io.to(to).emit(&#039;offer&#039;, { from: socket.id, offer });&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Relay a WebRTC answer to a specific user&lt;br /&gt;
  socket.on(&#039;answer&#039;, ({ to, answer }) =&amp;gt; {&lt;br /&gt;
    io.to(to).emit(&#039;answer&#039;, { from: socket.id, answer });&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Relay ICE candidates between peers&lt;br /&gt;
  socket.on(&#039;ice-candidate&#039;, ({ to, candidate }) =&amp;gt; {&lt;br /&gt;
    io.to(to).emit(&#039;ice-candidate&#039;, { from: socket.id, candidate });&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Clean up when a user disconnects&lt;br /&gt;
  socket.on(&#039;disconnect&#039;, () =&amp;gt; {&lt;br /&gt;
    for (const room in rooms) {&lt;br /&gt;
      rooms[room] = rooms[room].filter((id) =&amp;gt; id !== socket.id);&lt;br /&gt;
      // Tell others in the room this user left&lt;br /&gt;
      socket.to(room).emit(&#039;user-left&#039;, socket.id);&lt;br /&gt;
    }&lt;br /&gt;
    console.log(&#039;User disconnected:&#039;, socket.id);&lt;br /&gt;
  });&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
const PORT = 3000;&lt;br /&gt;
server.listen(PORT, &#039;0.0.0.0&#039;, () =&amp;gt; {&lt;br /&gt;
  console.log(`Signalling server running on port ${PORT}`);&lt;br /&gt;
});&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Save and exit: press &#039;&#039;&#039;Ctrl+X&#039;&#039;&#039;, then &#039;&#039;&#039;Y&#039;&#039;&#039;, then &#039;&#039;&#039;Enter&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
=== Step 3.5 — Create the Frontend ===&lt;br /&gt;
&lt;br /&gt;
Create a folder for the HTML file:&lt;br /&gt;
&lt;br /&gt;
 mkdir public&lt;br /&gt;
 nano public/index.html&lt;br /&gt;
&lt;br /&gt;
Paste in the following:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;&lt;br /&gt;
  &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;/&amp;gt;&lt;br /&gt;
  &amp;lt;title&amp;gt;WebRTC Chat&amp;lt;/title&amp;gt;&lt;br /&gt;
  &amp;lt;style&amp;gt;&lt;br /&gt;
    * { box-sizing: border-box; margin: 0; padding: 0; }&lt;br /&gt;
    body { font-family: sans-serif; background: #1a1a2e; color: #eee; display: flex; flex-direction: column; align-items: center; padding: 20px; }&lt;br /&gt;
    h1 { margin-bottom: 20px; color: #e94560; }&lt;br /&gt;
    #join-area { margin-bottom: 20px; display: flex; gap: 10px; }&lt;br /&gt;
    input { padding: 10px; border-radius: 6px; border: none; font-size: 1rem; width: 200px; }&lt;br /&gt;
    button { padding: 10px 20px; background: #e94560; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 1rem; }&lt;br /&gt;
    button:hover { background: #c73652; }&lt;br /&gt;
    #videos { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }&lt;br /&gt;
    video { width: 320px; height: 240px; background: #000; border-radius: 8px; border: 2px solid #e94560; }&lt;br /&gt;
    #status { margin-top: 15px; font-size: 0.9rem; color: #aaa; }&lt;br /&gt;
  &amp;lt;/style&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
  &amp;lt;h1&amp;gt;WebRTC Chat&amp;lt;/h1&amp;gt;&lt;br /&gt;
  &amp;lt;div id=&amp;quot;join-area&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;input id=&amp;quot;room-input&amp;quot; type=&amp;quot;text&amp;quot; placeholder=&amp;quot;Enter room name&amp;quot; value=&amp;quot;room1&amp;quot; /&amp;gt;&lt;br /&gt;
    &amp;lt;button onclick=&amp;quot;joinRoom()&amp;quot;&amp;gt;Join Room&amp;lt;/button&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
  &amp;lt;div id=&amp;quot;videos&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;video id=&amp;quot;local-video&amp;quot; autoplay muted playsinline&amp;gt;&amp;lt;/video&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
  &amp;lt;p id=&amp;quot;status&amp;quot;&amp;gt;Enter a room name and click Join.&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;!-- Socket.io client is served automatically by the server --&amp;gt;&lt;br /&gt;
  &amp;lt;script src=&amp;quot;/socket.io/socket.io.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;br /&gt;
  &amp;lt;script&amp;gt;&lt;br /&gt;
    const socket = io();&lt;br /&gt;
    let localStream;&lt;br /&gt;
    const peers = {}; // peerId -&amp;gt; RTCPeerConnection&lt;br /&gt;
&lt;br /&gt;
    // STUN server config — Google&#039;s free public STUN server&lt;br /&gt;
    const iceConfig = {&lt;br /&gt;
      iceServers: [{ urls: &#039;stun:stun.l.google.com:19302&#039; }]&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    async function joinRoom() {&lt;br /&gt;
      const room = document.getElementById(&#039;room-input&#039;).value.trim();&lt;br /&gt;
      if (!room) return alert(&#039;Please enter a room name&#039;);&lt;br /&gt;
&lt;br /&gt;
      document.getElementById(&#039;status&#039;).textContent = &#039;Getting camera and microphone...&#039;;&lt;br /&gt;
&lt;br /&gt;
      try {&lt;br /&gt;
        localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });&lt;br /&gt;
        document.getElementById(&#039;local-video&#039;).srcObject = localStream;&lt;br /&gt;
      } catch (err) {&lt;br /&gt;
        alert(&#039;Could not access camera/microphone: &#039; + err.message);&lt;br /&gt;
        return;&lt;br /&gt;
      }&lt;br /&gt;
&lt;br /&gt;
      document.getElementById(&#039;status&#039;).textContent = `Joining room: ${room}`;&lt;br /&gt;
      socket.emit(&#039;join&#039;, room);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Server tells us who is already in the room&lt;br /&gt;
    socket.on(&#039;room-users&#039;, async (users) =&amp;gt; {&lt;br /&gt;
      document.getElementById(&#039;status&#039;).textContent = `In room. ${users.length} other(s) here.`;&lt;br /&gt;
      // Send an offer to each existing user&lt;br /&gt;
      for (const userId of users) {&lt;br /&gt;
        await createOffer(userId);&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    // A remote peer sent us an offer&lt;br /&gt;
    socket.on(&#039;offer&#039;, async ({ from, offer }) =&amp;gt; {&lt;br /&gt;
      const pc = createPeerConnection(from);&lt;br /&gt;
      await pc.setRemoteDescription(new RTCSessionDescription(offer));&lt;br /&gt;
      const answer = await pc.createAnswer();&lt;br /&gt;
      await pc.setLocalDescription(answer);&lt;br /&gt;
      socket.emit(&#039;answer&#039;, { to: from, answer });&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    // A remote peer accepted our offer&lt;br /&gt;
    socket.on(&#039;answer&#039;, async ({ from, answer }) =&amp;gt; {&lt;br /&gt;
      const pc = peers[from];&lt;br /&gt;
      if (pc) await pc.setRemoteDescription(new RTCSessionDescription(answer));&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    // ICE candidate from a remote peer&lt;br /&gt;
    socket.on(&#039;ice-candidate&#039;, async ({ from, candidate }) =&amp;gt; {&lt;br /&gt;
      const pc = peers[from];&lt;br /&gt;
      if (pc &amp;amp;&amp;amp; candidate) {&lt;br /&gt;
        try { await pc.addIceCandidate(new RTCIceCandidate(candidate)); } catch (e) {}&lt;br /&gt;
      }&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    // A user left the room&lt;br /&gt;
    socket.on(&#039;user-left&#039;, (userId) =&amp;gt; {&lt;br /&gt;
      if (peers[userId]) {&lt;br /&gt;
        peers[userId].close();&lt;br /&gt;
        delete peers[userId];&lt;br /&gt;
      }&lt;br /&gt;
      const el = document.getElementById(&#039;video-&#039; + userId);&lt;br /&gt;
      if (el) el.remove();&lt;br /&gt;
      document.getElementById(&#039;status&#039;).textContent = &#039;A user left the room.&#039;;&lt;br /&gt;
    });&lt;br /&gt;
&lt;br /&gt;
    function createPeerConnection(peerId) {&lt;br /&gt;
      const pc = new RTCPeerConnection(iceConfig);&lt;br /&gt;
      peers[peerId] = pc;&lt;br /&gt;
&lt;br /&gt;
      // Add our local tracks so the remote peer gets our video/audio&lt;br /&gt;
      localStream.getTracks().forEach((track) =&amp;gt; pc.addTrack(track, localStream));&lt;br /&gt;
&lt;br /&gt;
      // When we get an ICE candidate, send it to the remote peer&lt;br /&gt;
      pc.onicecandidate = ({ candidate }) =&amp;gt; {&lt;br /&gt;
        if (candidate) socket.emit(&#039;ice-candidate&#039;, { to: peerId, candidate });&lt;br /&gt;
      };&lt;br /&gt;
&lt;br /&gt;
      // When we receive a remote track, display it&lt;br /&gt;
      pc.ontrack = ({ streams }) =&amp;gt; {&lt;br /&gt;
        let videoEl = document.getElementById(&#039;video-&#039; + peerId);&lt;br /&gt;
        if (!videoEl) {&lt;br /&gt;
          videoEl = document.createElement(&#039;video&#039;);&lt;br /&gt;
          videoEl.id = &#039;video-&#039; + peerId;&lt;br /&gt;
          videoEl.autoplay = true;&lt;br /&gt;
          videoEl.playsInline = true;&lt;br /&gt;
          document.getElementById(&#039;videos&#039;).appendChild(videoEl);&lt;br /&gt;
        }&lt;br /&gt;
        videoEl.srcObject = streams[0];&lt;br /&gt;
      };&lt;br /&gt;
&lt;br /&gt;
      return pc;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    async function createOffer(peerId) {&lt;br /&gt;
      const pc = createPeerConnection(peerId);&lt;br /&gt;
      const offer = await pc.createOffer();&lt;br /&gt;
      await pc.setLocalDescription(offer);&lt;br /&gt;
      socket.emit(&#039;offer&#039;, { to: peerId, offer });&lt;br /&gt;
    }&lt;br /&gt;
  &amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Save and exit: &#039;&#039;&#039;Ctrl+X&#039;&#039;&#039;, &#039;&#039;&#039;Y&#039;&#039;&#039;, &#039;&#039;&#039;Enter&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Part 4: Run the Server ==&lt;br /&gt;
&lt;br /&gt;
=== Step 4.1 — Test it Manually First ===&lt;br /&gt;
&lt;br /&gt;
Inside the container, run:&lt;br /&gt;
&lt;br /&gt;
 node /opt/webrtc/server.js&lt;br /&gt;
&lt;br /&gt;
You should see:&lt;br /&gt;
&lt;br /&gt;
 Signalling server running on port 3000&lt;br /&gt;
&lt;br /&gt;
Press &#039;&#039;&#039;Ctrl+C&#039;&#039;&#039; to stop it for now. We will set it up to run automatically in the next step.&lt;br /&gt;
&lt;br /&gt;
=== Step 4.2 — Find the Container&#039;s IP Address ===&lt;br /&gt;
&lt;br /&gt;
You need this to access the server from your host browser. Open a &#039;&#039;&#039;second terminal on your host&#039;&#039;&#039; (not inside the container) and run:&lt;br /&gt;
&lt;br /&gt;
 sudo lxc-ls --fancy&lt;br /&gt;
&lt;br /&gt;
Look for your &amp;lt;code&amp;gt;webrtc&amp;lt;/code&amp;gt; container and note the IP address in the &#039;&#039;&#039;IPV4&#039;&#039;&#039; column. It will look something like &amp;lt;code&amp;gt;10.0.3.15&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
=== Step 4.3 — Set Up Auto-start with systemd ===&lt;br /&gt;
&lt;br /&gt;
We want the server to start automatically when the container boots. Back inside the container, create a systemd service file:&lt;br /&gt;
&lt;br /&gt;
 nano /etc/systemd/system/webrtc.service&lt;br /&gt;
&lt;br /&gt;
Paste in:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ini&amp;quot;&amp;gt;&lt;br /&gt;
[Unit]&lt;br /&gt;
Description=WebRTC Signalling Server&lt;br /&gt;
After=network.target&lt;br /&gt;
&lt;br /&gt;
[Service]&lt;br /&gt;
Type=simple&lt;br /&gt;
WorkingDirectory=/opt/webrtc&lt;br /&gt;
ExecStart=/usr/bin/node /opt/webrtc/server.js&lt;br /&gt;
Restart=on-failure&lt;br /&gt;
RestartSec=5&lt;br /&gt;
&lt;br /&gt;
[Install]&lt;br /&gt;
WantedBy=multi-user.target&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Save and exit. Then enable and start it:&lt;br /&gt;
&lt;br /&gt;
 systemctl daemon-reload&lt;br /&gt;
 systemctl enable webrtc&lt;br /&gt;
 systemctl start webrtc&lt;br /&gt;
&lt;br /&gt;
Check it is running:&lt;br /&gt;
&lt;br /&gt;
 systemctl status webrtc&lt;br /&gt;
&lt;br /&gt;
You should see &#039;&#039;&#039;active (running)&#039;&#039;&#039; in green.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Part 5: Configure the LXC Container to Auto-start ==&lt;br /&gt;
&lt;br /&gt;
We also want the container itself to start when your host boots.&lt;br /&gt;
&lt;br /&gt;
Exit the container first (type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt; or press &#039;&#039;&#039;Ctrl+D&#039;&#039;&#039; to return to your host terminal), then run:&lt;br /&gt;
&lt;br /&gt;
 sudo nano /var/lib/lxc/webrtc/config&lt;br /&gt;
&lt;br /&gt;
Add these two lines at the bottom of the file:&lt;br /&gt;
&lt;br /&gt;
 lxc.start.auto = 1&lt;br /&gt;
 lxc.start.delay = 5&lt;br /&gt;
&lt;br /&gt;
Save and exit. Now the container (and the signalling server inside it) will start automatically on boot.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Part 6: Open the App in Your Browser ==&lt;br /&gt;
&lt;br /&gt;
=== Step 6.1 — Open the App ===&lt;br /&gt;
&lt;br /&gt;
On your host machine, open a browser and go to:&lt;br /&gt;
&lt;br /&gt;
 http://&amp;lt;container-ip&amp;gt;:3000&lt;br /&gt;
&lt;br /&gt;
Replace &amp;lt;code&amp;gt;&amp;lt;container-ip&amp;gt;&amp;lt;/code&amp;gt; with the IP address you noted in Step 4.2. For example:&lt;br /&gt;
&lt;br /&gt;
 http://10.0.3.15:3000&lt;br /&gt;
&lt;br /&gt;
You should see the WebRTC Chat page.&lt;br /&gt;
&lt;br /&gt;
=== Step 6.2 — Test With Two Browser Tabs ===&lt;br /&gt;
&lt;br /&gt;
# Open the app in &#039;&#039;&#039;Tab 1&#039;&#039;&#039;. Type a room name (e.g. &amp;lt;code&amp;gt;room1&amp;lt;/code&amp;gt;) and click &#039;&#039;&#039;Join Room&#039;&#039;&#039;. Allow camera and microphone access when the browser asks.&lt;br /&gt;
# Open the app in &#039;&#039;&#039;Tab 2&#039;&#039;&#039; (you can use a private/incognito window to get a separate camera stream). Type the &#039;&#039;&#039;same room name&#039;&#039;&#039; and click &#039;&#039;&#039;Join Room&#039;&#039;&#039;.&lt;br /&gt;
&lt;br /&gt;
The two tabs should now connect and you will see two video feeds.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
NOTE:If you see &amp;lt;code&amp;gt;Could not access camera/microphone: Cannot read properties of undefined (reading &#039;getUserMedia&#039;)&amp;lt;/code&amp;gt; &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;b&amp;gt;NOTE: if you see &amp;lt;code&amp;gt;Could not access camera/microphone: Cannot read properties of undefined (reading &#039;getUserMedia&#039;)&amp;lt;/code&amp;gt; Then you need to go to step 7 and setup HTTPS&amp;lt;/b&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This is the HTTPS issue mentioned in Part 7. Browsers block camera/microphone access on plain http:// from non-localhost addresses — navigator.mediaDevices is simply undefined on insecure origins.&lt;br /&gt;
&lt;br /&gt;
Since you&#039;re accessing via the container&#039;s IP (e.g. http://10.0.3.15:3000), the browser sees it as insecure and disables the API entirely.&lt;br /&gt;
&lt;br /&gt;
Quickest fix — use mkcert on your host:While you choose — here&#039;s a quick workaround you can do right now with zero config, if you just want to test immediately:&lt;br /&gt;
&lt;br /&gt;
Option: Chrome flag to allow insecure origins&lt;br /&gt;
&lt;br /&gt;
Open Chrome and go to: chrome://flags/#unsafely-treat-insecure-origin-as-secure&lt;br /&gt;
&lt;br /&gt;
Paste your container URL (e.g. http://10.0.3.15:3000) into the text box and enable it&lt;br /&gt;
&lt;br /&gt;
Relaunch Chrome when prompted&lt;br /&gt;
&lt;br /&gt;
This tells Chrome to treat that specific IP as secure, enabling getUserMedia. Only use this for local dev/testing — never on a production machine.&lt;br /&gt;
Firefox equivalent: go to about:config, search for media.devices.insecure.enabled and set it to true.&lt;br /&gt;
Once you pick your preferred HTTPS method above I&#039;ll give you the exact step-by-step commands to do it properly inside your LXC container.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Step 6.3 — Test From Another Device on Your Network ===&lt;br /&gt;
&lt;br /&gt;
On any other device connected to the same Wi-Fi or LAN, open a browser and go to the same URL:&lt;br /&gt;
&lt;br /&gt;
 http://&amp;lt;container-ip&amp;gt;:3000&lt;br /&gt;
&lt;br /&gt;
Join the same room name and you should connect peer-to-peer with the other browser.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note:&#039;&#039;&#039; Camera and microphone access in browsers requires either &amp;lt;code&amp;gt;localhost&amp;lt;/code&amp;gt; or a secure HTTPS connection. For testing on the same machine, &amp;lt;code&amp;gt;http://localhost&amp;lt;/code&amp;gt; works fine. For other devices on your network, you may need to set up HTTPS (see Part 7).&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Part 7: (Optional) Enable HTTPS for Other Devices ==&lt;br /&gt;
&lt;br /&gt;
Browsers block camera/microphone access on plain HTTP from non-localhost addresses. To use this from other devices on your network, you have two options:&lt;br /&gt;
&lt;br /&gt;
=== Option A — Use a Self-Signed Certificate (Quick) ===&lt;br /&gt;
&lt;br /&gt;
Inside the container:&lt;br /&gt;
&lt;br /&gt;
 apt install -y openssl&lt;br /&gt;
 cd /opt/webrtc&lt;br /&gt;
 openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj &amp;quot;/CN=localhost&amp;quot;&lt;br /&gt;
&lt;br /&gt;
Edit &amp;lt;code&amp;gt;server.js&amp;lt;/code&amp;gt; and replace the &amp;lt;code&amp;gt;http&amp;lt;/code&amp;gt; section:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;javascript&amp;quot;&amp;gt;&lt;br /&gt;
const https = require(&#039;https&#039;);&lt;br /&gt;
const fs = require(&#039;fs&#039;);&lt;br /&gt;
&lt;br /&gt;
const server = https.createServer({&lt;br /&gt;
  key: fs.readFileSync(&#039;/opt/webrtc/key.pem&#039;),&lt;br /&gt;
  cert: fs.readFileSync(&#039;/opt/webrtc/cert.pem&#039;),&lt;br /&gt;
}, app);&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Appended &amp;lt;code&amp;gt;server.js&amp;lt;/code&amp;gt; file:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
const express = require(&#039;express&#039;);&lt;br /&gt;
const https = require(&#039;https&#039;);&lt;br /&gt;
const { Server } = require(&#039;socket.io&#039;);&lt;br /&gt;
const path = require(&#039;path&#039;);&lt;br /&gt;
&lt;br /&gt;
const app = express();&lt;br /&gt;
const fs = require(&#039;fs&#039;);&lt;br /&gt;
&lt;br /&gt;
const server = https.createServer({&lt;br /&gt;
  key: fs.readFileSync(&#039;/opt/webrtc/key.pem&#039;),&lt;br /&gt;
  cert: fs.readFileSync(&#039;/opt/webrtc/cert.pem&#039;),&lt;br /&gt;
}, app);&lt;br /&gt;
const io = new Server(server);&lt;br /&gt;
&lt;br /&gt;
// Serve the frontend HTML file&lt;br /&gt;
app.use(express.static(path.join(__dirname, &#039;public&#039;)));&lt;br /&gt;
&lt;br /&gt;
// Keep track of who is in each room&lt;br /&gt;
const rooms = {};&lt;br /&gt;
&lt;br /&gt;
io.on(&#039;connection&#039;, (socket) =&amp;gt; {&lt;br /&gt;
  console.log(&#039;A user connected:&#039;, socket.id);&lt;br /&gt;
&lt;br /&gt;
  // User wants to join a room&lt;br /&gt;
  socket.on(&#039;join&#039;, (room) =&amp;gt; {&lt;br /&gt;
    socket.join(room);&lt;br /&gt;
&lt;br /&gt;
    if (!rooms[room]) {&lt;br /&gt;
      rooms[room] = [];&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Tell the new user who is already in the room&lt;br /&gt;
    socket.emit(&#039;room-users&#039;, rooms[room]);&lt;br /&gt;
&lt;br /&gt;
    // Add this user to the room list&lt;br /&gt;
    rooms[room].push(socket.id);&lt;br /&gt;
&lt;br /&gt;
    console.log(`${socket.id} joined room: ${room}`);&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Relay a WebRTC offer to a specific user&lt;br /&gt;
  socket.on(&#039;offer&#039;, ({ to, offer }) =&amp;gt; {&lt;br /&gt;
    io.to(to).emit(&#039;offer&#039;, { from: socket.id, offer });&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Relay a WebRTC answer to a specific user&lt;br /&gt;
  socket.on(&#039;answer&#039;, ({ to, answer }) =&amp;gt; {&lt;br /&gt;
    io.to(to).emit(&#039;answer&#039;, { from: socket.id, answer });&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Relay ICE candidates between peers&lt;br /&gt;
  socket.on(&#039;ice-candidate&#039;, ({ to, candidate }) =&amp;gt; {&lt;br /&gt;
    io.to(to).emit(&#039;ice-candidate&#039;, { from: socket.id, candidate });&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Clean up when a user disconnects&lt;br /&gt;
  socket.on(&#039;disconnect&#039;, () =&amp;gt; {&lt;br /&gt;
    for (const room in rooms) {&lt;br /&gt;
      rooms[room] = rooms[room].filter((id) =&amp;gt; id !== socket.id);&lt;br /&gt;
      // Tell others in the room this user left&lt;br /&gt;
      socket.to(room).emit(&#039;user-left&#039;, socket.id);&lt;br /&gt;
    }&lt;br /&gt;
    console.log(&#039;User disconnected:&#039;, socket.id);&lt;br /&gt;
  });&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
const PORT = 3000;&lt;br /&gt;
server.listen(PORT, &#039;0.0.0.0&#039;, () =&amp;gt; {&lt;br /&gt;
  console.log(`Signalling server running on port ${PORT}`);&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Change &amp;lt;code&amp;gt;const http = require(&#039;http&#039;);&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;const server = http.createServer(app);&amp;lt;/code&amp;gt; lines accordingly.&lt;br /&gt;
&lt;br /&gt;
Restart the service:&lt;br /&gt;
&lt;br /&gt;
 systemctl restart webrtc&lt;br /&gt;
&lt;br /&gt;
Access via:&lt;br /&gt;
&lt;br /&gt;
 https://&amp;lt;container-ip&amp;gt;:3000&lt;br /&gt;
&lt;br /&gt;
Your browser will warn about the self-signed certificate — click &#039;&#039;&#039;Advanced&#039;&#039;&#039; and &#039;&#039;&#039;Proceed&#039;&#039;&#039; to continue.&lt;br /&gt;
&lt;br /&gt;
=== Option B — Use mkcert (Trusted Certificate, Recommended) ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;mkcert&amp;lt;/code&amp;gt; creates locally trusted certificates without browser warnings.&lt;br /&gt;
&lt;br /&gt;
On your &#039;&#039;&#039;host machine&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
 sudo apt install mkcert&lt;br /&gt;
 mkcert -install&lt;br /&gt;
 mkcert &amp;lt;container-ip&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This creates &amp;lt;code&amp;gt;&amp;lt;container-ip&amp;gt;.pem&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;&amp;lt;container-ip&amp;gt;-key.pem&amp;lt;/code&amp;gt;. Copy them into the container and follow the same server.js edits as Option A.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Part 8: Useful Commands Reference ==&lt;br /&gt;
&lt;br /&gt;
=== Managing the Container ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Command !! What it does&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;sudo lxc-start -n webrtc&amp;lt;/code&amp;gt; || Start the container&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;sudo lxc-stop -n webrtc&amp;lt;/code&amp;gt; || Stop the container&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;sudo lxc-attach -n webrtc&amp;lt;/code&amp;gt; || Open a shell inside the container&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;sudo lxc-ls --fancy&amp;lt;/code&amp;gt; || List containers and their status/IP&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Managing the Server (run inside the container) ===&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Command !! What it does&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;systemctl status webrtc&amp;lt;/code&amp;gt; || Check if the server is running&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;systemctl restart webrtc&amp;lt;/code&amp;gt; || Restart the server&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;systemctl stop webrtc&amp;lt;/code&amp;gt; || Stop the server&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;journalctl -u webrtc -f&amp;lt;/code&amp;gt; || Watch live server logs&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Adding a ChatBox ==&lt;br /&gt;
&lt;br /&gt;
* Added: &lt;br /&gt;
** A name input on the join screen so people aren&#039;t just &amp;quot;Anonymous&amp;quot;&lt;br /&gt;
** A chat panel on the right with message bubbles (your messages on the right, others on the left)&lt;br /&gt;
** Automatic link detection — any URL typed becomes a clickable link&lt;br /&gt;
** Enter to send, Shift+Enter for a newline&lt;br /&gt;
** A 500 character counter that turns red when you&#039;re close to the limit&lt;br /&gt;
** System messages when users join/leave&lt;br /&gt;
** A live peer count header showing how many others are in the room&lt;br /&gt;
** The textarea auto-resizes as you type&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Edit &amp;lt;code&amp;gt;public/index.html&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Appended &amp;lt;code&amp;gt;public/index.html&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;en&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
  &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot; /&amp;gt;&lt;br /&gt;
  &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;/&amp;gt;&lt;br /&gt;
  &amp;lt;title&amp;gt;WebRTC Chat&amp;lt;/title&amp;gt;&lt;br /&gt;
  &amp;lt;link rel=&amp;quot;preconnect&amp;quot; href=&amp;quot;https://fonts.googleapis.com&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;link href=&amp;quot;https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&amp;amp;family=IBM+Plex+Sans:wght@400;500;600&amp;amp;display=swap&amp;quot; rel=&amp;quot;stylesheet&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;style&amp;gt;&lt;br /&gt;
    :root {&lt;br /&gt;
      --bg:       #0d0f14;&lt;br /&gt;
      --surface:  #161920;&lt;br /&gt;
      --border:   #252932;&lt;br /&gt;
      --accent:   #4ade80;&lt;br /&gt;
      --accent2:  #22d3ee;&lt;br /&gt;
      --muted:    #4b5263;&lt;br /&gt;
      --text:     #e2e8f0;&lt;br /&gt;
      --subtext:  #8892a4;&lt;br /&gt;
      --self-msg: #1a2e22;&lt;br /&gt;
      --other-msg:#161d2e;&lt;br /&gt;
      --danger:   #f87171;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    * { box-sizing: border-box; margin: 0; padding: 0; }&lt;br /&gt;
&lt;br /&gt;
    body {&lt;br /&gt;
      font-family: &#039;IBM Plex Sans&#039;, sans-serif;&lt;br /&gt;
      background: var(--bg);&lt;br /&gt;
      color: var(--text);&lt;br /&gt;
      height: 100dvh;&lt;br /&gt;
      display: flex;&lt;br /&gt;
      flex-direction: column;&lt;br /&gt;
      overflow: hidden;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /* ── Top bar ── */&lt;br /&gt;
    header {&lt;br /&gt;
      display: flex;&lt;br /&gt;
      align-items: center;&lt;br /&gt;
      gap: 12px;&lt;br /&gt;
      padding: 12px 20px;&lt;br /&gt;
      border-bottom: 1px solid var(--border);&lt;br /&gt;
      background: var(--surface);&lt;br /&gt;
      flex-shrink: 0;&lt;br /&gt;
    }&lt;br /&gt;
    header h1 {&lt;br /&gt;
      font-family: &#039;IBM Plex Mono&#039;, monospace;&lt;br /&gt;
      font-size: 1rem;&lt;br /&gt;
      color: var(--accent);&lt;br /&gt;
      letter-spacing: 0.05em;&lt;br /&gt;
    }&lt;br /&gt;
    #room-badge {&lt;br /&gt;
      font-family: &#039;IBM Plex Mono&#039;, monospace;&lt;br /&gt;
      font-size: 0.75rem;&lt;br /&gt;
      color: var(--subtext);&lt;br /&gt;
      background: var(--border);&lt;br /&gt;
      padding: 2px 10px;&lt;br /&gt;
      border-radius: 999px;&lt;br /&gt;
      display: none;&lt;br /&gt;
    }&lt;br /&gt;
    #status-dot {&lt;br /&gt;
      width: 8px; height: 8px;&lt;br /&gt;
      border-radius: 50%;&lt;br /&gt;
      background: var(--muted);&lt;br /&gt;
      margin-left: auto;&lt;br /&gt;
      flex-shrink: 0;&lt;br /&gt;
      transition: background 0.3s;&lt;br /&gt;
    }&lt;br /&gt;
    #status-dot.live { background: var(--accent); box-shadow: 0 0 6px var(--accent); }&lt;br /&gt;
&lt;br /&gt;
    /* ── Main layout ── */&lt;br /&gt;
    main {&lt;br /&gt;
      display: flex;&lt;br /&gt;
      flex: 1;&lt;br /&gt;
      overflow: hidden;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /* ── Join screen ── */&lt;br /&gt;
    #join-screen {&lt;br /&gt;
      display: flex;&lt;br /&gt;
      flex-direction: column;&lt;br /&gt;
      align-items: center;&lt;br /&gt;
      justify-content: center;&lt;br /&gt;
      gap: 16px;&lt;br /&gt;
      flex: 1;&lt;br /&gt;
      padding: 40px 20px;&lt;br /&gt;
    }&lt;br /&gt;
    #join-screen h2 {&lt;br /&gt;
      font-family: &#039;IBM Plex Mono&#039;, monospace;&lt;br /&gt;
      font-size: 1.4rem;&lt;br /&gt;
      color: var(--accent);&lt;br /&gt;
    }&lt;br /&gt;
    #join-screen p { color: var(--subtext); font-size: 0.9rem; text-align: center; max-width: 340px; }&lt;br /&gt;
    .field-group {&lt;br /&gt;
      display: flex;&lt;br /&gt;
      flex-direction: column;&lt;br /&gt;
      gap: 8px;&lt;br /&gt;
      width: 100%;&lt;br /&gt;
      max-width: 320px;&lt;br /&gt;
    }&lt;br /&gt;
    .field-group label { font-size: 0.8rem; color: var(--subtext); font-family: &#039;IBM Plex Mono&#039;, monospace; }&lt;br /&gt;
    input[type=&amp;quot;text&amp;quot;] {&lt;br /&gt;
      padding: 10px 14px;&lt;br /&gt;
      background: var(--surface);&lt;br /&gt;
      border: 1px solid var(--border);&lt;br /&gt;
      border-radius: 6px;&lt;br /&gt;
      color: var(--text);&lt;br /&gt;
      font-family: &#039;IBM Plex Sans&#039;, sans-serif;&lt;br /&gt;
      font-size: 0.95rem;&lt;br /&gt;
      width: 100%;&lt;br /&gt;
      transition: border-color 0.2s;&lt;br /&gt;
      outline: none;&lt;br /&gt;
    }&lt;br /&gt;
    input[type=&amp;quot;text&amp;quot;]:focus { border-color: var(--accent); }&lt;br /&gt;
    button.primary {&lt;br /&gt;
      padding: 11px 24px;&lt;br /&gt;
      background: var(--accent);&lt;br /&gt;
      color: #0d0f14;&lt;br /&gt;
      border: none;&lt;br /&gt;
      border-radius: 6px;&lt;br /&gt;
      cursor: pointer;&lt;br /&gt;
      font-family: &#039;IBM Plex Mono&#039;, monospace;&lt;br /&gt;
      font-weight: 600;&lt;br /&gt;
      font-size: 0.9rem;&lt;br /&gt;
      width: 100%;&lt;br /&gt;
      max-width: 320px;&lt;br /&gt;
      transition: opacity 0.2s, transform 0.1s;&lt;br /&gt;
    }&lt;br /&gt;
    button.primary:hover { opacity: 0.85; }&lt;br /&gt;
    button.primary:active { transform: scale(0.98); }&lt;br /&gt;
&lt;br /&gt;
    /* ── App layout (after join) ── */&lt;br /&gt;
    #app { display: none; flex: 1; overflow: hidden; }&lt;br /&gt;
    #app.visible { display: flex; }&lt;br /&gt;
&lt;br /&gt;
    /* ── Video panel ── */&lt;br /&gt;
    #video-panel {&lt;br /&gt;
      display: flex;&lt;br /&gt;
      flex-direction: column;&lt;br /&gt;
      gap: 0;&lt;br /&gt;
      background: #0a0c10;&lt;br /&gt;
      border-right: 1px solid var(--border);&lt;br /&gt;
      overflow-y: auto;&lt;br /&gt;
      flex-shrink: 0;&lt;br /&gt;
      width: 340px;&lt;br /&gt;
    }&lt;br /&gt;
    @media (max-width: 700px) {&lt;br /&gt;
      #app.visible { flex-direction: column; }&lt;br /&gt;
      #video-panel { width: 100%; max-height: 220px; flex-direction: row; overflow-x: auto; overflow-y: hidden; border-right: none; border-bottom: 1px solid var(--border); }&lt;br /&gt;
    }&lt;br /&gt;
    .video-wrapper {&lt;br /&gt;
      position: relative;&lt;br /&gt;
      background: #000;&lt;br /&gt;
    }&lt;br /&gt;
    .video-wrapper video {&lt;br /&gt;
      width: 100%;&lt;br /&gt;
      display: block;&lt;br /&gt;
      aspect-ratio: 4/3;&lt;br /&gt;
      object-fit: cover;&lt;br /&gt;
    }&lt;br /&gt;
    .video-label {&lt;br /&gt;
      position: absolute;&lt;br /&gt;
      bottom: 6px; left: 8px;&lt;br /&gt;
      font-family: &#039;IBM Plex Mono&#039;, monospace;&lt;br /&gt;
      font-size: 0.7rem;&lt;br /&gt;
      color: #fff;&lt;br /&gt;
      background: rgba(0,0,0,0.55);&lt;br /&gt;
      padding: 2px 7px;&lt;br /&gt;
      border-radius: 4px;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /* ── Chat panel ── */&lt;br /&gt;
    #chat-panel {&lt;br /&gt;
      display: flex;&lt;br /&gt;
      flex-direction: column;&lt;br /&gt;
      flex: 1;&lt;br /&gt;
      overflow: hidden;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    #chat-header {&lt;br /&gt;
      padding: 10px 16px;&lt;br /&gt;
      border-bottom: 1px solid var(--border);&lt;br /&gt;
      font-family: &#039;IBM Plex Mono&#039;, monospace;&lt;br /&gt;
      font-size: 0.78rem;&lt;br /&gt;
      color: var(--subtext);&lt;br /&gt;
      background: var(--surface);&lt;br /&gt;
      flex-shrink: 0;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    #messages {&lt;br /&gt;
      flex: 1;&lt;br /&gt;
      overflow-y: auto;&lt;br /&gt;
      padding: 16px;&lt;br /&gt;
      display: flex;&lt;br /&gt;
      flex-direction: column;&lt;br /&gt;
      gap: 10px;&lt;br /&gt;
      scroll-behavior: smooth;&lt;br /&gt;
    }&lt;br /&gt;
    #messages::-webkit-scrollbar { width: 4px; }&lt;br /&gt;
    #messages::-webkit-scrollbar-track { background: transparent; }&lt;br /&gt;
    #messages::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }&lt;br /&gt;
&lt;br /&gt;
    /* System messages */&lt;br /&gt;
    .msg-system {&lt;br /&gt;
      text-align: center;&lt;br /&gt;
      font-size: 0.75rem;&lt;br /&gt;
      color: var(--muted);&lt;br /&gt;
      font-family: &#039;IBM Plex Mono&#039;, monospace;&lt;br /&gt;
      padding: 2px 0;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /* Chat bubbles */&lt;br /&gt;
    .msg-bubble {&lt;br /&gt;
      display: flex;&lt;br /&gt;
      flex-direction: column;&lt;br /&gt;
      gap: 3px;&lt;br /&gt;
      max-width: 85%;&lt;br /&gt;
    }&lt;br /&gt;
    .msg-bubble.self { align-self: flex-end; align-items: flex-end; }&lt;br /&gt;
    .msg-bubble.other { align-self: flex-start; align-items: flex-start; }&lt;br /&gt;
&lt;br /&gt;
    .msg-meta {&lt;br /&gt;
      font-size: 0.7rem;&lt;br /&gt;
      color: var(--subtext);&lt;br /&gt;
      font-family: &#039;IBM Plex Mono&#039;, monospace;&lt;br /&gt;
      padding: 0 4px;&lt;br /&gt;
    }&lt;br /&gt;
    .msg-bubble.self .msg-meta { color: var(--accent); }&lt;br /&gt;
&lt;br /&gt;
    .msg-text {&lt;br /&gt;
      padding: 9px 13px;&lt;br /&gt;
      border-radius: 12px;&lt;br /&gt;
      font-size: 0.9rem;&lt;br /&gt;
      line-height: 1.5;&lt;br /&gt;
      word-break: break-word;&lt;br /&gt;
    }&lt;br /&gt;
    .msg-bubble.self .msg-text {&lt;br /&gt;
      background: var(--self-msg);&lt;br /&gt;
      border: 1px solid rgba(74,222,128,0.2);&lt;br /&gt;
      border-bottom-right-radius: 3px;&lt;br /&gt;
    }&lt;br /&gt;
    .msg-bubble.other .msg-text {&lt;br /&gt;
      background: var(--other-msg);&lt;br /&gt;
      border: 1px solid var(--border);&lt;br /&gt;
      border-bottom-left-radius: 3px;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    /* Links inside messages */&lt;br /&gt;
    .msg-text a {&lt;br /&gt;
      color: var(--accent2);&lt;br /&gt;
      text-decoration: underline;&lt;br /&gt;
      text-underline-offset: 2px;&lt;br /&gt;
      word-break: break-all;&lt;br /&gt;
    }&lt;br /&gt;
    .msg-text a:hover { opacity: 0.8; }&lt;br /&gt;
&lt;br /&gt;
    /* ── Input bar ── */&lt;br /&gt;
    #input-bar {&lt;br /&gt;
      display: flex;&lt;br /&gt;
      gap: 8px;&lt;br /&gt;
      padding: 12px 16px;&lt;br /&gt;
      border-top: 1px solid var(--border);&lt;br /&gt;
      background: var(--surface);&lt;br /&gt;
      flex-shrink: 0;&lt;br /&gt;
    }&lt;br /&gt;
    #msg-input {&lt;br /&gt;
      flex: 1;&lt;br /&gt;
      padding: 10px 14px;&lt;br /&gt;
      background: var(--bg);&lt;br /&gt;
      border: 1px solid var(--border);&lt;br /&gt;
      border-radius: 8px;&lt;br /&gt;
      color: var(--text);&lt;br /&gt;
      font-family: &#039;IBM Plex Sans&#039;, sans-serif;&lt;br /&gt;
      font-size: 0.9rem;&lt;br /&gt;
      outline: none;&lt;br /&gt;
      resize: none;&lt;br /&gt;
      height: 42px;&lt;br /&gt;
      max-height: 120px;&lt;br /&gt;
      overflow-y: auto;&lt;br /&gt;
      transition: border-color 0.2s;&lt;br /&gt;
    }&lt;br /&gt;
    #msg-input:focus { border-color: var(--accent); }&lt;br /&gt;
    #send-btn {&lt;br /&gt;
      padding: 0 16px;&lt;br /&gt;
      background: var(--accent);&lt;br /&gt;
      color: #0d0f14;&lt;br /&gt;
      border: none;&lt;br /&gt;
      border-radius: 8px;&lt;br /&gt;
      cursor: pointer;&lt;br /&gt;
      font-family: &#039;IBM Plex Mono&#039;, monospace;&lt;br /&gt;
      font-weight: 600;&lt;br /&gt;
      font-size: 0.85rem;&lt;br /&gt;
      flex-shrink: 0;&lt;br /&gt;
      transition: opacity 0.2s;&lt;br /&gt;
    }&lt;br /&gt;
    #send-btn:hover { opacity: 0.85; }&lt;br /&gt;
    #char-count {&lt;br /&gt;
      font-size: 0.7rem;&lt;br /&gt;
      color: var(--muted);&lt;br /&gt;
      font-family: &#039;IBM Plex Mono&#039;, monospace;&lt;br /&gt;
      align-self: flex-end;&lt;br /&gt;
      padding-bottom: 4px;&lt;br /&gt;
      flex-shrink: 0;&lt;br /&gt;
      width: 36px;&lt;br /&gt;
      text-align: right;&lt;br /&gt;
    }&lt;br /&gt;
    #char-count.warn { color: var(--danger); }&lt;br /&gt;
  &amp;lt;/style&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;header&amp;gt;&lt;br /&gt;
  &amp;lt;h1&amp;gt;// webrtc&amp;lt;/h1&amp;gt;&lt;br /&gt;
  &amp;lt;span id=&amp;quot;room-badge&amp;quot;&amp;gt;&amp;lt;/span&amp;gt;&lt;br /&gt;
  &amp;lt;div id=&amp;quot;status-dot&amp;quot; title=&amp;quot;Connecting...&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/header&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- Join screen --&amp;gt;&lt;br /&gt;
&amp;lt;div id=&amp;quot;join-screen&amp;quot;&amp;gt;&lt;br /&gt;
  &amp;lt;h2&amp;gt;join a room&amp;lt;/h2&amp;gt;&lt;br /&gt;
  &amp;lt;p&amp;gt;Enter your name and a room name. Anyone with the same room name will connect with you.&amp;lt;/p&amp;gt;&lt;br /&gt;
  &amp;lt;div class=&amp;quot;field-group&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;label&amp;gt;YOUR NAME&amp;lt;/label&amp;gt;&lt;br /&gt;
    &amp;lt;input id=&amp;quot;name-input&amp;quot; type=&amp;quot;text&amp;quot; placeholder=&amp;quot;e.g. Alice&amp;quot; maxlength=&amp;quot;24&amp;quot; /&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
  &amp;lt;div class=&amp;quot;field-group&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;label&amp;gt;ROOM NAME&amp;lt;/label&amp;gt;&lt;br /&gt;
    &amp;lt;input id=&amp;quot;room-input&amp;quot; type=&amp;quot;text&amp;quot; placeholder=&amp;quot;e.g. room1&amp;quot; value=&amp;quot;room1&amp;quot; maxlength=&amp;quot;32&amp;quot; /&amp;gt;&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
  &amp;lt;button class=&amp;quot;primary&amp;quot; onclick=&amp;quot;joinRoom()&amp;quot;&amp;gt;Join Room →&amp;lt;/button&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- App (shown after joining) --&amp;gt;&lt;br /&gt;
&amp;lt;main&amp;gt;&lt;br /&gt;
  &amp;lt;div id=&amp;quot;app&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- Left: videos --&amp;gt;&lt;br /&gt;
    &amp;lt;div id=&amp;quot;video-panel&amp;quot;&amp;gt;&lt;br /&gt;
      &amp;lt;div class=&amp;quot;video-wrapper&amp;quot; id=&amp;quot;local-wrapper&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;video id=&amp;quot;local-video&amp;quot; autoplay muted playsinline&amp;gt;&amp;lt;/video&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;video-label&amp;quot; id=&amp;quot;local-label&amp;quot;&amp;gt;you&amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
    &amp;lt;!-- Right: chat --&amp;gt;&lt;br /&gt;
    &amp;lt;div id=&amp;quot;chat-panel&amp;quot;&amp;gt;&lt;br /&gt;
      &amp;lt;div id=&amp;quot;chat-header&amp;quot;&amp;gt;MESSAGES — &amp;lt;span id=&amp;quot;peer-count&amp;quot;&amp;gt;0 others in room&amp;lt;/span&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;div id=&amp;quot;messages&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;msg-system&amp;quot;&amp;gt;Chat is end-to-end via your server. Video is peer-to-peer.&amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;div id=&amp;quot;input-bar&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;textarea id=&amp;quot;msg-input&amp;quot; placeholder=&amp;quot;Type a message… (Enter to send, Shift+Enter for newline)&amp;quot; rows=&amp;quot;1&amp;quot;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;br /&gt;
        &amp;lt;span id=&amp;quot;char-count&amp;quot;&amp;gt;500&amp;lt;/span&amp;gt;&lt;br /&gt;
        &amp;lt;button id=&amp;quot;send-btn&amp;quot; onclick=&amp;quot;sendMessage()&amp;quot;&amp;gt;Send&amp;lt;/button&amp;gt;&lt;br /&gt;
      &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/main&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;script src=&amp;quot;/socket.io/socket.io.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;script&amp;gt;&lt;br /&gt;
  const socket = io();&lt;br /&gt;
  let localStream;&lt;br /&gt;
  let myName = &#039;Anonymous&#039;;&lt;br /&gt;
  let currentRoom = &#039;&#039;;&lt;br /&gt;
  let peerCount = 0;&lt;br /&gt;
  const peers = {};&lt;br /&gt;
  const MAX_MSG = 500;&lt;br /&gt;
&lt;br /&gt;
  const iceConfig = {&lt;br /&gt;
    iceServers: [{ urls: &#039;stun:stun.l.google.com:19302&#039; }]&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  // ── Helpers ──────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
  // Detect URLs in text and wrap them in &amp;lt;a&amp;gt; tags&lt;br /&gt;
  function linkify(text) {&lt;br /&gt;
    const escaped = text&lt;br /&gt;
      .replace(/&amp;amp;/g, &#039;&amp;amp;amp;&#039;)&lt;br /&gt;
      .replace(/&amp;lt;/g, &#039;&amp;amp;lt;&#039;)&lt;br /&gt;
      .replace(/&amp;gt;/g, &#039;&amp;amp;gt;&#039;);&lt;br /&gt;
    const urlRegex = /(https?:\/\/[^\s&amp;lt;&amp;gt;&amp;quot;]+)/g;&lt;br /&gt;
    return escaped.replace(urlRegex, &#039;&amp;lt;a href=&amp;quot;$1&amp;quot; target=&amp;quot;_blank&amp;quot; rel=&amp;quot;noopener noreferrer&amp;quot;&amp;gt;$1&amp;lt;/a&amp;gt;&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function addMessage({ name, message, isSelf, isSystem }) {&lt;br /&gt;
    const container = document.getElementById(&#039;messages&#039;);&lt;br /&gt;
&lt;br /&gt;
    if (isSystem) {&lt;br /&gt;
      const el = document.createElement(&#039;div&#039;);&lt;br /&gt;
      el.className = &#039;msg-system&#039;;&lt;br /&gt;
      el.textContent = message;&lt;br /&gt;
      container.appendChild(el);&lt;br /&gt;
    } else {&lt;br /&gt;
      const bubble = document.createElement(&#039;div&#039;);&lt;br /&gt;
      bubble.className = &#039;msg-bubble &#039; + (isSelf ? &#039;self&#039; : &#039;other&#039;);&lt;br /&gt;
&lt;br /&gt;
      const now = new Date().toLocaleTimeString([], { hour: &#039;2-digit&#039;, minute: &#039;2-digit&#039; });&lt;br /&gt;
      const meta = document.createElement(&#039;div&#039;);&lt;br /&gt;
      meta.className = &#039;msg-meta&#039;;&lt;br /&gt;
      meta.textContent = (isSelf ? &#039;you&#039; : name) + &#039; · &#039; + now;&lt;br /&gt;
&lt;br /&gt;
      const body = document.createElement(&#039;div&#039;);&lt;br /&gt;
      body.className = &#039;msg-text&#039;;&lt;br /&gt;
      // Linkify and preserve newlines&lt;br /&gt;
      body.innerHTML = linkify(message).replace(/\n/g, &#039;&amp;lt;br&amp;gt;&#039;);&lt;br /&gt;
&lt;br /&gt;
      bubble.appendChild(meta);&lt;br /&gt;
      bubble.appendChild(body);&lt;br /&gt;
      container.appendChild(bubble);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Auto-scroll to bottom&lt;br /&gt;
    container.scrollTop = container.scrollHeight;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  function updatePeerCount() {&lt;br /&gt;
    document.getElementById(&#039;peer-count&#039;).textContent =&lt;br /&gt;
      peerCount === 0 ? &#039;no others in room yet&#039;&lt;br /&gt;
      : peerCount === 1 ? &#039;1 other in room&#039;&lt;br /&gt;
      : `${peerCount} others in room`;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ── Join ─────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
  async function joinRoom() {&lt;br /&gt;
    const nameVal = document.getElementById(&#039;name-input&#039;).value.trim();&lt;br /&gt;
    const roomVal = document.getElementById(&#039;room-input&#039;).value.trim();&lt;br /&gt;
&lt;br /&gt;
    if (!roomVal) { alert(&#039;Please enter a room name&#039;); return; }&lt;br /&gt;
    myName = nameVal || &#039;Anonymous&#039;;&lt;br /&gt;
    currentRoom = roomVal;&lt;br /&gt;
&lt;br /&gt;
    try {&lt;br /&gt;
      localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });&lt;br /&gt;
      document.getElementById(&#039;local-video&#039;).srcObject = localStream;&lt;br /&gt;
      document.getElementById(&#039;local-label&#039;).textContent = myName + &#039; (you)&#039;;&lt;br /&gt;
    } catch (err) {&lt;br /&gt;
      alert(&#039;Could not access camera/microphone: &#039; + err.message);&lt;br /&gt;
      return;&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    document.getElementById(&#039;join-screen&#039;).style.display = &#039;none&#039;;&lt;br /&gt;
    document.getElementById(&#039;app&#039;).classList.add(&#039;visible&#039;);&lt;br /&gt;
    document.getElementById(&#039;room-badge&#039;).textContent = &#039;# &#039; + currentRoom;&lt;br /&gt;
    document.getElementById(&#039;room-badge&#039;).style.display = &#039;&#039;;&lt;br /&gt;
    document.getElementById(&#039;status-dot&#039;).classList.add(&#039;live&#039;);&lt;br /&gt;
&lt;br /&gt;
    socket.emit(&#039;join&#039;, currentRoom);&lt;br /&gt;
    addMessage({ isSystem: true, message: `You joined #${currentRoom} as &amp;quot;${myName}&amp;quot;` });&lt;br /&gt;
    updatePeerCount();&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  // ── Chat ─────────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
  function sendMessage() {&lt;br /&gt;
    const input = document.getElementById(&#039;msg-input&#039;);&lt;br /&gt;
    const text = input.value.trim();&lt;br /&gt;
    if (!text || !currentRoom) return;&lt;br /&gt;
    if (text.length &amp;gt; MAX_MSG) { alert(`Max ${MAX_MSG} characters`); return; }&lt;br /&gt;
&lt;br /&gt;
    socket.emit(&#039;chat-message&#039;, { room: currentRoom, message: text, name: myName });&lt;br /&gt;
    addMessage({ name: myName, message: text, isSelf: true });&lt;br /&gt;
&lt;br /&gt;
    input.value = &#039;&#039;;&lt;br /&gt;
    input.style.height = &#039;42px&#039;;&lt;br /&gt;
    document.getElementById(&#039;char-count&#039;).textContent = MAX_MSG;&lt;br /&gt;
    document.getElementById(&#039;char-count&#039;).classList.remove(&#039;warn&#039;);&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  socket.on(&#039;chat-message&#039;, ({ name, message }) =&amp;gt; {&lt;br /&gt;
    addMessage({ name, message, isSelf: false });&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Enter to send, Shift+Enter for newline&lt;br /&gt;
  document.getElementById(&#039;msg-input&#039;).addEventListener(&#039;keydown&#039;, (e) =&amp;gt; {&lt;br /&gt;
    if (e.key === &#039;Enter&#039; &amp;amp;&amp;amp; !e.shiftKey) {&lt;br /&gt;
      e.preventDefault();&lt;br /&gt;
      sendMessage();&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Char counter + auto-resize textarea&lt;br /&gt;
  document.getElementById(&#039;msg-input&#039;).addEventListener(&#039;input&#039;, function () {&lt;br /&gt;
    const remaining = MAX_MSG - this.value.length;&lt;br /&gt;
    const counter = document.getElementById(&#039;char-count&#039;);&lt;br /&gt;
    counter.textContent = remaining;&lt;br /&gt;
    counter.classList.toggle(&#039;warn&#039;, remaining &amp;lt; 50);&lt;br /&gt;
    this.style.height = &#039;42px&#039;;&lt;br /&gt;
    this.style.height = Math.min(this.scrollHeight, 120) + &#039;px&#039;;&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // ── WebRTC ───────────────────────────────────────────────&lt;br /&gt;
&lt;br /&gt;
  socket.on(&#039;room-users&#039;, async (users) =&amp;gt; {&lt;br /&gt;
    peerCount = users.length;&lt;br /&gt;
    updatePeerCount();&lt;br /&gt;
    for (const userId of users) {&lt;br /&gt;
      await createOffer(userId);&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  socket.on(&#039;offer&#039;, async ({ from, offer }) =&amp;gt; {&lt;br /&gt;
    const pc = createPeerConnection(from);&lt;br /&gt;
    await pc.setRemoteDescription(new RTCSessionDescription(offer));&lt;br /&gt;
    const answer = await pc.createAnswer();&lt;br /&gt;
    await pc.setLocalDescription(answer);&lt;br /&gt;
    socket.emit(&#039;answer&#039;, { to: from, answer });&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  socket.on(&#039;answer&#039;, async ({ from, answer }) =&amp;gt; {&lt;br /&gt;
    const pc = peers[from];&lt;br /&gt;
    if (pc) await pc.setRemoteDescription(new RTCSessionDescription(answer));&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  socket.on(&#039;ice-candidate&#039;, async ({ from, candidate }) =&amp;gt; {&lt;br /&gt;
    const pc = peers[from];&lt;br /&gt;
    if (pc &amp;amp;&amp;amp; candidate) {&lt;br /&gt;
      try { await pc.addIceCandidate(new RTCIceCandidate(candidate)); } catch (e) {}&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  socket.on(&#039;user-left&#039;, (userId) =&amp;gt; {&lt;br /&gt;
    if (peers[userId]) { peers[userId].close(); delete peers[userId]; }&lt;br /&gt;
    const el = document.getElementById(&#039;video-&#039; + userId);&lt;br /&gt;
    if (el) el.closest(&#039;.video-wrapper&#039;).remove();&lt;br /&gt;
    peerCount = Math.max(0, peerCount - 1);&lt;br /&gt;
    updatePeerCount();&lt;br /&gt;
    addMessage({ isSystem: true, message: &#039;A user left the room.&#039; });&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  function createPeerConnection(peerId) {&lt;br /&gt;
    const pc = new RTCPeerConnection(iceConfig);&lt;br /&gt;
    peers[peerId] = pc;&lt;br /&gt;
&lt;br /&gt;
    localStream.getTracks().forEach((track) =&amp;gt; pc.addTrack(track, localStream));&lt;br /&gt;
&lt;br /&gt;
    pc.onicecandidate = ({ candidate }) =&amp;gt; {&lt;br /&gt;
      if (candidate) socket.emit(&#039;ice-candidate&#039;, { to: peerId, candidate });&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    pc.ontrack = ({ streams }) =&amp;gt; {&lt;br /&gt;
      let wrapper = document.getElementById(&#039;wrapper-&#039; + peerId);&lt;br /&gt;
      if (!wrapper) {&lt;br /&gt;
        wrapper = document.createElement(&#039;div&#039;);&lt;br /&gt;
        wrapper.className = &#039;video-wrapper&#039;;&lt;br /&gt;
        wrapper.id = &#039;wrapper-&#039; + peerId;&lt;br /&gt;
&lt;br /&gt;
        const vid = document.createElement(&#039;video&#039;);&lt;br /&gt;
        vid.id = &#039;video-&#039; + peerId;&lt;br /&gt;
        vid.autoplay = true;&lt;br /&gt;
        vid.playsInline = true;&lt;br /&gt;
&lt;br /&gt;
        const label = document.createElement(&#039;div&#039;);&lt;br /&gt;
        label.className = &#039;video-label&#039;;&lt;br /&gt;
        label.textContent = &#039;peer&#039;;&lt;br /&gt;
&lt;br /&gt;
        wrapper.appendChild(vid);&lt;br /&gt;
        wrapper.appendChild(label);&lt;br /&gt;
        document.getElementById(&#039;video-panel&#039;).appendChild(wrapper);&lt;br /&gt;
&lt;br /&gt;
        peerCount = Object.keys(peers).length;&lt;br /&gt;
        updatePeerCount();&lt;br /&gt;
      }&lt;br /&gt;
      document.getElementById(&#039;video-&#039; + peerId).srcObject = streams[0];&lt;br /&gt;
    };&lt;br /&gt;
&lt;br /&gt;
    return pc;&lt;br /&gt;
  }&lt;br /&gt;
&lt;br /&gt;
  async function createOffer(peerId) {&lt;br /&gt;
    const pc = createPeerConnection(peerId);&lt;br /&gt;
    const offer = await pc.createOffer();&lt;br /&gt;
    await pc.setLocalDescription(offer);&lt;br /&gt;
    socket.emit(&#039;offer&#039;, { to: peerId, offer });&lt;br /&gt;
  }&lt;br /&gt;
&amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Edit &amp;lt;code&amp;gt;server.js&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Appended &amp;lt;b&amp;gt;server.js&amp;lt;/b&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
const express = require(&#039;express&#039;);&lt;br /&gt;
const https = require(&#039;https&#039;);&lt;br /&gt;
const { Server } = require(&#039;socket.io&#039;);&lt;br /&gt;
const path = require(&#039;path&#039;);&lt;br /&gt;
&lt;br /&gt;
const app = express();&lt;br /&gt;
const fs = require(&#039;fs&#039;);&lt;br /&gt;
&lt;br /&gt;
const server = https.createServer({&lt;br /&gt;
  key: fs.readFileSync(&#039;/opt/webrtc/key.pem&#039;),&lt;br /&gt;
  cert: fs.readFileSync(&#039;/opt/webrtc/cert.pem&#039;),&lt;br /&gt;
}, app);&lt;br /&gt;
const io = new Server(server);&lt;br /&gt;
&lt;br /&gt;
// Serve the frontend HTML file&lt;br /&gt;
app.use(express.static(path.join(__dirname, &#039;public&#039;)));&lt;br /&gt;
&lt;br /&gt;
// Keep track of who is in each room&lt;br /&gt;
const rooms = {};&lt;br /&gt;
&lt;br /&gt;
io.on(&#039;connection&#039;, (socket) =&amp;gt; {&lt;br /&gt;
  console.log(&#039;A user connected:&#039;, socket.id);&lt;br /&gt;
&lt;br /&gt;
  // User wants to join a room&lt;br /&gt;
  socket.on(&#039;join&#039;, (room) =&amp;gt; {&lt;br /&gt;
    socket.join(room);&lt;br /&gt;
&lt;br /&gt;
    if (!rooms[room]) {&lt;br /&gt;
      rooms[room] = [];&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    // Tell the new user who is already in the room&lt;br /&gt;
    socket.emit(&#039;room-users&#039;, rooms[room]);&lt;br /&gt;
&lt;br /&gt;
    // Add this user to the room list&lt;br /&gt;
    rooms[room].push(socket.id);&lt;br /&gt;
&lt;br /&gt;
    console.log(`${socket.id} joined room: ${room}`);&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Relay a WebRTC offer to a specific user&lt;br /&gt;
  socket.on(&#039;offer&#039;, ({ to, offer }) =&amp;gt; {&lt;br /&gt;
    io.to(to).emit(&#039;offer&#039;, { from: socket.id, offer });&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Relay a WebRTC answer to a specific user&lt;br /&gt;
  socket.on(&#039;answer&#039;, ({ to, answer }) =&amp;gt; {&lt;br /&gt;
    io.to(to).emit(&#039;answer&#039;, { from: socket.id, answer });&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // Relay ICE candidates between peers&lt;br /&gt;
  socket.on(&#039;ice-candidate&#039;, ({ to, candidate }) =&amp;gt; {&lt;br /&gt;
    io.to(to).emit(&#039;ice-candidate&#039;, { from: socket.id, candidate });&lt;br /&gt;
  });&lt;br /&gt;
&lt;br /&gt;
  // ---- CHAT ----&lt;br /&gt;
  // Relay a chat message to everyone else in the room&lt;br /&gt;
  socket.on(&#039;chat-message&#039;, ({ room, message, name }) =&amp;gt; {&lt;br /&gt;
    // Broadcast to everyone in the room EXCEPT the sender&lt;br /&gt;
    socket.to(room).emit(&#039;chat-message&#039;, {&lt;br /&gt;
      from: socket.id,&lt;br /&gt;
      name: name || &#039;Anonymous&#039;,&lt;br /&gt;
      message,&lt;br /&gt;
      time: new Date().toLocaleTimeString([], { hour: &#039;2-digit&#039;, minute: &#039;2-digit&#039; })&lt;br /&gt;
    });&lt;br /&gt;
  });&lt;br /&gt;
  // ---- END CHAT ----&lt;br /&gt;
&lt;br /&gt;
  // Clean up when a user disconnects&lt;br /&gt;
  socket.on(&#039;disconnect&#039;, () =&amp;gt; {&lt;br /&gt;
    for (const room in rooms) {&lt;br /&gt;
      rooms[room] = rooms[room].filter((id) =&amp;gt; id !== socket.id);&lt;br /&gt;
      // Tell others in the room this user left&lt;br /&gt;
      socket.to(room).emit(&#039;user-left&#039;, socket.id);&lt;br /&gt;
    }&lt;br /&gt;
    console.log(&#039;User disconnected:&#039;, socket.id);&lt;br /&gt;
  });&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
const PORT = 3000;&lt;br /&gt;
server.listen(PORT, &#039;0.0.0.0&#039;, () =&amp;gt; {&lt;br /&gt;
  console.log(`Signalling server running on port ${PORT}`);&lt;br /&gt;
});&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Restart webrtc: &amp;lt;code&amp;gt;systemctl restart webrtc&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Test - Should be working&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== &amp;quot;Cannot connect to the page&amp;quot; ===&lt;br /&gt;
* Make sure the container is running: &amp;lt;code&amp;gt;sudo lxc-ls --fancy&amp;lt;/code&amp;gt;&lt;br /&gt;
* Make sure the service is running: attach to the container and run &amp;lt;code&amp;gt;systemctl status webrtc&amp;lt;/code&amp;gt;&lt;br /&gt;
* Double-check the IP address — it can change after a container restart. Assign a static IP via LXC config if needed.&lt;br /&gt;
&lt;br /&gt;
=== &amp;quot;Camera/Microphone access denied&amp;quot; ===&lt;br /&gt;
* On non-localhost addresses, browsers require HTTPS. See Part 7.&lt;br /&gt;
* Make sure you clicked &#039;&#039;&#039;Allow&#039;&#039;&#039; (not Block) when the browser asked for permissions.&lt;br /&gt;
* Try in a different browser, or clear site permissions in browser settings.&lt;br /&gt;
&lt;br /&gt;
=== Two tabs connect but no video appears ===&lt;br /&gt;
* This usually means ICE negotiation is failing. For local testing this should not happen.&lt;br /&gt;
* If testing across networks (not just local LAN), you will need a &#039;&#039;&#039;TURN server&#039;&#039;&#039; (e.g. coturn). This is beyond the scope of this guide.&lt;br /&gt;
&lt;br /&gt;
=== Port 3000 not reachable ===&lt;br /&gt;
* Check no firewall is blocking it on the host: &amp;lt;code&amp;gt;sudo ufw status&amp;lt;/code&amp;gt;&lt;br /&gt;
* LXC containers on the default &amp;lt;code&amp;gt;lxcbr0&amp;lt;/code&amp;gt; bridge are reachable from the host by default. No port forwarding should be needed for local access.&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=User:ChandraArgueta&amp;diff=713</id>
		<title>User:ChandraArgueta</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=User:ChandraArgueta&amp;diff=713"/>
		<updated>2026-01-06T20:23:02Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Blanked the page&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=User:StephaniaEberhar&amp;diff=711</id>
		<title>User:StephaniaEberhar</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=User:StephaniaEberhar&amp;diff=711"/>
		<updated>2026-01-06T20:22:16Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Blanked the page&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=User:ReinaldoTonga76&amp;diff=710</id>
		<title>User:ReinaldoTonga76</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=User:ReinaldoTonga76&amp;diff=710"/>
		<updated>2026-01-06T20:21:40Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Blanked the page&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Why_New_18_Video_Platforms_Are_Improving_The_User_Experience&amp;diff=709</id>
		<title>Why New 18 Video Platforms Are Improving The User Experience</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Why_New_18_Video_Platforms_Are_Improving_The_User_Experience&amp;diff=709"/>
		<updated>2026-01-06T20:21:05Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Blanked the page&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_2&amp;diff=678</id>
		<title>CompleteNoobs Docker Image 2</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_2&amp;diff=678"/>
		<updated>2025-09-02T14:46:21Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* 5.2: Test Extensions */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
==todo==&lt;br /&gt;
* Use Dir for import export of xml.&lt;br /&gt;
* Min scripts - just setup mediawiki with extensions.&lt;br /&gt;
&lt;br /&gt;
= Complete Noobs Docker Wiki Tutorial v0.2 =&lt;br /&gt;
* Version 0.2 - Simplified with manual import/export&lt;br /&gt;
* [[Docker_Install_Guide| Docker install guide]]&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
This tutorial creates a MediaWiki Docker container with:&lt;br /&gt;
* MediaWiki 1.44&lt;br /&gt;
* PageNotice extension (for license notices)&lt;br /&gt;
* YouTube extension (for video embedding)&lt;br /&gt;
* Shared directory for XML import/export&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
* Ubuntu 24.04&lt;br /&gt;
* Docker installed and running&lt;br /&gt;
* Your user in docker group: &amp;lt;code&amp;gt;sudo usermod -aG docker $USER&amp;lt;/code&amp;gt; (then logout/login)&lt;br /&gt;
&lt;br /&gt;
== Step 1: Create Directory Structure ==&lt;br /&gt;
&lt;br /&gt;
=== 1.1: Create Project Directory ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir ~/completenoobs-docker-v2&lt;br /&gt;
cd ~/completenoobs-docker-v2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 1.2: Create Shared Directory ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This directory will be used for importing and exporting XML files.&lt;br /&gt;
&lt;br /&gt;
== Step 2: Create All Files ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1: Dockerfile ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM mediawiki:1.44&lt;br /&gt;
&lt;br /&gt;
# Install dependencies&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \&lt;br /&gt;
    mariadb-server \&lt;br /&gt;
    python3-pygments \&lt;br /&gt;
    curl \&lt;br /&gt;
    wget \&lt;br /&gt;
    unzip \&lt;br /&gt;
    nano \&lt;br /&gt;
    vim \&lt;br /&gt;
    git \&lt;br /&gt;
    &amp;amp;&amp;amp; apt-get clean&lt;br /&gt;
&lt;br /&gt;
# Copy setup script&lt;br /&gt;
COPY setup_wiki.sh /usr/src/setup_wiki.sh&lt;br /&gt;
COPY entrypoint.sh /entrypoint.sh&lt;br /&gt;
&lt;br /&gt;
# Make executable&lt;br /&gt;
RUN chmod +x /usr/src/setup_wiki.sh /entrypoint.sh&lt;br /&gt;
&lt;br /&gt;
# Setup wiki&lt;br /&gt;
RUN /usr/src/setup_wiki.sh&lt;br /&gt;
&lt;br /&gt;
EXPOSE 80&lt;br /&gt;
VOLUME /var/lib/mysql&lt;br /&gt;
VOLUME /var/www/html/images&lt;br /&gt;
VOLUME /export&lt;br /&gt;
&lt;br /&gt;
ENTRYPOINT [&amp;quot;/entrypoint.sh&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.2: Main Setup Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano setup_wiki.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Setting up CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Initialize MariaDB&lt;br /&gt;
if [ ! -d &amp;quot;/var/lib/mysql/mysql&amp;quot; ]; then&lt;br /&gt;
    mysql_install_db --user=mysql --datadir=/var/lib/mysql&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 2&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
# Setup database&lt;br /&gt;
mysql -e &amp;quot;CREATE DATABASE IF NOT EXISTS completenoobs_wiki CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;127.0.0.1&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;127.0.0.1&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;localhost&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;localhost&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;FLUSH PRIVILEGES;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Install MediaWiki&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
php maintenance/install.php \&lt;br /&gt;
    --dbtype=mysql \&lt;br /&gt;
    --dbserver=127.0.0.1 \&lt;br /&gt;
    --dbname=completenoobs_wiki \&lt;br /&gt;
    --dbuser=wikiuser \&lt;br /&gt;
    --dbpass=wikipass \&lt;br /&gt;
    --server=&amp;quot;http://localhost:8080&amp;quot; \&lt;br /&gt;
    --scriptpath=&amp;quot;&amp;quot; \&lt;br /&gt;
    --lang=en \&lt;br /&gt;
    --pass=AdminPass123! \&lt;br /&gt;
    &amp;quot;CompleteNoobs Wiki&amp;quot; \&lt;br /&gt;
    &amp;quot;admin&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Download and install extensions&lt;br /&gt;
cd extensions/&lt;br /&gt;
echo &amp;quot;Installing PageNotice extension...&amp;quot;&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/PageNotice --branch REL1_44 || echo &amp;quot;PageNotice download failed, continuing...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Installing YouTube extension...&amp;quot;&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/YouTube --branch REL1_44 || echo &amp;quot;YouTube download failed, continuing...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
&lt;br /&gt;
# Configure LocalSettings.php&lt;br /&gt;
cat &amp;gt;&amp;gt; LocalSettings.php &amp;lt;&amp;lt; &#039;EOF&#039;&lt;br /&gt;
&lt;br /&gt;
# Basic settings&lt;br /&gt;
$wgEnableUploads = true;&lt;br /&gt;
$wgUseImageMagick = true;&lt;br /&gt;
$wgImageMagickConvertCommand = &amp;quot;/usr/bin/convert&amp;quot;;&lt;br /&gt;
$wgDefaultSkin = &amp;quot;vector-2022&amp;quot;;&lt;br /&gt;
$wgAllowExternalImages = true;&lt;br /&gt;
&lt;br /&gt;
# Debug settings (remove in production)&lt;br /&gt;
$wgShowExceptionDetails = true;&lt;br /&gt;
$wgDebugLogFile = &amp;quot;/tmp/mediawiki-debug.log&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
# PageNotice extension&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/PageNotice/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;PageNotice&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# YouTube extension&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/YouTube/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;YouTube&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# SyntaxHighlight (usually bundled)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/SyntaxHighlight_GeSHi/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;SyntaxHighlight_GeSHi&#039; );&lt;br /&gt;
    $wgPygmentizePath = &#039;/usr/bin/pygmentize&#039;;&lt;br /&gt;
}&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
# Create helper scripts&lt;br /&gt;
cat &amp;gt; /var/www/html/export_wiki.sh &amp;lt;&amp;lt; &#039;EXPORT_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
echo &amp;quot;=== Wiki Export Tool ===&amp;quot;&lt;br /&gt;
DATE=$(date +%Y%m%d)&lt;br /&gt;
OUTPUT_FILE=&amp;quot;/export/${DATE}_wiki_export.xml&amp;quot;&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Exporting wiki to: $OUTPUT_FILE&amp;quot;&lt;br /&gt;
php /var/www/html/maintenance/run.php dumpBackup.php --full --output=file:$OUTPUT_FILE&lt;br /&gt;
&lt;br /&gt;
if [ -f &amp;quot;$OUTPUT_FILE&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Export successful!&amp;quot;&lt;br /&gt;
    echo &amp;quot;File saved to: $OUTPUT_FILE&amp;quot;&lt;br /&gt;
    echo &amp;quot;On host system: ~/wiki-container/${DATE}_wiki_export.xml&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;Export failed!&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
EXPORT_EOF&lt;br /&gt;
chmod +x /var/www/html/export_wiki.sh&lt;br /&gt;
&lt;br /&gt;
cat &amp;gt; /var/www/html/import_wiki.sh &amp;lt;&amp;lt; &#039;IMPORT_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
echo &amp;quot;=== Wiki Import Tool ===&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Available XML files in /export:&amp;quot;&lt;br /&gt;
ls -la /export/*.xml 2&amp;gt;/dev/null || echo &amp;quot;No XML files found&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
if [ -z &amp;quot;$1&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Usage: /var/www/html/import_wiki.sh &amp;lt;filename&amp;gt;&amp;quot;&lt;br /&gt;
    echo &amp;quot;Example: /var/www/html/import_wiki.sh /export/wiki.xml&amp;quot;&lt;br /&gt;
    exit 1&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
if [ ! -f &amp;quot;$1&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Error: File $1 not found!&amp;quot;&lt;br /&gt;
    exit 1&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Importing from: $1&amp;quot;&lt;br /&gt;
echo &amp;quot;This may take several minutes...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
php /var/www/html/maintenance/run.php importDump.php &amp;quot;$1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
if [ $? -eq 0 ]; then&lt;br /&gt;
    echo &amp;quot;Import completed!&amp;quot;&lt;br /&gt;
    echo &amp;quot;Rebuilding indexes...&amp;quot;&lt;br /&gt;
    php /var/www/html/maintenance/run.php rebuildrecentchanges.php&lt;br /&gt;
    php /var/www/html/maintenance/run.php initSiteStats.php&lt;br /&gt;
    echo &amp;quot;Done!&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;Import failed! Check /tmp/mediawiki-debug.log for details&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
IMPORT_EOF&lt;br /&gt;
chmod +x /var/www/html/import_wiki.sh&lt;br /&gt;
&lt;br /&gt;
# Create status check script&lt;br /&gt;
cat &amp;gt; /var/www/html/check_status.sh &amp;lt;&amp;lt; &#039;STATUS_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
echo &amp;quot;=== Wiki Status ===&amp;quot;&lt;br /&gt;
echo &amp;quot;Pages: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;Users: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM user WHERE user_id &amp;gt; 0;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Extensions ===&amp;quot;&lt;br /&gt;
if [ -d &amp;quot;extensions/PageNotice&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;PageNotice: Installed ✓&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;PageNotice: Not installed ✗&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/YouTube&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;YouTube: Installed ✓&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;YouTube: Not installed ✗&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/SyntaxHighlight_GeSHi&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;SyntaxHighlight: Installed ✓&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;SyntaxHighlight: Not installed ✗&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Shared Directory ===&amp;quot;&lt;br /&gt;
if [ -d &amp;quot;/export&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;/export mounted ✓&amp;quot;&lt;br /&gt;
    echo &amp;quot;Files in /export:&amp;quot;&lt;br /&gt;
    ls -la /export/*.xml 2&amp;gt;/dev/null || echo &amp;quot;  No XML files&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;/export not mounted ✗&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
STATUS_EOF&lt;br /&gt;
chmod +x /var/www/html/check_status.sh&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Setup completed!&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin: admin / AdminPass123!&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb stop&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.3: Entrypoint Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano entrypoint.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Starting CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 1&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=========================================&amp;quot;&lt;br /&gt;
echo &amp;quot;CompleteNoobs Wiki v0.2 Ready!&amp;quot;&lt;br /&gt;
echo &amp;quot;=========================================&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Access: http://localhost:8080&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Features:&amp;quot;&lt;br /&gt;
echo &amp;quot;- MediaWiki 1.44&amp;quot;&lt;br /&gt;
echo &amp;quot;- PageNotice extension&amp;quot;&lt;br /&gt;
echo &amp;quot;- YouTube extension&amp;quot;&lt;br /&gt;
echo &amp;quot;- SyntaxHighlight extension&amp;quot;&lt;br /&gt;
echo &amp;quot;- Import/Export via ~/wiki-container&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Available commands:&amp;quot;&lt;br /&gt;
echo &amp;quot;- Export wiki: docker exec completenoobs_wiki /var/www/html/export_wiki.sh&amp;quot;&lt;br /&gt;
echo &amp;quot;- Import XML: docker exec completenoobs_wiki /var/www/html/import_wiki.sh /export/filename.xml&amp;quot;&lt;br /&gt;
echo &amp;quot;- Check status: docker exec completenoobs_wiki /var/www/html/check_status.sh&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
apache2-foreground&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 3: Build and Run ==&lt;br /&gt;
&lt;br /&gt;
=== 3.1: Build the Image ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t completenoobs/wiki:v0.2 .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3.2: Run the Container with Shared Directory ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v ~/wiki-container:/export \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/wiki:v0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 4: Import/Export Operations ==&lt;br /&gt;
&lt;br /&gt;
=== 4.1: Import an XML File ===&lt;br /&gt;
* https://xml.completenoobs.com/xmlDumps/&lt;br /&gt;
==== Place XML file in shared directory ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Copy your XML file to the shared directory&lt;br /&gt;
cp ~/Downloads/completenoobs.xml ~/wiki-container/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Import the file ====&lt;br /&gt;
* All files in host &amp;lt;code&amp;gt;wiki-container&amp;lt;/code&amp;gt; directory will also be in the containers &amp;lt;code&amp;gt;/export/&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Run the import command&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/import_wiki.sh /export/completenoobs.xml&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{:Restore_the_completenoobs_Main_Page}}&lt;br /&gt;
&lt;br /&gt;
=== 4.2: Export Your Wiki ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Export wiki to dated XML file&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/export_wiki.sh&lt;br /&gt;
&lt;br /&gt;
# Check the exported file&lt;br /&gt;
ls -la ~/wiki-container/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.3: Manual Import/Export (Advanced) ===&lt;br /&gt;
&lt;br /&gt;
==== Access container shell ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Manual export with custom filename ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php dumpBackup.php --full --output=file:/export/my_wiki_backup.xml&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Manual import with options ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Basic import&lt;br /&gt;
php /var/www/html/maintenance/run.php importDump.php /export/wiki.xml&lt;br /&gt;
&lt;br /&gt;
# Import with image uploads&lt;br /&gt;
php /var/www/html/maintenance/run.php importDump.php --uploads /export/wiki.xml&lt;br /&gt;
&lt;br /&gt;
# Then rebuild indexes&lt;br /&gt;
php /var/www/html/maintenance/run.php rebuildall.php&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 5: Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
=== 5.1: Check Wiki Status ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/check_status.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5.2: Test Extensions ===&lt;br /&gt;
1. Visit http://localhost:8080&lt;br /&gt;
2. Login with admin / AdminPass123!&lt;br /&gt;
3. Create or edit a page and test:&lt;br /&gt;
   * YouTube: Add/Test &amp;lt;nowiki&amp;gt;&amp;lt;youtube&amp;gt;gBML6zuUpK0&amp;lt;/youtube&amp;gt;&amp;lt;/nowiki&amp;gt;  &amp;lt;code&amp;gt;&amp;lt;youtube&amp;gt;gBML6zuUpK0&amp;lt;/youtube&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
   * SyntaxHighlight: Add &amp;lt;nowiki&amp;gt;&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;print(&amp;quot;Hello&amp;quot;)&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5.3: View Logs ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Container logs&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&lt;br /&gt;
# MediaWiki debug log&lt;br /&gt;
docker exec completenoobs_wiki tail -f /tmp/mediawiki-debug.log&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 6: Common Operations ==&lt;br /&gt;
&lt;br /&gt;
=== 6.1: Change Admin Password ===&lt;br /&gt;
&lt;br /&gt;
==== Via Web Interface ====&lt;br /&gt;
1. Login at http://localhost:8080&lt;br /&gt;
2. Click username → Preferences&lt;br /&gt;
3. Go to Password tab&lt;br /&gt;
4. Change password&lt;br /&gt;
&lt;br /&gt;
==== Via Terminal ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6.2: Backup Your Wiki ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Create dated backup&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/export_wiki.sh&lt;br /&gt;
&lt;br /&gt;
# Copy to safe location&lt;br /&gt;
cp ~/wiki-container/*_wiki_export.xml ~/backups/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6.3: Restore from Backup ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Place backup in shared directory&lt;br /&gt;
cp ~/backups/20250101_wiki_export.xml ~/wiki-container/&lt;br /&gt;
&lt;br /&gt;
# Import the backup&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/import_wiki.sh /export/20250101_wiki_export.xml&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6.4: Complete Reset ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Stop and remove container&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
&lt;br /&gt;
# Remove volumes (WARNING: This deletes all data!)&lt;br /&gt;
docker volume rm completenoobs_mysql completenoobs_images&lt;br /&gt;
&lt;br /&gt;
# Rebuild and start fresh&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v ~/wiki-container:/export \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/wiki:v0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Permission Issues ===&lt;br /&gt;
If you encounter permission errors with the shared directory:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Fix permissions on host&lt;br /&gt;
chmod 777 ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Import Failures ===&lt;br /&gt;
If imports fail, check:&lt;br /&gt;
* File exists in ~/wiki-container&lt;br /&gt;
* File is valid XML format&lt;br /&gt;
* Sufficient disk space&lt;br /&gt;
* Check debug log: &amp;lt;code&amp;gt;docker exec completenoobs_wiki tail /tmp/mediawiki-debug.log&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Container Won&#039;t Start ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Check logs&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&lt;br /&gt;
# Try interactive mode&lt;br /&gt;
docker run -it --rm completenoobs/wiki:v0.2 bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
This setup provides:&lt;br /&gt;
* Clean MediaWiki 1.44 installation&lt;br /&gt;
* Essential extensions (PageNotice, YouTube, SyntaxHighlight)&lt;br /&gt;
* Simple import/export via ~/wiki-container directory&lt;br /&gt;
* No automatic updates - full control over your content&lt;br /&gt;
* Easy backup and restore capabilities&lt;br /&gt;
&lt;br /&gt;
The shared directory approach gives you complete control over when and what to import/export, making it ideal for:&lt;br /&gt;
* Migrating content between wikis&lt;br /&gt;
* Regular backups&lt;br /&gt;
* Sharing wiki content&lt;br /&gt;
* Testing imports before applying to production&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_2&amp;diff=677</id>
		<title>CompleteNoobs Docker Image 2</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_2&amp;diff=677"/>
		<updated>2025-09-02T14:42:36Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* 5.2: Test Extensions */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
==todo==&lt;br /&gt;
* Use Dir for import export of xml.&lt;br /&gt;
* Min scripts - just setup mediawiki with extensions.&lt;br /&gt;
&lt;br /&gt;
= Complete Noobs Docker Wiki Tutorial v0.2 =&lt;br /&gt;
* Version 0.2 - Simplified with manual import/export&lt;br /&gt;
* [[Docker_Install_Guide| Docker install guide]]&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
This tutorial creates a MediaWiki Docker container with:&lt;br /&gt;
* MediaWiki 1.44&lt;br /&gt;
* PageNotice extension (for license notices)&lt;br /&gt;
* YouTube extension (for video embedding)&lt;br /&gt;
* Shared directory for XML import/export&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
* Ubuntu 24.04&lt;br /&gt;
* Docker installed and running&lt;br /&gt;
* Your user in docker group: &amp;lt;code&amp;gt;sudo usermod -aG docker $USER&amp;lt;/code&amp;gt; (then logout/login)&lt;br /&gt;
&lt;br /&gt;
== Step 1: Create Directory Structure ==&lt;br /&gt;
&lt;br /&gt;
=== 1.1: Create Project Directory ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir ~/completenoobs-docker-v2&lt;br /&gt;
cd ~/completenoobs-docker-v2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 1.2: Create Shared Directory ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This directory will be used for importing and exporting XML files.&lt;br /&gt;
&lt;br /&gt;
== Step 2: Create All Files ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1: Dockerfile ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM mediawiki:1.44&lt;br /&gt;
&lt;br /&gt;
# Install dependencies&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \&lt;br /&gt;
    mariadb-server \&lt;br /&gt;
    python3-pygments \&lt;br /&gt;
    curl \&lt;br /&gt;
    wget \&lt;br /&gt;
    unzip \&lt;br /&gt;
    nano \&lt;br /&gt;
    vim \&lt;br /&gt;
    git \&lt;br /&gt;
    &amp;amp;&amp;amp; apt-get clean&lt;br /&gt;
&lt;br /&gt;
# Copy setup script&lt;br /&gt;
COPY setup_wiki.sh /usr/src/setup_wiki.sh&lt;br /&gt;
COPY entrypoint.sh /entrypoint.sh&lt;br /&gt;
&lt;br /&gt;
# Make executable&lt;br /&gt;
RUN chmod +x /usr/src/setup_wiki.sh /entrypoint.sh&lt;br /&gt;
&lt;br /&gt;
# Setup wiki&lt;br /&gt;
RUN /usr/src/setup_wiki.sh&lt;br /&gt;
&lt;br /&gt;
EXPOSE 80&lt;br /&gt;
VOLUME /var/lib/mysql&lt;br /&gt;
VOLUME /var/www/html/images&lt;br /&gt;
VOLUME /export&lt;br /&gt;
&lt;br /&gt;
ENTRYPOINT [&amp;quot;/entrypoint.sh&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.2: Main Setup Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano setup_wiki.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Setting up CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Initialize MariaDB&lt;br /&gt;
if [ ! -d &amp;quot;/var/lib/mysql/mysql&amp;quot; ]; then&lt;br /&gt;
    mysql_install_db --user=mysql --datadir=/var/lib/mysql&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 2&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
# Setup database&lt;br /&gt;
mysql -e &amp;quot;CREATE DATABASE IF NOT EXISTS completenoobs_wiki CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;127.0.0.1&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;127.0.0.1&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;localhost&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;localhost&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;FLUSH PRIVILEGES;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Install MediaWiki&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
php maintenance/install.php \&lt;br /&gt;
    --dbtype=mysql \&lt;br /&gt;
    --dbserver=127.0.0.1 \&lt;br /&gt;
    --dbname=completenoobs_wiki \&lt;br /&gt;
    --dbuser=wikiuser \&lt;br /&gt;
    --dbpass=wikipass \&lt;br /&gt;
    --server=&amp;quot;http://localhost:8080&amp;quot; \&lt;br /&gt;
    --scriptpath=&amp;quot;&amp;quot; \&lt;br /&gt;
    --lang=en \&lt;br /&gt;
    --pass=AdminPass123! \&lt;br /&gt;
    &amp;quot;CompleteNoobs Wiki&amp;quot; \&lt;br /&gt;
    &amp;quot;admin&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Download and install extensions&lt;br /&gt;
cd extensions/&lt;br /&gt;
echo &amp;quot;Installing PageNotice extension...&amp;quot;&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/PageNotice --branch REL1_44 || echo &amp;quot;PageNotice download failed, continuing...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Installing YouTube extension...&amp;quot;&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/YouTube --branch REL1_44 || echo &amp;quot;YouTube download failed, continuing...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
&lt;br /&gt;
# Configure LocalSettings.php&lt;br /&gt;
cat &amp;gt;&amp;gt; LocalSettings.php &amp;lt;&amp;lt; &#039;EOF&#039;&lt;br /&gt;
&lt;br /&gt;
# Basic settings&lt;br /&gt;
$wgEnableUploads = true;&lt;br /&gt;
$wgUseImageMagick = true;&lt;br /&gt;
$wgImageMagickConvertCommand = &amp;quot;/usr/bin/convert&amp;quot;;&lt;br /&gt;
$wgDefaultSkin = &amp;quot;vector-2022&amp;quot;;&lt;br /&gt;
$wgAllowExternalImages = true;&lt;br /&gt;
&lt;br /&gt;
# Debug settings (remove in production)&lt;br /&gt;
$wgShowExceptionDetails = true;&lt;br /&gt;
$wgDebugLogFile = &amp;quot;/tmp/mediawiki-debug.log&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
# PageNotice extension&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/PageNotice/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;PageNotice&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# YouTube extension&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/YouTube/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;YouTube&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# SyntaxHighlight (usually bundled)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/SyntaxHighlight_GeSHi/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;SyntaxHighlight_GeSHi&#039; );&lt;br /&gt;
    $wgPygmentizePath = &#039;/usr/bin/pygmentize&#039;;&lt;br /&gt;
}&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
# Create helper scripts&lt;br /&gt;
cat &amp;gt; /var/www/html/export_wiki.sh &amp;lt;&amp;lt; &#039;EXPORT_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
echo &amp;quot;=== Wiki Export Tool ===&amp;quot;&lt;br /&gt;
DATE=$(date +%Y%m%d)&lt;br /&gt;
OUTPUT_FILE=&amp;quot;/export/${DATE}_wiki_export.xml&amp;quot;&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Exporting wiki to: $OUTPUT_FILE&amp;quot;&lt;br /&gt;
php /var/www/html/maintenance/run.php dumpBackup.php --full --output=file:$OUTPUT_FILE&lt;br /&gt;
&lt;br /&gt;
if [ -f &amp;quot;$OUTPUT_FILE&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Export successful!&amp;quot;&lt;br /&gt;
    echo &amp;quot;File saved to: $OUTPUT_FILE&amp;quot;&lt;br /&gt;
    echo &amp;quot;On host system: ~/wiki-container/${DATE}_wiki_export.xml&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;Export failed!&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
EXPORT_EOF&lt;br /&gt;
chmod +x /var/www/html/export_wiki.sh&lt;br /&gt;
&lt;br /&gt;
cat &amp;gt; /var/www/html/import_wiki.sh &amp;lt;&amp;lt; &#039;IMPORT_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
echo &amp;quot;=== Wiki Import Tool ===&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Available XML files in /export:&amp;quot;&lt;br /&gt;
ls -la /export/*.xml 2&amp;gt;/dev/null || echo &amp;quot;No XML files found&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
if [ -z &amp;quot;$1&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Usage: /var/www/html/import_wiki.sh &amp;lt;filename&amp;gt;&amp;quot;&lt;br /&gt;
    echo &amp;quot;Example: /var/www/html/import_wiki.sh /export/wiki.xml&amp;quot;&lt;br /&gt;
    exit 1&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
if [ ! -f &amp;quot;$1&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Error: File $1 not found!&amp;quot;&lt;br /&gt;
    exit 1&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Importing from: $1&amp;quot;&lt;br /&gt;
echo &amp;quot;This may take several minutes...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
php /var/www/html/maintenance/run.php importDump.php &amp;quot;$1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
if [ $? -eq 0 ]; then&lt;br /&gt;
    echo &amp;quot;Import completed!&amp;quot;&lt;br /&gt;
    echo &amp;quot;Rebuilding indexes...&amp;quot;&lt;br /&gt;
    php /var/www/html/maintenance/run.php rebuildrecentchanges.php&lt;br /&gt;
    php /var/www/html/maintenance/run.php initSiteStats.php&lt;br /&gt;
    echo &amp;quot;Done!&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;Import failed! Check /tmp/mediawiki-debug.log for details&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
IMPORT_EOF&lt;br /&gt;
chmod +x /var/www/html/import_wiki.sh&lt;br /&gt;
&lt;br /&gt;
# Create status check script&lt;br /&gt;
cat &amp;gt; /var/www/html/check_status.sh &amp;lt;&amp;lt; &#039;STATUS_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
echo &amp;quot;=== Wiki Status ===&amp;quot;&lt;br /&gt;
echo &amp;quot;Pages: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;Users: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM user WHERE user_id &amp;gt; 0;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Extensions ===&amp;quot;&lt;br /&gt;
if [ -d &amp;quot;extensions/PageNotice&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;PageNotice: Installed ✓&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;PageNotice: Not installed ✗&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/YouTube&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;YouTube: Installed ✓&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;YouTube: Not installed ✗&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/SyntaxHighlight_GeSHi&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;SyntaxHighlight: Installed ✓&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;SyntaxHighlight: Not installed ✗&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Shared Directory ===&amp;quot;&lt;br /&gt;
if [ -d &amp;quot;/export&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;/export mounted ✓&amp;quot;&lt;br /&gt;
    echo &amp;quot;Files in /export:&amp;quot;&lt;br /&gt;
    ls -la /export/*.xml 2&amp;gt;/dev/null || echo &amp;quot;  No XML files&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;/export not mounted ✗&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
STATUS_EOF&lt;br /&gt;
chmod +x /var/www/html/check_status.sh&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Setup completed!&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin: admin / AdminPass123!&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb stop&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.3: Entrypoint Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano entrypoint.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Starting CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 1&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=========================================&amp;quot;&lt;br /&gt;
echo &amp;quot;CompleteNoobs Wiki v0.2 Ready!&amp;quot;&lt;br /&gt;
echo &amp;quot;=========================================&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Access: http://localhost:8080&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Features:&amp;quot;&lt;br /&gt;
echo &amp;quot;- MediaWiki 1.44&amp;quot;&lt;br /&gt;
echo &amp;quot;- PageNotice extension&amp;quot;&lt;br /&gt;
echo &amp;quot;- YouTube extension&amp;quot;&lt;br /&gt;
echo &amp;quot;- SyntaxHighlight extension&amp;quot;&lt;br /&gt;
echo &amp;quot;- Import/Export via ~/wiki-container&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Available commands:&amp;quot;&lt;br /&gt;
echo &amp;quot;- Export wiki: docker exec completenoobs_wiki /var/www/html/export_wiki.sh&amp;quot;&lt;br /&gt;
echo &amp;quot;- Import XML: docker exec completenoobs_wiki /var/www/html/import_wiki.sh /export/filename.xml&amp;quot;&lt;br /&gt;
echo &amp;quot;- Check status: docker exec completenoobs_wiki /var/www/html/check_status.sh&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
apache2-foreground&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 3: Build and Run ==&lt;br /&gt;
&lt;br /&gt;
=== 3.1: Build the Image ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t completenoobs/wiki:v0.2 .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3.2: Run the Container with Shared Directory ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v ~/wiki-container:/export \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/wiki:v0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 4: Import/Export Operations ==&lt;br /&gt;
&lt;br /&gt;
=== 4.1: Import an XML File ===&lt;br /&gt;
* https://xml.completenoobs.com/xmlDumps/&lt;br /&gt;
==== Place XML file in shared directory ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Copy your XML file to the shared directory&lt;br /&gt;
cp ~/Downloads/completenoobs.xml ~/wiki-container/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Import the file ====&lt;br /&gt;
* All files in host &amp;lt;code&amp;gt;wiki-container&amp;lt;/code&amp;gt; directory will also be in the containers &amp;lt;code&amp;gt;/export/&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Run the import command&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/import_wiki.sh /export/completenoobs.xml&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{:Restore_the_completenoobs_Main_Page}}&lt;br /&gt;
&lt;br /&gt;
=== 4.2: Export Your Wiki ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Export wiki to dated XML file&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/export_wiki.sh&lt;br /&gt;
&lt;br /&gt;
# Check the exported file&lt;br /&gt;
ls -la ~/wiki-container/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.3: Manual Import/Export (Advanced) ===&lt;br /&gt;
&lt;br /&gt;
==== Access container shell ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Manual export with custom filename ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php dumpBackup.php --full --output=file:/export/my_wiki_backup.xml&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Manual import with options ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Basic import&lt;br /&gt;
php /var/www/html/maintenance/run.php importDump.php /export/wiki.xml&lt;br /&gt;
&lt;br /&gt;
# Import with image uploads&lt;br /&gt;
php /var/www/html/maintenance/run.php importDump.php --uploads /export/wiki.xml&lt;br /&gt;
&lt;br /&gt;
# Then rebuild indexes&lt;br /&gt;
php /var/www/html/maintenance/run.php rebuildall.php&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 5: Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
=== 5.1: Check Wiki Status ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/check_status.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5.2: Test Extensions ===&lt;br /&gt;
1. Visit http://localhost:8080&lt;br /&gt;
2. Login with admin / AdminPass123!&lt;br /&gt;
3. Create or edit a page and test:&lt;br /&gt;
   * YouTube: Add &amp;lt;code&amp;gt;&amp;lt;youtube&amp;gt;gBML6zuUpK0&amp;lt;/youtube&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
   * SyntaxHighlight: Add &amp;lt;nowiki&amp;gt;&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;print(&amp;quot;Hello&amp;quot;)&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5.3: View Logs ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Container logs&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&lt;br /&gt;
# MediaWiki debug log&lt;br /&gt;
docker exec completenoobs_wiki tail -f /tmp/mediawiki-debug.log&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 6: Common Operations ==&lt;br /&gt;
&lt;br /&gt;
=== 6.1: Change Admin Password ===&lt;br /&gt;
&lt;br /&gt;
==== Via Web Interface ====&lt;br /&gt;
1. Login at http://localhost:8080&lt;br /&gt;
2. Click username → Preferences&lt;br /&gt;
3. Go to Password tab&lt;br /&gt;
4. Change password&lt;br /&gt;
&lt;br /&gt;
==== Via Terminal ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6.2: Backup Your Wiki ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Create dated backup&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/export_wiki.sh&lt;br /&gt;
&lt;br /&gt;
# Copy to safe location&lt;br /&gt;
cp ~/wiki-container/*_wiki_export.xml ~/backups/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6.3: Restore from Backup ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Place backup in shared directory&lt;br /&gt;
cp ~/backups/20250101_wiki_export.xml ~/wiki-container/&lt;br /&gt;
&lt;br /&gt;
# Import the backup&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/import_wiki.sh /export/20250101_wiki_export.xml&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6.4: Complete Reset ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Stop and remove container&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
&lt;br /&gt;
# Remove volumes (WARNING: This deletes all data!)&lt;br /&gt;
docker volume rm completenoobs_mysql completenoobs_images&lt;br /&gt;
&lt;br /&gt;
# Rebuild and start fresh&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v ~/wiki-container:/export \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/wiki:v0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Permission Issues ===&lt;br /&gt;
If you encounter permission errors with the shared directory:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Fix permissions on host&lt;br /&gt;
chmod 777 ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Import Failures ===&lt;br /&gt;
If imports fail, check:&lt;br /&gt;
* File exists in ~/wiki-container&lt;br /&gt;
* File is valid XML format&lt;br /&gt;
* Sufficient disk space&lt;br /&gt;
* Check debug log: &amp;lt;code&amp;gt;docker exec completenoobs_wiki tail /tmp/mediawiki-debug.log&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Container Won&#039;t Start ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Check logs&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&lt;br /&gt;
# Try interactive mode&lt;br /&gt;
docker run -it --rm completenoobs/wiki:v0.2 bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
This setup provides:&lt;br /&gt;
* Clean MediaWiki 1.44 installation&lt;br /&gt;
* Essential extensions (PageNotice, YouTube, SyntaxHighlight)&lt;br /&gt;
* Simple import/export via ~/wiki-container directory&lt;br /&gt;
* No automatic updates - full control over your content&lt;br /&gt;
* Easy backup and restore capabilities&lt;br /&gt;
&lt;br /&gt;
The shared directory approach gives you complete control over when and what to import/export, making it ideal for:&lt;br /&gt;
* Migrating content between wikis&lt;br /&gt;
* Regular backups&lt;br /&gt;
* Sharing wiki content&lt;br /&gt;
* Testing imports before applying to production&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Install&amp;diff=676</id>
		<title>CompleteNoobs Docker Image Install</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Install&amp;diff=676"/>
		<updated>2025-09-02T14:36:54Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
* [[CompleteNoobs_Docker_Image_0.2_Install_Basics| cnoobs-wiki 0.2 basic install guide]]&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_0.2_Install_Basics&amp;diff=675</id>
		<title>CompleteNoobs Docker Image 0.2 Install Basics</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_0.2_Install_Basics&amp;diff=675"/>
		<updated>2025-09-02T14:31:39Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* Create directory for file sharing */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
* Requires docker installed on system&lt;br /&gt;
&lt;br /&gt;
== Download image==&lt;br /&gt;
* Download the completenoobs container image.&lt;br /&gt;
&amp;lt;code&amp;gt;docker pull completenoobs/cnoobs-wiki:0.2&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Create directory for file sharing ==&lt;br /&gt;
* If you do not create this directory - docker will create one for you when you run the &amp;lt;code&amp;gt;docker run&amp;lt;/code&amp;gt; command.&lt;br /&gt;
** If docker run creates the directory, you will not have permissions to send data to that directory.&lt;br /&gt;
** Change permission&#039;s if that happens &amp;lt;code&amp;gt;sudo chmod 777 ~/wiki-container&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;mkdir ~/wiki-container&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Run image ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v ~/wiki-container:/export \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/cnoobs-wiki:0.2&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Download XMl ==&lt;br /&gt;
&lt;br /&gt;
* https://xml.completenoobs.com/xmlDumps/&lt;br /&gt;
Download an xml file, like:&amp;lt;code&amp;gt;01_09_25.Noobs.xml&amp;lt;/code&amp;gt;&lt;br /&gt;
* Move to &amp;lt;code&amp;gt;~/wiki-container/&amp;lt;/code&amp;gt; directory&lt;br /&gt;
&lt;br /&gt;
== Import XML ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker exec completenoobs_wiki /var/www/html/import_wiki.sh /export/01_09_25.Noobs.xml&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Check Wiki ==&lt;br /&gt;
* In browser go to this address&lt;br /&gt;
&amp;lt;code&amp;gt;http://localhost:8080&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
{{:Restore_the_completenoobs_Main_Page}}&lt;br /&gt;
&lt;br /&gt;
==Change Admin Password==&lt;br /&gt;
&lt;br /&gt;
* Can be done with &#039;Web Browser GUI&#039; or &#039;Terminal&#039;&lt;br /&gt;
* Default Password for user &amp;lt;b&amp;gt;Admin&amp;lt;/b&amp;gt; = &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;&lt;br /&gt;
=== Via Web Interface ===&lt;br /&gt;
1. Login at http://localhost:8080&lt;br /&gt;
2. Click username → Preferences&lt;br /&gt;
3. Go to Password tab&lt;br /&gt;
4. Change password&lt;br /&gt;
&lt;br /&gt;
=== Via Terminal ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Export a backup of your wiki==&lt;br /&gt;
* This will create a backup of your wiki to a dated.xml file &amp;lt;code&amp;gt;20250902_wiki_export.xml&amp;lt;/code&amp;gt;, which you can find in your &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory on host, or &amp;lt;code&amp;gt;/export/&amp;lt;/code&amp;gt; directory in container.&lt;br /&gt;
&amp;lt;code&amp;gt;docker exec completenoobs_wiki /var/www/html/export_wiki.sh&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Remove container==&lt;br /&gt;
&lt;br /&gt;
To completely remove the &amp;lt;code&amp;gt;completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; container and image from your Ubuntu 24.04 system, follow these steps. &amp;lt;br&amp;gt;&lt;br /&gt;
You can also remove associated persistent storage volumes if they were created.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 1: Stop and Remove the Container&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If the container is running, stop it and then remove it.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Alternatively, stop and remove in one command:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rm -f completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 2: Remove the Docker Image&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Remove the &amp;lt;code&amp;gt;completenoobs/cnoobs-wiki:0.2&amp;lt;/code&amp;gt; image.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rmi completenoobs/cnoobs-wiki:0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Note: If the image is in use by other containers, remove those containers first or use &amp;lt;code&amp;gt;docker rmi -f completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; to force removal (use with caution).&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 3: Remove Persistent Storage Volumes (Optional)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you used persistent storage, remove the associated volumes to free up space.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume rm completenoobs_mysql completenoobs_images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Note: Ensure no other containers are using these volumes, as this will delete all stored data.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 4: Verify Removal&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Check that the container, image, and volumes are removed.&amp;lt;br&amp;gt;&lt;br /&gt;
- List all containers (including stopped ones):&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps -a&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- List all images:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- List all volumes:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
If any items remain, repeat the relevant removal commands or check for dependencies.&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_0.2_Install_Basics&amp;diff=674</id>
		<title>CompleteNoobs Docker Image 0.2 Install Basics</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_0.2_Install_Basics&amp;diff=674"/>
		<updated>2025-09-02T14:31:06Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot; * Requires docker installed on system  == Download image== * Download the completenoobs container image. &amp;lt;code&amp;gt;docker pull completenoobs/cnoobs-wiki:0.2&amp;lt;/code&amp;gt;  == Create directory for file sharing == * If you do not create this file - docker will create one for you when you run the &amp;lt;code&amp;gt;docker run&amp;lt;/code&amp;gt; command. ** If docker run creates the file, you will not have permissions to send data to that directory. ** Change permission&amp;#039;s if that happens &amp;lt;code&amp;gt;sudo chmod 777...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
* Requires docker installed on system&lt;br /&gt;
&lt;br /&gt;
== Download image==&lt;br /&gt;
* Download the completenoobs container image.&lt;br /&gt;
&amp;lt;code&amp;gt;docker pull completenoobs/cnoobs-wiki:0.2&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Create directory for file sharing ==&lt;br /&gt;
* If you do not create this file - docker will create one for you when you run the &amp;lt;code&amp;gt;docker run&amp;lt;/code&amp;gt; command.&lt;br /&gt;
** If docker run creates the file, you will not have permissions to send data to that directory.&lt;br /&gt;
** Change permission&#039;s if that happens &amp;lt;code&amp;gt;sudo chmod 777 ~/wiki-container&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;mkdir ~/wiki-container&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Run image ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v ~/wiki-container:/export \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/cnoobs-wiki:0.2&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Download XMl ==&lt;br /&gt;
&lt;br /&gt;
* https://xml.completenoobs.com/xmlDumps/&lt;br /&gt;
Download an xml file, like:&amp;lt;code&amp;gt;01_09_25.Noobs.xml&amp;lt;/code&amp;gt;&lt;br /&gt;
* Move to &amp;lt;code&amp;gt;~/wiki-container/&amp;lt;/code&amp;gt; directory&lt;br /&gt;
&lt;br /&gt;
== Import XML ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker exec completenoobs_wiki /var/www/html/import_wiki.sh /export/01_09_25.Noobs.xml&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Check Wiki ==&lt;br /&gt;
* In browser go to this address&lt;br /&gt;
&amp;lt;code&amp;gt;http://localhost:8080&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
{{:Restore_the_completenoobs_Main_Page}}&lt;br /&gt;
&lt;br /&gt;
==Change Admin Password==&lt;br /&gt;
&lt;br /&gt;
* Can be done with &#039;Web Browser GUI&#039; or &#039;Terminal&#039;&lt;br /&gt;
* Default Password for user &amp;lt;b&amp;gt;Admin&amp;lt;/b&amp;gt; = &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;&lt;br /&gt;
=== Via Web Interface ===&lt;br /&gt;
1. Login at http://localhost:8080&lt;br /&gt;
2. Click username → Preferences&lt;br /&gt;
3. Go to Password tab&lt;br /&gt;
4. Change password&lt;br /&gt;
&lt;br /&gt;
=== Via Terminal ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==Export a backup of your wiki==&lt;br /&gt;
* This will create a backup of your wiki to a dated.xml file &amp;lt;code&amp;gt;20250902_wiki_export.xml&amp;lt;/code&amp;gt;, which you can find in your &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory on host, or &amp;lt;code&amp;gt;/export/&amp;lt;/code&amp;gt; directory in container.&lt;br /&gt;
&amp;lt;code&amp;gt;docker exec completenoobs_wiki /var/www/html/export_wiki.sh&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Remove container==&lt;br /&gt;
&lt;br /&gt;
To completely remove the &amp;lt;code&amp;gt;completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; container and image from your Ubuntu 24.04 system, follow these steps. &amp;lt;br&amp;gt;&lt;br /&gt;
You can also remove associated persistent storage volumes if they were created.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 1: Stop and Remove the Container&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If the container is running, stop it and then remove it.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Alternatively, stop and remove in one command:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rm -f completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 2: Remove the Docker Image&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Remove the &amp;lt;code&amp;gt;completenoobs/cnoobs-wiki:0.2&amp;lt;/code&amp;gt; image.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rmi completenoobs/cnoobs-wiki:0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Note: If the image is in use by other containers, remove those containers first or use &amp;lt;code&amp;gt;docker rmi -f completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; to force removal (use with caution).&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 3: Remove Persistent Storage Volumes (Optional)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you used persistent storage, remove the associated volumes to free up space.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume rm completenoobs_mysql completenoobs_images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Note: Ensure no other containers are using these volumes, as this will delete all stored data.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 4: Verify Removal&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Check that the container, image, and volumes are removed.&amp;lt;br&amp;gt;&lt;br /&gt;
- List all containers (including stopped ones):&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps -a&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- List all images:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- List all volumes:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
If any items remain, repeat the relevant removal commands or check for dependencies.&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Install&amp;diff=673</id>
		<title>CompleteNoobs Docker Image Install</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Install&amp;diff=673"/>
		<updated>2025-09-02T12:27:21Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot;placeholder&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;placeholder&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_2&amp;diff=672</id>
		<title>CompleteNoobs Docker Image 2</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_2&amp;diff=672"/>
		<updated>2025-09-02T12:24:39Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot; ==todo== * Use Dir for import export of xml. * Min scripts - just setup mediawiki with extensions.  = Complete Noobs Docker Wiki Tutorial v0.2 = * Version 0.2 - Simplified with manual import/export *  Docker install guide  == Overview == This tutorial creates a MediaWiki Docker container with: * MediaWiki 1.44 * PageNotice extension (for license notices) * YouTube extension (for video embedding) * Shared directory for XML import/export  == Prere...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
==todo==&lt;br /&gt;
* Use Dir for import export of xml.&lt;br /&gt;
* Min scripts - just setup mediawiki with extensions.&lt;br /&gt;
&lt;br /&gt;
= Complete Noobs Docker Wiki Tutorial v0.2 =&lt;br /&gt;
* Version 0.2 - Simplified with manual import/export&lt;br /&gt;
* [[Docker_Install_Guide| Docker install guide]]&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
This tutorial creates a MediaWiki Docker container with:&lt;br /&gt;
* MediaWiki 1.44&lt;br /&gt;
* PageNotice extension (for license notices)&lt;br /&gt;
* YouTube extension (for video embedding)&lt;br /&gt;
* Shared directory for XML import/export&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
* Ubuntu 24.04&lt;br /&gt;
* Docker installed and running&lt;br /&gt;
* Your user in docker group: &amp;lt;code&amp;gt;sudo usermod -aG docker $USER&amp;lt;/code&amp;gt; (then logout/login)&lt;br /&gt;
&lt;br /&gt;
== Step 1: Create Directory Structure ==&lt;br /&gt;
&lt;br /&gt;
=== 1.1: Create Project Directory ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir ~/completenoobs-docker-v2&lt;br /&gt;
cd ~/completenoobs-docker-v2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 1.2: Create Shared Directory ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This directory will be used for importing and exporting XML files.&lt;br /&gt;
&lt;br /&gt;
== Step 2: Create All Files ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1: Dockerfile ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM mediawiki:1.44&lt;br /&gt;
&lt;br /&gt;
# Install dependencies&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \&lt;br /&gt;
    mariadb-server \&lt;br /&gt;
    python3-pygments \&lt;br /&gt;
    curl \&lt;br /&gt;
    wget \&lt;br /&gt;
    unzip \&lt;br /&gt;
    nano \&lt;br /&gt;
    vim \&lt;br /&gt;
    git \&lt;br /&gt;
    &amp;amp;&amp;amp; apt-get clean&lt;br /&gt;
&lt;br /&gt;
# Copy setup script&lt;br /&gt;
COPY setup_wiki.sh /usr/src/setup_wiki.sh&lt;br /&gt;
COPY entrypoint.sh /entrypoint.sh&lt;br /&gt;
&lt;br /&gt;
# Make executable&lt;br /&gt;
RUN chmod +x /usr/src/setup_wiki.sh /entrypoint.sh&lt;br /&gt;
&lt;br /&gt;
# Setup wiki&lt;br /&gt;
RUN /usr/src/setup_wiki.sh&lt;br /&gt;
&lt;br /&gt;
EXPOSE 80&lt;br /&gt;
VOLUME /var/lib/mysql&lt;br /&gt;
VOLUME /var/www/html/images&lt;br /&gt;
VOLUME /export&lt;br /&gt;
&lt;br /&gt;
ENTRYPOINT [&amp;quot;/entrypoint.sh&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.2: Main Setup Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano setup_wiki.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Setting up CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Initialize MariaDB&lt;br /&gt;
if [ ! -d &amp;quot;/var/lib/mysql/mysql&amp;quot; ]; then&lt;br /&gt;
    mysql_install_db --user=mysql --datadir=/var/lib/mysql&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 2&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
# Setup database&lt;br /&gt;
mysql -e &amp;quot;CREATE DATABASE IF NOT EXISTS completenoobs_wiki CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;127.0.0.1&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;127.0.0.1&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;localhost&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;localhost&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;FLUSH PRIVILEGES;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Install MediaWiki&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
php maintenance/install.php \&lt;br /&gt;
    --dbtype=mysql \&lt;br /&gt;
    --dbserver=127.0.0.1 \&lt;br /&gt;
    --dbname=completenoobs_wiki \&lt;br /&gt;
    --dbuser=wikiuser \&lt;br /&gt;
    --dbpass=wikipass \&lt;br /&gt;
    --server=&amp;quot;http://localhost:8080&amp;quot; \&lt;br /&gt;
    --scriptpath=&amp;quot;&amp;quot; \&lt;br /&gt;
    --lang=en \&lt;br /&gt;
    --pass=AdminPass123! \&lt;br /&gt;
    &amp;quot;CompleteNoobs Wiki&amp;quot; \&lt;br /&gt;
    &amp;quot;admin&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Download and install extensions&lt;br /&gt;
cd extensions/&lt;br /&gt;
echo &amp;quot;Installing PageNotice extension...&amp;quot;&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/PageNotice --branch REL1_44 || echo &amp;quot;PageNotice download failed, continuing...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Installing YouTube extension...&amp;quot;&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/YouTube --branch REL1_44 || echo &amp;quot;YouTube download failed, continuing...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
&lt;br /&gt;
# Configure LocalSettings.php&lt;br /&gt;
cat &amp;gt;&amp;gt; LocalSettings.php &amp;lt;&amp;lt; &#039;EOF&#039;&lt;br /&gt;
&lt;br /&gt;
# Basic settings&lt;br /&gt;
$wgEnableUploads = true;&lt;br /&gt;
$wgUseImageMagick = true;&lt;br /&gt;
$wgImageMagickConvertCommand = &amp;quot;/usr/bin/convert&amp;quot;;&lt;br /&gt;
$wgDefaultSkin = &amp;quot;vector-2022&amp;quot;;&lt;br /&gt;
$wgAllowExternalImages = true;&lt;br /&gt;
&lt;br /&gt;
# Debug settings (remove in production)&lt;br /&gt;
$wgShowExceptionDetails = true;&lt;br /&gt;
$wgDebugLogFile = &amp;quot;/tmp/mediawiki-debug.log&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
# PageNotice extension&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/PageNotice/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;PageNotice&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# YouTube extension&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/YouTube/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;YouTube&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# SyntaxHighlight (usually bundled)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/SyntaxHighlight_GeSHi/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;SyntaxHighlight_GeSHi&#039; );&lt;br /&gt;
    $wgPygmentizePath = &#039;/usr/bin/pygmentize&#039;;&lt;br /&gt;
}&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
# Create helper scripts&lt;br /&gt;
cat &amp;gt; /var/www/html/export_wiki.sh &amp;lt;&amp;lt; &#039;EXPORT_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
echo &amp;quot;=== Wiki Export Tool ===&amp;quot;&lt;br /&gt;
DATE=$(date +%Y%m%d)&lt;br /&gt;
OUTPUT_FILE=&amp;quot;/export/${DATE}_wiki_export.xml&amp;quot;&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Exporting wiki to: $OUTPUT_FILE&amp;quot;&lt;br /&gt;
php /var/www/html/maintenance/run.php dumpBackup.php --full --output=file:$OUTPUT_FILE&lt;br /&gt;
&lt;br /&gt;
if [ -f &amp;quot;$OUTPUT_FILE&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Export successful!&amp;quot;&lt;br /&gt;
    echo &amp;quot;File saved to: $OUTPUT_FILE&amp;quot;&lt;br /&gt;
    echo &amp;quot;On host system: ~/wiki-container/${DATE}_wiki_export.xml&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;Export failed!&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
EXPORT_EOF&lt;br /&gt;
chmod +x /var/www/html/export_wiki.sh&lt;br /&gt;
&lt;br /&gt;
cat &amp;gt; /var/www/html/import_wiki.sh &amp;lt;&amp;lt; &#039;IMPORT_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
echo &amp;quot;=== Wiki Import Tool ===&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Available XML files in /export:&amp;quot;&lt;br /&gt;
ls -la /export/*.xml 2&amp;gt;/dev/null || echo &amp;quot;No XML files found&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
if [ -z &amp;quot;$1&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Usage: /var/www/html/import_wiki.sh &amp;lt;filename&amp;gt;&amp;quot;&lt;br /&gt;
    echo &amp;quot;Example: /var/www/html/import_wiki.sh /export/wiki.xml&amp;quot;&lt;br /&gt;
    exit 1&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
if [ ! -f &amp;quot;$1&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Error: File $1 not found!&amp;quot;&lt;br /&gt;
    exit 1&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Importing from: $1&amp;quot;&lt;br /&gt;
echo &amp;quot;This may take several minutes...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
php /var/www/html/maintenance/run.php importDump.php &amp;quot;$1&amp;quot;&lt;br /&gt;
&lt;br /&gt;
if [ $? -eq 0 ]; then&lt;br /&gt;
    echo &amp;quot;Import completed!&amp;quot;&lt;br /&gt;
    echo &amp;quot;Rebuilding indexes...&amp;quot;&lt;br /&gt;
    php /var/www/html/maintenance/run.php rebuildrecentchanges.php&lt;br /&gt;
    php /var/www/html/maintenance/run.php initSiteStats.php&lt;br /&gt;
    echo &amp;quot;Done!&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;Import failed! Check /tmp/mediawiki-debug.log for details&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
IMPORT_EOF&lt;br /&gt;
chmod +x /var/www/html/import_wiki.sh&lt;br /&gt;
&lt;br /&gt;
# Create status check script&lt;br /&gt;
cat &amp;gt; /var/www/html/check_status.sh &amp;lt;&amp;lt; &#039;STATUS_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
echo &amp;quot;=== Wiki Status ===&amp;quot;&lt;br /&gt;
echo &amp;quot;Pages: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;Users: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM user WHERE user_id &amp;gt; 0;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Extensions ===&amp;quot;&lt;br /&gt;
if [ -d &amp;quot;extensions/PageNotice&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;PageNotice: Installed ✓&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;PageNotice: Not installed ✗&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/YouTube&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;YouTube: Installed ✓&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;YouTube: Not installed ✗&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/SyntaxHighlight_GeSHi&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;SyntaxHighlight: Installed ✓&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;SyntaxHighlight: Not installed ✗&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Shared Directory ===&amp;quot;&lt;br /&gt;
if [ -d &amp;quot;/export&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;/export mounted ✓&amp;quot;&lt;br /&gt;
    echo &amp;quot;Files in /export:&amp;quot;&lt;br /&gt;
    ls -la /export/*.xml 2&amp;gt;/dev/null || echo &amp;quot;  No XML files&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;/export not mounted ✗&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
STATUS_EOF&lt;br /&gt;
chmod +x /var/www/html/check_status.sh&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Setup completed!&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin: admin / AdminPass123!&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb stop&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.3: Entrypoint Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano entrypoint.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Starting CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 1&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=========================================&amp;quot;&lt;br /&gt;
echo &amp;quot;CompleteNoobs Wiki v0.2 Ready!&amp;quot;&lt;br /&gt;
echo &amp;quot;=========================================&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Access: http://localhost:8080&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Features:&amp;quot;&lt;br /&gt;
echo &amp;quot;- MediaWiki 1.44&amp;quot;&lt;br /&gt;
echo &amp;quot;- PageNotice extension&amp;quot;&lt;br /&gt;
echo &amp;quot;- YouTube extension&amp;quot;&lt;br /&gt;
echo &amp;quot;- SyntaxHighlight extension&amp;quot;&lt;br /&gt;
echo &amp;quot;- Import/Export via ~/wiki-container&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Available commands:&amp;quot;&lt;br /&gt;
echo &amp;quot;- Export wiki: docker exec completenoobs_wiki /var/www/html/export_wiki.sh&amp;quot;&lt;br /&gt;
echo &amp;quot;- Import XML: docker exec completenoobs_wiki /var/www/html/import_wiki.sh /export/filename.xml&amp;quot;&lt;br /&gt;
echo &amp;quot;- Check status: docker exec completenoobs_wiki /var/www/html/check_status.sh&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
apache2-foreground&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 3: Build and Run ==&lt;br /&gt;
&lt;br /&gt;
=== 3.1: Build the Image ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t completenoobs/wiki:v0.2 .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3.2: Run the Container with Shared Directory ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v ~/wiki-container:/export \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/wiki:v0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 4: Import/Export Operations ==&lt;br /&gt;
&lt;br /&gt;
=== 4.1: Import an XML File ===&lt;br /&gt;
* https://xml.completenoobs.com/xmlDumps/&lt;br /&gt;
==== Place XML file in shared directory ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Copy your XML file to the shared directory&lt;br /&gt;
cp ~/Downloads/completenoobs.xml ~/wiki-container/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Import the file ====&lt;br /&gt;
* All files in host &amp;lt;code&amp;gt;wiki-container&amp;lt;/code&amp;gt; directory will also be in the containers &amp;lt;code&amp;gt;/export/&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Run the import command&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/import_wiki.sh /export/completenoobs.xml&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{:Restore_the_completenoobs_Main_Page}}&lt;br /&gt;
&lt;br /&gt;
=== 4.2: Export Your Wiki ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Export wiki to dated XML file&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/export_wiki.sh&lt;br /&gt;
&lt;br /&gt;
# Check the exported file&lt;br /&gt;
ls -la ~/wiki-container/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.3: Manual Import/Export (Advanced) ===&lt;br /&gt;
&lt;br /&gt;
==== Access container shell ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Manual export with custom filename ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php dumpBackup.php --full --output=file:/export/my_wiki_backup.xml&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Manual import with options ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Basic import&lt;br /&gt;
php /var/www/html/maintenance/run.php importDump.php /export/wiki.xml&lt;br /&gt;
&lt;br /&gt;
# Import with image uploads&lt;br /&gt;
php /var/www/html/maintenance/run.php importDump.php --uploads /export/wiki.xml&lt;br /&gt;
&lt;br /&gt;
# Then rebuild indexes&lt;br /&gt;
php /var/www/html/maintenance/run.php rebuildall.php&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 5: Testing and Verification ==&lt;br /&gt;
&lt;br /&gt;
=== 5.1: Check Wiki Status ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/check_status.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5.2: Test Extensions ===&lt;br /&gt;
1. Visit http://localhost:8080&lt;br /&gt;
2. Login with admin / AdminPass123!&lt;br /&gt;
3. Create or edit a page and test:&lt;br /&gt;
   * YouTube: Add &amp;lt;code&amp;gt;&amp;lt;youtube&amp;gt;VIDEO_ID&amp;lt;/youtube&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
   * SyntaxHighlight: Add &amp;lt;nowiki&amp;gt;&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;print(&amp;quot;Hello&amp;quot;)&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5.3: View Logs ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Container logs&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&lt;br /&gt;
# MediaWiki debug log&lt;br /&gt;
docker exec completenoobs_wiki tail -f /tmp/mediawiki-debug.log&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 6: Common Operations ==&lt;br /&gt;
&lt;br /&gt;
=== 6.1: Change Admin Password ===&lt;br /&gt;
&lt;br /&gt;
==== Via Web Interface ====&lt;br /&gt;
1. Login at http://localhost:8080&lt;br /&gt;
2. Click username → Preferences&lt;br /&gt;
3. Go to Password tab&lt;br /&gt;
4. Change password&lt;br /&gt;
&lt;br /&gt;
==== Via Terminal ====&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6.2: Backup Your Wiki ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Create dated backup&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/export_wiki.sh&lt;br /&gt;
&lt;br /&gt;
# Copy to safe location&lt;br /&gt;
cp ~/wiki-container/*_wiki_export.xml ~/backups/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6.3: Restore from Backup ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Place backup in shared directory&lt;br /&gt;
cp ~/backups/20250101_wiki_export.xml ~/wiki-container/&lt;br /&gt;
&lt;br /&gt;
# Import the backup&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/import_wiki.sh /export/20250101_wiki_export.xml&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 6.4: Complete Reset ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Stop and remove container&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
&lt;br /&gt;
# Remove volumes (WARNING: This deletes all data!)&lt;br /&gt;
docker volume rm completenoobs_mysql completenoobs_images&lt;br /&gt;
&lt;br /&gt;
# Rebuild and start fresh&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v ~/wiki-container:/export \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/wiki:v0.2&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
&lt;br /&gt;
=== Permission Issues ===&lt;br /&gt;
If you encounter permission errors with the shared directory:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Fix permissions on host&lt;br /&gt;
chmod 777 ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Import Failures ===&lt;br /&gt;
If imports fail, check:&lt;br /&gt;
* File exists in ~/wiki-container&lt;br /&gt;
* File is valid XML format&lt;br /&gt;
* Sufficient disk space&lt;br /&gt;
* Check debug log: &amp;lt;code&amp;gt;docker exec completenoobs_wiki tail /tmp/mediawiki-debug.log&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Container Won&#039;t Start ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Check logs&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&lt;br /&gt;
# Try interactive mode&lt;br /&gt;
docker run -it --rm completenoobs/wiki:v0.2 bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Summary ==&lt;br /&gt;
This setup provides:&lt;br /&gt;
* Clean MediaWiki 1.44 installation&lt;br /&gt;
* Essential extensions (PageNotice, YouTube, SyntaxHighlight)&lt;br /&gt;
* Simple import/export via ~/wiki-container directory&lt;br /&gt;
* No automatic updates - full control over your content&lt;br /&gt;
* Easy backup and restore capabilities&lt;br /&gt;
&lt;br /&gt;
The shared directory approach gives you complete control over when and what to import/export, making it ideal for:&lt;br /&gt;
* Migrating content between wikis&lt;br /&gt;
* Regular backups&lt;br /&gt;
* Sharing wiki content&lt;br /&gt;
* Testing imports before applying to production&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=671</id>
		<title>CompleteNoobs Docker Image Creation</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=671"/>
		<updated>2025-09-02T10:53:40Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* Complete Noobs Docker Wiki Tutorial */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Versions =&lt;br /&gt;
* This is version 0.1 - many bugs&lt;br /&gt;
* [[todo| CompleteNoobs Docker Image 0.2]]&lt;br /&gt;
&lt;br /&gt;
= Complete Noobs Docker Wiki Tutorial =&lt;br /&gt;
* [[Docker_Install_Guide| Docker install guide]]&lt;br /&gt;
==errors==&lt;br /&gt;
* This mainly works - just need to fix the extensions popular pages and contrubtion scores&lt;br /&gt;
* The XML updater requires more work - currently idea placeholder&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
* Ubuntu 24.04&lt;br /&gt;
* Docker installed and running&lt;br /&gt;
* Your user in docker group: &amp;lt;code&amp;gt;sudo usermod -aG docker $USER&amp;lt;/code&amp;gt; (then logout/login)&lt;br /&gt;
&lt;br /&gt;
== Step 2: Create All Files ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1: Dockerfile ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Copy this exactly:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM mediawiki:1.44&lt;br /&gt;
# Mediawiki 1.44 used over latest because can confirm extensions youtube and pagenotice works&lt;br /&gt;
# Install dependencies&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \&lt;br /&gt;
    mariadb-server \&lt;br /&gt;
    python3 \&lt;br /&gt;
    python3-requests \&lt;br /&gt;
    python3-bs4 \&lt;br /&gt;
    python3-pygments \&lt;br /&gt;
    curl \&lt;br /&gt;
    wget \&lt;br /&gt;
    unzip \&lt;br /&gt;
    nano \&lt;br /&gt;
    git \&lt;br /&gt;
    &amp;amp;&amp;amp; apt-get clean&lt;br /&gt;
&lt;br /&gt;
# Copy scripts&lt;br /&gt;
COPY download_latest_xml.py /usr/src/download_latest_xml.py&lt;br /&gt;
COPY setup_wiki.sh /usr/src/setup_wiki.sh&lt;br /&gt;
COPY update_xml.sh /usr/src/update_xml.sh&lt;br /&gt;
COPY entrypoint.sh /entrypoint.sh&lt;br /&gt;
&lt;br /&gt;
# Make executable&lt;br /&gt;
RUN chmod +x /usr/src/setup_wiki.sh /entrypoint.sh /usr/src/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Download XML&lt;br /&gt;
RUN python3 /usr/src/download_latest_xml.py&lt;br /&gt;
&lt;br /&gt;
# Setup wiki&lt;br /&gt;
RUN /usr/src/setup_wiki.sh&lt;br /&gt;
&lt;br /&gt;
EXPOSE 80&lt;br /&gt;
VOLUME /var/lib/mysql&lt;br /&gt;
VOLUME /var/www/html/images&lt;br /&gt;
&lt;br /&gt;
ENTRYPOINT [&amp;quot;/entrypoint.sh&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.2: XML Download Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano download_latest_xml.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_available_dumps():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        return sorted(dumps, key=parse_date_from_dump, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dumps: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def get_dump_files(dump):&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(f&amp;quot;{BASE_URL}{dump}/&amp;quot;, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
        return sorted(files, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dump files: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def download_file(url, filename):&lt;br /&gt;
    try:&lt;br /&gt;
        print(f&amp;quot;Downloading {filename}...&amp;quot;)&lt;br /&gt;
        response = requests.get(url, stream=True, timeout=60)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        &lt;br /&gt;
        total_size = int(response.headers.get(&#039;content-length&#039;, 0))&lt;br /&gt;
        downloaded = 0&lt;br /&gt;
        &lt;br /&gt;
        with open(filename, &#039;wb&#039;) as f:&lt;br /&gt;
            for chunk in response.iter_content(chunk_size=8192):&lt;br /&gt;
                if chunk:&lt;br /&gt;
                    f.write(chunk)&lt;br /&gt;
                    downloaded += len(chunk)&lt;br /&gt;
                    if total_size &amp;gt; 0:&lt;br /&gt;
                        progress = (downloaded / total_size) * 100&lt;br /&gt;
                        print(f&amp;quot;\rProgress: {progress:.1f}%&amp;quot;, end=&#039;&#039;, flush=True)&lt;br /&gt;
        print()&lt;br /&gt;
        return True&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error downloading {filename}: {e}&amp;quot;)&lt;br /&gt;
        return False&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    print(&amp;quot;Fetching available XML dumps...&amp;quot;)&lt;br /&gt;
    dumps = get_available_dumps()&lt;br /&gt;
    &lt;br /&gt;
    if not dumps:&lt;br /&gt;
        print(&amp;quot;No dumps found!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_dump = dumps[0]&lt;br /&gt;
    print(f&amp;quot;Latest dump: {newest_dump}&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    files = get_dump_files(newest_dump)&lt;br /&gt;
    if not files:&lt;br /&gt;
        print(&amp;quot;No XML files found in latest dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_xml = files[0]&lt;br /&gt;
    xml_url = f&amp;quot;{BASE_URL}{newest_dump}/{newest_xml}&amp;quot;&lt;br /&gt;
    local_filename = &amp;quot;/tmp/completenoobs_dump.xml&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if download_file(xml_url, local_filename):&lt;br /&gt;
        print(f&amp;quot;Successfully downloaded {newest_xml}&amp;quot;)&lt;br /&gt;
        with open(&amp;quot;/tmp/dump_info.txt&amp;quot;, &amp;quot;w&amp;quot;) as f:&lt;br /&gt;
            f.write(f&amp;quot;{newest_dump}/{newest_xml}&amp;quot;)&lt;br /&gt;
    else:&lt;br /&gt;
        print(&amp;quot;Failed to download XML dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.3: Main Setup Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano setup_wiki.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Setting up CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Initialize MariaDB&lt;br /&gt;
if [ ! -d &amp;quot;/var/lib/mysql/mysql&amp;quot; ]; then&lt;br /&gt;
    mysql_install_db --user=mysql --datadir=/var/lib/mysql&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 2&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
# Setup database&lt;br /&gt;
mysql -e &amp;quot;CREATE DATABASE IF NOT EXISTS completenoobs_wiki CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;127.0.0.1&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;127.0.0.1&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;localhost&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;localhost&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;FLUSH PRIVILEGES;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Install MediaWiki&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
php maintenance/install.php \&lt;br /&gt;
    --dbtype=mysql \&lt;br /&gt;
    --dbserver=127.0.0.1 \&lt;br /&gt;
    --dbname=completenoobs_wiki \&lt;br /&gt;
    --dbuser=wikiuser \&lt;br /&gt;
    --dbpass=wikipass \&lt;br /&gt;
    --server=&amp;quot;http://localhost:8080&amp;quot; \&lt;br /&gt;
    --scriptpath=&amp;quot;&amp;quot; \&lt;br /&gt;
    --lang=en \&lt;br /&gt;
    --pass=AdminPass123! \&lt;br /&gt;
    &amp;quot;CompleteNoobs Wiki&amp;quot; \&lt;br /&gt;
    &amp;quot;admin&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Download and install extensions&lt;br /&gt;
cd extensions/&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/PageNotice --branch REL1_44 || echo &amp;quot;PageNotice download failed, continuing...&amp;quot;&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/YouTube --branch REL1_44 || echo &amp;quot;YouTube download failed, continuing...&amp;quot;&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
&lt;br /&gt;
# Configure LocalSettings.php&lt;br /&gt;
cat &amp;gt;&amp;gt; LocalSettings.php &amp;lt;&amp;lt; &#039;EOF&#039;&lt;br /&gt;
# Basic settings&lt;br /&gt;
$wgEnableUploads = true;&lt;br /&gt;
$wgUseImageMagick = true;&lt;br /&gt;
$wgImageMagickConvertCommand = &amp;quot;/usr/bin/convert&amp;quot;;&lt;br /&gt;
$wgDefaultSkin = &amp;quot;vector-2022&amp;quot;;&lt;br /&gt;
$wgAllowExternalImages = true;&lt;br /&gt;
&lt;br /&gt;
# Debug (can be removed later)&lt;br /&gt;
$wgShowExceptionDetails = true;&lt;br /&gt;
$wgDebugLogFile = &amp;quot;/tmp/mediawiki-debug.log&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
# PageNotice extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/PageNotice/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;PageNotice&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# YouTube extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/YouTube/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;YouTube&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# SyntaxHighlight (usually bundled)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/SyntaxHighlight_GeSHi/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;SyntaxHighlight_GeSHi&#039; );&lt;br /&gt;
    $wgPygmentizePath = &#039;/usr/bin/pygmentize&#039;;&lt;br /&gt;
}&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
# Import XML dump if available&lt;br /&gt;
if [ -f &amp;quot;/tmp/completenoobs_dump.xml&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Importing XML dump...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if php maintenance/importDump.php --uploads &amp;lt; /tmp/completenoobs_dump.xml; then&lt;br /&gt;
        echo &amp;quot;XML import completed!&amp;quot;&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;XML import had warnings&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Basic maintenance&lt;br /&gt;
    php maintenance/update.php --quick || echo &amp;quot;Update completed with warnings&amp;quot;&lt;br /&gt;
    php maintenance/rebuildrecentchanges.php || echo &amp;quot;RecentChanges rebuilt with warnings&amp;quot;&lt;br /&gt;
    php maintenance/initSiteStats.php || echo &amp;quot;SiteStats initialized with warnings&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if [ -f &amp;quot;/tmp/dump_info.txt&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Import: $(cat /tmp/dump_info.txt)&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
        echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
    fi&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No XML dump found - starting with empty wiki&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
# Copy update script to accessible location&lt;br /&gt;
cp /usr/src/update_xml.sh /var/www/html/update_xml.sh&lt;br /&gt;
chmod +x /var/www/html/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Create user-friendly update wrapper&lt;br /&gt;
cat &amp;gt; /var/www/html/check_updates.sh &amp;lt;&amp;lt; &#039;UPDATE_WRAPPER_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki Update Checker ===&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;This tool checks for new content from the CompleteNoobs XML repository&amp;quot;&lt;br /&gt;
echo &amp;quot;and imports ONLY new pages, preserving all your local edits.&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
/var/www/html/update_xml.sh&lt;br /&gt;
UPDATE_WRAPPER_EOF&lt;br /&gt;
chmod +x /var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# Create simple status script&lt;br /&gt;
cat &amp;gt; /var/www/html/check_status.sh &amp;lt;&amp;lt; &#039;STATUS_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
echo &amp;quot;=== Wiki Status ===&amp;quot;&lt;br /&gt;
echo &amp;quot;Pages: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;Users: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM user WHERE user_id &amp;gt; 0;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Extensions ===&amp;quot;&lt;br /&gt;
if [ -d &amp;quot;extensions/PageNotice&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;PageNotice: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;PageNotice: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/YouTube&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;YouTube: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;YouTube: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/ContributionScores&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;ContributionScores: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;ContributionScores: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Update System ===&amp;quot;&lt;br /&gt;
if [ -f &amp;quot;.last_import&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Current version: $(grep &#039;Import:&#039; .last_import | cut -d&#039; &#039; -f2)&amp;quot;&lt;br /&gt;
    echo &amp;quot;Import date: $(grep &#039;Date:&#039; .last_import | cut -d&#039; &#039; -f2-)&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No version info available&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
STATUS_EOF&lt;br /&gt;
chmod +x /var/www/html/check_status.sh&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Setup completed!&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;Update scripts installed:&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/check_updates.sh (user-friendly)&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/update_xml.sh (direct)&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Final counts&lt;br /&gt;
PAGES=$(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;0&amp;quot;)&lt;br /&gt;
echo &amp;quot;Pages imported: $PAGES&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb stop&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.4: XML Update Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano update_xml.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki XML Updater ===&amp;quot;&lt;br /&gt;
echo &amp;quot;This will check for new and updated pages&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Function to check if running in container&lt;br /&gt;
check_environment() {&lt;br /&gt;
    if [ ! -f &amp;quot;/var/www/html/LocalSettings.php&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Error: This script must be run inside the wiki container&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to get current XML version&lt;br /&gt;
get_current_version() {&lt;br /&gt;
    if [ -f &amp;quot;/var/www/html/.last_import&amp;quot; ]; then&lt;br /&gt;
        grep &amp;quot;Import:&amp;quot; /var/www/html/.last_import | cut -d&#039; &#039; -f2&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;none&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to check for updates&lt;br /&gt;
check_for_updates() {&lt;br /&gt;
    python3 - &amp;lt;&amp;lt; &#039;PYTHON_EOF&#039;&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_latest_dump():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        if dumps:&lt;br /&gt;
            latest = sorted(dumps, key=parse_date_from_dump, reverse=True)[0]&lt;br /&gt;
            &lt;br /&gt;
            # Get XML files from latest dump&lt;br /&gt;
            response = requests.get(f&amp;quot;{BASE_URL}{latest}/&amp;quot;, timeout=30)&lt;br /&gt;
            response.raise_for_status()&lt;br /&gt;
            soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
            files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                    if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
            &lt;br /&gt;
            if files:&lt;br /&gt;
                newest_xml = sorted(files, reverse=True)[0]&lt;br /&gt;
                print(f&amp;quot;{latest}/{newest_xml}&amp;quot;)&lt;br /&gt;
                sys.exit(0)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;ERROR: {e}&amp;quot;, file=sys.stderr)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
    &lt;br /&gt;
    print(&amp;quot;ERROR: No dumps found&amp;quot;, file=sys.stderr)&lt;br /&gt;
    sys.exit(1)&lt;br /&gt;
&lt;br /&gt;
get_latest_dump()&lt;br /&gt;
PYTHON_EOF&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to download new XML&lt;br /&gt;
download_xml() {&lt;br /&gt;
    local dump_info=&amp;quot;$1&amp;quot;&lt;br /&gt;
    local dump_dir=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f1)&lt;br /&gt;
    local xml_file=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f2)&lt;br /&gt;
    local url=&amp;quot;https://xml.completenoobs.com/xmlDumps/${dump_info}&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Downloading: $xml_file&amp;quot;&lt;br /&gt;
    echo &amp;quot;From: $dump_dir&amp;quot;&lt;br /&gt;
    echo &amp;quot;URL: $url&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if wget -O /tmp/new_dump.xml &amp;quot;$url&amp;quot; --progress=bar:force 2&amp;gt;&amp;amp;1; then&lt;br /&gt;
        echo &amp;quot;Download successful!&amp;quot;&lt;br /&gt;
        return 0&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;Download failed!&amp;quot;&lt;br /&gt;
        return 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to backup current database&lt;br /&gt;
backup_database() {&lt;br /&gt;
    echo &amp;quot;Creating database backup...&amp;quot;&lt;br /&gt;
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)&lt;br /&gt;
    mysqldump --user=wikiuser --password=wikipass completenoobs_wiki &amp;gt; /tmp/wiki_backup_${TIMESTAMP}.sql&lt;br /&gt;
    echo &amp;quot;Backup created: /tmp/wiki_backup_${TIMESTAMP}.sql&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to analyze and import changes&lt;br /&gt;
analyze_and_import() {&lt;br /&gt;
    echo &amp;quot;Analyzing differences between XML and local wiki...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Create analysis and import script for MediaWiki 1.44+&lt;br /&gt;
    cat &amp;gt; /tmp/analyze_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class AnalyzeAndImport extends Maintenance {&lt;br /&gt;
    private $db;&lt;br /&gt;
    private $new_pages = [];&lt;br /&gt;
    private $changed_pages = [];&lt;br /&gt;
    private $unchanged_pages = [];&lt;br /&gt;
    &lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addDescription(&#039;Analyze and selectively import from XML dump&#039;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $this-&amp;gt;db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // MediaWiki 1.35+ uses slots and content tables&lt;br /&gt;
        // Get existing pages with their content&lt;br /&gt;
        $query = &amp;quot;&lt;br /&gt;
            SELECT p.page_title, p.page_id, c.content_address, c.content_sha1&lt;br /&gt;
            FROM page p&lt;br /&gt;
            JOIN revision r ON p.page_latest = r.rev_id&lt;br /&gt;
            JOIN slots s ON r.rev_id = s.slot_revision_id&lt;br /&gt;
            JOIN slot_roles sr ON s.slot_role_id = sr.role_id&lt;br /&gt;
            JOIN content c ON s.slot_content_id = c.content_id&lt;br /&gt;
            WHERE p.page_namespace = 0 AND sr.role_name = &#039;main&#039;&lt;br /&gt;
        &amp;quot;;&lt;br /&gt;
        &lt;br /&gt;
        $result = $this-&amp;gt;db-&amp;gt;query($query);&lt;br /&gt;
        if (!$result) {&lt;br /&gt;
            $this-&amp;gt;error(&amp;quot;Database query failed: &amp;quot; . $this-&amp;gt;db-&amp;gt;error);&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $existing = [];&lt;br /&gt;
        while ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
            // Get actual text content&lt;br /&gt;
            $text_content = $this-&amp;gt;getTextContent($row[&#039;content_address&#039;]);&lt;br /&gt;
            $existing[$row[&#039;page_title&#039;]] = [&lt;br /&gt;
                &#039;id&#039; =&amp;gt; $row[&#039;page_id&#039;],&lt;br /&gt;
                &#039;content&#039; =&amp;gt; $text_content,&lt;br /&gt;
                &#039;sha1&#039; =&amp;gt; $row[&#039;content_sha1&#039;]&lt;br /&gt;
            ];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Parse XML and compare&lt;br /&gt;
        $xml = simplexml_load_file(&#039;/tmp/new_dump.xml&#039;);&lt;br /&gt;
        &lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            $title = str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title);&lt;br /&gt;
            $xml_content = (string)$page-&amp;gt;revision-&amp;gt;text;&lt;br /&gt;
            &lt;br /&gt;
            if (!isset($existing[$title])) {&lt;br /&gt;
                // New page&lt;br /&gt;
                $this-&amp;gt;new_pages[$title] = $xml_content;&lt;br /&gt;
            } else {&lt;br /&gt;
                // Compare content using SHA1 for efficiency&lt;br /&gt;
                $xml_sha1 = sha1($xml_content);&lt;br /&gt;
                &lt;br /&gt;
                if ($existing[$title][&#039;sha1&#039;] !== $xml_sha1) {&lt;br /&gt;
                    // Content is different&lt;br /&gt;
                    $this-&amp;gt;changed_pages[$title] = [&lt;br /&gt;
                        &#039;local&#039; =&amp;gt; $existing[$title][&#039;content&#039;],&lt;br /&gt;
                        &#039;xml&#039; =&amp;gt; $xml_content,&lt;br /&gt;
                        &#039;page_id&#039; =&amp;gt; $existing[$title][&#039;id&#039;]&lt;br /&gt;
                    ];&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;unchanged_pages[] = $title;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Display summary&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Update Analysis ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages to import: &amp;quot; . count($this-&amp;gt;new_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Changed pages found: &amp;quot; . count($this-&amp;gt;changed_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Unchanged pages: &amp;quot; . count($this-&amp;gt;unchanged_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Save analysis for review&lt;br /&gt;
        file_put_contents(&#039;/tmp/update_analysis.json&#039;, json_encode([&lt;br /&gt;
            &#039;new&#039; =&amp;gt; array_keys($this-&amp;gt;new_pages),&lt;br /&gt;
            &#039;changed&#039; =&amp;gt; array_keys($this-&amp;gt;changed_pages),&lt;br /&gt;
            &#039;unchanged&#039; =&amp;gt; $this-&amp;gt;unchanged_pages&lt;br /&gt;
        ], JSON_PRETTY_PRINT));&lt;br /&gt;
        &lt;br /&gt;
        // Show changed pages with preview (limit to first 20 for readability)&lt;br /&gt;
        if (count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Changed Pages ===\n&amp;quot;);&lt;br /&gt;
            $count = 0;&lt;br /&gt;
            $total_changed = count($this-&amp;gt;changed_pages);&lt;br /&gt;
            &lt;br /&gt;
            foreach ($this-&amp;gt;changed_pages as $title =&amp;gt; $data) {&lt;br /&gt;
                $count++;&lt;br /&gt;
                if ($count &amp;lt;= 20) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;\n$count. $title\n&amp;quot;);&lt;br /&gt;
                    &lt;br /&gt;
                    // Create a simple diff preview (first 300 chars)&lt;br /&gt;
                    $xml_preview = substr($data[&#039;xml&#039;], 0, 100);&lt;br /&gt;
                    &lt;br /&gt;
                    // Save full diff to file&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;=== FULL DIFF FOR: $title ===\n\n&amp;quot;);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- LOCAL VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;local&#039;] . &amp;quot;\n\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- XML VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;xml&#039;], FILE_APPEND);&lt;br /&gt;
                    &lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;   Preview: &amp;quot; . $xml_preview . &amp;quot;...\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ($total_changed &amp;gt; 20) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\n... and &amp;quot; . ($total_changed - 20) . &amp;quot; more changed pages.\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;All diff files saved to /tmp/diff_*.txt\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Interactive selection&lt;br /&gt;
        if (count($this-&amp;gt;new_pages) &amp;gt; 0 || count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Import Options ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;1. Import new pages only (preserve all local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;2. Import new pages + update ALL changed pages (overwrites local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;3. Selective import (choose which updates to apply)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;4. Cancel (no changes)\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            // Save state for import script WITHOUT the XML object&lt;br /&gt;
            $import_data = [&lt;br /&gt;
                &#039;new_pages&#039; =&amp;gt; $this-&amp;gt;new_pages,&lt;br /&gt;
                &#039;changed_pages&#039; =&amp;gt; $this-&amp;gt;changed_pages,&lt;br /&gt;
                &#039;xml_file&#039; =&amp;gt; &#039;/tmp/new_dump.xml&#039;  // Save path instead of object&lt;br /&gt;
            ];&lt;br /&gt;
            &lt;br /&gt;
            file_put_contents(&#039;/tmp/import_data.ser&#039;, serialize($import_data));&lt;br /&gt;
        } else {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\nNo changes detected. Your wiki is up to date!\n&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function getTextContent($address) {&lt;br /&gt;
        // Handle different content storage formats in MW 1.35+&lt;br /&gt;
        if (strpos($address, &#039;tt:&#039;) === 0) {&lt;br /&gt;
            // Text table reference&lt;br /&gt;
            $text_id = substr($address, 3);&lt;br /&gt;
            $result = $this-&amp;gt;db-&amp;gt;query(&amp;quot;SELECT old_text FROM text WHERE old_id = $text_id&amp;quot;);&lt;br /&gt;
            if ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
                return $row[&#039;old_text&#039;];&lt;br /&gt;
            }&lt;br /&gt;
        } elseif (strpos($address, &#039;es:&#039;) === 0) {&lt;br /&gt;
            // External storage - would need special handling&lt;br /&gt;
            return &amp;quot;[External storage content]&amp;quot;;&lt;br /&gt;
        }&lt;br /&gt;
        // Direct content&lt;br /&gt;
        return $address;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = AnalyzeAndImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/analyze_import.php&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to perform selected import&lt;br /&gt;
perform_import() {&lt;br /&gt;
    local choice=$1&lt;br /&gt;
    &lt;br /&gt;
    cat &amp;gt; /tmp/do_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class DoImport extends Maintenance {&lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addOption(&#039;mode&#039;, &#039;Import mode&#039;, true, true);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $mode = $this-&amp;gt;getOption(&#039;mode&#039;);&lt;br /&gt;
        $data = unserialize(file_get_contents(&#039;/tmp/import_data.ser&#039;));&lt;br /&gt;
        &lt;br /&gt;
        // Load XML file&lt;br /&gt;
        $xml = simplexml_load_file($data[&#039;xml_file&#039;]);&lt;br /&gt;
        &lt;br /&gt;
        $new_imported = 0;&lt;br /&gt;
        $updated = 0;&lt;br /&gt;
        &lt;br /&gt;
        // Import new pages (always for modes 1-3)&lt;br /&gt;
        if ($mode != &#039;4&#039;) {&lt;br /&gt;
            foreach ($data[&#039;new_pages&#039;] as $title =&amp;gt; $content) {&lt;br /&gt;
                $this-&amp;gt;importPage($title, $content, $xml);&lt;br /&gt;
                $new_imported++;&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Imported new page: $title\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Handle changed pages based on mode&lt;br /&gt;
        if ($mode == &#039;2&#039;) {&lt;br /&gt;
            // Update all changed pages&lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Updating: $title\n&amp;quot;);&lt;br /&gt;
                if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                    $updated++;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Updated page: $title\n&amp;quot;);&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        } elseif ($mode == &#039;3&#039;) {&lt;br /&gt;
            // Selective update&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Selective Import Mode ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;For each changed page, choose:\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  y = yes, update this page\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  n = no, keep local version\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  d = show diff file\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  a = update all remaining pages\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  s = skip all remaining pages\n\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            $update_all = false;&lt;br /&gt;
            $skip_all = false;&lt;br /&gt;
            &lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                if ($skip_all) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($update_all) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\nPage: $title\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Action (y/n/d/a/s): &amp;quot;);&lt;br /&gt;
                $handle = fopen(&amp;quot;php://stdin&amp;quot;, &amp;quot;r&amp;quot;);&lt;br /&gt;
                $line = trim(fgets($handle));&lt;br /&gt;
                &lt;br /&gt;
                while ($line == &#039;d&#039;) {&lt;br /&gt;
                    // Show diff&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    if (file_exists($diff_file)) {&lt;br /&gt;
                        system(&amp;quot;head -50 $diff_file&amp;quot;);&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;\n[Showing first 50 lines - full file at $diff_file]\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Action (y/n/a/s): &amp;quot;);&lt;br /&gt;
                    $line = trim(fgets($handle));&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($line == &#039;a&#039;) {&lt;br /&gt;
                    $update_all = true;&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } elseif ($line == &#039;s&#039;) {&lt;br /&gt;
                    $skip_all = true;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                } elseif ($line == &#039;y&#039;) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Import Complete ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages imported: $new_imported\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Pages updated: $updated\n&amp;quot;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function importPage($title, $content, $xml) {&lt;br /&gt;
        // Create single page XML for import&lt;br /&gt;
        $tempFile = &#039;/tmp/single_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
        $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
        &lt;br /&gt;
        if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
            $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
            foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Find and add the page&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                break;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
        exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;);&lt;br /&gt;
        unlink($tempFile);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function reimportPage($title, $xml) {&lt;br /&gt;
        // For updating existing pages, delete then reimport&lt;br /&gt;
        $db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Delete the existing page&lt;br /&gt;
        $safe_title = $db-&amp;gt;real_escape_string(str_replace(&#039; &#039;, &#039;_&#039;, $title));&lt;br /&gt;
        $db-&amp;gt;query(&amp;quot;DELETE FROM page WHERE page_title = &#039;$safe_title&#039; AND page_namespace = 0&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        $db-&amp;gt;close();&lt;br /&gt;
        &lt;br /&gt;
        // Now import the new version&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $tempFile = &#039;/tmp/update_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
                $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
                &lt;br /&gt;
                if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
                    $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
                    foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                        $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
                $result = exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;, $output, $return);&lt;br /&gt;
                unlink($tempFile);&lt;br /&gt;
                &lt;br /&gt;
                return ($return === 0);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = DoImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/do_import.php --mode=&amp;quot;$choice&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Main execution&lt;br /&gt;
main() {&lt;br /&gt;
    check_environment&lt;br /&gt;
    &lt;br /&gt;
    # Start MariaDB if not running&lt;br /&gt;
    service mariadb status &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 || service mariadb start&lt;br /&gt;
    &lt;br /&gt;
    # Wait for MariaDB&lt;br /&gt;
    for i in {1..30}; do&lt;br /&gt;
        if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
            break&lt;br /&gt;
        fi&lt;br /&gt;
        sleep 1&lt;br /&gt;
    done&lt;br /&gt;
    &lt;br /&gt;
    CURRENT=$(get_current_version)&lt;br /&gt;
    echo &amp;quot;Current version: $CURRENT&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Checking for updates...&amp;quot;&lt;br /&gt;
    LATEST=$(check_for_updates 2&amp;gt;/dev/null)&lt;br /&gt;
    if [ $? -ne 0 ] || [ -z &amp;quot;$LATEST&amp;quot; ] || [[ &amp;quot;$LATEST&amp;quot; == *&amp;quot;ERROR&amp;quot;* ]]; then&lt;br /&gt;
        echo &amp;quot;Failed to check for updates&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Latest available: $LATEST&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Always proceed to analysis even if versions match&lt;br /&gt;
    # (there might be content updates in the same version)&lt;br /&gt;
    echo &amp;quot;Proceeding with content analysis...&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Backup database&lt;br /&gt;
    backup_database&lt;br /&gt;
    &lt;br /&gt;
    # Download new XML&lt;br /&gt;
    if ! download_xml &amp;quot;$LATEST&amp;quot;; then&lt;br /&gt;
        echo &amp;quot;Failed to download new XML&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Analyze differences&lt;br /&gt;
    analyze_and_import&lt;br /&gt;
    &lt;br /&gt;
    # Check if there are changes to import&lt;br /&gt;
    if [ -f &amp;quot;/tmp/import_data.ser&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;&amp;quot;&lt;br /&gt;
        read -p &amp;quot;Choose option (1-4): &amp;quot; -n 1 -r&lt;br /&gt;
        echo&lt;br /&gt;
        &lt;br /&gt;
        if [[ $REPLY =~ ^[1-4]$ ]]; then&lt;br /&gt;
            if [ &amp;quot;$REPLY&amp;quot; != &amp;quot;4&amp;quot; ]; then&lt;br /&gt;
                perform_import &amp;quot;$REPLY&amp;quot;&lt;br /&gt;
                &lt;br /&gt;
                # Update version info&lt;br /&gt;
                echo &amp;quot;Import: $LATEST&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
                echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
                &lt;br /&gt;
                # Rebuild indices&lt;br /&gt;
                echo &amp;quot;Rebuilding indices...&amp;quot;&lt;br /&gt;
                php maintenance/rebuildrecentchanges.php&lt;br /&gt;
                php maintenance/initSiteStats.php&lt;br /&gt;
            else&lt;br /&gt;
                echo &amp;quot;Update cancelled&amp;quot;&lt;br /&gt;
            fi&lt;br /&gt;
        else&lt;br /&gt;
            echo &amp;quot;Invalid option. Update cancelled&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Clean up temp files&lt;br /&gt;
    rm -f /tmp/import_data.ser /tmp/update_analysis.json /tmp/diff_*.txt /tmp/analyze_import.php /tmp/do_import.php 2&amp;gt;/dev/null&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    echo &amp;quot;Done!&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Run main function&lt;br /&gt;
main &amp;quot;$@&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.5: Entrypoint Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano entrypoint.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Starting CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 1&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;CompleteNoobs Wiki ready at: http://localhost:8080&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin login: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Features:&amp;quot;&lt;br /&gt;
echo &amp;quot;- Complete wiki content imported from XML&amp;quot;&lt;br /&gt;
echo &amp;quot;- License notices on all pages (via PageNotice)&amp;quot;&lt;br /&gt;
echo &amp;quot;- SyntaxHighlight for code blocks&amp;quot;&lt;br /&gt;
echo &amp;quot;- YouTube video embedding&amp;quot;&lt;br /&gt;
echo &amp;quot;- Contribution Scores special page&amp;quot;&lt;br /&gt;
echo &amp;quot;- XML update system (preserves local edits)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
apache2-foreground&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 3: Build and Run ==&lt;br /&gt;
&lt;br /&gt;
=== 3.1: Build the Image ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t completenoobs/wiki:latest .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will take several minutes.&lt;br /&gt;
&lt;br /&gt;
=== 3.2: Run the Container ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* NOTE: The above &amp;lt;code&amp;gt;docker run&amp;lt;/code&amp;gt; command is for quick testing, if you want to be able to export your wiki&#039;s database to an XML file you can backup and share, please use method in expanding info box below. &lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
If you want to be able to Export Your MediaWiki Database to Dated XML File - use this &amp;lt;code&amp;gt;docker run&amp;lt;/code&amp;gt; method:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To export your MediaWiki database to a dated XML file (e.g., &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;) and save it to the host’s &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory, run the export script inside the Docker container and use a volume mount to write the file to the host.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 1: Create Host Directory&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
On the host, ensure the &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory exists:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir -p ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 2: Run Container with Volume Mount&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If your container isn’t already using a volume for &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt;, stop and remove it, then restart with a volume mount:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v ~/wiki-container:/export \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/cnoobs-wiki:0.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 3: Run Export Script in Container&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Access the container’s shell:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Inside the container, run the export script to create a dated XML file:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DATE=$(date +%Y%m%d)&lt;br /&gt;
php /var/www/html/maintenance/run.php dumpBackup.php --full --output=file:/export/$DATE.xml&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This writes the file (e.g., &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;) to &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt; in the container, which maps to &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; on the host.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 4: Verify the File&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
On the host, check for the XML file:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ls ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
You should see a file like &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Notes&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
- If you encounter permission issues, ensure the container’s user has write access to &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt;:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
chmod -R 777 /export&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- The script must be run inside the container, as it requires MediaWiki’s environment and database access.&amp;lt;br&amp;gt;&lt;br /&gt;
- If your container uses a different volume setup, adjust the mount point accordingly.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 4: Test Everything ==&lt;br /&gt;
&lt;br /&gt;
=== 4.1: Check the Wiki ===&lt;br /&gt;
* Visit: http://localhost:8080&lt;br /&gt;
* You should see the PageNotice at the top&lt;br /&gt;
* Login with: admin / AdminPass123!&lt;br /&gt;
{{:Restore_the_completenoobs_Main_Page}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Change Admin Password:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By default, the admin user&#039;s password is &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;. It&#039;s highly recommended to change this immediately. You can do this either through the wiki&#039;s web interface or directly in the Docker terminal.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 1: Change Password via Web Interface&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
This is the easiest method. You can change your password directly from the wiki itself.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Log in to your wiki with the default credentials: &amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
2. Once logged in, click your username (&amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt;) in the top-right corner of the page.&amp;lt;br&amp;gt;&lt;br /&gt;
3. From the drop-down menu, select &amp;lt;b&amp;gt;Preferences&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
4. On the Preferences page, go to the &amp;lt;b&amp;gt;Password&amp;lt;/b&amp;gt; tab.&amp;lt;br&amp;gt;&lt;br /&gt;
5. Enter the current password (&amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;), then enter your new password twice.&amp;lt;br&amp;gt;&lt;br /&gt;
6. Click &amp;lt;b&amp;gt;Change password&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Your password is now changed, and you will need to use the new one for future logins.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 2: Change Password via Terminal (No-Email Reset)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you have forgotten the password or prefer to use the command line, you can reset it directly inside the Docker container using a MediaWiki maintenance script.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;b&amp;gt;Access the container&#039;s shell&amp;lt;/b&amp;gt; with the following command from your host machine:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Once inside the container, use the &amp;lt;code&amp;gt;changePassword.php&amp;lt;/code&amp;gt; maintenance script to change the password. This is the modern, recommended way to run MediaWiki maintenance scripts.&amp;lt;br&amp;gt;&lt;br /&gt;
* Change &amp;lt;b&amp;gt;NEWPASSWORD&amp;lt;/b&amp;gt; to your new password&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt; to leave the container&#039;s shell.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The admin password has now been reset. You can log in to your wiki with the new password.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.2: Test Extensions ===&lt;br /&gt;
* &#039;&#039;&#039;YouTube&#039;&#039;&#039;: Edit any page, add &amp;lt;code&amp;gt;&amp;lt;youtube&amp;gt;N9qYF9DZPdw&amp;lt;/youtube&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;PageNotice&#039;&#039;&#039;: Should already be visible at the top&lt;br /&gt;
* &#039;&#039;&#039;SyntaxHighlight&#039;&#039;&#039;: Add code blocks with &amp;lt;nowiki&amp;gt;&amp;lt;code&amp;gt;&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;code here&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.3: Check Status ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/check_status.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 5: XML Update Operations ==&lt;br /&gt;
*NOTE: update_xml.sh still needs alot of work, this just idea placeholder for now.&lt;br /&gt;
=== 5.1: Check for Updates (Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will:&lt;br /&gt;
* Check the CompleteNoobs XML repository for new dumps&lt;br /&gt;
* Compare with your current version&lt;br /&gt;
* Ask for confirmation before updating&lt;br /&gt;
* Import ONLY new pages (preserves your edits)&lt;br /&gt;
* Create a backup before making changes&lt;br /&gt;
&lt;br /&gt;
=== 5.2: Force Update (Non-Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki bash -c &amp;quot;echo &#039;y&#039; | /var/www/html/update_xml.sh&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5.3: Manual Update Process ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# 1. Enter container&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&lt;br /&gt;
# 2. Check current version&lt;br /&gt;
cat /var/www/html/.last_import&lt;br /&gt;
&lt;br /&gt;
# 3. Run update&lt;br /&gt;
/var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# 4. Exit container&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 6: Troubleshooting Commands ==&lt;br /&gt;
=== Access container shell: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Edit configuration: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki nano /var/www/html/LocalSettings.php&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Check logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== View update logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki tail -f /tmp/mediawiki-debug.log&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Complete restart: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Expected Results ==&lt;br /&gt;
* Working wiki with imported CompleteNoobs content&lt;br /&gt;
* PageNotice visible at top of all pages&lt;br /&gt;
* All extensions functional&lt;br /&gt;
* Text editors (nano/vim) available in container&lt;br /&gt;
* Utility scripts for maintenance&lt;br /&gt;
* XML update system that preserves local edits&lt;br /&gt;
&lt;br /&gt;
==need to add==&lt;br /&gt;
* way for user to backup there local custom wiki - xml exporter&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Talk:Restore_the_completenoobs_Main_Page&amp;diff=670</id>
		<title>Talk:Restore the completenoobs Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Talk:Restore_the_completenoobs_Main_Page&amp;diff=670"/>
		<updated>2025-09-02T10:38:21Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot;* Enter in page - saves retyping  &amp;lt;pre&amp;gt;{{:Restore_the_completenoobs_Main_Page}}&amp;lt;/pre&amp;gt;&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;* Enter in page - saves retyping &lt;br /&gt;
&amp;lt;pre&amp;gt;{{:Restore_the_completenoobs_Main_Page}}&amp;lt;/pre&amp;gt;&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=669</id>
		<title>CompleteNoobs Docker Image Creation</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=669"/>
		<updated>2025-09-02T10:23:46Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* 4.1: Check the Wiki */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Complete Noobs Docker Wiki Tutorial =&lt;br /&gt;
* [[Docker_Install_Guide| Docker install guide]]&lt;br /&gt;
==errors==&lt;br /&gt;
* This mainly works - just need to fix the extensions popular pages and contrubtion scores&lt;br /&gt;
* The XML updater requires more work - currently idea placeholder&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
* Ubuntu 24.04&lt;br /&gt;
* Docker installed and running&lt;br /&gt;
* Your user in docker group: &amp;lt;code&amp;gt;sudo usermod -aG docker $USER&amp;lt;/code&amp;gt; (then logout/login)&lt;br /&gt;
&lt;br /&gt;
== Step 2: Create All Files ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1: Dockerfile ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Copy this exactly:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM mediawiki:1.44&lt;br /&gt;
# Mediawiki 1.44 used over latest because can confirm extensions youtube and pagenotice works&lt;br /&gt;
# Install dependencies&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \&lt;br /&gt;
    mariadb-server \&lt;br /&gt;
    python3 \&lt;br /&gt;
    python3-requests \&lt;br /&gt;
    python3-bs4 \&lt;br /&gt;
    python3-pygments \&lt;br /&gt;
    curl \&lt;br /&gt;
    wget \&lt;br /&gt;
    unzip \&lt;br /&gt;
    nano \&lt;br /&gt;
    git \&lt;br /&gt;
    &amp;amp;&amp;amp; apt-get clean&lt;br /&gt;
&lt;br /&gt;
# Copy scripts&lt;br /&gt;
COPY download_latest_xml.py /usr/src/download_latest_xml.py&lt;br /&gt;
COPY setup_wiki.sh /usr/src/setup_wiki.sh&lt;br /&gt;
COPY update_xml.sh /usr/src/update_xml.sh&lt;br /&gt;
COPY entrypoint.sh /entrypoint.sh&lt;br /&gt;
&lt;br /&gt;
# Make executable&lt;br /&gt;
RUN chmod +x /usr/src/setup_wiki.sh /entrypoint.sh /usr/src/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Download XML&lt;br /&gt;
RUN python3 /usr/src/download_latest_xml.py&lt;br /&gt;
&lt;br /&gt;
# Setup wiki&lt;br /&gt;
RUN /usr/src/setup_wiki.sh&lt;br /&gt;
&lt;br /&gt;
EXPOSE 80&lt;br /&gt;
VOLUME /var/lib/mysql&lt;br /&gt;
VOLUME /var/www/html/images&lt;br /&gt;
&lt;br /&gt;
ENTRYPOINT [&amp;quot;/entrypoint.sh&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.2: XML Download Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano download_latest_xml.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_available_dumps():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        return sorted(dumps, key=parse_date_from_dump, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dumps: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def get_dump_files(dump):&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(f&amp;quot;{BASE_URL}{dump}/&amp;quot;, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
        return sorted(files, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dump files: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def download_file(url, filename):&lt;br /&gt;
    try:&lt;br /&gt;
        print(f&amp;quot;Downloading {filename}...&amp;quot;)&lt;br /&gt;
        response = requests.get(url, stream=True, timeout=60)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        &lt;br /&gt;
        total_size = int(response.headers.get(&#039;content-length&#039;, 0))&lt;br /&gt;
        downloaded = 0&lt;br /&gt;
        &lt;br /&gt;
        with open(filename, &#039;wb&#039;) as f:&lt;br /&gt;
            for chunk in response.iter_content(chunk_size=8192):&lt;br /&gt;
                if chunk:&lt;br /&gt;
                    f.write(chunk)&lt;br /&gt;
                    downloaded += len(chunk)&lt;br /&gt;
                    if total_size &amp;gt; 0:&lt;br /&gt;
                        progress = (downloaded / total_size) * 100&lt;br /&gt;
                        print(f&amp;quot;\rProgress: {progress:.1f}%&amp;quot;, end=&#039;&#039;, flush=True)&lt;br /&gt;
        print()&lt;br /&gt;
        return True&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error downloading {filename}: {e}&amp;quot;)&lt;br /&gt;
        return False&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    print(&amp;quot;Fetching available XML dumps...&amp;quot;)&lt;br /&gt;
    dumps = get_available_dumps()&lt;br /&gt;
    &lt;br /&gt;
    if not dumps:&lt;br /&gt;
        print(&amp;quot;No dumps found!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_dump = dumps[0]&lt;br /&gt;
    print(f&amp;quot;Latest dump: {newest_dump}&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    files = get_dump_files(newest_dump)&lt;br /&gt;
    if not files:&lt;br /&gt;
        print(&amp;quot;No XML files found in latest dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_xml = files[0]&lt;br /&gt;
    xml_url = f&amp;quot;{BASE_URL}{newest_dump}/{newest_xml}&amp;quot;&lt;br /&gt;
    local_filename = &amp;quot;/tmp/completenoobs_dump.xml&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if download_file(xml_url, local_filename):&lt;br /&gt;
        print(f&amp;quot;Successfully downloaded {newest_xml}&amp;quot;)&lt;br /&gt;
        with open(&amp;quot;/tmp/dump_info.txt&amp;quot;, &amp;quot;w&amp;quot;) as f:&lt;br /&gt;
            f.write(f&amp;quot;{newest_dump}/{newest_xml}&amp;quot;)&lt;br /&gt;
    else:&lt;br /&gt;
        print(&amp;quot;Failed to download XML dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.3: Main Setup Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano setup_wiki.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Setting up CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Initialize MariaDB&lt;br /&gt;
if [ ! -d &amp;quot;/var/lib/mysql/mysql&amp;quot; ]; then&lt;br /&gt;
    mysql_install_db --user=mysql --datadir=/var/lib/mysql&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 2&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
# Setup database&lt;br /&gt;
mysql -e &amp;quot;CREATE DATABASE IF NOT EXISTS completenoobs_wiki CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;127.0.0.1&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;127.0.0.1&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;localhost&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;localhost&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;FLUSH PRIVILEGES;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Install MediaWiki&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
php maintenance/install.php \&lt;br /&gt;
    --dbtype=mysql \&lt;br /&gt;
    --dbserver=127.0.0.1 \&lt;br /&gt;
    --dbname=completenoobs_wiki \&lt;br /&gt;
    --dbuser=wikiuser \&lt;br /&gt;
    --dbpass=wikipass \&lt;br /&gt;
    --server=&amp;quot;http://localhost:8080&amp;quot; \&lt;br /&gt;
    --scriptpath=&amp;quot;&amp;quot; \&lt;br /&gt;
    --lang=en \&lt;br /&gt;
    --pass=AdminPass123! \&lt;br /&gt;
    &amp;quot;CompleteNoobs Wiki&amp;quot; \&lt;br /&gt;
    &amp;quot;admin&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Download and install extensions&lt;br /&gt;
cd extensions/&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/PageNotice --branch REL1_44 || echo &amp;quot;PageNotice download failed, continuing...&amp;quot;&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/YouTube --branch REL1_44 || echo &amp;quot;YouTube download failed, continuing...&amp;quot;&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
&lt;br /&gt;
# Configure LocalSettings.php&lt;br /&gt;
cat &amp;gt;&amp;gt; LocalSettings.php &amp;lt;&amp;lt; &#039;EOF&#039;&lt;br /&gt;
# Basic settings&lt;br /&gt;
$wgEnableUploads = true;&lt;br /&gt;
$wgUseImageMagick = true;&lt;br /&gt;
$wgImageMagickConvertCommand = &amp;quot;/usr/bin/convert&amp;quot;;&lt;br /&gt;
$wgDefaultSkin = &amp;quot;vector-2022&amp;quot;;&lt;br /&gt;
$wgAllowExternalImages = true;&lt;br /&gt;
&lt;br /&gt;
# Debug (can be removed later)&lt;br /&gt;
$wgShowExceptionDetails = true;&lt;br /&gt;
$wgDebugLogFile = &amp;quot;/tmp/mediawiki-debug.log&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
# PageNotice extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/PageNotice/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;PageNotice&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# YouTube extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/YouTube/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;YouTube&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# SyntaxHighlight (usually bundled)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/SyntaxHighlight_GeSHi/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;SyntaxHighlight_GeSHi&#039; );&lt;br /&gt;
    $wgPygmentizePath = &#039;/usr/bin/pygmentize&#039;;&lt;br /&gt;
}&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
# Import XML dump if available&lt;br /&gt;
if [ -f &amp;quot;/tmp/completenoobs_dump.xml&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Importing XML dump...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if php maintenance/importDump.php --uploads &amp;lt; /tmp/completenoobs_dump.xml; then&lt;br /&gt;
        echo &amp;quot;XML import completed!&amp;quot;&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;XML import had warnings&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Basic maintenance&lt;br /&gt;
    php maintenance/update.php --quick || echo &amp;quot;Update completed with warnings&amp;quot;&lt;br /&gt;
    php maintenance/rebuildrecentchanges.php || echo &amp;quot;RecentChanges rebuilt with warnings&amp;quot;&lt;br /&gt;
    php maintenance/initSiteStats.php || echo &amp;quot;SiteStats initialized with warnings&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if [ -f &amp;quot;/tmp/dump_info.txt&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Import: $(cat /tmp/dump_info.txt)&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
        echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
    fi&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No XML dump found - starting with empty wiki&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
# Copy update script to accessible location&lt;br /&gt;
cp /usr/src/update_xml.sh /var/www/html/update_xml.sh&lt;br /&gt;
chmod +x /var/www/html/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Create user-friendly update wrapper&lt;br /&gt;
cat &amp;gt; /var/www/html/check_updates.sh &amp;lt;&amp;lt; &#039;UPDATE_WRAPPER_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki Update Checker ===&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;This tool checks for new content from the CompleteNoobs XML repository&amp;quot;&lt;br /&gt;
echo &amp;quot;and imports ONLY new pages, preserving all your local edits.&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
/var/www/html/update_xml.sh&lt;br /&gt;
UPDATE_WRAPPER_EOF&lt;br /&gt;
chmod +x /var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# Create simple status script&lt;br /&gt;
cat &amp;gt; /var/www/html/check_status.sh &amp;lt;&amp;lt; &#039;STATUS_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
echo &amp;quot;=== Wiki Status ===&amp;quot;&lt;br /&gt;
echo &amp;quot;Pages: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;Users: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM user WHERE user_id &amp;gt; 0;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Extensions ===&amp;quot;&lt;br /&gt;
if [ -d &amp;quot;extensions/PageNotice&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;PageNotice: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;PageNotice: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/YouTube&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;YouTube: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;YouTube: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/ContributionScores&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;ContributionScores: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;ContributionScores: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Update System ===&amp;quot;&lt;br /&gt;
if [ -f &amp;quot;.last_import&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Current version: $(grep &#039;Import:&#039; .last_import | cut -d&#039; &#039; -f2)&amp;quot;&lt;br /&gt;
    echo &amp;quot;Import date: $(grep &#039;Date:&#039; .last_import | cut -d&#039; &#039; -f2-)&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No version info available&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
STATUS_EOF&lt;br /&gt;
chmod +x /var/www/html/check_status.sh&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Setup completed!&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;Update scripts installed:&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/check_updates.sh (user-friendly)&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/update_xml.sh (direct)&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Final counts&lt;br /&gt;
PAGES=$(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;0&amp;quot;)&lt;br /&gt;
echo &amp;quot;Pages imported: $PAGES&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb stop&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.4: XML Update Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano update_xml.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki XML Updater ===&amp;quot;&lt;br /&gt;
echo &amp;quot;This will check for new and updated pages&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Function to check if running in container&lt;br /&gt;
check_environment() {&lt;br /&gt;
    if [ ! -f &amp;quot;/var/www/html/LocalSettings.php&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Error: This script must be run inside the wiki container&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to get current XML version&lt;br /&gt;
get_current_version() {&lt;br /&gt;
    if [ -f &amp;quot;/var/www/html/.last_import&amp;quot; ]; then&lt;br /&gt;
        grep &amp;quot;Import:&amp;quot; /var/www/html/.last_import | cut -d&#039; &#039; -f2&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;none&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to check for updates&lt;br /&gt;
check_for_updates() {&lt;br /&gt;
    python3 - &amp;lt;&amp;lt; &#039;PYTHON_EOF&#039;&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_latest_dump():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        if dumps:&lt;br /&gt;
            latest = sorted(dumps, key=parse_date_from_dump, reverse=True)[0]&lt;br /&gt;
            &lt;br /&gt;
            # Get XML files from latest dump&lt;br /&gt;
            response = requests.get(f&amp;quot;{BASE_URL}{latest}/&amp;quot;, timeout=30)&lt;br /&gt;
            response.raise_for_status()&lt;br /&gt;
            soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
            files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                    if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
            &lt;br /&gt;
            if files:&lt;br /&gt;
                newest_xml = sorted(files, reverse=True)[0]&lt;br /&gt;
                print(f&amp;quot;{latest}/{newest_xml}&amp;quot;)&lt;br /&gt;
                sys.exit(0)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;ERROR: {e}&amp;quot;, file=sys.stderr)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
    &lt;br /&gt;
    print(&amp;quot;ERROR: No dumps found&amp;quot;, file=sys.stderr)&lt;br /&gt;
    sys.exit(1)&lt;br /&gt;
&lt;br /&gt;
get_latest_dump()&lt;br /&gt;
PYTHON_EOF&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to download new XML&lt;br /&gt;
download_xml() {&lt;br /&gt;
    local dump_info=&amp;quot;$1&amp;quot;&lt;br /&gt;
    local dump_dir=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f1)&lt;br /&gt;
    local xml_file=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f2)&lt;br /&gt;
    local url=&amp;quot;https://xml.completenoobs.com/xmlDumps/${dump_info}&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Downloading: $xml_file&amp;quot;&lt;br /&gt;
    echo &amp;quot;From: $dump_dir&amp;quot;&lt;br /&gt;
    echo &amp;quot;URL: $url&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if wget -O /tmp/new_dump.xml &amp;quot;$url&amp;quot; --progress=bar:force 2&amp;gt;&amp;amp;1; then&lt;br /&gt;
        echo &amp;quot;Download successful!&amp;quot;&lt;br /&gt;
        return 0&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;Download failed!&amp;quot;&lt;br /&gt;
        return 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to backup current database&lt;br /&gt;
backup_database() {&lt;br /&gt;
    echo &amp;quot;Creating database backup...&amp;quot;&lt;br /&gt;
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)&lt;br /&gt;
    mysqldump --user=wikiuser --password=wikipass completenoobs_wiki &amp;gt; /tmp/wiki_backup_${TIMESTAMP}.sql&lt;br /&gt;
    echo &amp;quot;Backup created: /tmp/wiki_backup_${TIMESTAMP}.sql&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to analyze and import changes&lt;br /&gt;
analyze_and_import() {&lt;br /&gt;
    echo &amp;quot;Analyzing differences between XML and local wiki...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Create analysis and import script for MediaWiki 1.44+&lt;br /&gt;
    cat &amp;gt; /tmp/analyze_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class AnalyzeAndImport extends Maintenance {&lt;br /&gt;
    private $db;&lt;br /&gt;
    private $new_pages = [];&lt;br /&gt;
    private $changed_pages = [];&lt;br /&gt;
    private $unchanged_pages = [];&lt;br /&gt;
    &lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addDescription(&#039;Analyze and selectively import from XML dump&#039;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $this-&amp;gt;db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // MediaWiki 1.35+ uses slots and content tables&lt;br /&gt;
        // Get existing pages with their content&lt;br /&gt;
        $query = &amp;quot;&lt;br /&gt;
            SELECT p.page_title, p.page_id, c.content_address, c.content_sha1&lt;br /&gt;
            FROM page p&lt;br /&gt;
            JOIN revision r ON p.page_latest = r.rev_id&lt;br /&gt;
            JOIN slots s ON r.rev_id = s.slot_revision_id&lt;br /&gt;
            JOIN slot_roles sr ON s.slot_role_id = sr.role_id&lt;br /&gt;
            JOIN content c ON s.slot_content_id = c.content_id&lt;br /&gt;
            WHERE p.page_namespace = 0 AND sr.role_name = &#039;main&#039;&lt;br /&gt;
        &amp;quot;;&lt;br /&gt;
        &lt;br /&gt;
        $result = $this-&amp;gt;db-&amp;gt;query($query);&lt;br /&gt;
        if (!$result) {&lt;br /&gt;
            $this-&amp;gt;error(&amp;quot;Database query failed: &amp;quot; . $this-&amp;gt;db-&amp;gt;error);&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $existing = [];&lt;br /&gt;
        while ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
            // Get actual text content&lt;br /&gt;
            $text_content = $this-&amp;gt;getTextContent($row[&#039;content_address&#039;]);&lt;br /&gt;
            $existing[$row[&#039;page_title&#039;]] = [&lt;br /&gt;
                &#039;id&#039; =&amp;gt; $row[&#039;page_id&#039;],&lt;br /&gt;
                &#039;content&#039; =&amp;gt; $text_content,&lt;br /&gt;
                &#039;sha1&#039; =&amp;gt; $row[&#039;content_sha1&#039;]&lt;br /&gt;
            ];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Parse XML and compare&lt;br /&gt;
        $xml = simplexml_load_file(&#039;/tmp/new_dump.xml&#039;);&lt;br /&gt;
        &lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            $title = str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title);&lt;br /&gt;
            $xml_content = (string)$page-&amp;gt;revision-&amp;gt;text;&lt;br /&gt;
            &lt;br /&gt;
            if (!isset($existing[$title])) {&lt;br /&gt;
                // New page&lt;br /&gt;
                $this-&amp;gt;new_pages[$title] = $xml_content;&lt;br /&gt;
            } else {&lt;br /&gt;
                // Compare content using SHA1 for efficiency&lt;br /&gt;
                $xml_sha1 = sha1($xml_content);&lt;br /&gt;
                &lt;br /&gt;
                if ($existing[$title][&#039;sha1&#039;] !== $xml_sha1) {&lt;br /&gt;
                    // Content is different&lt;br /&gt;
                    $this-&amp;gt;changed_pages[$title] = [&lt;br /&gt;
                        &#039;local&#039; =&amp;gt; $existing[$title][&#039;content&#039;],&lt;br /&gt;
                        &#039;xml&#039; =&amp;gt; $xml_content,&lt;br /&gt;
                        &#039;page_id&#039; =&amp;gt; $existing[$title][&#039;id&#039;]&lt;br /&gt;
                    ];&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;unchanged_pages[] = $title;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Display summary&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Update Analysis ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages to import: &amp;quot; . count($this-&amp;gt;new_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Changed pages found: &amp;quot; . count($this-&amp;gt;changed_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Unchanged pages: &amp;quot; . count($this-&amp;gt;unchanged_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Save analysis for review&lt;br /&gt;
        file_put_contents(&#039;/tmp/update_analysis.json&#039;, json_encode([&lt;br /&gt;
            &#039;new&#039; =&amp;gt; array_keys($this-&amp;gt;new_pages),&lt;br /&gt;
            &#039;changed&#039; =&amp;gt; array_keys($this-&amp;gt;changed_pages),&lt;br /&gt;
            &#039;unchanged&#039; =&amp;gt; $this-&amp;gt;unchanged_pages&lt;br /&gt;
        ], JSON_PRETTY_PRINT));&lt;br /&gt;
        &lt;br /&gt;
        // Show changed pages with preview (limit to first 20 for readability)&lt;br /&gt;
        if (count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Changed Pages ===\n&amp;quot;);&lt;br /&gt;
            $count = 0;&lt;br /&gt;
            $total_changed = count($this-&amp;gt;changed_pages);&lt;br /&gt;
            &lt;br /&gt;
            foreach ($this-&amp;gt;changed_pages as $title =&amp;gt; $data) {&lt;br /&gt;
                $count++;&lt;br /&gt;
                if ($count &amp;lt;= 20) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;\n$count. $title\n&amp;quot;);&lt;br /&gt;
                    &lt;br /&gt;
                    // Create a simple diff preview (first 300 chars)&lt;br /&gt;
                    $xml_preview = substr($data[&#039;xml&#039;], 0, 100);&lt;br /&gt;
                    &lt;br /&gt;
                    // Save full diff to file&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;=== FULL DIFF FOR: $title ===\n\n&amp;quot;);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- LOCAL VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;local&#039;] . &amp;quot;\n\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- XML VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;xml&#039;], FILE_APPEND);&lt;br /&gt;
                    &lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;   Preview: &amp;quot; . $xml_preview . &amp;quot;...\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ($total_changed &amp;gt; 20) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\n... and &amp;quot; . ($total_changed - 20) . &amp;quot; more changed pages.\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;All diff files saved to /tmp/diff_*.txt\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Interactive selection&lt;br /&gt;
        if (count($this-&amp;gt;new_pages) &amp;gt; 0 || count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Import Options ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;1. Import new pages only (preserve all local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;2. Import new pages + update ALL changed pages (overwrites local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;3. Selective import (choose which updates to apply)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;4. Cancel (no changes)\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            // Save state for import script WITHOUT the XML object&lt;br /&gt;
            $import_data = [&lt;br /&gt;
                &#039;new_pages&#039; =&amp;gt; $this-&amp;gt;new_pages,&lt;br /&gt;
                &#039;changed_pages&#039; =&amp;gt; $this-&amp;gt;changed_pages,&lt;br /&gt;
                &#039;xml_file&#039; =&amp;gt; &#039;/tmp/new_dump.xml&#039;  // Save path instead of object&lt;br /&gt;
            ];&lt;br /&gt;
            &lt;br /&gt;
            file_put_contents(&#039;/tmp/import_data.ser&#039;, serialize($import_data));&lt;br /&gt;
        } else {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\nNo changes detected. Your wiki is up to date!\n&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function getTextContent($address) {&lt;br /&gt;
        // Handle different content storage formats in MW 1.35+&lt;br /&gt;
        if (strpos($address, &#039;tt:&#039;) === 0) {&lt;br /&gt;
            // Text table reference&lt;br /&gt;
            $text_id = substr($address, 3);&lt;br /&gt;
            $result = $this-&amp;gt;db-&amp;gt;query(&amp;quot;SELECT old_text FROM text WHERE old_id = $text_id&amp;quot;);&lt;br /&gt;
            if ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
                return $row[&#039;old_text&#039;];&lt;br /&gt;
            }&lt;br /&gt;
        } elseif (strpos($address, &#039;es:&#039;) === 0) {&lt;br /&gt;
            // External storage - would need special handling&lt;br /&gt;
            return &amp;quot;[External storage content]&amp;quot;;&lt;br /&gt;
        }&lt;br /&gt;
        // Direct content&lt;br /&gt;
        return $address;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = AnalyzeAndImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/analyze_import.php&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to perform selected import&lt;br /&gt;
perform_import() {&lt;br /&gt;
    local choice=$1&lt;br /&gt;
    &lt;br /&gt;
    cat &amp;gt; /tmp/do_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class DoImport extends Maintenance {&lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addOption(&#039;mode&#039;, &#039;Import mode&#039;, true, true);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $mode = $this-&amp;gt;getOption(&#039;mode&#039;);&lt;br /&gt;
        $data = unserialize(file_get_contents(&#039;/tmp/import_data.ser&#039;));&lt;br /&gt;
        &lt;br /&gt;
        // Load XML file&lt;br /&gt;
        $xml = simplexml_load_file($data[&#039;xml_file&#039;]);&lt;br /&gt;
        &lt;br /&gt;
        $new_imported = 0;&lt;br /&gt;
        $updated = 0;&lt;br /&gt;
        &lt;br /&gt;
        // Import new pages (always for modes 1-3)&lt;br /&gt;
        if ($mode != &#039;4&#039;) {&lt;br /&gt;
            foreach ($data[&#039;new_pages&#039;] as $title =&amp;gt; $content) {&lt;br /&gt;
                $this-&amp;gt;importPage($title, $content, $xml);&lt;br /&gt;
                $new_imported++;&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Imported new page: $title\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Handle changed pages based on mode&lt;br /&gt;
        if ($mode == &#039;2&#039;) {&lt;br /&gt;
            // Update all changed pages&lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Updating: $title\n&amp;quot;);&lt;br /&gt;
                if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                    $updated++;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Updated page: $title\n&amp;quot;);&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        } elseif ($mode == &#039;3&#039;) {&lt;br /&gt;
            // Selective update&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Selective Import Mode ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;For each changed page, choose:\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  y = yes, update this page\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  n = no, keep local version\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  d = show diff file\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  a = update all remaining pages\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  s = skip all remaining pages\n\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            $update_all = false;&lt;br /&gt;
            $skip_all = false;&lt;br /&gt;
            &lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                if ($skip_all) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($update_all) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\nPage: $title\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Action (y/n/d/a/s): &amp;quot;);&lt;br /&gt;
                $handle = fopen(&amp;quot;php://stdin&amp;quot;, &amp;quot;r&amp;quot;);&lt;br /&gt;
                $line = trim(fgets($handle));&lt;br /&gt;
                &lt;br /&gt;
                while ($line == &#039;d&#039;) {&lt;br /&gt;
                    // Show diff&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    if (file_exists($diff_file)) {&lt;br /&gt;
                        system(&amp;quot;head -50 $diff_file&amp;quot;);&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;\n[Showing first 50 lines - full file at $diff_file]\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Action (y/n/a/s): &amp;quot;);&lt;br /&gt;
                    $line = trim(fgets($handle));&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($line == &#039;a&#039;) {&lt;br /&gt;
                    $update_all = true;&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } elseif ($line == &#039;s&#039;) {&lt;br /&gt;
                    $skip_all = true;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                } elseif ($line == &#039;y&#039;) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Import Complete ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages imported: $new_imported\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Pages updated: $updated\n&amp;quot;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function importPage($title, $content, $xml) {&lt;br /&gt;
        // Create single page XML for import&lt;br /&gt;
        $tempFile = &#039;/tmp/single_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
        $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
        &lt;br /&gt;
        if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
            $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
            foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Find and add the page&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                break;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
        exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;);&lt;br /&gt;
        unlink($tempFile);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function reimportPage($title, $xml) {&lt;br /&gt;
        // For updating existing pages, delete then reimport&lt;br /&gt;
        $db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Delete the existing page&lt;br /&gt;
        $safe_title = $db-&amp;gt;real_escape_string(str_replace(&#039; &#039;, &#039;_&#039;, $title));&lt;br /&gt;
        $db-&amp;gt;query(&amp;quot;DELETE FROM page WHERE page_title = &#039;$safe_title&#039; AND page_namespace = 0&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        $db-&amp;gt;close();&lt;br /&gt;
        &lt;br /&gt;
        // Now import the new version&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $tempFile = &#039;/tmp/update_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
                $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
                &lt;br /&gt;
                if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
                    $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
                    foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                        $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
                $result = exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;, $output, $return);&lt;br /&gt;
                unlink($tempFile);&lt;br /&gt;
                &lt;br /&gt;
                return ($return === 0);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = DoImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/do_import.php --mode=&amp;quot;$choice&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Main execution&lt;br /&gt;
main() {&lt;br /&gt;
    check_environment&lt;br /&gt;
    &lt;br /&gt;
    # Start MariaDB if not running&lt;br /&gt;
    service mariadb status &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 || service mariadb start&lt;br /&gt;
    &lt;br /&gt;
    # Wait for MariaDB&lt;br /&gt;
    for i in {1..30}; do&lt;br /&gt;
        if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
            break&lt;br /&gt;
        fi&lt;br /&gt;
        sleep 1&lt;br /&gt;
    done&lt;br /&gt;
    &lt;br /&gt;
    CURRENT=$(get_current_version)&lt;br /&gt;
    echo &amp;quot;Current version: $CURRENT&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Checking for updates...&amp;quot;&lt;br /&gt;
    LATEST=$(check_for_updates 2&amp;gt;/dev/null)&lt;br /&gt;
    if [ $? -ne 0 ] || [ -z &amp;quot;$LATEST&amp;quot; ] || [[ &amp;quot;$LATEST&amp;quot; == *&amp;quot;ERROR&amp;quot;* ]]; then&lt;br /&gt;
        echo &amp;quot;Failed to check for updates&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Latest available: $LATEST&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Always proceed to analysis even if versions match&lt;br /&gt;
    # (there might be content updates in the same version)&lt;br /&gt;
    echo &amp;quot;Proceeding with content analysis...&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Backup database&lt;br /&gt;
    backup_database&lt;br /&gt;
    &lt;br /&gt;
    # Download new XML&lt;br /&gt;
    if ! download_xml &amp;quot;$LATEST&amp;quot;; then&lt;br /&gt;
        echo &amp;quot;Failed to download new XML&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Analyze differences&lt;br /&gt;
    analyze_and_import&lt;br /&gt;
    &lt;br /&gt;
    # Check if there are changes to import&lt;br /&gt;
    if [ -f &amp;quot;/tmp/import_data.ser&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;&amp;quot;&lt;br /&gt;
        read -p &amp;quot;Choose option (1-4): &amp;quot; -n 1 -r&lt;br /&gt;
        echo&lt;br /&gt;
        &lt;br /&gt;
        if [[ $REPLY =~ ^[1-4]$ ]]; then&lt;br /&gt;
            if [ &amp;quot;$REPLY&amp;quot; != &amp;quot;4&amp;quot; ]; then&lt;br /&gt;
                perform_import &amp;quot;$REPLY&amp;quot;&lt;br /&gt;
                &lt;br /&gt;
                # Update version info&lt;br /&gt;
                echo &amp;quot;Import: $LATEST&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
                echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
                &lt;br /&gt;
                # Rebuild indices&lt;br /&gt;
                echo &amp;quot;Rebuilding indices...&amp;quot;&lt;br /&gt;
                php maintenance/rebuildrecentchanges.php&lt;br /&gt;
                php maintenance/initSiteStats.php&lt;br /&gt;
            else&lt;br /&gt;
                echo &amp;quot;Update cancelled&amp;quot;&lt;br /&gt;
            fi&lt;br /&gt;
        else&lt;br /&gt;
            echo &amp;quot;Invalid option. Update cancelled&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Clean up temp files&lt;br /&gt;
    rm -f /tmp/import_data.ser /tmp/update_analysis.json /tmp/diff_*.txt /tmp/analyze_import.php /tmp/do_import.php 2&amp;gt;/dev/null&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    echo &amp;quot;Done!&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Run main function&lt;br /&gt;
main &amp;quot;$@&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.5: Entrypoint Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano entrypoint.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Starting CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 1&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;CompleteNoobs Wiki ready at: http://localhost:8080&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin login: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Features:&amp;quot;&lt;br /&gt;
echo &amp;quot;- Complete wiki content imported from XML&amp;quot;&lt;br /&gt;
echo &amp;quot;- License notices on all pages (via PageNotice)&amp;quot;&lt;br /&gt;
echo &amp;quot;- SyntaxHighlight for code blocks&amp;quot;&lt;br /&gt;
echo &amp;quot;- YouTube video embedding&amp;quot;&lt;br /&gt;
echo &amp;quot;- Contribution Scores special page&amp;quot;&lt;br /&gt;
echo &amp;quot;- XML update system (preserves local edits)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
apache2-foreground&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 3: Build and Run ==&lt;br /&gt;
&lt;br /&gt;
=== 3.1: Build the Image ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t completenoobs/wiki:latest .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will take several minutes.&lt;br /&gt;
&lt;br /&gt;
=== 3.2: Run the Container ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* NOTE: The above &amp;lt;code&amp;gt;docker run&amp;lt;/code&amp;gt; command is for quick testing, if you want to be able to export your wiki&#039;s database to an XML file you can backup and share, please use method in expanding info box below. &lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
If you want to be able to Export Your MediaWiki Database to Dated XML File - use this &amp;lt;code&amp;gt;docker run&amp;lt;/code&amp;gt; method:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To export your MediaWiki database to a dated XML file (e.g., &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;) and save it to the host’s &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory, run the export script inside the Docker container and use a volume mount to write the file to the host.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 1: Create Host Directory&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
On the host, ensure the &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory exists:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir -p ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 2: Run Container with Volume Mount&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If your container isn’t already using a volume for &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt;, stop and remove it, then restart with a volume mount:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v ~/wiki-container:/export \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/cnoobs-wiki:0.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 3: Run Export Script in Container&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Access the container’s shell:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Inside the container, run the export script to create a dated XML file:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DATE=$(date +%Y%m%d)&lt;br /&gt;
php /var/www/html/maintenance/run.php dumpBackup.php --full --output=file:/export/$DATE.xml&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This writes the file (e.g., &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;) to &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt; in the container, which maps to &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; on the host.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 4: Verify the File&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
On the host, check for the XML file:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ls ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
You should see a file like &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Notes&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
- If you encounter permission issues, ensure the container’s user has write access to &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt;:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
chmod -R 777 /export&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- The script must be run inside the container, as it requires MediaWiki’s environment and database access.&amp;lt;br&amp;gt;&lt;br /&gt;
- If your container uses a different volume setup, adjust the mount point accordingly.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 4: Test Everything ==&lt;br /&gt;
&lt;br /&gt;
=== 4.1: Check the Wiki ===&lt;br /&gt;
* Visit: http://localhost:8080&lt;br /&gt;
* You should see the PageNotice at the top&lt;br /&gt;
* Login with: admin / AdminPass123!&lt;br /&gt;
{{:Restore_the_completenoobs_Main_Page}}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Change Admin Password:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By default, the admin user&#039;s password is &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;. It&#039;s highly recommended to change this immediately. You can do this either through the wiki&#039;s web interface or directly in the Docker terminal.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 1: Change Password via Web Interface&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
This is the easiest method. You can change your password directly from the wiki itself.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Log in to your wiki with the default credentials: &amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
2. Once logged in, click your username (&amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt;) in the top-right corner of the page.&amp;lt;br&amp;gt;&lt;br /&gt;
3. From the drop-down menu, select &amp;lt;b&amp;gt;Preferences&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
4. On the Preferences page, go to the &amp;lt;b&amp;gt;Password&amp;lt;/b&amp;gt; tab.&amp;lt;br&amp;gt;&lt;br /&gt;
5. Enter the current password (&amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;), then enter your new password twice.&amp;lt;br&amp;gt;&lt;br /&gt;
6. Click &amp;lt;b&amp;gt;Change password&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Your password is now changed, and you will need to use the new one for future logins.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 2: Change Password via Terminal (No-Email Reset)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you have forgotten the password or prefer to use the command line, you can reset it directly inside the Docker container using a MediaWiki maintenance script.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;b&amp;gt;Access the container&#039;s shell&amp;lt;/b&amp;gt; with the following command from your host machine:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Once inside the container, use the &amp;lt;code&amp;gt;changePassword.php&amp;lt;/code&amp;gt; maintenance script to change the password. This is the modern, recommended way to run MediaWiki maintenance scripts.&amp;lt;br&amp;gt;&lt;br /&gt;
* Change &amp;lt;b&amp;gt;NEWPASSWORD&amp;lt;/b&amp;gt; to your new password&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt; to leave the container&#039;s shell.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The admin password has now been reset. You can log in to your wiki with the new password.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.2: Test Extensions ===&lt;br /&gt;
* &#039;&#039;&#039;YouTube&#039;&#039;&#039;: Edit any page, add &amp;lt;code&amp;gt;&amp;lt;youtube&amp;gt;N9qYF9DZPdw&amp;lt;/youtube&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;PageNotice&#039;&#039;&#039;: Should already be visible at the top&lt;br /&gt;
* &#039;&#039;&#039;SyntaxHighlight&#039;&#039;&#039;: Add code blocks with &amp;lt;nowiki&amp;gt;&amp;lt;code&amp;gt;&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;code here&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.3: Check Status ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/check_status.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 5: XML Update Operations ==&lt;br /&gt;
*NOTE: update_xml.sh still needs alot of work, this just idea placeholder for now.&lt;br /&gt;
=== 5.1: Check for Updates (Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will:&lt;br /&gt;
* Check the CompleteNoobs XML repository for new dumps&lt;br /&gt;
* Compare with your current version&lt;br /&gt;
* Ask for confirmation before updating&lt;br /&gt;
* Import ONLY new pages (preserves your edits)&lt;br /&gt;
* Create a backup before making changes&lt;br /&gt;
&lt;br /&gt;
=== 5.2: Force Update (Non-Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki bash -c &amp;quot;echo &#039;y&#039; | /var/www/html/update_xml.sh&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5.3: Manual Update Process ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# 1. Enter container&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&lt;br /&gt;
# 2. Check current version&lt;br /&gt;
cat /var/www/html/.last_import&lt;br /&gt;
&lt;br /&gt;
# 3. Run update&lt;br /&gt;
/var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# 4. Exit container&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 6: Troubleshooting Commands ==&lt;br /&gt;
=== Access container shell: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Edit configuration: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki nano /var/www/html/LocalSettings.php&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Check logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== View update logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki tail -f /tmp/mediawiki-debug.log&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Complete restart: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Expected Results ==&lt;br /&gt;
* Working wiki with imported CompleteNoobs content&lt;br /&gt;
* PageNotice visible at top of all pages&lt;br /&gt;
* All extensions functional&lt;br /&gt;
* Text editors (nano/vim) available in container&lt;br /&gt;
* Utility scripts for maintenance&lt;br /&gt;
* XML update system that preserves local edits&lt;br /&gt;
&lt;br /&gt;
==need to add==&lt;br /&gt;
* way for user to backup there local custom wiki - xml exporter&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Restore_the_completenoobs_Main_Page&amp;diff=668</id>
		<title>Restore the completenoobs Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Restore_the_completenoobs_Main_Page&amp;diff=668"/>
		<updated>2025-09-02T10:21:02Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot;&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt; Restore the completenoobs Main Page: &amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;  By default, the &amp;lt;code&amp;gt;completenoobs&amp;lt;/code&amp;gt; MediaWiki instance overwrites the Main Page with content like &amp;quot;&amp;lt;strong&amp;gt;MediaWiki has been installed.&amp;lt;/strong&amp;gt;&amp;quot; To revert to the page’s state before this change, undo the initial revision:&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;  1. Go to &amp;lt;code&amp;gt;http://localhost:8080&amp;lt;/code&amp;gt; in your browser.&amp;lt;br&amp;gt; 2. On the Main Page, click &amp;lt;b&amp;gt;View Histo...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Restore the completenoobs Main Page:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By default, the &amp;lt;code&amp;gt;completenoobs&amp;lt;/code&amp;gt; MediaWiki instance overwrites the Main Page with content like &amp;quot;&amp;lt;strong&amp;gt;MediaWiki has been installed.&amp;lt;/strong&amp;gt;&amp;quot; To revert to the page’s state before this change, undo the initial revision:&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Go to &amp;lt;code&amp;gt;http://localhost:8080&amp;lt;/code&amp;gt; in your browser.&amp;lt;br&amp;gt;&lt;br /&gt;
2. On the Main Page, click &amp;lt;b&amp;gt;View History&amp;lt;/b&amp;gt; (top-right corner).&amp;lt;br&amp;gt;&lt;br /&gt;
3. Find the top revision by &amp;lt;code&amp;gt;MediaWiki default&amp;lt;/code&amp;gt; and click &amp;lt;b&amp;gt;Undo&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
4. Scroll down and click &amp;lt;b&amp;gt;Save changes&amp;lt;/b&amp;gt; to revert the Main Page.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Ubuntu2404_Install_Docker_and_Docker_Compose&amp;diff=667</id>
		<title>Ubuntu2404 Install Docker and Docker Compose</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Ubuntu2404_Install_Docker_and_Docker_Compose&amp;diff=667"/>
		<updated>2025-09-01T21:16:15Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* Install First Container/Image */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Preparation ===&lt;br /&gt;
&lt;br /&gt;
Before we begin, make sure you&#039;re logged in with a user account that has sudo privileges.&lt;br /&gt;
&lt;br /&gt;
=== Update System Packages ===&lt;br /&gt;
&lt;br /&gt;
Update your package list to ensure you have the latest versions of packages:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Install Docker Prerequisites ===&lt;br /&gt;
&lt;br /&gt;
Install the necessary packages for Docker setup:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Setup Docker Repository ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Add Docker&#039;s Official GPG Key&#039;&#039;&#039;:&lt;br /&gt;
 &lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Add the Docker Repository&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  echo &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&amp;quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Install Docker and Docker Compose ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Update Package List Again&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo apt update&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Install Docker Engine, CLI, Containerd, and Additional Tools&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo apt install -y docker-ce docker-ce-cli containerd.io python3-bs4 python3-requests docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- &lt;br /&gt;
* &#039;&#039;&#039;Install Docker Compose&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
Here we&#039;re downloading the latest version of Docker Compose:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo curl -L &amp;quot;https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)&amp;quot; -o /usr/local/bin/docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Make the Docker Compose binary executable:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo chmod +x /usr/local/bin/docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Verify Installation ===&lt;br /&gt;
&lt;br /&gt;
Check if Docker and Docker Compose are installed correctly:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker --version&lt;br /&gt;
docker-compose --version&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Configure User Permissions ===&lt;br /&gt;
&lt;br /&gt;
To run Docker commands without &amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt;, add your user to the &amp;lt;code&amp;gt;docker&amp;lt;/code&amp;gt; group:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo usermod -aG docker $USER&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note&#039;&#039;&#039;: After adding your user to the docker group, you&#039;ll need to &#039;&#039;&#039;log out and log back in&#039;&#039;&#039; for the changes to take effect.&lt;br /&gt;
If you do not log out and back in, Or you do not add your $USER to the docker group, you will be required to use sudo in some cases. such as ..&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
a way to apply group changes without logging out and back in - tip:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
exec sudo su -l $USER&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will replace your current shell with a new login shell for your user, which will have the updated group memberships. Both of these methods will apply the group changes immediately, allowing you to use LXD commands without having to log out and back in. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Remember&amp;lt;/b&amp;gt;, these changes only apply to the current terminal session. If you open a new terminal window, you might need to run the command again or log out and back in for the changes to take effect system-wide.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===If Installing Docker messed up your LXC/LXD Networking===&lt;br /&gt;
To resolve networking conflicts between Docker and LXC containers on Ubuntu 24.04, enable IP forwarding on the host system:&lt;br /&gt;
* Open the sysctl configuration file in your preferred editor&lt;br /&gt;
&amp;lt;code&amp;gt;sudo $EDITOR /etc/sysctl.conf&amp;lt;/code&amp;gt;&lt;br /&gt;
* Uncomment or add the following line (around line 28):&lt;br /&gt;
&amp;lt;pre&amp;gt;net.ipv4.ip_forward=1&amp;lt;/pre&amp;gt;&lt;br /&gt;
* Apply the updated configuration to enable IP forwarding:&lt;br /&gt;
&amp;lt;code&amp;gt;sysctl -p&amp;lt;/code&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
* Restart the system to ensure all changes take effect.&lt;br /&gt;
&lt;br /&gt;
This should resolve the networking issue for LXC containers when Docker is installed.&lt;br /&gt;
&lt;br /&gt;
==Install First Container/Image==&lt;br /&gt;
&lt;br /&gt;
Download the completenoobs container image, mediawiki with the completenoobs xml installed.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker pull completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Run container===&lt;br /&gt;
&lt;br /&gt;
* Quick Start&lt;br /&gt;
&amp;lt;code&amp;gt;docker run -d -p 8080:80 completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Quick Start with Persistent Storage&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/cnoobs-wiki:0.1&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* NOTE: The above &amp;lt;code&amp;gt;docker run&amp;lt;/code&amp;gt; command is for quick testing, if you want to be able to export your wiki&#039;s database to an XML file you can backup and share, please use method in expanding info box below. &lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
If you want to be able to Export Your MediaWiki Database to Dated XML File - use this &amp;lt;code&amp;gt;docker run&amp;lt;/code&amp;gt; method:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To export your MediaWiki database to a dated XML file (e.g., &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;) and save it to the host’s &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory, run the export script inside the Docker container and use a volume mount to write the file to the host.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 1: Create Host Directory&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
On the host, ensure the &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory exists:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir -p ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 2: Run Container with Volume Mount&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If your container isn’t already using a volume for &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt;, stop and remove it, then restart with a volume mount:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v ~/wiki-container:/export \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/cnoobs-wiki:0.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 3: Run Export Script in Container&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Access the container’s shell:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Inside the container, run the export script to create a dated XML file:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DATE=$(date +%Y%m%d)&lt;br /&gt;
php /var/www/html/maintenance/run.php dumpBackup.php --full --output=file:/export/$DATE.xml&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This writes the file (e.g., &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;) to &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt; in the container, which maps to &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; on the host.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 4: Verify the File&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
On the host, check for the XML file:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ls ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
You should see a file like &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Notes&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
- If you encounter permission issues, ensure the container’s user has write access to &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt;:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
chmod -R 777 /export&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- The script must be run inside the container, as it requires MediaWiki’s environment and database access.&amp;lt;br&amp;gt;&lt;br /&gt;
- If your container uses a different volume setup, adjust the mount point accordingly.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Import MediaWiki XML File from Host Directory:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To import a &amp;lt;code&amp;gt;wiki.xml&amp;lt;/code&amp;gt; file from the host’s &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory into your MediaWiki instance running in a Docker container, use the &amp;lt;code&amp;gt;importDump.php&amp;lt;/code&amp;gt; script inside the container. The &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory is mounted as &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt; in the container, allowing the container to read the file.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 1: Place the XML File&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
On the host, move or copy the &amp;lt;code&amp;gt;wiki.xml&amp;lt;/code&amp;gt; file to &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt;:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv ~/Downloads/wiki.xml ~/wiki-container/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Verify the file is present:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ls ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 2: Access the Container’s Shell&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Enter the container’s shell:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 3: Run the Import Script&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Inside the container, import the &amp;lt;code&amp;gt;wiki.xml&amp;lt;/code&amp;gt; file:&amp;lt;br&amp;gt;&lt;br /&gt;
Can check file is present in container with &amp;lt;code&amp;gt;ls /export/&amp;lt;/code&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php importDump.php /export/wiki.xml&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Note: This imports the XML content into the MediaWiki database. For large files, this may take time.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 4: Rebuild Wiki Indexes (Optional but Recommended)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Rebuild the wiki’s indexes to ensure imported content is accessible:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php rebuildall.php&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 5: Exit the Container&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Exit the container’s shell:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 6: Verify the Import&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Visit your wiki (e.g., &amp;lt;code&amp;gt;http://localhost:8080&amp;lt;/code&amp;gt;) to check if the imported pages appear. If issues arise, check the container logs:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Notes&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
- Ensure the container has read access to &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt;. Fix permissions if needed:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
chmod -R 777 /export&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- If you want to overwrite existing pages, use the &amp;lt;code&amp;gt;--no-updates&amp;lt;/code&amp;gt; flag with &amp;lt;code&amp;gt;importDump.php&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
- If the container wasn’t started with &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; mounted as &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt;, restart it with:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 -v ~/wiki-container:/export --name completenoobs_wiki completenoobs/cnoobs-wiki:0.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Now visit http://localhost:8080 on your browser&lt;br /&gt;
&lt;br /&gt;
* Due to (unknown) bug you might need to update the xml to download missing pages:&lt;br /&gt;
&amp;lt;code&amp;gt;docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Change Admin Password:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By default, the admin user&#039;s password is &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;. It&#039;s highly recommended to change this immediately. You can do this either through the wiki&#039;s web interface or directly in the Docker terminal.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 1: Change Password via Web Interface&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
This is the easiest method. You can change your password directly from the wiki itself.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Log in to your wiki with the default credentials: &amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
2. Once logged in, click your username (&amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt;) in the top-right corner of the page.&amp;lt;br&amp;gt;&lt;br /&gt;
3. From the drop-down menu, select &amp;lt;b&amp;gt;Preferences&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
4. On the Preferences page, go to the &amp;lt;b&amp;gt;Password&amp;lt;/b&amp;gt; tab.&amp;lt;br&amp;gt;&lt;br /&gt;
5. Enter the current password (&amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;), then enter your new password twice.&amp;lt;br&amp;gt;&lt;br /&gt;
6. Click &amp;lt;b&amp;gt;Change password&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Your password is now changed, and you will need to use the new one for future logins.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 2: Change Password via Terminal (No-Email Reset)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you have forgotten the password or prefer to use the command line, you can reset it directly inside the Docker container using a MediaWiki maintenance script.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;b&amp;gt;Access the container&#039;s shell&amp;lt;/b&amp;gt; with the following command from your host machine:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Once inside the container, use the &amp;lt;code&amp;gt;changePassword.php&amp;lt;/code&amp;gt; maintenance script to change the password. This is the modern, recommended way to run MediaWiki maintenance scripts.&amp;lt;br&amp;gt;&lt;br /&gt;
* Change &amp;lt;b&amp;gt;NEWPASSWORD&amp;lt;/b&amp;gt; to your new password&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt; to leave the container&#039;s shell.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The admin password has now been reset. You can log in to your wiki with the new password.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Remove container===&lt;br /&gt;
&lt;br /&gt;
To completely remove the &amp;lt;code&amp;gt;completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; container and image from your Ubuntu 24.04 system, follow these steps. You can also remove associated persistent storage volumes if they were created.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 1: Stop and Remove the Container&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If the container is running, stop it and then remove it.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Alternatively, stop and remove in one command:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rm -f completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 2: Remove the Docker Image&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Remove the &amp;lt;code&amp;gt;completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; image.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rmi completenoobs/cnoobs-wiki:0.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Note: If the image is in use by other containers, remove those containers first or use &amp;lt;code&amp;gt;docker rmi -f completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; to force removal (use with caution).&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 3: Remove Persistent Storage Volumes (Optional)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you used persistent storage, remove the associated volumes to free up space.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume rm completenoobs_mysql completenoobs_images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Note: Ensure no other containers are using these volumes, as this will delete all stored data.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 4: Verify Removal&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Check that the container, image, and volumes are removed.&amp;lt;br&amp;gt;&lt;br /&gt;
- List all containers (including stopped ones):&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps -a&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- List all images:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- List all volumes:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
If any items remain, repeat the relevant removal commands or check for dependencies.&lt;br /&gt;
&lt;br /&gt;
==Docker Compose container==&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Ubuntu2404_Install_Docker_and_Docker_Compose&amp;diff=666</id>
		<title>Ubuntu2404 Install Docker and Docker Compose</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Ubuntu2404_Install_Docker_and_Docker_Compose&amp;diff=666"/>
		<updated>2025-09-01T21:13:29Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* Run container */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Preparation ===&lt;br /&gt;
&lt;br /&gt;
Before we begin, make sure you&#039;re logged in with a user account that has sudo privileges.&lt;br /&gt;
&lt;br /&gt;
=== Update System Packages ===&lt;br /&gt;
&lt;br /&gt;
Update your package list to ensure you have the latest versions of packages:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Install Docker Prerequisites ===&lt;br /&gt;
&lt;br /&gt;
Install the necessary packages for Docker setup:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Setup Docker Repository ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Add Docker&#039;s Official GPG Key&#039;&#039;&#039;:&lt;br /&gt;
 &lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Add the Docker Repository&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  echo &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&amp;quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Install Docker and Docker Compose ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Update Package List Again&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo apt update&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Install Docker Engine, CLI, Containerd, and Additional Tools&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo apt install -y docker-ce docker-ce-cli containerd.io python3-bs4 python3-requests docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- &lt;br /&gt;
* &#039;&#039;&#039;Install Docker Compose&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
Here we&#039;re downloading the latest version of Docker Compose:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo curl -L &amp;quot;https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)&amp;quot; -o /usr/local/bin/docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Make the Docker Compose binary executable:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo chmod +x /usr/local/bin/docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Verify Installation ===&lt;br /&gt;
&lt;br /&gt;
Check if Docker and Docker Compose are installed correctly:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker --version&lt;br /&gt;
docker-compose --version&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Configure User Permissions ===&lt;br /&gt;
&lt;br /&gt;
To run Docker commands without &amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt;, add your user to the &amp;lt;code&amp;gt;docker&amp;lt;/code&amp;gt; group:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo usermod -aG docker $USER&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note&#039;&#039;&#039;: After adding your user to the docker group, you&#039;ll need to &#039;&#039;&#039;log out and log back in&#039;&#039;&#039; for the changes to take effect.&lt;br /&gt;
If you do not log out and back in, Or you do not add your $USER to the docker group, you will be required to use sudo in some cases. such as ..&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
a way to apply group changes without logging out and back in - tip:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
exec sudo su -l $USER&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will replace your current shell with a new login shell for your user, which will have the updated group memberships. Both of these methods will apply the group changes immediately, allowing you to use LXD commands without having to log out and back in. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Remember&amp;lt;/b&amp;gt;, these changes only apply to the current terminal session. If you open a new terminal window, you might need to run the command again or log out and back in for the changes to take effect system-wide.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===If Installing Docker messed up your LXC/LXD Networking===&lt;br /&gt;
To resolve networking conflicts between Docker and LXC containers on Ubuntu 24.04, enable IP forwarding on the host system:&lt;br /&gt;
* Open the sysctl configuration file in your preferred editor&lt;br /&gt;
&amp;lt;code&amp;gt;sudo $EDITOR /etc/sysctl.conf&amp;lt;/code&amp;gt;&lt;br /&gt;
* Uncomment or add the following line (around line 28):&lt;br /&gt;
&amp;lt;pre&amp;gt;net.ipv4.ip_forward=1&amp;lt;/pre&amp;gt;&lt;br /&gt;
* Apply the updated configuration to enable IP forwarding:&lt;br /&gt;
&amp;lt;code&amp;gt;sysctl -p&amp;lt;/code&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
* Restart the system to ensure all changes take effect.&lt;br /&gt;
&lt;br /&gt;
This should resolve the networking issue for LXC containers when Docker is installed.&lt;br /&gt;
&lt;br /&gt;
==Install First Container/Image==&lt;br /&gt;
&lt;br /&gt;
Download the completenoobs container image, mediawiki with the completenoobs xml installed.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker pull completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Run container===&lt;br /&gt;
&lt;br /&gt;
* Quick Start&lt;br /&gt;
&amp;lt;code&amp;gt;docker run -d -p 8080:80 completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Quick Start with Persistent Storage&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/cnoobs-wiki:0.1&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* NOTE: The above &amp;lt;code&amp;gt;docker run&amp;lt;/code&amp;gt; command is for quick testing, if you want to be able to export your wiki&#039;s database to an XML file you can backup and share, please use method in expanding info box below. &lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
If you want to be able to Export Your MediaWiki Database to Dated XML File - use this &amp;lt;code&amp;gt;docker run&amp;lt;/code&amp;gt; method:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To export your MediaWiki database to a dated XML file (e.g., &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;) and save it to the host’s &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory, run the export script inside the Docker container and use a volume mount to write the file to the host.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 1: Create Host Directory&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
On the host, ensure the &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory exists:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir -p ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 2: Run Container with Volume Mount&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If your container isn’t already using a volume for &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt;, stop and remove it, then restart with a volume mount:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v ~/wiki-container:/export \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/cnoobs-wiki:0.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 3: Run Export Script in Container&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Access the container’s shell:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Inside the container, run the export script to create a dated XML file:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DATE=$(date +%Y%m%d)&lt;br /&gt;
php /var/www/html/maintenance/run.php dumpBackup.php --full --output=file:/export/$DATE.xml&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This writes the file (e.g., &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;) to &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt; in the container, which maps to &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; on the host.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 4: Verify the File&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
On the host, check for the XML file:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ls ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
You should see a file like &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Notes&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
- If you encounter permission issues, ensure the container’s user has write access to &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt;:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
chmod -R 777 /export&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- The script must be run inside the container, as it requires MediaWiki’s environment and database access.&amp;lt;br&amp;gt;&lt;br /&gt;
- If your container uses a different volume setup, adjust the mount point accordingly.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Import MediaWiki XML File from Host Directory:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To import a &amp;lt;code&amp;gt;wiki.xml&amp;lt;/code&amp;gt; file from the host’s &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory into your MediaWiki instance running in a Docker container, use the &amp;lt;code&amp;gt;importDump.php&amp;lt;/code&amp;gt; script inside the container. The &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory is mounted as &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt; in the container, allowing the container to read the file.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 1: Place the XML File&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
On the host, move or copy the &amp;lt;code&amp;gt;wiki.xml&amp;lt;/code&amp;gt; file to &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt;:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mv ~/Downloads/wiki.xml ~/wiki-container/&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Verify the file is present:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ls ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 2: Access the Container’s Shell&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Enter the container’s shell:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 3: Run the Import Script&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Inside the container, import the &amp;lt;code&amp;gt;wiki.xml&amp;lt;/code&amp;gt; file:&amp;lt;br&amp;gt;&lt;br /&gt;
Can check file is present in container with &amp;lt;code&amp;gt;ls /export/&amp;lt;/code&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php importDump.php /export/wiki.xml&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Note: This imports the XML content into the MediaWiki database. For large files, this may take time.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 4: Rebuild Wiki Indexes (Optional but Recommended)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Rebuild the wiki’s indexes to ensure imported content is accessible:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php rebuildall.php&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 5: Exit the Container&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Exit the container’s shell:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 6: Verify the Import&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Visit your wiki (e.g., &amp;lt;code&amp;gt;http://localhost:8080&amp;lt;/code&amp;gt;) to check if the imported pages appear. If issues arise, check the container logs:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Notes&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
- Ensure the container has read access to &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt;. Fix permissions if needed:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
chmod -R 777 /export&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- If you want to overwrite existing pages, use the &amp;lt;code&amp;gt;--no-updates&amp;lt;/code&amp;gt; flag with &amp;lt;code&amp;gt;importDump.php&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
- If the container wasn’t started with &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; mounted as &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt;, restart it with:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 -v ~/wiki-container:/export --name completenoobs_wiki completenoobs/cnoobs-wiki:0.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Now visit http://localhost:8080 on your browser&lt;br /&gt;
&lt;br /&gt;
* Due to (unknown) bug you might need to update the xml to download missing pages:&lt;br /&gt;
&amp;lt;code&amp;gt;docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Change Admin Password:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By default, the admin user&#039;s password is &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;. It&#039;s highly recommended to change this immediately. You can do this either through the wiki&#039;s web interface or directly in the Docker terminal.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 1: Change Password via Web Interface&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
This is the easiest method. You can change your password directly from the wiki itself.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Log in to your wiki with the default credentials: &amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
2. Once logged in, click your username (&amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt;) in the top-right corner of the page.&amp;lt;br&amp;gt;&lt;br /&gt;
3. From the drop-down menu, select &amp;lt;b&amp;gt;Preferences&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
4. On the Preferences page, go to the &amp;lt;b&amp;gt;Password&amp;lt;/b&amp;gt; tab.&amp;lt;br&amp;gt;&lt;br /&gt;
5. Enter the current password (&amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;), then enter your new password twice.&amp;lt;br&amp;gt;&lt;br /&gt;
6. Click &amp;lt;b&amp;gt;Change password&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Your password is now changed, and you will need to use the new one for future logins.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 2: Change Password via Terminal (No-Email Reset)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you have forgotten the password or prefer to use the command line, you can reset it directly inside the Docker container using a MediaWiki maintenance script.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;b&amp;gt;Access the container&#039;s shell&amp;lt;/b&amp;gt; with the following command from your host machine:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Once inside the container, use the &amp;lt;code&amp;gt;changePassword.php&amp;lt;/code&amp;gt; maintenance script to change the password. This is the modern, recommended way to run MediaWiki maintenance scripts.&amp;lt;br&amp;gt;&lt;br /&gt;
* Change &amp;lt;b&amp;gt;NEWPASSWORD&amp;lt;/b&amp;gt; to your new password&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt; to leave the container&#039;s shell.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The admin password has now been reset. You can log in to your wiki with the new password.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Remove container===&lt;br /&gt;
&lt;br /&gt;
To completely remove the &amp;lt;code&amp;gt;completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; container and image from your Ubuntu 24.04 system, follow these steps. You can also remove associated persistent storage volumes if they were created.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 1: Stop and Remove the Container&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If the container is running, stop it and then remove it.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Alternatively, stop and remove in one command:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rm -f completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 2: Remove the Docker Image&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Remove the &amp;lt;code&amp;gt;completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; image.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rmi completenoobs/cnoobs-wiki:0.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Note: If the image is in use by other containers, remove those containers first or use &amp;lt;code&amp;gt;docker rmi -f completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; to force removal (use with caution).&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 3: Remove Persistent Storage Volumes (Optional)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you used persistent storage, remove the associated volumes to free up space.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume rm completenoobs_mysql completenoobs_images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Note: Ensure no other containers are using these volumes, as this will delete all stored data.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 4: Verify Removal&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Check that the container, image, and volumes are removed.&amp;lt;br&amp;gt;&lt;br /&gt;
- List all containers (including stopped ones):&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps -a&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- List all images:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- List all volumes:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
If any items remain, repeat the relevant removal commands or check for dependencies.&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=665</id>
		<title>CompleteNoobs Docker Image Creation</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=665"/>
		<updated>2025-09-01T20:52:16Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* 3.2: Run the Container */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Complete Noobs Docker Wiki Tutorial =&lt;br /&gt;
* [[Docker_Install_Guide| Docker install guide]]&lt;br /&gt;
==errors==&lt;br /&gt;
* This mainly works - just need to fix the extensions popular pages and contrubtion scores&lt;br /&gt;
* The XML updater requires more work - currently idea placeholder&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
* Ubuntu 24.04&lt;br /&gt;
* Docker installed and running&lt;br /&gt;
* Your user in docker group: &amp;lt;code&amp;gt;sudo usermod -aG docker $USER&amp;lt;/code&amp;gt; (then logout/login)&lt;br /&gt;
&lt;br /&gt;
== Step 2: Create All Files ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1: Dockerfile ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Copy this exactly:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM mediawiki:1.44&lt;br /&gt;
# Mediawiki 1.44 used over latest because can confirm extensions youtube and pagenotice works&lt;br /&gt;
# Install dependencies&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \&lt;br /&gt;
    mariadb-server \&lt;br /&gt;
    python3 \&lt;br /&gt;
    python3-requests \&lt;br /&gt;
    python3-bs4 \&lt;br /&gt;
    python3-pygments \&lt;br /&gt;
    curl \&lt;br /&gt;
    wget \&lt;br /&gt;
    unzip \&lt;br /&gt;
    nano \&lt;br /&gt;
    git \&lt;br /&gt;
    &amp;amp;&amp;amp; apt-get clean&lt;br /&gt;
&lt;br /&gt;
# Copy scripts&lt;br /&gt;
COPY download_latest_xml.py /usr/src/download_latest_xml.py&lt;br /&gt;
COPY setup_wiki.sh /usr/src/setup_wiki.sh&lt;br /&gt;
COPY update_xml.sh /usr/src/update_xml.sh&lt;br /&gt;
COPY entrypoint.sh /entrypoint.sh&lt;br /&gt;
&lt;br /&gt;
# Make executable&lt;br /&gt;
RUN chmod +x /usr/src/setup_wiki.sh /entrypoint.sh /usr/src/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Download XML&lt;br /&gt;
RUN python3 /usr/src/download_latest_xml.py&lt;br /&gt;
&lt;br /&gt;
# Setup wiki&lt;br /&gt;
RUN /usr/src/setup_wiki.sh&lt;br /&gt;
&lt;br /&gt;
EXPOSE 80&lt;br /&gt;
VOLUME /var/lib/mysql&lt;br /&gt;
VOLUME /var/www/html/images&lt;br /&gt;
&lt;br /&gt;
ENTRYPOINT [&amp;quot;/entrypoint.sh&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.2: XML Download Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano download_latest_xml.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_available_dumps():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        return sorted(dumps, key=parse_date_from_dump, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dumps: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def get_dump_files(dump):&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(f&amp;quot;{BASE_URL}{dump}/&amp;quot;, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
        return sorted(files, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dump files: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def download_file(url, filename):&lt;br /&gt;
    try:&lt;br /&gt;
        print(f&amp;quot;Downloading {filename}...&amp;quot;)&lt;br /&gt;
        response = requests.get(url, stream=True, timeout=60)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        &lt;br /&gt;
        total_size = int(response.headers.get(&#039;content-length&#039;, 0))&lt;br /&gt;
        downloaded = 0&lt;br /&gt;
        &lt;br /&gt;
        with open(filename, &#039;wb&#039;) as f:&lt;br /&gt;
            for chunk in response.iter_content(chunk_size=8192):&lt;br /&gt;
                if chunk:&lt;br /&gt;
                    f.write(chunk)&lt;br /&gt;
                    downloaded += len(chunk)&lt;br /&gt;
                    if total_size &amp;gt; 0:&lt;br /&gt;
                        progress = (downloaded / total_size) * 100&lt;br /&gt;
                        print(f&amp;quot;\rProgress: {progress:.1f}%&amp;quot;, end=&#039;&#039;, flush=True)&lt;br /&gt;
        print()&lt;br /&gt;
        return True&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error downloading {filename}: {e}&amp;quot;)&lt;br /&gt;
        return False&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    print(&amp;quot;Fetching available XML dumps...&amp;quot;)&lt;br /&gt;
    dumps = get_available_dumps()&lt;br /&gt;
    &lt;br /&gt;
    if not dumps:&lt;br /&gt;
        print(&amp;quot;No dumps found!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_dump = dumps[0]&lt;br /&gt;
    print(f&amp;quot;Latest dump: {newest_dump}&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    files = get_dump_files(newest_dump)&lt;br /&gt;
    if not files:&lt;br /&gt;
        print(&amp;quot;No XML files found in latest dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_xml = files[0]&lt;br /&gt;
    xml_url = f&amp;quot;{BASE_URL}{newest_dump}/{newest_xml}&amp;quot;&lt;br /&gt;
    local_filename = &amp;quot;/tmp/completenoobs_dump.xml&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if download_file(xml_url, local_filename):&lt;br /&gt;
        print(f&amp;quot;Successfully downloaded {newest_xml}&amp;quot;)&lt;br /&gt;
        with open(&amp;quot;/tmp/dump_info.txt&amp;quot;, &amp;quot;w&amp;quot;) as f:&lt;br /&gt;
            f.write(f&amp;quot;{newest_dump}/{newest_xml}&amp;quot;)&lt;br /&gt;
    else:&lt;br /&gt;
        print(&amp;quot;Failed to download XML dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.3: Main Setup Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano setup_wiki.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Setting up CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Initialize MariaDB&lt;br /&gt;
if [ ! -d &amp;quot;/var/lib/mysql/mysql&amp;quot; ]; then&lt;br /&gt;
    mysql_install_db --user=mysql --datadir=/var/lib/mysql&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 2&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
# Setup database&lt;br /&gt;
mysql -e &amp;quot;CREATE DATABASE IF NOT EXISTS completenoobs_wiki CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;127.0.0.1&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;127.0.0.1&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;localhost&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;localhost&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;FLUSH PRIVILEGES;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Install MediaWiki&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
php maintenance/install.php \&lt;br /&gt;
    --dbtype=mysql \&lt;br /&gt;
    --dbserver=127.0.0.1 \&lt;br /&gt;
    --dbname=completenoobs_wiki \&lt;br /&gt;
    --dbuser=wikiuser \&lt;br /&gt;
    --dbpass=wikipass \&lt;br /&gt;
    --server=&amp;quot;http://localhost:8080&amp;quot; \&lt;br /&gt;
    --scriptpath=&amp;quot;&amp;quot; \&lt;br /&gt;
    --lang=en \&lt;br /&gt;
    --pass=AdminPass123! \&lt;br /&gt;
    &amp;quot;CompleteNoobs Wiki&amp;quot; \&lt;br /&gt;
    &amp;quot;admin&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Download and install extensions&lt;br /&gt;
cd extensions/&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/PageNotice --branch REL1_44 || echo &amp;quot;PageNotice download failed, continuing...&amp;quot;&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/YouTube --branch REL1_44 || echo &amp;quot;YouTube download failed, continuing...&amp;quot;&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
&lt;br /&gt;
# Configure LocalSettings.php&lt;br /&gt;
cat &amp;gt;&amp;gt; LocalSettings.php &amp;lt;&amp;lt; &#039;EOF&#039;&lt;br /&gt;
# Basic settings&lt;br /&gt;
$wgEnableUploads = true;&lt;br /&gt;
$wgUseImageMagick = true;&lt;br /&gt;
$wgImageMagickConvertCommand = &amp;quot;/usr/bin/convert&amp;quot;;&lt;br /&gt;
$wgDefaultSkin = &amp;quot;vector-2022&amp;quot;;&lt;br /&gt;
$wgAllowExternalImages = true;&lt;br /&gt;
&lt;br /&gt;
# Debug (can be removed later)&lt;br /&gt;
$wgShowExceptionDetails = true;&lt;br /&gt;
$wgDebugLogFile = &amp;quot;/tmp/mediawiki-debug.log&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
# PageNotice extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/PageNotice/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;PageNotice&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# YouTube extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/YouTube/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;YouTube&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# SyntaxHighlight (usually bundled)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/SyntaxHighlight_GeSHi/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;SyntaxHighlight_GeSHi&#039; );&lt;br /&gt;
    $wgPygmentizePath = &#039;/usr/bin/pygmentize&#039;;&lt;br /&gt;
}&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
# Import XML dump if available&lt;br /&gt;
if [ -f &amp;quot;/tmp/completenoobs_dump.xml&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Importing XML dump...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if php maintenance/importDump.php --uploads &amp;lt; /tmp/completenoobs_dump.xml; then&lt;br /&gt;
        echo &amp;quot;XML import completed!&amp;quot;&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;XML import had warnings&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Basic maintenance&lt;br /&gt;
    php maintenance/update.php --quick || echo &amp;quot;Update completed with warnings&amp;quot;&lt;br /&gt;
    php maintenance/rebuildrecentchanges.php || echo &amp;quot;RecentChanges rebuilt with warnings&amp;quot;&lt;br /&gt;
    php maintenance/initSiteStats.php || echo &amp;quot;SiteStats initialized with warnings&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if [ -f &amp;quot;/tmp/dump_info.txt&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Import: $(cat /tmp/dump_info.txt)&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
        echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
    fi&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No XML dump found - starting with empty wiki&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
# Copy update script to accessible location&lt;br /&gt;
cp /usr/src/update_xml.sh /var/www/html/update_xml.sh&lt;br /&gt;
chmod +x /var/www/html/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Create user-friendly update wrapper&lt;br /&gt;
cat &amp;gt; /var/www/html/check_updates.sh &amp;lt;&amp;lt; &#039;UPDATE_WRAPPER_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki Update Checker ===&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;This tool checks for new content from the CompleteNoobs XML repository&amp;quot;&lt;br /&gt;
echo &amp;quot;and imports ONLY new pages, preserving all your local edits.&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
/var/www/html/update_xml.sh&lt;br /&gt;
UPDATE_WRAPPER_EOF&lt;br /&gt;
chmod +x /var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# Create simple status script&lt;br /&gt;
cat &amp;gt; /var/www/html/check_status.sh &amp;lt;&amp;lt; &#039;STATUS_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
echo &amp;quot;=== Wiki Status ===&amp;quot;&lt;br /&gt;
echo &amp;quot;Pages: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;Users: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM user WHERE user_id &amp;gt; 0;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Extensions ===&amp;quot;&lt;br /&gt;
if [ -d &amp;quot;extensions/PageNotice&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;PageNotice: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;PageNotice: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/YouTube&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;YouTube: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;YouTube: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/ContributionScores&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;ContributionScores: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;ContributionScores: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Update System ===&amp;quot;&lt;br /&gt;
if [ -f &amp;quot;.last_import&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Current version: $(grep &#039;Import:&#039; .last_import | cut -d&#039; &#039; -f2)&amp;quot;&lt;br /&gt;
    echo &amp;quot;Import date: $(grep &#039;Date:&#039; .last_import | cut -d&#039; &#039; -f2-)&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No version info available&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
STATUS_EOF&lt;br /&gt;
chmod +x /var/www/html/check_status.sh&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Setup completed!&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;Update scripts installed:&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/check_updates.sh (user-friendly)&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/update_xml.sh (direct)&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Final counts&lt;br /&gt;
PAGES=$(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;0&amp;quot;)&lt;br /&gt;
echo &amp;quot;Pages imported: $PAGES&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb stop&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.4: XML Update Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano update_xml.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki XML Updater ===&amp;quot;&lt;br /&gt;
echo &amp;quot;This will check for new and updated pages&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Function to check if running in container&lt;br /&gt;
check_environment() {&lt;br /&gt;
    if [ ! -f &amp;quot;/var/www/html/LocalSettings.php&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Error: This script must be run inside the wiki container&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to get current XML version&lt;br /&gt;
get_current_version() {&lt;br /&gt;
    if [ -f &amp;quot;/var/www/html/.last_import&amp;quot; ]; then&lt;br /&gt;
        grep &amp;quot;Import:&amp;quot; /var/www/html/.last_import | cut -d&#039; &#039; -f2&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;none&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to check for updates&lt;br /&gt;
check_for_updates() {&lt;br /&gt;
    python3 - &amp;lt;&amp;lt; &#039;PYTHON_EOF&#039;&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_latest_dump():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        if dumps:&lt;br /&gt;
            latest = sorted(dumps, key=parse_date_from_dump, reverse=True)[0]&lt;br /&gt;
            &lt;br /&gt;
            # Get XML files from latest dump&lt;br /&gt;
            response = requests.get(f&amp;quot;{BASE_URL}{latest}/&amp;quot;, timeout=30)&lt;br /&gt;
            response.raise_for_status()&lt;br /&gt;
            soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
            files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                    if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
            &lt;br /&gt;
            if files:&lt;br /&gt;
                newest_xml = sorted(files, reverse=True)[0]&lt;br /&gt;
                print(f&amp;quot;{latest}/{newest_xml}&amp;quot;)&lt;br /&gt;
                sys.exit(0)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;ERROR: {e}&amp;quot;, file=sys.stderr)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
    &lt;br /&gt;
    print(&amp;quot;ERROR: No dumps found&amp;quot;, file=sys.stderr)&lt;br /&gt;
    sys.exit(1)&lt;br /&gt;
&lt;br /&gt;
get_latest_dump()&lt;br /&gt;
PYTHON_EOF&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to download new XML&lt;br /&gt;
download_xml() {&lt;br /&gt;
    local dump_info=&amp;quot;$1&amp;quot;&lt;br /&gt;
    local dump_dir=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f1)&lt;br /&gt;
    local xml_file=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f2)&lt;br /&gt;
    local url=&amp;quot;https://xml.completenoobs.com/xmlDumps/${dump_info}&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Downloading: $xml_file&amp;quot;&lt;br /&gt;
    echo &amp;quot;From: $dump_dir&amp;quot;&lt;br /&gt;
    echo &amp;quot;URL: $url&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if wget -O /tmp/new_dump.xml &amp;quot;$url&amp;quot; --progress=bar:force 2&amp;gt;&amp;amp;1; then&lt;br /&gt;
        echo &amp;quot;Download successful!&amp;quot;&lt;br /&gt;
        return 0&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;Download failed!&amp;quot;&lt;br /&gt;
        return 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to backup current database&lt;br /&gt;
backup_database() {&lt;br /&gt;
    echo &amp;quot;Creating database backup...&amp;quot;&lt;br /&gt;
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)&lt;br /&gt;
    mysqldump --user=wikiuser --password=wikipass completenoobs_wiki &amp;gt; /tmp/wiki_backup_${TIMESTAMP}.sql&lt;br /&gt;
    echo &amp;quot;Backup created: /tmp/wiki_backup_${TIMESTAMP}.sql&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to analyze and import changes&lt;br /&gt;
analyze_and_import() {&lt;br /&gt;
    echo &amp;quot;Analyzing differences between XML and local wiki...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Create analysis and import script for MediaWiki 1.44+&lt;br /&gt;
    cat &amp;gt; /tmp/analyze_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class AnalyzeAndImport extends Maintenance {&lt;br /&gt;
    private $db;&lt;br /&gt;
    private $new_pages = [];&lt;br /&gt;
    private $changed_pages = [];&lt;br /&gt;
    private $unchanged_pages = [];&lt;br /&gt;
    &lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addDescription(&#039;Analyze and selectively import from XML dump&#039;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $this-&amp;gt;db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // MediaWiki 1.35+ uses slots and content tables&lt;br /&gt;
        // Get existing pages with their content&lt;br /&gt;
        $query = &amp;quot;&lt;br /&gt;
            SELECT p.page_title, p.page_id, c.content_address, c.content_sha1&lt;br /&gt;
            FROM page p&lt;br /&gt;
            JOIN revision r ON p.page_latest = r.rev_id&lt;br /&gt;
            JOIN slots s ON r.rev_id = s.slot_revision_id&lt;br /&gt;
            JOIN slot_roles sr ON s.slot_role_id = sr.role_id&lt;br /&gt;
            JOIN content c ON s.slot_content_id = c.content_id&lt;br /&gt;
            WHERE p.page_namespace = 0 AND sr.role_name = &#039;main&#039;&lt;br /&gt;
        &amp;quot;;&lt;br /&gt;
        &lt;br /&gt;
        $result = $this-&amp;gt;db-&amp;gt;query($query);&lt;br /&gt;
        if (!$result) {&lt;br /&gt;
            $this-&amp;gt;error(&amp;quot;Database query failed: &amp;quot; . $this-&amp;gt;db-&amp;gt;error);&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $existing = [];&lt;br /&gt;
        while ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
            // Get actual text content&lt;br /&gt;
            $text_content = $this-&amp;gt;getTextContent($row[&#039;content_address&#039;]);&lt;br /&gt;
            $existing[$row[&#039;page_title&#039;]] = [&lt;br /&gt;
                &#039;id&#039; =&amp;gt; $row[&#039;page_id&#039;],&lt;br /&gt;
                &#039;content&#039; =&amp;gt; $text_content,&lt;br /&gt;
                &#039;sha1&#039; =&amp;gt; $row[&#039;content_sha1&#039;]&lt;br /&gt;
            ];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Parse XML and compare&lt;br /&gt;
        $xml = simplexml_load_file(&#039;/tmp/new_dump.xml&#039;);&lt;br /&gt;
        &lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            $title = str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title);&lt;br /&gt;
            $xml_content = (string)$page-&amp;gt;revision-&amp;gt;text;&lt;br /&gt;
            &lt;br /&gt;
            if (!isset($existing[$title])) {&lt;br /&gt;
                // New page&lt;br /&gt;
                $this-&amp;gt;new_pages[$title] = $xml_content;&lt;br /&gt;
            } else {&lt;br /&gt;
                // Compare content using SHA1 for efficiency&lt;br /&gt;
                $xml_sha1 = sha1($xml_content);&lt;br /&gt;
                &lt;br /&gt;
                if ($existing[$title][&#039;sha1&#039;] !== $xml_sha1) {&lt;br /&gt;
                    // Content is different&lt;br /&gt;
                    $this-&amp;gt;changed_pages[$title] = [&lt;br /&gt;
                        &#039;local&#039; =&amp;gt; $existing[$title][&#039;content&#039;],&lt;br /&gt;
                        &#039;xml&#039; =&amp;gt; $xml_content,&lt;br /&gt;
                        &#039;page_id&#039; =&amp;gt; $existing[$title][&#039;id&#039;]&lt;br /&gt;
                    ];&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;unchanged_pages[] = $title;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Display summary&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Update Analysis ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages to import: &amp;quot; . count($this-&amp;gt;new_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Changed pages found: &amp;quot; . count($this-&amp;gt;changed_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Unchanged pages: &amp;quot; . count($this-&amp;gt;unchanged_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Save analysis for review&lt;br /&gt;
        file_put_contents(&#039;/tmp/update_analysis.json&#039;, json_encode([&lt;br /&gt;
            &#039;new&#039; =&amp;gt; array_keys($this-&amp;gt;new_pages),&lt;br /&gt;
            &#039;changed&#039; =&amp;gt; array_keys($this-&amp;gt;changed_pages),&lt;br /&gt;
            &#039;unchanged&#039; =&amp;gt; $this-&amp;gt;unchanged_pages&lt;br /&gt;
        ], JSON_PRETTY_PRINT));&lt;br /&gt;
        &lt;br /&gt;
        // Show changed pages with preview (limit to first 20 for readability)&lt;br /&gt;
        if (count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Changed Pages ===\n&amp;quot;);&lt;br /&gt;
            $count = 0;&lt;br /&gt;
            $total_changed = count($this-&amp;gt;changed_pages);&lt;br /&gt;
            &lt;br /&gt;
            foreach ($this-&amp;gt;changed_pages as $title =&amp;gt; $data) {&lt;br /&gt;
                $count++;&lt;br /&gt;
                if ($count &amp;lt;= 20) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;\n$count. $title\n&amp;quot;);&lt;br /&gt;
                    &lt;br /&gt;
                    // Create a simple diff preview (first 300 chars)&lt;br /&gt;
                    $xml_preview = substr($data[&#039;xml&#039;], 0, 100);&lt;br /&gt;
                    &lt;br /&gt;
                    // Save full diff to file&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;=== FULL DIFF FOR: $title ===\n\n&amp;quot;);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- LOCAL VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;local&#039;] . &amp;quot;\n\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- XML VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;xml&#039;], FILE_APPEND);&lt;br /&gt;
                    &lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;   Preview: &amp;quot; . $xml_preview . &amp;quot;...\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ($total_changed &amp;gt; 20) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\n... and &amp;quot; . ($total_changed - 20) . &amp;quot; more changed pages.\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;All diff files saved to /tmp/diff_*.txt\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Interactive selection&lt;br /&gt;
        if (count($this-&amp;gt;new_pages) &amp;gt; 0 || count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Import Options ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;1. Import new pages only (preserve all local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;2. Import new pages + update ALL changed pages (overwrites local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;3. Selective import (choose which updates to apply)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;4. Cancel (no changes)\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            // Save state for import script WITHOUT the XML object&lt;br /&gt;
            $import_data = [&lt;br /&gt;
                &#039;new_pages&#039; =&amp;gt; $this-&amp;gt;new_pages,&lt;br /&gt;
                &#039;changed_pages&#039; =&amp;gt; $this-&amp;gt;changed_pages,&lt;br /&gt;
                &#039;xml_file&#039; =&amp;gt; &#039;/tmp/new_dump.xml&#039;  // Save path instead of object&lt;br /&gt;
            ];&lt;br /&gt;
            &lt;br /&gt;
            file_put_contents(&#039;/tmp/import_data.ser&#039;, serialize($import_data));&lt;br /&gt;
        } else {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\nNo changes detected. Your wiki is up to date!\n&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function getTextContent($address) {&lt;br /&gt;
        // Handle different content storage formats in MW 1.35+&lt;br /&gt;
        if (strpos($address, &#039;tt:&#039;) === 0) {&lt;br /&gt;
            // Text table reference&lt;br /&gt;
            $text_id = substr($address, 3);&lt;br /&gt;
            $result = $this-&amp;gt;db-&amp;gt;query(&amp;quot;SELECT old_text FROM text WHERE old_id = $text_id&amp;quot;);&lt;br /&gt;
            if ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
                return $row[&#039;old_text&#039;];&lt;br /&gt;
            }&lt;br /&gt;
        } elseif (strpos($address, &#039;es:&#039;) === 0) {&lt;br /&gt;
            // External storage - would need special handling&lt;br /&gt;
            return &amp;quot;[External storage content]&amp;quot;;&lt;br /&gt;
        }&lt;br /&gt;
        // Direct content&lt;br /&gt;
        return $address;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = AnalyzeAndImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/analyze_import.php&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to perform selected import&lt;br /&gt;
perform_import() {&lt;br /&gt;
    local choice=$1&lt;br /&gt;
    &lt;br /&gt;
    cat &amp;gt; /tmp/do_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class DoImport extends Maintenance {&lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addOption(&#039;mode&#039;, &#039;Import mode&#039;, true, true);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $mode = $this-&amp;gt;getOption(&#039;mode&#039;);&lt;br /&gt;
        $data = unserialize(file_get_contents(&#039;/tmp/import_data.ser&#039;));&lt;br /&gt;
        &lt;br /&gt;
        // Load XML file&lt;br /&gt;
        $xml = simplexml_load_file($data[&#039;xml_file&#039;]);&lt;br /&gt;
        &lt;br /&gt;
        $new_imported = 0;&lt;br /&gt;
        $updated = 0;&lt;br /&gt;
        &lt;br /&gt;
        // Import new pages (always for modes 1-3)&lt;br /&gt;
        if ($mode != &#039;4&#039;) {&lt;br /&gt;
            foreach ($data[&#039;new_pages&#039;] as $title =&amp;gt; $content) {&lt;br /&gt;
                $this-&amp;gt;importPage($title, $content, $xml);&lt;br /&gt;
                $new_imported++;&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Imported new page: $title\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Handle changed pages based on mode&lt;br /&gt;
        if ($mode == &#039;2&#039;) {&lt;br /&gt;
            // Update all changed pages&lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Updating: $title\n&amp;quot;);&lt;br /&gt;
                if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                    $updated++;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Updated page: $title\n&amp;quot;);&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        } elseif ($mode == &#039;3&#039;) {&lt;br /&gt;
            // Selective update&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Selective Import Mode ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;For each changed page, choose:\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  y = yes, update this page\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  n = no, keep local version\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  d = show diff file\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  a = update all remaining pages\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  s = skip all remaining pages\n\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            $update_all = false;&lt;br /&gt;
            $skip_all = false;&lt;br /&gt;
            &lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                if ($skip_all) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($update_all) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\nPage: $title\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Action (y/n/d/a/s): &amp;quot;);&lt;br /&gt;
                $handle = fopen(&amp;quot;php://stdin&amp;quot;, &amp;quot;r&amp;quot;);&lt;br /&gt;
                $line = trim(fgets($handle));&lt;br /&gt;
                &lt;br /&gt;
                while ($line == &#039;d&#039;) {&lt;br /&gt;
                    // Show diff&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    if (file_exists($diff_file)) {&lt;br /&gt;
                        system(&amp;quot;head -50 $diff_file&amp;quot;);&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;\n[Showing first 50 lines - full file at $diff_file]\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Action (y/n/a/s): &amp;quot;);&lt;br /&gt;
                    $line = trim(fgets($handle));&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($line == &#039;a&#039;) {&lt;br /&gt;
                    $update_all = true;&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } elseif ($line == &#039;s&#039;) {&lt;br /&gt;
                    $skip_all = true;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                } elseif ($line == &#039;y&#039;) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Import Complete ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages imported: $new_imported\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Pages updated: $updated\n&amp;quot;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function importPage($title, $content, $xml) {&lt;br /&gt;
        // Create single page XML for import&lt;br /&gt;
        $tempFile = &#039;/tmp/single_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
        $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
        &lt;br /&gt;
        if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
            $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
            foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Find and add the page&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                break;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
        exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;);&lt;br /&gt;
        unlink($tempFile);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function reimportPage($title, $xml) {&lt;br /&gt;
        // For updating existing pages, delete then reimport&lt;br /&gt;
        $db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Delete the existing page&lt;br /&gt;
        $safe_title = $db-&amp;gt;real_escape_string(str_replace(&#039; &#039;, &#039;_&#039;, $title));&lt;br /&gt;
        $db-&amp;gt;query(&amp;quot;DELETE FROM page WHERE page_title = &#039;$safe_title&#039; AND page_namespace = 0&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        $db-&amp;gt;close();&lt;br /&gt;
        &lt;br /&gt;
        // Now import the new version&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $tempFile = &#039;/tmp/update_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
                $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
                &lt;br /&gt;
                if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
                    $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
                    foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                        $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
                $result = exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;, $output, $return);&lt;br /&gt;
                unlink($tempFile);&lt;br /&gt;
                &lt;br /&gt;
                return ($return === 0);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = DoImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/do_import.php --mode=&amp;quot;$choice&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Main execution&lt;br /&gt;
main() {&lt;br /&gt;
    check_environment&lt;br /&gt;
    &lt;br /&gt;
    # Start MariaDB if not running&lt;br /&gt;
    service mariadb status &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 || service mariadb start&lt;br /&gt;
    &lt;br /&gt;
    # Wait for MariaDB&lt;br /&gt;
    for i in {1..30}; do&lt;br /&gt;
        if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
            break&lt;br /&gt;
        fi&lt;br /&gt;
        sleep 1&lt;br /&gt;
    done&lt;br /&gt;
    &lt;br /&gt;
    CURRENT=$(get_current_version)&lt;br /&gt;
    echo &amp;quot;Current version: $CURRENT&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Checking for updates...&amp;quot;&lt;br /&gt;
    LATEST=$(check_for_updates 2&amp;gt;/dev/null)&lt;br /&gt;
    if [ $? -ne 0 ] || [ -z &amp;quot;$LATEST&amp;quot; ] || [[ &amp;quot;$LATEST&amp;quot; == *&amp;quot;ERROR&amp;quot;* ]]; then&lt;br /&gt;
        echo &amp;quot;Failed to check for updates&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Latest available: $LATEST&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Always proceed to analysis even if versions match&lt;br /&gt;
    # (there might be content updates in the same version)&lt;br /&gt;
    echo &amp;quot;Proceeding with content analysis...&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Backup database&lt;br /&gt;
    backup_database&lt;br /&gt;
    &lt;br /&gt;
    # Download new XML&lt;br /&gt;
    if ! download_xml &amp;quot;$LATEST&amp;quot;; then&lt;br /&gt;
        echo &amp;quot;Failed to download new XML&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Analyze differences&lt;br /&gt;
    analyze_and_import&lt;br /&gt;
    &lt;br /&gt;
    # Check if there are changes to import&lt;br /&gt;
    if [ -f &amp;quot;/tmp/import_data.ser&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;&amp;quot;&lt;br /&gt;
        read -p &amp;quot;Choose option (1-4): &amp;quot; -n 1 -r&lt;br /&gt;
        echo&lt;br /&gt;
        &lt;br /&gt;
        if [[ $REPLY =~ ^[1-4]$ ]]; then&lt;br /&gt;
            if [ &amp;quot;$REPLY&amp;quot; != &amp;quot;4&amp;quot; ]; then&lt;br /&gt;
                perform_import &amp;quot;$REPLY&amp;quot;&lt;br /&gt;
                &lt;br /&gt;
                # Update version info&lt;br /&gt;
                echo &amp;quot;Import: $LATEST&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
                echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
                &lt;br /&gt;
                # Rebuild indices&lt;br /&gt;
                echo &amp;quot;Rebuilding indices...&amp;quot;&lt;br /&gt;
                php maintenance/rebuildrecentchanges.php&lt;br /&gt;
                php maintenance/initSiteStats.php&lt;br /&gt;
            else&lt;br /&gt;
                echo &amp;quot;Update cancelled&amp;quot;&lt;br /&gt;
            fi&lt;br /&gt;
        else&lt;br /&gt;
            echo &amp;quot;Invalid option. Update cancelled&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Clean up temp files&lt;br /&gt;
    rm -f /tmp/import_data.ser /tmp/update_analysis.json /tmp/diff_*.txt /tmp/analyze_import.php /tmp/do_import.php 2&amp;gt;/dev/null&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    echo &amp;quot;Done!&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Run main function&lt;br /&gt;
main &amp;quot;$@&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.5: Entrypoint Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano entrypoint.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Starting CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 1&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;CompleteNoobs Wiki ready at: http://localhost:8080&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin login: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Features:&amp;quot;&lt;br /&gt;
echo &amp;quot;- Complete wiki content imported from XML&amp;quot;&lt;br /&gt;
echo &amp;quot;- License notices on all pages (via PageNotice)&amp;quot;&lt;br /&gt;
echo &amp;quot;- SyntaxHighlight for code blocks&amp;quot;&lt;br /&gt;
echo &amp;quot;- YouTube video embedding&amp;quot;&lt;br /&gt;
echo &amp;quot;- Contribution Scores special page&amp;quot;&lt;br /&gt;
echo &amp;quot;- XML update system (preserves local edits)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
apache2-foreground&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 3: Build and Run ==&lt;br /&gt;
&lt;br /&gt;
=== 3.1: Build the Image ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t completenoobs/wiki:latest .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will take several minutes.&lt;br /&gt;
&lt;br /&gt;
=== 3.2: Run the Container ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* NOTE: The above &amp;lt;code&amp;gt;docker run&amp;lt;/code&amp;gt; command is for quick testing, if you want to be able to export your wiki&#039;s database to an XML file you can backup and share, please use method in expanding info box below. &lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
If you want to be able to Export Your MediaWiki Database to Dated XML File - use this &amp;lt;code&amp;gt;docker run&amp;lt;/code&amp;gt; method:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To export your MediaWiki database to a dated XML file (e.g., &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;) and save it to the host’s &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory, run the export script inside the Docker container and use a volume mount to write the file to the host.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 1: Create Host Directory&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
On the host, ensure the &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory exists:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir -p ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 2: Run Container with Volume Mount&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If your container isn’t already using a volume for &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt;, stop and remove it, then restart with a volume mount:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v ~/wiki-container:/export \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/cnoobs-wiki:0.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 3: Run Export Script in Container&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Access the container’s shell:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Inside the container, run the export script to create a dated XML file:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DATE=$(date +%Y%m%d)&lt;br /&gt;
php /var/www/html/maintenance/run.php dumpBackup.php --full --output=file:/export/$DATE.xml&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This writes the file (e.g., &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;) to &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt; in the container, which maps to &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; on the host.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 4: Verify the File&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
On the host, check for the XML file:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ls ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
You should see a file like &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Notes&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
- If you encounter permission issues, ensure the container’s user has write access to &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt;:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
chmod -R 777 /export&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- The script must be run inside the container, as it requires MediaWiki’s environment and database access.&amp;lt;br&amp;gt;&lt;br /&gt;
- If your container uses a different volume setup, adjust the mount point accordingly.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 4: Test Everything ==&lt;br /&gt;
&lt;br /&gt;
=== 4.1: Check the Wiki ===&lt;br /&gt;
* Visit: http://localhost:8080&lt;br /&gt;
* You should see the PageNotice at the top&lt;br /&gt;
* Login with: admin / AdminPass123!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Change Admin Password:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By default, the admin user&#039;s password is &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;. It&#039;s highly recommended to change this immediately. You can do this either through the wiki&#039;s web interface or directly in the Docker terminal.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 1: Change Password via Web Interface&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
This is the easiest method. You can change your password directly from the wiki itself.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Log in to your wiki with the default credentials: &amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
2. Once logged in, click your username (&amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt;) in the top-right corner of the page.&amp;lt;br&amp;gt;&lt;br /&gt;
3. From the drop-down menu, select &amp;lt;b&amp;gt;Preferences&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
4. On the Preferences page, go to the &amp;lt;b&amp;gt;Password&amp;lt;/b&amp;gt; tab.&amp;lt;br&amp;gt;&lt;br /&gt;
5. Enter the current password (&amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;), then enter your new password twice.&amp;lt;br&amp;gt;&lt;br /&gt;
6. Click &amp;lt;b&amp;gt;Change password&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Your password is now changed, and you will need to use the new one for future logins.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 2: Change Password via Terminal (No-Email Reset)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you have forgotten the password or prefer to use the command line, you can reset it directly inside the Docker container using a MediaWiki maintenance script.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;b&amp;gt;Access the container&#039;s shell&amp;lt;/b&amp;gt; with the following command from your host machine:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Once inside the container, use the &amp;lt;code&amp;gt;changePassword.php&amp;lt;/code&amp;gt; maintenance script to change the password. This is the modern, recommended way to run MediaWiki maintenance scripts.&amp;lt;br&amp;gt;&lt;br /&gt;
* Change &amp;lt;b&amp;gt;NEWPASSWORD&amp;lt;/b&amp;gt; to your new password&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt; to leave the container&#039;s shell.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The admin password has now been reset. You can log in to your wiki with the new password.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.2: Test Extensions ===&lt;br /&gt;
* &#039;&#039;&#039;YouTube&#039;&#039;&#039;: Edit any page, add &amp;lt;code&amp;gt;&amp;lt;youtube&amp;gt;N9qYF9DZPdw&amp;lt;/youtube&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;PageNotice&#039;&#039;&#039;: Should already be visible at the top&lt;br /&gt;
* &#039;&#039;&#039;SyntaxHighlight&#039;&#039;&#039;: Add code blocks with &amp;lt;nowiki&amp;gt;&amp;lt;code&amp;gt;&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;code here&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.3: Check Status ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/check_status.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 5: XML Update Operations ==&lt;br /&gt;
*NOTE: update_xml.sh still needs alot of work, this just idea placeholder for now.&lt;br /&gt;
=== 5.1: Check for Updates (Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will:&lt;br /&gt;
* Check the CompleteNoobs XML repository for new dumps&lt;br /&gt;
* Compare with your current version&lt;br /&gt;
* Ask for confirmation before updating&lt;br /&gt;
* Import ONLY new pages (preserves your edits)&lt;br /&gt;
* Create a backup before making changes&lt;br /&gt;
&lt;br /&gt;
=== 5.2: Force Update (Non-Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki bash -c &amp;quot;echo &#039;y&#039; | /var/www/html/update_xml.sh&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5.3: Manual Update Process ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# 1. Enter container&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&lt;br /&gt;
# 2. Check current version&lt;br /&gt;
cat /var/www/html/.last_import&lt;br /&gt;
&lt;br /&gt;
# 3. Run update&lt;br /&gt;
/var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# 4. Exit container&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 6: Troubleshooting Commands ==&lt;br /&gt;
=== Access container shell: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Edit configuration: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki nano /var/www/html/LocalSettings.php&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Check logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== View update logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki tail -f /tmp/mediawiki-debug.log&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Complete restart: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Expected Results ==&lt;br /&gt;
* Working wiki with imported CompleteNoobs content&lt;br /&gt;
* PageNotice visible at top of all pages&lt;br /&gt;
* All extensions functional&lt;br /&gt;
* Text editors (nano/vim) available in container&lt;br /&gt;
* Utility scripts for maintenance&lt;br /&gt;
* XML update system that preserves local edits&lt;br /&gt;
&lt;br /&gt;
==need to add==&lt;br /&gt;
* way for user to backup there local custom wiki - xml exporter&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Ubuntu2404_Install_Docker_and_Docker_Compose&amp;diff=664</id>
		<title>Ubuntu2404 Install Docker and Docker Compose</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Ubuntu2404_Install_Docker_and_Docker_Compose&amp;diff=664"/>
		<updated>2025-09-01T20:50:32Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* Run container */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Preparation ===&lt;br /&gt;
&lt;br /&gt;
Before we begin, make sure you&#039;re logged in with a user account that has sudo privileges.&lt;br /&gt;
&lt;br /&gt;
=== Update System Packages ===&lt;br /&gt;
&lt;br /&gt;
Update your package list to ensure you have the latest versions of packages:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Install Docker Prerequisites ===&lt;br /&gt;
&lt;br /&gt;
Install the necessary packages for Docker setup:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Setup Docker Repository ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Add Docker&#039;s Official GPG Key&#039;&#039;&#039;:&lt;br /&gt;
 &lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Add the Docker Repository&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  echo &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&amp;quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Install Docker and Docker Compose ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Update Package List Again&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo apt update&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Install Docker Engine, CLI, Containerd, and Additional Tools&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo apt install -y docker-ce docker-ce-cli containerd.io python3-bs4 python3-requests docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- &lt;br /&gt;
* &#039;&#039;&#039;Install Docker Compose&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
Here we&#039;re downloading the latest version of Docker Compose:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo curl -L &amp;quot;https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)&amp;quot; -o /usr/local/bin/docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Make the Docker Compose binary executable:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo chmod +x /usr/local/bin/docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Verify Installation ===&lt;br /&gt;
&lt;br /&gt;
Check if Docker and Docker Compose are installed correctly:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker --version&lt;br /&gt;
docker-compose --version&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Configure User Permissions ===&lt;br /&gt;
&lt;br /&gt;
To run Docker commands without &amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt;, add your user to the &amp;lt;code&amp;gt;docker&amp;lt;/code&amp;gt; group:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo usermod -aG docker $USER&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note&#039;&#039;&#039;: After adding your user to the docker group, you&#039;ll need to &#039;&#039;&#039;log out and log back in&#039;&#039;&#039; for the changes to take effect.&lt;br /&gt;
If you do not log out and back in, Or you do not add your $USER to the docker group, you will be required to use sudo in some cases. such as ..&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
a way to apply group changes without logging out and back in - tip:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
exec sudo su -l $USER&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will replace your current shell with a new login shell for your user, which will have the updated group memberships. Both of these methods will apply the group changes immediately, allowing you to use LXD commands without having to log out and back in. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Remember&amp;lt;/b&amp;gt;, these changes only apply to the current terminal session. If you open a new terminal window, you might need to run the command again or log out and back in for the changes to take effect system-wide.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===If Installing Docker messed up your LXC/LXD Networking===&lt;br /&gt;
To resolve networking conflicts between Docker and LXC containers on Ubuntu 24.04, enable IP forwarding on the host system:&lt;br /&gt;
* Open the sysctl configuration file in your preferred editor&lt;br /&gt;
&amp;lt;code&amp;gt;sudo $EDITOR /etc/sysctl.conf&amp;lt;/code&amp;gt;&lt;br /&gt;
* Uncomment or add the following line (around line 28):&lt;br /&gt;
&amp;lt;pre&amp;gt;net.ipv4.ip_forward=1&amp;lt;/pre&amp;gt;&lt;br /&gt;
* Apply the updated configuration to enable IP forwarding:&lt;br /&gt;
&amp;lt;code&amp;gt;sysctl -p&amp;lt;/code&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
* Restart the system to ensure all changes take effect.&lt;br /&gt;
&lt;br /&gt;
This should resolve the networking issue for LXC containers when Docker is installed.&lt;br /&gt;
&lt;br /&gt;
==Install First Container/Image==&lt;br /&gt;
&lt;br /&gt;
Download the completenoobs container image, mediawiki with the completenoobs xml installed.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker pull completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Run container===&lt;br /&gt;
&lt;br /&gt;
* Quick Start&lt;br /&gt;
&amp;lt;code&amp;gt;docker run -d -p 8080:80 completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Quick Start with Persistent Storage&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/cnoobs-wiki:0.1&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* NOTE: The above &amp;lt;code&amp;gt;docker run&amp;lt;/code&amp;gt; command is for quick testing, if you want to be able to export your wiki&#039;s database to an XML file you can backup and share, please use method in expanding info box below. &lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
If you want to be able to Export Your MediaWiki Database to Dated XML File - use this &amp;lt;code&amp;gt;docker run&amp;lt;/code&amp;gt; method:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To export your MediaWiki database to a dated XML file (e.g., &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;) and save it to the host’s &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory, run the export script inside the Docker container and use a volume mount to write the file to the host.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 1: Create Host Directory&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
On the host, ensure the &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; directory exists:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir -p ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 2: Run Container with Volume Mount&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If your container isn’t already using a volume for &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt;, stop and remove it, then restart with a volume mount:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v ~/wiki-container:/export \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/cnoobs-wiki:0.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 3: Run Export Script in Container&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Access the container’s shell:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Inside the container, run the export script to create a dated XML file:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
DATE=$(date +%Y%m%d)&lt;br /&gt;
php /var/www/html/maintenance/run.php dumpBackup.php --full --output=file:/export/$DATE.xml&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This writes the file (e.g., &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;) to &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt; in the container, which maps to &amp;lt;code&amp;gt;~/wiki-container&amp;lt;/code&amp;gt; on the host.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 4: Verify the File&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
On the host, check for the XML file:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
ls ~/wiki-container&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
You should see a file like &amp;lt;code&amp;gt;20250901.xml&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Notes&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
- If you encounter permission issues, ensure the container’s user has write access to &amp;lt;code&amp;gt;/export&amp;lt;/code&amp;gt;:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
chmod -R 777 /export&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- The script must be run inside the container, as it requires MediaWiki’s environment and database access.&amp;lt;br&amp;gt;&lt;br /&gt;
- If your container uses a different volume setup, adjust the mount point accordingly.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* Now visit http://localhost:8080 on your browser&lt;br /&gt;
&lt;br /&gt;
* Due to (unknown) bug you might need to update the xml to download missing pages:&lt;br /&gt;
&amp;lt;code&amp;gt;docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Change Admin Password:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By default, the admin user&#039;s password is &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;. It&#039;s highly recommended to change this immediately. You can do this either through the wiki&#039;s web interface or directly in the Docker terminal.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 1: Change Password via Web Interface&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
This is the easiest method. You can change your password directly from the wiki itself.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Log in to your wiki with the default credentials: &amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
2. Once logged in, click your username (&amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt;) in the top-right corner of the page.&amp;lt;br&amp;gt;&lt;br /&gt;
3. From the drop-down menu, select &amp;lt;b&amp;gt;Preferences&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
4. On the Preferences page, go to the &amp;lt;b&amp;gt;Password&amp;lt;/b&amp;gt; tab.&amp;lt;br&amp;gt;&lt;br /&gt;
5. Enter the current password (&amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;), then enter your new password twice.&amp;lt;br&amp;gt;&lt;br /&gt;
6. Click &amp;lt;b&amp;gt;Change password&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Your password is now changed, and you will need to use the new one for future logins.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 2: Change Password via Terminal (No-Email Reset)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you have forgotten the password or prefer to use the command line, you can reset it directly inside the Docker container using a MediaWiki maintenance script.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;b&amp;gt;Access the container&#039;s shell&amp;lt;/b&amp;gt; with the following command from your host machine:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Once inside the container, use the &amp;lt;code&amp;gt;changePassword.php&amp;lt;/code&amp;gt; maintenance script to change the password. This is the modern, recommended way to run MediaWiki maintenance scripts.&amp;lt;br&amp;gt;&lt;br /&gt;
* Change &amp;lt;b&amp;gt;NEWPASSWORD&amp;lt;/b&amp;gt; to your new password&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt; to leave the container&#039;s shell.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The admin password has now been reset. You can log in to your wiki with the new password.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Remove container===&lt;br /&gt;
&lt;br /&gt;
To completely remove the &amp;lt;code&amp;gt;completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; container and image from your Ubuntu 24.04 system, follow these steps. You can also remove associated persistent storage volumes if they were created.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 1: Stop and Remove the Container&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If the container is running, stop it and then remove it.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Alternatively, stop and remove in one command:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rm -f completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 2: Remove the Docker Image&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Remove the &amp;lt;code&amp;gt;completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; image.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rmi completenoobs/cnoobs-wiki:0.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Note: If the image is in use by other containers, remove those containers first or use &amp;lt;code&amp;gt;docker rmi -f completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; to force removal (use with caution).&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 3: Remove Persistent Storage Volumes (Optional)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you used persistent storage, remove the associated volumes to free up space.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume rm completenoobs_mysql completenoobs_images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Note: Ensure no other containers are using these volumes, as this will delete all stored data.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 4: Verify Removal&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Check that the container, image, and volumes are removed.&amp;lt;br&amp;gt;&lt;br /&gt;
- List all containers (including stopped ones):&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps -a&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- List all images:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- List all volumes:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
If any items remain, repeat the relevant removal commands or check for dependencies.&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Docker_Mediawiki_Local_Install&amp;diff=663</id>
		<title>Docker Mediawiki Local Install</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Docker_Mediawiki_Local_Install&amp;diff=663"/>
		<updated>2025-09-01T20:35:47Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* Step 2: Create a Docker Compose File */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Setting Up a Plain MediaWiki Instance with Docker on Ubuntu 24.04 =&lt;br /&gt;
&lt;br /&gt;
This guide provides a simple, step-by-step process to set up a basic MediaWiki instance locally using Docker and Docker Compose on Ubuntu 24.04. &lt;br /&gt;
&lt;br /&gt;
It is designed for beginners and assumes you have Docker and Docker Compose installed (see [[Docker_Install_Guide]] for setup details). &lt;br /&gt;
&lt;br /&gt;
The setup includes MediaWiki with a MariaDB database, running in Docker containers.&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
* Ubuntu 24.04&lt;br /&gt;
* Docker and Docker Compose installed&lt;br /&gt;
* Basic familiarity with terminal commands&lt;br /&gt;
* Internet connection for pulling Docker images&lt;br /&gt;
&lt;br /&gt;
== Step 1: Create a Project Directory ==&lt;br /&gt;
Create a dedicated directory to organize your MediaWiki setup files.&lt;br /&gt;
&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;mkdir mediawiki-docker&amp;lt;/code&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;cd mediawiki-docker&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 2: Create a Docker Compose File ==&lt;br /&gt;
Create a &#039;&#039;&#039;docker-compose.yml&#039;&#039;&#039; file to define the MediaWiki and MariaDB services.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;nano docker-compose.yml&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Paste the following configuration into &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
version: &#039;3.8&#039;&lt;br /&gt;
services:&lt;br /&gt;
  mediawiki:&lt;br /&gt;
    image: mediawiki:1.41&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;8080:80&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      database:&lt;br /&gt;
        condition: service_healthy&lt;br /&gt;
    volumes:  #  will uncomment line below after init setup of wiki&lt;br /&gt;
#      - ./LocalSettings.php:/var/www/html/LocalSettings.php:ro    &lt;br /&gt;
    environment:&lt;br /&gt;
      - MEDIAWIKI_DB_HOST=database&lt;br /&gt;
      - MEDIAWIKI_DB_USER=wikiuser&lt;br /&gt;
      - MEDIAWIKI_DB_PASSWORD=securepassword&lt;br /&gt;
      - MEDIAWIKI_DB_NAME=mediawiki&lt;br /&gt;
  database:&lt;br /&gt;
    image: mariadb:10.11&lt;br /&gt;
    environment:&lt;br /&gt;
      - MARIADB_ROOT_PASSWORD=securepassword&lt;br /&gt;
      - MARIADB_DATABASE=mediawiki&lt;br /&gt;
      - MARIADB_USER=wikiuser&lt;br /&gt;
      - MARIADB_PASSWORD=securepassword&lt;br /&gt;
      - MARIADB_AUTO_UPGRADE=1&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    healthcheck:&lt;br /&gt;
      test: [&amp;quot;CMD&amp;quot;, &amp;quot;mysqladmin&amp;quot;, &amp;quot;ping&amp;quot;, &amp;quot;-h&amp;quot;, &amp;quot;localhost&amp;quot;]&lt;br /&gt;
      interval: 10s&lt;br /&gt;
      timeout: 5s&lt;br /&gt;
      retries: 5&lt;br /&gt;
    mem_limit: 512m&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Save and exit (`Ctrl+O`, `Enter`, `Ctrl+X`).&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Explanation:&#039;&#039;&#039;&lt;br /&gt;
* The mediawiki service uses MediaWiki 1.41 for stability.&lt;br /&gt;
* `database` service uses the MariaDB image for the database.&lt;br /&gt;
* Port `8080` maps to MediaWiki&#039;s web server port `80`.&lt;br /&gt;
* Environment variables set up the database connection.&lt;br /&gt;
* A volume persists MariaDB data.&lt;br /&gt;
* `LocalSettings.php` will be mounted later after setup.&lt;br /&gt;
&lt;br /&gt;
== Step 3: Start the Containers ==&lt;br /&gt;
Run the following command to start the MediaWiki and MariaDB containers:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker-compose up -d&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* `-d` runs containers in the background.&lt;br /&gt;
* This pulls the MediaWiki and MariaDB images and starts the services.&lt;br /&gt;
&lt;br /&gt;
== Step 4: Verify Containers Are Running ==&lt;br /&gt;
Check that both containers are running:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker ps&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You should see two containers: one for `mediawiki` and one for `mariadb`.&lt;br /&gt;
&lt;br /&gt;
== Step 5: Access the MediaWiki Setup Page ==&lt;br /&gt;
Open a web browser and navigate to:&lt;br /&gt;
&lt;br /&gt;
http://localhost:8080&lt;br /&gt;
&lt;br /&gt;
You should see the MediaWiki setup page. If not, ensure containers are running and port `8080` is not blocked.&lt;br /&gt;
&lt;br /&gt;
== Step 6: Complete the MediaWiki Web Installer ==&lt;br /&gt;
Follow the on-screen instructions in the browser:&lt;br /&gt;
* Select your language and click &amp;quot;Continue.&amp;quot;&lt;br /&gt;
* Accept the defaults for database settings, these details can be found in your &#039;&#039;&#039;docker-compose.yml&#039;&#039;&#039; file.&lt;br /&gt;
** host: &#039;&#039;&#039;database&#039;&#039;&#039;  - change from default &#039;&#039;&#039;localhost&#039;&#039;&#039;&lt;br /&gt;
** user: &#039;&#039;&#039;wikiuser&#039;&#039;&#039; - change from default &#039;&#039;&#039;root&#039;&#039;&#039;&lt;br /&gt;
** password: &#039;&#039;&#039;securepassword&#039;&#039;&#039; &lt;br /&gt;
** database name: &#039;&#039;&#039;mediawiki&#039;&#039;&#039; - change from default &#039;&#039;&#039;my_wiki&#039;&#039;&#039;&lt;br /&gt;
* Set up an admin user and password for your wiki.&lt;br /&gt;
* Complete the installation. At the end, MediaWiki generates a &#039;&#039;&#039;LocalSettings.php&#039;&#039;&#039; file.&lt;br /&gt;
&lt;br /&gt;
== Step 7: Download and Place LocalSettings.php ==&lt;br /&gt;
The installer prompts you to download `LocalSettings.php`. Save it to your `mediawiki-docker` directory:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;mv ~/Downloads/LocalSettings.php ~/mediawiki-docker/&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ensure the file is named exactly &#039;&#039;&#039;LocalSettings.php&#039;&#039;&#039;.&amp;lt;br&amp;gt;&lt;br /&gt;
The Docker Compose configuration mounts this file into the MediaWiki container.&lt;br /&gt;
&lt;br /&gt;
== Step 8: Restart Containers ==&lt;br /&gt;
Restart the containers to apply &#039;&#039;&#039;LocalSettings.php&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
* Turn off container&lt;br /&gt;
&amp;lt;code&amp;gt;docker-compose down&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Edit &#039;&#039;&#039;docker-compose.yml&#039;&#039;&#039; and uncomment the lines&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#    volumes:  #  will uncomment these 2 lines after init setup of wiki&lt;br /&gt;
#      - ./LocalSettings.php:/var/www/html/LocalSettings.php:ro&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
version: &#039;3.8&#039;&lt;br /&gt;
services:&lt;br /&gt;
  mediawiki:&lt;br /&gt;
    image: mediawiki:1.41&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;8080:80&amp;quot;&lt;br /&gt;
    depends_on:&lt;br /&gt;
      database:&lt;br /&gt;
        condition: service_healthy&lt;br /&gt;
    volumes:  #  will uncomment these 2 lines after init setup of wiki&lt;br /&gt;
      - ./LocalSettings.php:/var/www/html/LocalSettings.php:ro    &lt;br /&gt;
    environment:&lt;br /&gt;
      - MEDIAWIKI_DB_HOST=database&lt;br /&gt;
      - MEDIAWIKI_DB_USER=wikiuser&lt;br /&gt;
      - MEDIAWIKI_DB_PASSWORD=securepassword&lt;br /&gt;
      - MEDIAWIKI_DB_NAME=mediawiki&lt;br /&gt;
  database:&lt;br /&gt;
    image: mariadb:10.11&lt;br /&gt;
    environment:&lt;br /&gt;
      - MARIADB_ROOT_PASSWORD=securepassword&lt;br /&gt;
      - MARIADB_DATABASE=mediawiki&lt;br /&gt;
      - MARIADB_USER=wikiuser&lt;br /&gt;
      - MARIADB_PASSWORD=securepassword&lt;br /&gt;
      - MARIADB_AUTO_UPGRADE=1&lt;br /&gt;
    volumes:&lt;br /&gt;
      - db_data:/var/lib/mysql&lt;br /&gt;
    healthcheck:&lt;br /&gt;
      test: [&amp;quot;CMD&amp;quot;, &amp;quot;mysqladmin&amp;quot;, &amp;quot;ping&amp;quot;, &amp;quot;-h&amp;quot;, &amp;quot;localhost&amp;quot;]&lt;br /&gt;
      interval: 10s&lt;br /&gt;
      timeout: 5s&lt;br /&gt;
      retries: 5&lt;br /&gt;
    mem_limit: 512m&lt;br /&gt;
volumes:&lt;br /&gt;
  db_data:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* start container&lt;br /&gt;
&amp;lt;code&amp;gt;docker-compose up -d&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 9: Access Your Wiki ==&lt;br /&gt;
Visit `http://localhost:8080` again. You should now see your MediaWiki instance. Log in with the admin credentials you set during installation.&lt;br /&gt;
&lt;br /&gt;
== Step 10: Basic Usage ==&lt;br /&gt;
* Create and edit pages using the MediaWiki interface.&lt;br /&gt;
* Access the wiki at `http://localhost:8080`.&lt;br /&gt;
* Manage users and settings via the admin account.&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Stopping and Removing Containers ==&lt;br /&gt;
To stop the containers:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker-compose stop&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
To stop and remove containers (data persists in the `db_data` volume):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker-compose down&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
To remove all data (including the database), also delete the volume:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker-compose down -v&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Network Configuration ==&lt;br /&gt;
&lt;br /&gt;
By default, your wiki might only be accessible from the host machine where Docker is running, using &amp;lt;code&amp;gt;localhost&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;127.0.0.1&amp;lt;/code&amp;gt;. However, if you want others on your network to access your wiki, you need to make some adjustments.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Current Setup:&#039;&#039;&#039; The computer running Docker Compose has an IP address of &amp;lt;code&amp;gt;192.168.0.44&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
* You can find the IP address of your computer running Docker using the command:&lt;br /&gt;
&amp;lt;code&amp;gt;ip addr&amp;lt;/code&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
OutPut from &amp;lt;code&amp;gt;ip addr&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
noob@noob-HP-EliteDesk-800-G1-DM:~$ ip addr&lt;br /&gt;
1: lo: &amp;lt;LOOPBACK,UP,LOWER_UP&amp;gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000&lt;br /&gt;
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00&lt;br /&gt;
    inet 127.0.0.1/8 scope host lo&lt;br /&gt;
       valid_lft forever preferred_lft forever&lt;br /&gt;
    inet6 ::1/128 scope host noprefixroute &lt;br /&gt;
       valid_lft forever preferred_lft forever&lt;br /&gt;
2: eno1: &amp;lt;NO-CARRIER,BROADCAST,MULTICAST,UP&amp;gt; mtu 1500 qdisc fq_codel state DOWN group default qlen 1000&lt;br /&gt;
    link/ether 8c:dc:d4:3d:93:49 brd ff:ff:ff:ff:ff:ff&lt;br /&gt;
    altname enp0s25&lt;br /&gt;
3: wlxe8de27142be2: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc noqueue state UP group default qlen 1000&lt;br /&gt;
    link/ether e8:de:27:14:2b:e2 brd ff:ff:ff:ff:ff:ff&lt;br /&gt;
    inet 192.168.0.44/24 brd 192.168.0.255 scope global dynamic noprefixroute wlxe8de27142be2&lt;br /&gt;
       valid_lft 86357sec preferred_lft 86357sec&lt;br /&gt;
    inet6 fe80::afbe:cc73:73a2:fcdf/64 scope link noprefixroute &lt;br /&gt;
       valid_lft forever preferred_lft forever&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
My IP is &amp;lt;code&amp;gt;192.168.0.44&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If someone from another computer on the network tries to visit &amp;lt;code&amp;gt;192.168.0.44:8080&amp;lt;/code&amp;gt;, they might encounter a &amp;quot;cannot connect&amp;quot; error. This happens because MediaWiki, by default, redirects to &amp;lt;code&amp;gt;127.0.0.1:8080&amp;lt;/code&amp;gt;, which is only accessible from the host machine itself. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Solution:&#039;&#039;&#039; To allow access from other devices on the same network, you need to:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Edit the &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt; File:&#039;&#039;&#039; This isn&#039;t done inside the Docker container but rather in the directory where your &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt; file is located (~/mediawiki-docker/LocalSettings.php). Here, you need to change the server URL configuration.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Modify URL Configuration:&#039;&#039;&#039; On lines 34-35 of &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt;, you&#039;ll find the following:&lt;br /&gt;
&lt;br /&gt;
=== Allowing Access from Other Network Devices ===&lt;br /&gt;
Edit &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt; in the Docker Compose directory to change the server URL:&lt;br /&gt;
* Default - around line 33&lt;br /&gt;
&amp;lt;source lang=&amp;quot;php&amp;quot;&amp;gt;&lt;br /&gt;
## The protocol and server name to use in fully-qualified URLs&lt;br /&gt;
$wgServer = &#039;http://127.0.0.1:8080&#039;;&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
*Changed&lt;br /&gt;
&amp;lt;source lang=&amp;quot;php&amp;quot;&amp;gt;&lt;br /&gt;
## The protocol and server name to use in fully-qualified URLs&lt;br /&gt;
$wgServer = &#039;http://192.168.0.44:8080&#039;;&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Restart Docker to apply changes:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker-compose restart&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This allows access to your wiki from other devices on the network using &amp;lt;code&amp;gt;192.168.0.44:8080&amp;lt;/code&amp;gt;.&lt;br /&gt;
This adjustment tells MediaWiki to use the network IP of the host (&amp;lt;code&amp;gt;192.168.0.44&amp;lt;/code&amp;gt;) instead of the local loopback (&amp;lt;code&amp;gt;127.0.0.1&amp;lt;/code&amp;gt;), allowing other devices on the network to access the wiki through &amp;lt;code&amp;gt;192.168.0.44:8080&amp;lt;/code&amp;gt;. Even after this change, &amp;lt;code&amp;gt;localhost:8080&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;127.0.0.1:8080&amp;lt;/code&amp;gt; will still work on the host machine, but now the wiki is also accessible via the network IP from other devices.&lt;br /&gt;
&lt;br /&gt;
== Troubleshooting ==&lt;br /&gt;
* &#039;&#039;&#039;Cannot access localhost:8080&#039;&#039;&#039;: Check `docker ps` to ensure containers are running. Verify port `8080` is not used by another service (`sudo netstat -tuln | grep 8080`).&lt;br /&gt;
* &#039;&#039;&#039;Database connection error&#039;&#039;&#039;: Ensure environment variables in `docker-compose.yml` match the installer settings.&lt;br /&gt;
* &#039;&#039;&#039;LocalSettings.php not found&#039;&#039;&#039;: Confirm the file is in the `mediawiki-docker` directory and named correctly.&lt;br /&gt;
&lt;br /&gt;
== Notes ==&lt;br /&gt;
* The database data is stored in a Docker volume (`db_data`) and persists between container restarts.&lt;br /&gt;
* To customize MediaWiki, edit `LocalSettings.php` and restart containers.&lt;br /&gt;
* For production, secure `MYSQL_ROOT_PASSWORD` and `MEDIAWIKI_DB_PASSWORD` with stronger values.&lt;br /&gt;
&lt;br /&gt;
== References ==&lt;br /&gt;
* [[Docker_Install_Guide]]&lt;br /&gt;
* [https://hub.docker.com/_/mediawiki Official MediaWiki Docker Image]&lt;br /&gt;
* [https://hub.docker.com/_/mariadb Official MariaDB Docker Image]&lt;br /&gt;
* [https://www.mediawiki.org/wiki/Manual:Installing_MediaWiki MediaWiki Installation Guide]&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=662</id>
		<title>CompleteNoobs Docker Image Creation</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=662"/>
		<updated>2025-09-01T20:20:28Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* Expected Results */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Complete Noobs Docker Wiki Tutorial =&lt;br /&gt;
* [[Docker_Install_Guide| Docker install guide]]&lt;br /&gt;
==errors==&lt;br /&gt;
* This mainly works - just need to fix the extensions popular pages and contrubtion scores&lt;br /&gt;
* The XML updater requires more work - currently idea placeholder&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
* Ubuntu 24.04&lt;br /&gt;
* Docker installed and running&lt;br /&gt;
* Your user in docker group: &amp;lt;code&amp;gt;sudo usermod -aG docker $USER&amp;lt;/code&amp;gt; (then logout/login)&lt;br /&gt;
&lt;br /&gt;
== Step 2: Create All Files ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1: Dockerfile ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Copy this exactly:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM mediawiki:1.44&lt;br /&gt;
# Mediawiki 1.44 used over latest because can confirm extensions youtube and pagenotice works&lt;br /&gt;
# Install dependencies&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \&lt;br /&gt;
    mariadb-server \&lt;br /&gt;
    python3 \&lt;br /&gt;
    python3-requests \&lt;br /&gt;
    python3-bs4 \&lt;br /&gt;
    python3-pygments \&lt;br /&gt;
    curl \&lt;br /&gt;
    wget \&lt;br /&gt;
    unzip \&lt;br /&gt;
    nano \&lt;br /&gt;
    git \&lt;br /&gt;
    &amp;amp;&amp;amp; apt-get clean&lt;br /&gt;
&lt;br /&gt;
# Copy scripts&lt;br /&gt;
COPY download_latest_xml.py /usr/src/download_latest_xml.py&lt;br /&gt;
COPY setup_wiki.sh /usr/src/setup_wiki.sh&lt;br /&gt;
COPY update_xml.sh /usr/src/update_xml.sh&lt;br /&gt;
COPY entrypoint.sh /entrypoint.sh&lt;br /&gt;
&lt;br /&gt;
# Make executable&lt;br /&gt;
RUN chmod +x /usr/src/setup_wiki.sh /entrypoint.sh /usr/src/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Download XML&lt;br /&gt;
RUN python3 /usr/src/download_latest_xml.py&lt;br /&gt;
&lt;br /&gt;
# Setup wiki&lt;br /&gt;
RUN /usr/src/setup_wiki.sh&lt;br /&gt;
&lt;br /&gt;
EXPOSE 80&lt;br /&gt;
VOLUME /var/lib/mysql&lt;br /&gt;
VOLUME /var/www/html/images&lt;br /&gt;
&lt;br /&gt;
ENTRYPOINT [&amp;quot;/entrypoint.sh&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.2: XML Download Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano download_latest_xml.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_available_dumps():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        return sorted(dumps, key=parse_date_from_dump, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dumps: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def get_dump_files(dump):&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(f&amp;quot;{BASE_URL}{dump}/&amp;quot;, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
        return sorted(files, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dump files: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def download_file(url, filename):&lt;br /&gt;
    try:&lt;br /&gt;
        print(f&amp;quot;Downloading {filename}...&amp;quot;)&lt;br /&gt;
        response = requests.get(url, stream=True, timeout=60)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        &lt;br /&gt;
        total_size = int(response.headers.get(&#039;content-length&#039;, 0))&lt;br /&gt;
        downloaded = 0&lt;br /&gt;
        &lt;br /&gt;
        with open(filename, &#039;wb&#039;) as f:&lt;br /&gt;
            for chunk in response.iter_content(chunk_size=8192):&lt;br /&gt;
                if chunk:&lt;br /&gt;
                    f.write(chunk)&lt;br /&gt;
                    downloaded += len(chunk)&lt;br /&gt;
                    if total_size &amp;gt; 0:&lt;br /&gt;
                        progress = (downloaded / total_size) * 100&lt;br /&gt;
                        print(f&amp;quot;\rProgress: {progress:.1f}%&amp;quot;, end=&#039;&#039;, flush=True)&lt;br /&gt;
        print()&lt;br /&gt;
        return True&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error downloading {filename}: {e}&amp;quot;)&lt;br /&gt;
        return False&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    print(&amp;quot;Fetching available XML dumps...&amp;quot;)&lt;br /&gt;
    dumps = get_available_dumps()&lt;br /&gt;
    &lt;br /&gt;
    if not dumps:&lt;br /&gt;
        print(&amp;quot;No dumps found!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_dump = dumps[0]&lt;br /&gt;
    print(f&amp;quot;Latest dump: {newest_dump}&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    files = get_dump_files(newest_dump)&lt;br /&gt;
    if not files:&lt;br /&gt;
        print(&amp;quot;No XML files found in latest dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_xml = files[0]&lt;br /&gt;
    xml_url = f&amp;quot;{BASE_URL}{newest_dump}/{newest_xml}&amp;quot;&lt;br /&gt;
    local_filename = &amp;quot;/tmp/completenoobs_dump.xml&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if download_file(xml_url, local_filename):&lt;br /&gt;
        print(f&amp;quot;Successfully downloaded {newest_xml}&amp;quot;)&lt;br /&gt;
        with open(&amp;quot;/tmp/dump_info.txt&amp;quot;, &amp;quot;w&amp;quot;) as f:&lt;br /&gt;
            f.write(f&amp;quot;{newest_dump}/{newest_xml}&amp;quot;)&lt;br /&gt;
    else:&lt;br /&gt;
        print(&amp;quot;Failed to download XML dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.3: Main Setup Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano setup_wiki.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Setting up CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Initialize MariaDB&lt;br /&gt;
if [ ! -d &amp;quot;/var/lib/mysql/mysql&amp;quot; ]; then&lt;br /&gt;
    mysql_install_db --user=mysql --datadir=/var/lib/mysql&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 2&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
# Setup database&lt;br /&gt;
mysql -e &amp;quot;CREATE DATABASE IF NOT EXISTS completenoobs_wiki CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;127.0.0.1&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;127.0.0.1&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;localhost&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;localhost&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;FLUSH PRIVILEGES;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Install MediaWiki&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
php maintenance/install.php \&lt;br /&gt;
    --dbtype=mysql \&lt;br /&gt;
    --dbserver=127.0.0.1 \&lt;br /&gt;
    --dbname=completenoobs_wiki \&lt;br /&gt;
    --dbuser=wikiuser \&lt;br /&gt;
    --dbpass=wikipass \&lt;br /&gt;
    --server=&amp;quot;http://localhost:8080&amp;quot; \&lt;br /&gt;
    --scriptpath=&amp;quot;&amp;quot; \&lt;br /&gt;
    --lang=en \&lt;br /&gt;
    --pass=AdminPass123! \&lt;br /&gt;
    &amp;quot;CompleteNoobs Wiki&amp;quot; \&lt;br /&gt;
    &amp;quot;admin&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Download and install extensions&lt;br /&gt;
cd extensions/&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/PageNotice --branch REL1_44 || echo &amp;quot;PageNotice download failed, continuing...&amp;quot;&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/YouTube --branch REL1_44 || echo &amp;quot;YouTube download failed, continuing...&amp;quot;&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
&lt;br /&gt;
# Configure LocalSettings.php&lt;br /&gt;
cat &amp;gt;&amp;gt; LocalSettings.php &amp;lt;&amp;lt; &#039;EOF&#039;&lt;br /&gt;
# Basic settings&lt;br /&gt;
$wgEnableUploads = true;&lt;br /&gt;
$wgUseImageMagick = true;&lt;br /&gt;
$wgImageMagickConvertCommand = &amp;quot;/usr/bin/convert&amp;quot;;&lt;br /&gt;
$wgDefaultSkin = &amp;quot;vector-2022&amp;quot;;&lt;br /&gt;
$wgAllowExternalImages = true;&lt;br /&gt;
&lt;br /&gt;
# Debug (can be removed later)&lt;br /&gt;
$wgShowExceptionDetails = true;&lt;br /&gt;
$wgDebugLogFile = &amp;quot;/tmp/mediawiki-debug.log&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
# PageNotice extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/PageNotice/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;PageNotice&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# YouTube extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/YouTube/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;YouTube&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# SyntaxHighlight (usually bundled)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/SyntaxHighlight_GeSHi/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;SyntaxHighlight_GeSHi&#039; );&lt;br /&gt;
    $wgPygmentizePath = &#039;/usr/bin/pygmentize&#039;;&lt;br /&gt;
}&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
# Import XML dump if available&lt;br /&gt;
if [ -f &amp;quot;/tmp/completenoobs_dump.xml&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Importing XML dump...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if php maintenance/importDump.php --uploads &amp;lt; /tmp/completenoobs_dump.xml; then&lt;br /&gt;
        echo &amp;quot;XML import completed!&amp;quot;&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;XML import had warnings&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Basic maintenance&lt;br /&gt;
    php maintenance/update.php --quick || echo &amp;quot;Update completed with warnings&amp;quot;&lt;br /&gt;
    php maintenance/rebuildrecentchanges.php || echo &amp;quot;RecentChanges rebuilt with warnings&amp;quot;&lt;br /&gt;
    php maintenance/initSiteStats.php || echo &amp;quot;SiteStats initialized with warnings&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if [ -f &amp;quot;/tmp/dump_info.txt&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Import: $(cat /tmp/dump_info.txt)&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
        echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
    fi&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No XML dump found - starting with empty wiki&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
# Copy update script to accessible location&lt;br /&gt;
cp /usr/src/update_xml.sh /var/www/html/update_xml.sh&lt;br /&gt;
chmod +x /var/www/html/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Create user-friendly update wrapper&lt;br /&gt;
cat &amp;gt; /var/www/html/check_updates.sh &amp;lt;&amp;lt; &#039;UPDATE_WRAPPER_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki Update Checker ===&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;This tool checks for new content from the CompleteNoobs XML repository&amp;quot;&lt;br /&gt;
echo &amp;quot;and imports ONLY new pages, preserving all your local edits.&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
/var/www/html/update_xml.sh&lt;br /&gt;
UPDATE_WRAPPER_EOF&lt;br /&gt;
chmod +x /var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# Create simple status script&lt;br /&gt;
cat &amp;gt; /var/www/html/check_status.sh &amp;lt;&amp;lt; &#039;STATUS_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
echo &amp;quot;=== Wiki Status ===&amp;quot;&lt;br /&gt;
echo &amp;quot;Pages: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;Users: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM user WHERE user_id &amp;gt; 0;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Extensions ===&amp;quot;&lt;br /&gt;
if [ -d &amp;quot;extensions/PageNotice&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;PageNotice: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;PageNotice: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/YouTube&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;YouTube: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;YouTube: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/ContributionScores&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;ContributionScores: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;ContributionScores: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Update System ===&amp;quot;&lt;br /&gt;
if [ -f &amp;quot;.last_import&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Current version: $(grep &#039;Import:&#039; .last_import | cut -d&#039; &#039; -f2)&amp;quot;&lt;br /&gt;
    echo &amp;quot;Import date: $(grep &#039;Date:&#039; .last_import | cut -d&#039; &#039; -f2-)&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No version info available&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
STATUS_EOF&lt;br /&gt;
chmod +x /var/www/html/check_status.sh&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Setup completed!&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;Update scripts installed:&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/check_updates.sh (user-friendly)&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/update_xml.sh (direct)&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Final counts&lt;br /&gt;
PAGES=$(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;0&amp;quot;)&lt;br /&gt;
echo &amp;quot;Pages imported: $PAGES&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb stop&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.4: XML Update Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano update_xml.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki XML Updater ===&amp;quot;&lt;br /&gt;
echo &amp;quot;This will check for new and updated pages&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Function to check if running in container&lt;br /&gt;
check_environment() {&lt;br /&gt;
    if [ ! -f &amp;quot;/var/www/html/LocalSettings.php&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Error: This script must be run inside the wiki container&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to get current XML version&lt;br /&gt;
get_current_version() {&lt;br /&gt;
    if [ -f &amp;quot;/var/www/html/.last_import&amp;quot; ]; then&lt;br /&gt;
        grep &amp;quot;Import:&amp;quot; /var/www/html/.last_import | cut -d&#039; &#039; -f2&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;none&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to check for updates&lt;br /&gt;
check_for_updates() {&lt;br /&gt;
    python3 - &amp;lt;&amp;lt; &#039;PYTHON_EOF&#039;&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_latest_dump():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        if dumps:&lt;br /&gt;
            latest = sorted(dumps, key=parse_date_from_dump, reverse=True)[0]&lt;br /&gt;
            &lt;br /&gt;
            # Get XML files from latest dump&lt;br /&gt;
            response = requests.get(f&amp;quot;{BASE_URL}{latest}/&amp;quot;, timeout=30)&lt;br /&gt;
            response.raise_for_status()&lt;br /&gt;
            soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
            files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                    if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
            &lt;br /&gt;
            if files:&lt;br /&gt;
                newest_xml = sorted(files, reverse=True)[0]&lt;br /&gt;
                print(f&amp;quot;{latest}/{newest_xml}&amp;quot;)&lt;br /&gt;
                sys.exit(0)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;ERROR: {e}&amp;quot;, file=sys.stderr)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
    &lt;br /&gt;
    print(&amp;quot;ERROR: No dumps found&amp;quot;, file=sys.stderr)&lt;br /&gt;
    sys.exit(1)&lt;br /&gt;
&lt;br /&gt;
get_latest_dump()&lt;br /&gt;
PYTHON_EOF&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to download new XML&lt;br /&gt;
download_xml() {&lt;br /&gt;
    local dump_info=&amp;quot;$1&amp;quot;&lt;br /&gt;
    local dump_dir=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f1)&lt;br /&gt;
    local xml_file=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f2)&lt;br /&gt;
    local url=&amp;quot;https://xml.completenoobs.com/xmlDumps/${dump_info}&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Downloading: $xml_file&amp;quot;&lt;br /&gt;
    echo &amp;quot;From: $dump_dir&amp;quot;&lt;br /&gt;
    echo &amp;quot;URL: $url&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if wget -O /tmp/new_dump.xml &amp;quot;$url&amp;quot; --progress=bar:force 2&amp;gt;&amp;amp;1; then&lt;br /&gt;
        echo &amp;quot;Download successful!&amp;quot;&lt;br /&gt;
        return 0&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;Download failed!&amp;quot;&lt;br /&gt;
        return 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to backup current database&lt;br /&gt;
backup_database() {&lt;br /&gt;
    echo &amp;quot;Creating database backup...&amp;quot;&lt;br /&gt;
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)&lt;br /&gt;
    mysqldump --user=wikiuser --password=wikipass completenoobs_wiki &amp;gt; /tmp/wiki_backup_${TIMESTAMP}.sql&lt;br /&gt;
    echo &amp;quot;Backup created: /tmp/wiki_backup_${TIMESTAMP}.sql&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to analyze and import changes&lt;br /&gt;
analyze_and_import() {&lt;br /&gt;
    echo &amp;quot;Analyzing differences between XML and local wiki...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Create analysis and import script for MediaWiki 1.44+&lt;br /&gt;
    cat &amp;gt; /tmp/analyze_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class AnalyzeAndImport extends Maintenance {&lt;br /&gt;
    private $db;&lt;br /&gt;
    private $new_pages = [];&lt;br /&gt;
    private $changed_pages = [];&lt;br /&gt;
    private $unchanged_pages = [];&lt;br /&gt;
    &lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addDescription(&#039;Analyze and selectively import from XML dump&#039;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $this-&amp;gt;db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // MediaWiki 1.35+ uses slots and content tables&lt;br /&gt;
        // Get existing pages with their content&lt;br /&gt;
        $query = &amp;quot;&lt;br /&gt;
            SELECT p.page_title, p.page_id, c.content_address, c.content_sha1&lt;br /&gt;
            FROM page p&lt;br /&gt;
            JOIN revision r ON p.page_latest = r.rev_id&lt;br /&gt;
            JOIN slots s ON r.rev_id = s.slot_revision_id&lt;br /&gt;
            JOIN slot_roles sr ON s.slot_role_id = sr.role_id&lt;br /&gt;
            JOIN content c ON s.slot_content_id = c.content_id&lt;br /&gt;
            WHERE p.page_namespace = 0 AND sr.role_name = &#039;main&#039;&lt;br /&gt;
        &amp;quot;;&lt;br /&gt;
        &lt;br /&gt;
        $result = $this-&amp;gt;db-&amp;gt;query($query);&lt;br /&gt;
        if (!$result) {&lt;br /&gt;
            $this-&amp;gt;error(&amp;quot;Database query failed: &amp;quot; . $this-&amp;gt;db-&amp;gt;error);&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $existing = [];&lt;br /&gt;
        while ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
            // Get actual text content&lt;br /&gt;
            $text_content = $this-&amp;gt;getTextContent($row[&#039;content_address&#039;]);&lt;br /&gt;
            $existing[$row[&#039;page_title&#039;]] = [&lt;br /&gt;
                &#039;id&#039; =&amp;gt; $row[&#039;page_id&#039;],&lt;br /&gt;
                &#039;content&#039; =&amp;gt; $text_content,&lt;br /&gt;
                &#039;sha1&#039; =&amp;gt; $row[&#039;content_sha1&#039;]&lt;br /&gt;
            ];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Parse XML and compare&lt;br /&gt;
        $xml = simplexml_load_file(&#039;/tmp/new_dump.xml&#039;);&lt;br /&gt;
        &lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            $title = str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title);&lt;br /&gt;
            $xml_content = (string)$page-&amp;gt;revision-&amp;gt;text;&lt;br /&gt;
            &lt;br /&gt;
            if (!isset($existing[$title])) {&lt;br /&gt;
                // New page&lt;br /&gt;
                $this-&amp;gt;new_pages[$title] = $xml_content;&lt;br /&gt;
            } else {&lt;br /&gt;
                // Compare content using SHA1 for efficiency&lt;br /&gt;
                $xml_sha1 = sha1($xml_content);&lt;br /&gt;
                &lt;br /&gt;
                if ($existing[$title][&#039;sha1&#039;] !== $xml_sha1) {&lt;br /&gt;
                    // Content is different&lt;br /&gt;
                    $this-&amp;gt;changed_pages[$title] = [&lt;br /&gt;
                        &#039;local&#039; =&amp;gt; $existing[$title][&#039;content&#039;],&lt;br /&gt;
                        &#039;xml&#039; =&amp;gt; $xml_content,&lt;br /&gt;
                        &#039;page_id&#039; =&amp;gt; $existing[$title][&#039;id&#039;]&lt;br /&gt;
                    ];&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;unchanged_pages[] = $title;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Display summary&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Update Analysis ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages to import: &amp;quot; . count($this-&amp;gt;new_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Changed pages found: &amp;quot; . count($this-&amp;gt;changed_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Unchanged pages: &amp;quot; . count($this-&amp;gt;unchanged_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Save analysis for review&lt;br /&gt;
        file_put_contents(&#039;/tmp/update_analysis.json&#039;, json_encode([&lt;br /&gt;
            &#039;new&#039; =&amp;gt; array_keys($this-&amp;gt;new_pages),&lt;br /&gt;
            &#039;changed&#039; =&amp;gt; array_keys($this-&amp;gt;changed_pages),&lt;br /&gt;
            &#039;unchanged&#039; =&amp;gt; $this-&amp;gt;unchanged_pages&lt;br /&gt;
        ], JSON_PRETTY_PRINT));&lt;br /&gt;
        &lt;br /&gt;
        // Show changed pages with preview (limit to first 20 for readability)&lt;br /&gt;
        if (count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Changed Pages ===\n&amp;quot;);&lt;br /&gt;
            $count = 0;&lt;br /&gt;
            $total_changed = count($this-&amp;gt;changed_pages);&lt;br /&gt;
            &lt;br /&gt;
            foreach ($this-&amp;gt;changed_pages as $title =&amp;gt; $data) {&lt;br /&gt;
                $count++;&lt;br /&gt;
                if ($count &amp;lt;= 20) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;\n$count. $title\n&amp;quot;);&lt;br /&gt;
                    &lt;br /&gt;
                    // Create a simple diff preview (first 300 chars)&lt;br /&gt;
                    $xml_preview = substr($data[&#039;xml&#039;], 0, 100);&lt;br /&gt;
                    &lt;br /&gt;
                    // Save full diff to file&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;=== FULL DIFF FOR: $title ===\n\n&amp;quot;);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- LOCAL VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;local&#039;] . &amp;quot;\n\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- XML VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;xml&#039;], FILE_APPEND);&lt;br /&gt;
                    &lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;   Preview: &amp;quot; . $xml_preview . &amp;quot;...\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ($total_changed &amp;gt; 20) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\n... and &amp;quot; . ($total_changed - 20) . &amp;quot; more changed pages.\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;All diff files saved to /tmp/diff_*.txt\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Interactive selection&lt;br /&gt;
        if (count($this-&amp;gt;new_pages) &amp;gt; 0 || count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Import Options ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;1. Import new pages only (preserve all local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;2. Import new pages + update ALL changed pages (overwrites local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;3. Selective import (choose which updates to apply)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;4. Cancel (no changes)\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            // Save state for import script WITHOUT the XML object&lt;br /&gt;
            $import_data = [&lt;br /&gt;
                &#039;new_pages&#039; =&amp;gt; $this-&amp;gt;new_pages,&lt;br /&gt;
                &#039;changed_pages&#039; =&amp;gt; $this-&amp;gt;changed_pages,&lt;br /&gt;
                &#039;xml_file&#039; =&amp;gt; &#039;/tmp/new_dump.xml&#039;  // Save path instead of object&lt;br /&gt;
            ];&lt;br /&gt;
            &lt;br /&gt;
            file_put_contents(&#039;/tmp/import_data.ser&#039;, serialize($import_data));&lt;br /&gt;
        } else {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\nNo changes detected. Your wiki is up to date!\n&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function getTextContent($address) {&lt;br /&gt;
        // Handle different content storage formats in MW 1.35+&lt;br /&gt;
        if (strpos($address, &#039;tt:&#039;) === 0) {&lt;br /&gt;
            // Text table reference&lt;br /&gt;
            $text_id = substr($address, 3);&lt;br /&gt;
            $result = $this-&amp;gt;db-&amp;gt;query(&amp;quot;SELECT old_text FROM text WHERE old_id = $text_id&amp;quot;);&lt;br /&gt;
            if ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
                return $row[&#039;old_text&#039;];&lt;br /&gt;
            }&lt;br /&gt;
        } elseif (strpos($address, &#039;es:&#039;) === 0) {&lt;br /&gt;
            // External storage - would need special handling&lt;br /&gt;
            return &amp;quot;[External storage content]&amp;quot;;&lt;br /&gt;
        }&lt;br /&gt;
        // Direct content&lt;br /&gt;
        return $address;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = AnalyzeAndImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/analyze_import.php&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to perform selected import&lt;br /&gt;
perform_import() {&lt;br /&gt;
    local choice=$1&lt;br /&gt;
    &lt;br /&gt;
    cat &amp;gt; /tmp/do_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class DoImport extends Maintenance {&lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addOption(&#039;mode&#039;, &#039;Import mode&#039;, true, true);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $mode = $this-&amp;gt;getOption(&#039;mode&#039;);&lt;br /&gt;
        $data = unserialize(file_get_contents(&#039;/tmp/import_data.ser&#039;));&lt;br /&gt;
        &lt;br /&gt;
        // Load XML file&lt;br /&gt;
        $xml = simplexml_load_file($data[&#039;xml_file&#039;]);&lt;br /&gt;
        &lt;br /&gt;
        $new_imported = 0;&lt;br /&gt;
        $updated = 0;&lt;br /&gt;
        &lt;br /&gt;
        // Import new pages (always for modes 1-3)&lt;br /&gt;
        if ($mode != &#039;4&#039;) {&lt;br /&gt;
            foreach ($data[&#039;new_pages&#039;] as $title =&amp;gt; $content) {&lt;br /&gt;
                $this-&amp;gt;importPage($title, $content, $xml);&lt;br /&gt;
                $new_imported++;&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Imported new page: $title\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Handle changed pages based on mode&lt;br /&gt;
        if ($mode == &#039;2&#039;) {&lt;br /&gt;
            // Update all changed pages&lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Updating: $title\n&amp;quot;);&lt;br /&gt;
                if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                    $updated++;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Updated page: $title\n&amp;quot;);&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        } elseif ($mode == &#039;3&#039;) {&lt;br /&gt;
            // Selective update&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Selective Import Mode ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;For each changed page, choose:\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  y = yes, update this page\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  n = no, keep local version\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  d = show diff file\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  a = update all remaining pages\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  s = skip all remaining pages\n\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            $update_all = false;&lt;br /&gt;
            $skip_all = false;&lt;br /&gt;
            &lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                if ($skip_all) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($update_all) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\nPage: $title\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Action (y/n/d/a/s): &amp;quot;);&lt;br /&gt;
                $handle = fopen(&amp;quot;php://stdin&amp;quot;, &amp;quot;r&amp;quot;);&lt;br /&gt;
                $line = trim(fgets($handle));&lt;br /&gt;
                &lt;br /&gt;
                while ($line == &#039;d&#039;) {&lt;br /&gt;
                    // Show diff&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    if (file_exists($diff_file)) {&lt;br /&gt;
                        system(&amp;quot;head -50 $diff_file&amp;quot;);&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;\n[Showing first 50 lines - full file at $diff_file]\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Action (y/n/a/s): &amp;quot;);&lt;br /&gt;
                    $line = trim(fgets($handle));&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($line == &#039;a&#039;) {&lt;br /&gt;
                    $update_all = true;&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } elseif ($line == &#039;s&#039;) {&lt;br /&gt;
                    $skip_all = true;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                } elseif ($line == &#039;y&#039;) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Import Complete ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages imported: $new_imported\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Pages updated: $updated\n&amp;quot;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function importPage($title, $content, $xml) {&lt;br /&gt;
        // Create single page XML for import&lt;br /&gt;
        $tempFile = &#039;/tmp/single_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
        $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
        &lt;br /&gt;
        if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
            $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
            foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Find and add the page&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                break;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
        exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;);&lt;br /&gt;
        unlink($tempFile);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function reimportPage($title, $xml) {&lt;br /&gt;
        // For updating existing pages, delete then reimport&lt;br /&gt;
        $db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Delete the existing page&lt;br /&gt;
        $safe_title = $db-&amp;gt;real_escape_string(str_replace(&#039; &#039;, &#039;_&#039;, $title));&lt;br /&gt;
        $db-&amp;gt;query(&amp;quot;DELETE FROM page WHERE page_title = &#039;$safe_title&#039; AND page_namespace = 0&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        $db-&amp;gt;close();&lt;br /&gt;
        &lt;br /&gt;
        // Now import the new version&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $tempFile = &#039;/tmp/update_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
                $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
                &lt;br /&gt;
                if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
                    $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
                    foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                        $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
                $result = exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;, $output, $return);&lt;br /&gt;
                unlink($tempFile);&lt;br /&gt;
                &lt;br /&gt;
                return ($return === 0);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = DoImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/do_import.php --mode=&amp;quot;$choice&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Main execution&lt;br /&gt;
main() {&lt;br /&gt;
    check_environment&lt;br /&gt;
    &lt;br /&gt;
    # Start MariaDB if not running&lt;br /&gt;
    service mariadb status &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 || service mariadb start&lt;br /&gt;
    &lt;br /&gt;
    # Wait for MariaDB&lt;br /&gt;
    for i in {1..30}; do&lt;br /&gt;
        if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
            break&lt;br /&gt;
        fi&lt;br /&gt;
        sleep 1&lt;br /&gt;
    done&lt;br /&gt;
    &lt;br /&gt;
    CURRENT=$(get_current_version)&lt;br /&gt;
    echo &amp;quot;Current version: $CURRENT&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Checking for updates...&amp;quot;&lt;br /&gt;
    LATEST=$(check_for_updates 2&amp;gt;/dev/null)&lt;br /&gt;
    if [ $? -ne 0 ] || [ -z &amp;quot;$LATEST&amp;quot; ] || [[ &amp;quot;$LATEST&amp;quot; == *&amp;quot;ERROR&amp;quot;* ]]; then&lt;br /&gt;
        echo &amp;quot;Failed to check for updates&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Latest available: $LATEST&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Always proceed to analysis even if versions match&lt;br /&gt;
    # (there might be content updates in the same version)&lt;br /&gt;
    echo &amp;quot;Proceeding with content analysis...&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Backup database&lt;br /&gt;
    backup_database&lt;br /&gt;
    &lt;br /&gt;
    # Download new XML&lt;br /&gt;
    if ! download_xml &amp;quot;$LATEST&amp;quot;; then&lt;br /&gt;
        echo &amp;quot;Failed to download new XML&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Analyze differences&lt;br /&gt;
    analyze_and_import&lt;br /&gt;
    &lt;br /&gt;
    # Check if there are changes to import&lt;br /&gt;
    if [ -f &amp;quot;/tmp/import_data.ser&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;&amp;quot;&lt;br /&gt;
        read -p &amp;quot;Choose option (1-4): &amp;quot; -n 1 -r&lt;br /&gt;
        echo&lt;br /&gt;
        &lt;br /&gt;
        if [[ $REPLY =~ ^[1-4]$ ]]; then&lt;br /&gt;
            if [ &amp;quot;$REPLY&amp;quot; != &amp;quot;4&amp;quot; ]; then&lt;br /&gt;
                perform_import &amp;quot;$REPLY&amp;quot;&lt;br /&gt;
                &lt;br /&gt;
                # Update version info&lt;br /&gt;
                echo &amp;quot;Import: $LATEST&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
                echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
                &lt;br /&gt;
                # Rebuild indices&lt;br /&gt;
                echo &amp;quot;Rebuilding indices...&amp;quot;&lt;br /&gt;
                php maintenance/rebuildrecentchanges.php&lt;br /&gt;
                php maintenance/initSiteStats.php&lt;br /&gt;
            else&lt;br /&gt;
                echo &amp;quot;Update cancelled&amp;quot;&lt;br /&gt;
            fi&lt;br /&gt;
        else&lt;br /&gt;
            echo &amp;quot;Invalid option. Update cancelled&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Clean up temp files&lt;br /&gt;
    rm -f /tmp/import_data.ser /tmp/update_analysis.json /tmp/diff_*.txt /tmp/analyze_import.php /tmp/do_import.php 2&amp;gt;/dev/null&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    echo &amp;quot;Done!&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Run main function&lt;br /&gt;
main &amp;quot;$@&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.5: Entrypoint Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano entrypoint.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Starting CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 1&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;CompleteNoobs Wiki ready at: http://localhost:8080&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin login: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Features:&amp;quot;&lt;br /&gt;
echo &amp;quot;- Complete wiki content imported from XML&amp;quot;&lt;br /&gt;
echo &amp;quot;- License notices on all pages (via PageNotice)&amp;quot;&lt;br /&gt;
echo &amp;quot;- SyntaxHighlight for code blocks&amp;quot;&lt;br /&gt;
echo &amp;quot;- YouTube video embedding&amp;quot;&lt;br /&gt;
echo &amp;quot;- Contribution Scores special page&amp;quot;&lt;br /&gt;
echo &amp;quot;- XML update system (preserves local edits)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
apache2-foreground&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 3: Build and Run ==&lt;br /&gt;
&lt;br /&gt;
=== 3.1: Build the Image ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t completenoobs/wiki:latest .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will take several minutes.&lt;br /&gt;
&lt;br /&gt;
=== 3.2: Run the Container ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 4: Test Everything ==&lt;br /&gt;
&lt;br /&gt;
=== 4.1: Check the Wiki ===&lt;br /&gt;
* Visit: http://localhost:8080&lt;br /&gt;
* You should see the PageNotice at the top&lt;br /&gt;
* Login with: admin / AdminPass123!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Change Admin Password:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By default, the admin user&#039;s password is &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;. It&#039;s highly recommended to change this immediately. You can do this either through the wiki&#039;s web interface or directly in the Docker terminal.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 1: Change Password via Web Interface&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
This is the easiest method. You can change your password directly from the wiki itself.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Log in to your wiki with the default credentials: &amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
2. Once logged in, click your username (&amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt;) in the top-right corner of the page.&amp;lt;br&amp;gt;&lt;br /&gt;
3. From the drop-down menu, select &amp;lt;b&amp;gt;Preferences&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
4. On the Preferences page, go to the &amp;lt;b&amp;gt;Password&amp;lt;/b&amp;gt; tab.&amp;lt;br&amp;gt;&lt;br /&gt;
5. Enter the current password (&amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;), then enter your new password twice.&amp;lt;br&amp;gt;&lt;br /&gt;
6. Click &amp;lt;b&amp;gt;Change password&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Your password is now changed, and you will need to use the new one for future logins.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 2: Change Password via Terminal (No-Email Reset)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you have forgotten the password or prefer to use the command line, you can reset it directly inside the Docker container using a MediaWiki maintenance script.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;b&amp;gt;Access the container&#039;s shell&amp;lt;/b&amp;gt; with the following command from your host machine:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Once inside the container, use the &amp;lt;code&amp;gt;changePassword.php&amp;lt;/code&amp;gt; maintenance script to change the password. This is the modern, recommended way to run MediaWiki maintenance scripts.&amp;lt;br&amp;gt;&lt;br /&gt;
* Change &amp;lt;b&amp;gt;NEWPASSWORD&amp;lt;/b&amp;gt; to your new password&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt; to leave the container&#039;s shell.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The admin password has now been reset. You can log in to your wiki with the new password.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.2: Test Extensions ===&lt;br /&gt;
* &#039;&#039;&#039;YouTube&#039;&#039;&#039;: Edit any page, add &amp;lt;code&amp;gt;&amp;lt;youtube&amp;gt;N9qYF9DZPdw&amp;lt;/youtube&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;PageNotice&#039;&#039;&#039;: Should already be visible at the top&lt;br /&gt;
* &#039;&#039;&#039;SyntaxHighlight&#039;&#039;&#039;: Add code blocks with &amp;lt;nowiki&amp;gt;&amp;lt;code&amp;gt;&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;code here&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.3: Check Status ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/check_status.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 5: XML Update Operations ==&lt;br /&gt;
*NOTE: update_xml.sh still needs alot of work, this just idea placeholder for now.&lt;br /&gt;
=== 5.1: Check for Updates (Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will:&lt;br /&gt;
* Check the CompleteNoobs XML repository for new dumps&lt;br /&gt;
* Compare with your current version&lt;br /&gt;
* Ask for confirmation before updating&lt;br /&gt;
* Import ONLY new pages (preserves your edits)&lt;br /&gt;
* Create a backup before making changes&lt;br /&gt;
&lt;br /&gt;
=== 5.2: Force Update (Non-Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki bash -c &amp;quot;echo &#039;y&#039; | /var/www/html/update_xml.sh&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5.3: Manual Update Process ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# 1. Enter container&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&lt;br /&gt;
# 2. Check current version&lt;br /&gt;
cat /var/www/html/.last_import&lt;br /&gt;
&lt;br /&gt;
# 3. Run update&lt;br /&gt;
/var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# 4. Exit container&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 6: Troubleshooting Commands ==&lt;br /&gt;
=== Access container shell: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Edit configuration: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki nano /var/www/html/LocalSettings.php&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Check logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== View update logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki tail -f /tmp/mediawiki-debug.log&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Complete restart: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Expected Results ==&lt;br /&gt;
* Working wiki with imported CompleteNoobs content&lt;br /&gt;
* PageNotice visible at top of all pages&lt;br /&gt;
* All extensions functional&lt;br /&gt;
* Text editors (nano/vim) available in container&lt;br /&gt;
* Utility scripts for maintenance&lt;br /&gt;
* XML update system that preserves local edits&lt;br /&gt;
&lt;br /&gt;
==need to add==&lt;br /&gt;
* way for user to backup there local custom wiki - xml exporter&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=661</id>
		<title>CompleteNoobs Docker Image Creation</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=661"/>
		<updated>2025-09-01T20:04:48Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* Important Notes */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Complete Noobs Docker Wiki Tutorial =&lt;br /&gt;
* [[Docker_Install_Guide| Docker install guide]]&lt;br /&gt;
==errors==&lt;br /&gt;
* This mainly works - just need to fix the extensions popular pages and contrubtion scores&lt;br /&gt;
* The XML updater requires more work - currently idea placeholder&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
* Ubuntu 24.04&lt;br /&gt;
* Docker installed and running&lt;br /&gt;
* Your user in docker group: &amp;lt;code&amp;gt;sudo usermod -aG docker $USER&amp;lt;/code&amp;gt; (then logout/login)&lt;br /&gt;
&lt;br /&gt;
== Step 2: Create All Files ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1: Dockerfile ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Copy this exactly:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM mediawiki:1.44&lt;br /&gt;
# Mediawiki 1.44 used over latest because can confirm extensions youtube and pagenotice works&lt;br /&gt;
# Install dependencies&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \&lt;br /&gt;
    mariadb-server \&lt;br /&gt;
    python3 \&lt;br /&gt;
    python3-requests \&lt;br /&gt;
    python3-bs4 \&lt;br /&gt;
    python3-pygments \&lt;br /&gt;
    curl \&lt;br /&gt;
    wget \&lt;br /&gt;
    unzip \&lt;br /&gt;
    nano \&lt;br /&gt;
    git \&lt;br /&gt;
    &amp;amp;&amp;amp; apt-get clean&lt;br /&gt;
&lt;br /&gt;
# Copy scripts&lt;br /&gt;
COPY download_latest_xml.py /usr/src/download_latest_xml.py&lt;br /&gt;
COPY setup_wiki.sh /usr/src/setup_wiki.sh&lt;br /&gt;
COPY update_xml.sh /usr/src/update_xml.sh&lt;br /&gt;
COPY entrypoint.sh /entrypoint.sh&lt;br /&gt;
&lt;br /&gt;
# Make executable&lt;br /&gt;
RUN chmod +x /usr/src/setup_wiki.sh /entrypoint.sh /usr/src/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Download XML&lt;br /&gt;
RUN python3 /usr/src/download_latest_xml.py&lt;br /&gt;
&lt;br /&gt;
# Setup wiki&lt;br /&gt;
RUN /usr/src/setup_wiki.sh&lt;br /&gt;
&lt;br /&gt;
EXPOSE 80&lt;br /&gt;
VOLUME /var/lib/mysql&lt;br /&gt;
VOLUME /var/www/html/images&lt;br /&gt;
&lt;br /&gt;
ENTRYPOINT [&amp;quot;/entrypoint.sh&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.2: XML Download Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano download_latest_xml.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_available_dumps():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        return sorted(dumps, key=parse_date_from_dump, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dumps: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def get_dump_files(dump):&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(f&amp;quot;{BASE_URL}{dump}/&amp;quot;, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
        return sorted(files, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dump files: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def download_file(url, filename):&lt;br /&gt;
    try:&lt;br /&gt;
        print(f&amp;quot;Downloading {filename}...&amp;quot;)&lt;br /&gt;
        response = requests.get(url, stream=True, timeout=60)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        &lt;br /&gt;
        total_size = int(response.headers.get(&#039;content-length&#039;, 0))&lt;br /&gt;
        downloaded = 0&lt;br /&gt;
        &lt;br /&gt;
        with open(filename, &#039;wb&#039;) as f:&lt;br /&gt;
            for chunk in response.iter_content(chunk_size=8192):&lt;br /&gt;
                if chunk:&lt;br /&gt;
                    f.write(chunk)&lt;br /&gt;
                    downloaded += len(chunk)&lt;br /&gt;
                    if total_size &amp;gt; 0:&lt;br /&gt;
                        progress = (downloaded / total_size) * 100&lt;br /&gt;
                        print(f&amp;quot;\rProgress: {progress:.1f}%&amp;quot;, end=&#039;&#039;, flush=True)&lt;br /&gt;
        print()&lt;br /&gt;
        return True&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error downloading {filename}: {e}&amp;quot;)&lt;br /&gt;
        return False&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    print(&amp;quot;Fetching available XML dumps...&amp;quot;)&lt;br /&gt;
    dumps = get_available_dumps()&lt;br /&gt;
    &lt;br /&gt;
    if not dumps:&lt;br /&gt;
        print(&amp;quot;No dumps found!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_dump = dumps[0]&lt;br /&gt;
    print(f&amp;quot;Latest dump: {newest_dump}&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    files = get_dump_files(newest_dump)&lt;br /&gt;
    if not files:&lt;br /&gt;
        print(&amp;quot;No XML files found in latest dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_xml = files[0]&lt;br /&gt;
    xml_url = f&amp;quot;{BASE_URL}{newest_dump}/{newest_xml}&amp;quot;&lt;br /&gt;
    local_filename = &amp;quot;/tmp/completenoobs_dump.xml&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if download_file(xml_url, local_filename):&lt;br /&gt;
        print(f&amp;quot;Successfully downloaded {newest_xml}&amp;quot;)&lt;br /&gt;
        with open(&amp;quot;/tmp/dump_info.txt&amp;quot;, &amp;quot;w&amp;quot;) as f:&lt;br /&gt;
            f.write(f&amp;quot;{newest_dump}/{newest_xml}&amp;quot;)&lt;br /&gt;
    else:&lt;br /&gt;
        print(&amp;quot;Failed to download XML dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.3: Main Setup Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano setup_wiki.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Setting up CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Initialize MariaDB&lt;br /&gt;
if [ ! -d &amp;quot;/var/lib/mysql/mysql&amp;quot; ]; then&lt;br /&gt;
    mysql_install_db --user=mysql --datadir=/var/lib/mysql&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 2&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
# Setup database&lt;br /&gt;
mysql -e &amp;quot;CREATE DATABASE IF NOT EXISTS completenoobs_wiki CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;127.0.0.1&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;127.0.0.1&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;localhost&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;localhost&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;FLUSH PRIVILEGES;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Install MediaWiki&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
php maintenance/install.php \&lt;br /&gt;
    --dbtype=mysql \&lt;br /&gt;
    --dbserver=127.0.0.1 \&lt;br /&gt;
    --dbname=completenoobs_wiki \&lt;br /&gt;
    --dbuser=wikiuser \&lt;br /&gt;
    --dbpass=wikipass \&lt;br /&gt;
    --server=&amp;quot;http://localhost:8080&amp;quot; \&lt;br /&gt;
    --scriptpath=&amp;quot;&amp;quot; \&lt;br /&gt;
    --lang=en \&lt;br /&gt;
    --pass=AdminPass123! \&lt;br /&gt;
    &amp;quot;CompleteNoobs Wiki&amp;quot; \&lt;br /&gt;
    &amp;quot;admin&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Download and install extensions&lt;br /&gt;
cd extensions/&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/PageNotice --branch REL1_44 || echo &amp;quot;PageNotice download failed, continuing...&amp;quot;&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/YouTube --branch REL1_44 || echo &amp;quot;YouTube download failed, continuing...&amp;quot;&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
&lt;br /&gt;
# Configure LocalSettings.php&lt;br /&gt;
cat &amp;gt;&amp;gt; LocalSettings.php &amp;lt;&amp;lt; &#039;EOF&#039;&lt;br /&gt;
# Basic settings&lt;br /&gt;
$wgEnableUploads = true;&lt;br /&gt;
$wgUseImageMagick = true;&lt;br /&gt;
$wgImageMagickConvertCommand = &amp;quot;/usr/bin/convert&amp;quot;;&lt;br /&gt;
$wgDefaultSkin = &amp;quot;vector-2022&amp;quot;;&lt;br /&gt;
$wgAllowExternalImages = true;&lt;br /&gt;
&lt;br /&gt;
# Debug (can be removed later)&lt;br /&gt;
$wgShowExceptionDetails = true;&lt;br /&gt;
$wgDebugLogFile = &amp;quot;/tmp/mediawiki-debug.log&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
# PageNotice extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/PageNotice/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;PageNotice&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# YouTube extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/YouTube/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;YouTube&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# SyntaxHighlight (usually bundled)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/SyntaxHighlight_GeSHi/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;SyntaxHighlight_GeSHi&#039; );&lt;br /&gt;
    $wgPygmentizePath = &#039;/usr/bin/pygmentize&#039;;&lt;br /&gt;
}&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
# Import XML dump if available&lt;br /&gt;
if [ -f &amp;quot;/tmp/completenoobs_dump.xml&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Importing XML dump...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if php maintenance/importDump.php --uploads &amp;lt; /tmp/completenoobs_dump.xml; then&lt;br /&gt;
        echo &amp;quot;XML import completed!&amp;quot;&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;XML import had warnings&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Basic maintenance&lt;br /&gt;
    php maintenance/update.php --quick || echo &amp;quot;Update completed with warnings&amp;quot;&lt;br /&gt;
    php maintenance/rebuildrecentchanges.php || echo &amp;quot;RecentChanges rebuilt with warnings&amp;quot;&lt;br /&gt;
    php maintenance/initSiteStats.php || echo &amp;quot;SiteStats initialized with warnings&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if [ -f &amp;quot;/tmp/dump_info.txt&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Import: $(cat /tmp/dump_info.txt)&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
        echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
    fi&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No XML dump found - starting with empty wiki&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
# Copy update script to accessible location&lt;br /&gt;
cp /usr/src/update_xml.sh /var/www/html/update_xml.sh&lt;br /&gt;
chmod +x /var/www/html/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Create user-friendly update wrapper&lt;br /&gt;
cat &amp;gt; /var/www/html/check_updates.sh &amp;lt;&amp;lt; &#039;UPDATE_WRAPPER_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki Update Checker ===&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;This tool checks for new content from the CompleteNoobs XML repository&amp;quot;&lt;br /&gt;
echo &amp;quot;and imports ONLY new pages, preserving all your local edits.&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
/var/www/html/update_xml.sh&lt;br /&gt;
UPDATE_WRAPPER_EOF&lt;br /&gt;
chmod +x /var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# Create simple status script&lt;br /&gt;
cat &amp;gt; /var/www/html/check_status.sh &amp;lt;&amp;lt; &#039;STATUS_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
echo &amp;quot;=== Wiki Status ===&amp;quot;&lt;br /&gt;
echo &amp;quot;Pages: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;Users: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM user WHERE user_id &amp;gt; 0;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Extensions ===&amp;quot;&lt;br /&gt;
if [ -d &amp;quot;extensions/PageNotice&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;PageNotice: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;PageNotice: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/YouTube&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;YouTube: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;YouTube: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/ContributionScores&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;ContributionScores: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;ContributionScores: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Update System ===&amp;quot;&lt;br /&gt;
if [ -f &amp;quot;.last_import&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Current version: $(grep &#039;Import:&#039; .last_import | cut -d&#039; &#039; -f2)&amp;quot;&lt;br /&gt;
    echo &amp;quot;Import date: $(grep &#039;Date:&#039; .last_import | cut -d&#039; &#039; -f2-)&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No version info available&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
STATUS_EOF&lt;br /&gt;
chmod +x /var/www/html/check_status.sh&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Setup completed!&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;Update scripts installed:&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/check_updates.sh (user-friendly)&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/update_xml.sh (direct)&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Final counts&lt;br /&gt;
PAGES=$(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;0&amp;quot;)&lt;br /&gt;
echo &amp;quot;Pages imported: $PAGES&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb stop&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.4: XML Update Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano update_xml.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki XML Updater ===&amp;quot;&lt;br /&gt;
echo &amp;quot;This will check for new and updated pages&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Function to check if running in container&lt;br /&gt;
check_environment() {&lt;br /&gt;
    if [ ! -f &amp;quot;/var/www/html/LocalSettings.php&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Error: This script must be run inside the wiki container&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to get current XML version&lt;br /&gt;
get_current_version() {&lt;br /&gt;
    if [ -f &amp;quot;/var/www/html/.last_import&amp;quot; ]; then&lt;br /&gt;
        grep &amp;quot;Import:&amp;quot; /var/www/html/.last_import | cut -d&#039; &#039; -f2&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;none&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to check for updates&lt;br /&gt;
check_for_updates() {&lt;br /&gt;
    python3 - &amp;lt;&amp;lt; &#039;PYTHON_EOF&#039;&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_latest_dump():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        if dumps:&lt;br /&gt;
            latest = sorted(dumps, key=parse_date_from_dump, reverse=True)[0]&lt;br /&gt;
            &lt;br /&gt;
            # Get XML files from latest dump&lt;br /&gt;
            response = requests.get(f&amp;quot;{BASE_URL}{latest}/&amp;quot;, timeout=30)&lt;br /&gt;
            response.raise_for_status()&lt;br /&gt;
            soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
            files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                    if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
            &lt;br /&gt;
            if files:&lt;br /&gt;
                newest_xml = sorted(files, reverse=True)[0]&lt;br /&gt;
                print(f&amp;quot;{latest}/{newest_xml}&amp;quot;)&lt;br /&gt;
                sys.exit(0)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;ERROR: {e}&amp;quot;, file=sys.stderr)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
    &lt;br /&gt;
    print(&amp;quot;ERROR: No dumps found&amp;quot;, file=sys.stderr)&lt;br /&gt;
    sys.exit(1)&lt;br /&gt;
&lt;br /&gt;
get_latest_dump()&lt;br /&gt;
PYTHON_EOF&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to download new XML&lt;br /&gt;
download_xml() {&lt;br /&gt;
    local dump_info=&amp;quot;$1&amp;quot;&lt;br /&gt;
    local dump_dir=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f1)&lt;br /&gt;
    local xml_file=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f2)&lt;br /&gt;
    local url=&amp;quot;https://xml.completenoobs.com/xmlDumps/${dump_info}&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Downloading: $xml_file&amp;quot;&lt;br /&gt;
    echo &amp;quot;From: $dump_dir&amp;quot;&lt;br /&gt;
    echo &amp;quot;URL: $url&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if wget -O /tmp/new_dump.xml &amp;quot;$url&amp;quot; --progress=bar:force 2&amp;gt;&amp;amp;1; then&lt;br /&gt;
        echo &amp;quot;Download successful!&amp;quot;&lt;br /&gt;
        return 0&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;Download failed!&amp;quot;&lt;br /&gt;
        return 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to backup current database&lt;br /&gt;
backup_database() {&lt;br /&gt;
    echo &amp;quot;Creating database backup...&amp;quot;&lt;br /&gt;
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)&lt;br /&gt;
    mysqldump --user=wikiuser --password=wikipass completenoobs_wiki &amp;gt; /tmp/wiki_backup_${TIMESTAMP}.sql&lt;br /&gt;
    echo &amp;quot;Backup created: /tmp/wiki_backup_${TIMESTAMP}.sql&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to analyze and import changes&lt;br /&gt;
analyze_and_import() {&lt;br /&gt;
    echo &amp;quot;Analyzing differences between XML and local wiki...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Create analysis and import script for MediaWiki 1.44+&lt;br /&gt;
    cat &amp;gt; /tmp/analyze_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class AnalyzeAndImport extends Maintenance {&lt;br /&gt;
    private $db;&lt;br /&gt;
    private $new_pages = [];&lt;br /&gt;
    private $changed_pages = [];&lt;br /&gt;
    private $unchanged_pages = [];&lt;br /&gt;
    &lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addDescription(&#039;Analyze and selectively import from XML dump&#039;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $this-&amp;gt;db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // MediaWiki 1.35+ uses slots and content tables&lt;br /&gt;
        // Get existing pages with their content&lt;br /&gt;
        $query = &amp;quot;&lt;br /&gt;
            SELECT p.page_title, p.page_id, c.content_address, c.content_sha1&lt;br /&gt;
            FROM page p&lt;br /&gt;
            JOIN revision r ON p.page_latest = r.rev_id&lt;br /&gt;
            JOIN slots s ON r.rev_id = s.slot_revision_id&lt;br /&gt;
            JOIN slot_roles sr ON s.slot_role_id = sr.role_id&lt;br /&gt;
            JOIN content c ON s.slot_content_id = c.content_id&lt;br /&gt;
            WHERE p.page_namespace = 0 AND sr.role_name = &#039;main&#039;&lt;br /&gt;
        &amp;quot;;&lt;br /&gt;
        &lt;br /&gt;
        $result = $this-&amp;gt;db-&amp;gt;query($query);&lt;br /&gt;
        if (!$result) {&lt;br /&gt;
            $this-&amp;gt;error(&amp;quot;Database query failed: &amp;quot; . $this-&amp;gt;db-&amp;gt;error);&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $existing = [];&lt;br /&gt;
        while ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
            // Get actual text content&lt;br /&gt;
            $text_content = $this-&amp;gt;getTextContent($row[&#039;content_address&#039;]);&lt;br /&gt;
            $existing[$row[&#039;page_title&#039;]] = [&lt;br /&gt;
                &#039;id&#039; =&amp;gt; $row[&#039;page_id&#039;],&lt;br /&gt;
                &#039;content&#039; =&amp;gt; $text_content,&lt;br /&gt;
                &#039;sha1&#039; =&amp;gt; $row[&#039;content_sha1&#039;]&lt;br /&gt;
            ];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Parse XML and compare&lt;br /&gt;
        $xml = simplexml_load_file(&#039;/tmp/new_dump.xml&#039;);&lt;br /&gt;
        &lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            $title = str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title);&lt;br /&gt;
            $xml_content = (string)$page-&amp;gt;revision-&amp;gt;text;&lt;br /&gt;
            &lt;br /&gt;
            if (!isset($existing[$title])) {&lt;br /&gt;
                // New page&lt;br /&gt;
                $this-&amp;gt;new_pages[$title] = $xml_content;&lt;br /&gt;
            } else {&lt;br /&gt;
                // Compare content using SHA1 for efficiency&lt;br /&gt;
                $xml_sha1 = sha1($xml_content);&lt;br /&gt;
                &lt;br /&gt;
                if ($existing[$title][&#039;sha1&#039;] !== $xml_sha1) {&lt;br /&gt;
                    // Content is different&lt;br /&gt;
                    $this-&amp;gt;changed_pages[$title] = [&lt;br /&gt;
                        &#039;local&#039; =&amp;gt; $existing[$title][&#039;content&#039;],&lt;br /&gt;
                        &#039;xml&#039; =&amp;gt; $xml_content,&lt;br /&gt;
                        &#039;page_id&#039; =&amp;gt; $existing[$title][&#039;id&#039;]&lt;br /&gt;
                    ];&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;unchanged_pages[] = $title;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Display summary&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Update Analysis ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages to import: &amp;quot; . count($this-&amp;gt;new_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Changed pages found: &amp;quot; . count($this-&amp;gt;changed_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Unchanged pages: &amp;quot; . count($this-&amp;gt;unchanged_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Save analysis for review&lt;br /&gt;
        file_put_contents(&#039;/tmp/update_analysis.json&#039;, json_encode([&lt;br /&gt;
            &#039;new&#039; =&amp;gt; array_keys($this-&amp;gt;new_pages),&lt;br /&gt;
            &#039;changed&#039; =&amp;gt; array_keys($this-&amp;gt;changed_pages),&lt;br /&gt;
            &#039;unchanged&#039; =&amp;gt; $this-&amp;gt;unchanged_pages&lt;br /&gt;
        ], JSON_PRETTY_PRINT));&lt;br /&gt;
        &lt;br /&gt;
        // Show changed pages with preview (limit to first 20 for readability)&lt;br /&gt;
        if (count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Changed Pages ===\n&amp;quot;);&lt;br /&gt;
            $count = 0;&lt;br /&gt;
            $total_changed = count($this-&amp;gt;changed_pages);&lt;br /&gt;
            &lt;br /&gt;
            foreach ($this-&amp;gt;changed_pages as $title =&amp;gt; $data) {&lt;br /&gt;
                $count++;&lt;br /&gt;
                if ($count &amp;lt;= 20) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;\n$count. $title\n&amp;quot;);&lt;br /&gt;
                    &lt;br /&gt;
                    // Create a simple diff preview (first 300 chars)&lt;br /&gt;
                    $xml_preview = substr($data[&#039;xml&#039;], 0, 100);&lt;br /&gt;
                    &lt;br /&gt;
                    // Save full diff to file&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;=== FULL DIFF FOR: $title ===\n\n&amp;quot;);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- LOCAL VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;local&#039;] . &amp;quot;\n\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- XML VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;xml&#039;], FILE_APPEND);&lt;br /&gt;
                    &lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;   Preview: &amp;quot; . $xml_preview . &amp;quot;...\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ($total_changed &amp;gt; 20) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\n... and &amp;quot; . ($total_changed - 20) . &amp;quot; more changed pages.\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;All diff files saved to /tmp/diff_*.txt\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Interactive selection&lt;br /&gt;
        if (count($this-&amp;gt;new_pages) &amp;gt; 0 || count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Import Options ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;1. Import new pages only (preserve all local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;2. Import new pages + update ALL changed pages (overwrites local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;3. Selective import (choose which updates to apply)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;4. Cancel (no changes)\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            // Save state for import script WITHOUT the XML object&lt;br /&gt;
            $import_data = [&lt;br /&gt;
                &#039;new_pages&#039; =&amp;gt; $this-&amp;gt;new_pages,&lt;br /&gt;
                &#039;changed_pages&#039; =&amp;gt; $this-&amp;gt;changed_pages,&lt;br /&gt;
                &#039;xml_file&#039; =&amp;gt; &#039;/tmp/new_dump.xml&#039;  // Save path instead of object&lt;br /&gt;
            ];&lt;br /&gt;
            &lt;br /&gt;
            file_put_contents(&#039;/tmp/import_data.ser&#039;, serialize($import_data));&lt;br /&gt;
        } else {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\nNo changes detected. Your wiki is up to date!\n&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function getTextContent($address) {&lt;br /&gt;
        // Handle different content storage formats in MW 1.35+&lt;br /&gt;
        if (strpos($address, &#039;tt:&#039;) === 0) {&lt;br /&gt;
            // Text table reference&lt;br /&gt;
            $text_id = substr($address, 3);&lt;br /&gt;
            $result = $this-&amp;gt;db-&amp;gt;query(&amp;quot;SELECT old_text FROM text WHERE old_id = $text_id&amp;quot;);&lt;br /&gt;
            if ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
                return $row[&#039;old_text&#039;];&lt;br /&gt;
            }&lt;br /&gt;
        } elseif (strpos($address, &#039;es:&#039;) === 0) {&lt;br /&gt;
            // External storage - would need special handling&lt;br /&gt;
            return &amp;quot;[External storage content]&amp;quot;;&lt;br /&gt;
        }&lt;br /&gt;
        // Direct content&lt;br /&gt;
        return $address;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = AnalyzeAndImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/analyze_import.php&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to perform selected import&lt;br /&gt;
perform_import() {&lt;br /&gt;
    local choice=$1&lt;br /&gt;
    &lt;br /&gt;
    cat &amp;gt; /tmp/do_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class DoImport extends Maintenance {&lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addOption(&#039;mode&#039;, &#039;Import mode&#039;, true, true);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $mode = $this-&amp;gt;getOption(&#039;mode&#039;);&lt;br /&gt;
        $data = unserialize(file_get_contents(&#039;/tmp/import_data.ser&#039;));&lt;br /&gt;
        &lt;br /&gt;
        // Load XML file&lt;br /&gt;
        $xml = simplexml_load_file($data[&#039;xml_file&#039;]);&lt;br /&gt;
        &lt;br /&gt;
        $new_imported = 0;&lt;br /&gt;
        $updated = 0;&lt;br /&gt;
        &lt;br /&gt;
        // Import new pages (always for modes 1-3)&lt;br /&gt;
        if ($mode != &#039;4&#039;) {&lt;br /&gt;
            foreach ($data[&#039;new_pages&#039;] as $title =&amp;gt; $content) {&lt;br /&gt;
                $this-&amp;gt;importPage($title, $content, $xml);&lt;br /&gt;
                $new_imported++;&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Imported new page: $title\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Handle changed pages based on mode&lt;br /&gt;
        if ($mode == &#039;2&#039;) {&lt;br /&gt;
            // Update all changed pages&lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Updating: $title\n&amp;quot;);&lt;br /&gt;
                if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                    $updated++;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Updated page: $title\n&amp;quot;);&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        } elseif ($mode == &#039;3&#039;) {&lt;br /&gt;
            // Selective update&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Selective Import Mode ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;For each changed page, choose:\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  y = yes, update this page\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  n = no, keep local version\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  d = show diff file\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  a = update all remaining pages\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  s = skip all remaining pages\n\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            $update_all = false;&lt;br /&gt;
            $skip_all = false;&lt;br /&gt;
            &lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                if ($skip_all) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($update_all) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\nPage: $title\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Action (y/n/d/a/s): &amp;quot;);&lt;br /&gt;
                $handle = fopen(&amp;quot;php://stdin&amp;quot;, &amp;quot;r&amp;quot;);&lt;br /&gt;
                $line = trim(fgets($handle));&lt;br /&gt;
                &lt;br /&gt;
                while ($line == &#039;d&#039;) {&lt;br /&gt;
                    // Show diff&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    if (file_exists($diff_file)) {&lt;br /&gt;
                        system(&amp;quot;head -50 $diff_file&amp;quot;);&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;\n[Showing first 50 lines - full file at $diff_file]\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Action (y/n/a/s): &amp;quot;);&lt;br /&gt;
                    $line = trim(fgets($handle));&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($line == &#039;a&#039;) {&lt;br /&gt;
                    $update_all = true;&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } elseif ($line == &#039;s&#039;) {&lt;br /&gt;
                    $skip_all = true;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                } elseif ($line == &#039;y&#039;) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Import Complete ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages imported: $new_imported\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Pages updated: $updated\n&amp;quot;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function importPage($title, $content, $xml) {&lt;br /&gt;
        // Create single page XML for import&lt;br /&gt;
        $tempFile = &#039;/tmp/single_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
        $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
        &lt;br /&gt;
        if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
            $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
            foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Find and add the page&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                break;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
        exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;);&lt;br /&gt;
        unlink($tempFile);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function reimportPage($title, $xml) {&lt;br /&gt;
        // For updating existing pages, delete then reimport&lt;br /&gt;
        $db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Delete the existing page&lt;br /&gt;
        $safe_title = $db-&amp;gt;real_escape_string(str_replace(&#039; &#039;, &#039;_&#039;, $title));&lt;br /&gt;
        $db-&amp;gt;query(&amp;quot;DELETE FROM page WHERE page_title = &#039;$safe_title&#039; AND page_namespace = 0&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        $db-&amp;gt;close();&lt;br /&gt;
        &lt;br /&gt;
        // Now import the new version&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $tempFile = &#039;/tmp/update_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
                $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
                &lt;br /&gt;
                if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
                    $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
                    foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                        $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
                $result = exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;, $output, $return);&lt;br /&gt;
                unlink($tempFile);&lt;br /&gt;
                &lt;br /&gt;
                return ($return === 0);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = DoImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/do_import.php --mode=&amp;quot;$choice&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Main execution&lt;br /&gt;
main() {&lt;br /&gt;
    check_environment&lt;br /&gt;
    &lt;br /&gt;
    # Start MariaDB if not running&lt;br /&gt;
    service mariadb status &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 || service mariadb start&lt;br /&gt;
    &lt;br /&gt;
    # Wait for MariaDB&lt;br /&gt;
    for i in {1..30}; do&lt;br /&gt;
        if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
            break&lt;br /&gt;
        fi&lt;br /&gt;
        sleep 1&lt;br /&gt;
    done&lt;br /&gt;
    &lt;br /&gt;
    CURRENT=$(get_current_version)&lt;br /&gt;
    echo &amp;quot;Current version: $CURRENT&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Checking for updates...&amp;quot;&lt;br /&gt;
    LATEST=$(check_for_updates 2&amp;gt;/dev/null)&lt;br /&gt;
    if [ $? -ne 0 ] || [ -z &amp;quot;$LATEST&amp;quot; ] || [[ &amp;quot;$LATEST&amp;quot; == *&amp;quot;ERROR&amp;quot;* ]]; then&lt;br /&gt;
        echo &amp;quot;Failed to check for updates&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Latest available: $LATEST&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Always proceed to analysis even if versions match&lt;br /&gt;
    # (there might be content updates in the same version)&lt;br /&gt;
    echo &amp;quot;Proceeding with content analysis...&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Backup database&lt;br /&gt;
    backup_database&lt;br /&gt;
    &lt;br /&gt;
    # Download new XML&lt;br /&gt;
    if ! download_xml &amp;quot;$LATEST&amp;quot;; then&lt;br /&gt;
        echo &amp;quot;Failed to download new XML&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Analyze differences&lt;br /&gt;
    analyze_and_import&lt;br /&gt;
    &lt;br /&gt;
    # Check if there are changes to import&lt;br /&gt;
    if [ -f &amp;quot;/tmp/import_data.ser&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;&amp;quot;&lt;br /&gt;
        read -p &amp;quot;Choose option (1-4): &amp;quot; -n 1 -r&lt;br /&gt;
        echo&lt;br /&gt;
        &lt;br /&gt;
        if [[ $REPLY =~ ^[1-4]$ ]]; then&lt;br /&gt;
            if [ &amp;quot;$REPLY&amp;quot; != &amp;quot;4&amp;quot; ]; then&lt;br /&gt;
                perform_import &amp;quot;$REPLY&amp;quot;&lt;br /&gt;
                &lt;br /&gt;
                # Update version info&lt;br /&gt;
                echo &amp;quot;Import: $LATEST&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
                echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
                &lt;br /&gt;
                # Rebuild indices&lt;br /&gt;
                echo &amp;quot;Rebuilding indices...&amp;quot;&lt;br /&gt;
                php maintenance/rebuildrecentchanges.php&lt;br /&gt;
                php maintenance/initSiteStats.php&lt;br /&gt;
            else&lt;br /&gt;
                echo &amp;quot;Update cancelled&amp;quot;&lt;br /&gt;
            fi&lt;br /&gt;
        else&lt;br /&gt;
            echo &amp;quot;Invalid option. Update cancelled&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Clean up temp files&lt;br /&gt;
    rm -f /tmp/import_data.ser /tmp/update_analysis.json /tmp/diff_*.txt /tmp/analyze_import.php /tmp/do_import.php 2&amp;gt;/dev/null&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    echo &amp;quot;Done!&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Run main function&lt;br /&gt;
main &amp;quot;$@&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.5: Entrypoint Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano entrypoint.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Starting CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 1&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;CompleteNoobs Wiki ready at: http://localhost:8080&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin login: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Features:&amp;quot;&lt;br /&gt;
echo &amp;quot;- Complete wiki content imported from XML&amp;quot;&lt;br /&gt;
echo &amp;quot;- License notices on all pages (via PageNotice)&amp;quot;&lt;br /&gt;
echo &amp;quot;- SyntaxHighlight for code blocks&amp;quot;&lt;br /&gt;
echo &amp;quot;- YouTube video embedding&amp;quot;&lt;br /&gt;
echo &amp;quot;- Contribution Scores special page&amp;quot;&lt;br /&gt;
echo &amp;quot;- XML update system (preserves local edits)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
apache2-foreground&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 3: Build and Run ==&lt;br /&gt;
&lt;br /&gt;
=== 3.1: Build the Image ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t completenoobs/wiki:latest .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will take several minutes.&lt;br /&gt;
&lt;br /&gt;
=== 3.2: Run the Container ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 4: Test Everything ==&lt;br /&gt;
&lt;br /&gt;
=== 4.1: Check the Wiki ===&lt;br /&gt;
* Visit: http://localhost:8080&lt;br /&gt;
* You should see the PageNotice at the top&lt;br /&gt;
* Login with: admin / AdminPass123!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Change Admin Password:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By default, the admin user&#039;s password is &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;. It&#039;s highly recommended to change this immediately. You can do this either through the wiki&#039;s web interface or directly in the Docker terminal.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 1: Change Password via Web Interface&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
This is the easiest method. You can change your password directly from the wiki itself.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Log in to your wiki with the default credentials: &amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
2. Once logged in, click your username (&amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt;) in the top-right corner of the page.&amp;lt;br&amp;gt;&lt;br /&gt;
3. From the drop-down menu, select &amp;lt;b&amp;gt;Preferences&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
4. On the Preferences page, go to the &amp;lt;b&amp;gt;Password&amp;lt;/b&amp;gt; tab.&amp;lt;br&amp;gt;&lt;br /&gt;
5. Enter the current password (&amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;), then enter your new password twice.&amp;lt;br&amp;gt;&lt;br /&gt;
6. Click &amp;lt;b&amp;gt;Change password&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Your password is now changed, and you will need to use the new one for future logins.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 2: Change Password via Terminal (No-Email Reset)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you have forgotten the password or prefer to use the command line, you can reset it directly inside the Docker container using a MediaWiki maintenance script.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;b&amp;gt;Access the container&#039;s shell&amp;lt;/b&amp;gt; with the following command from your host machine:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Once inside the container, use the &amp;lt;code&amp;gt;changePassword.php&amp;lt;/code&amp;gt; maintenance script to change the password. This is the modern, recommended way to run MediaWiki maintenance scripts.&amp;lt;br&amp;gt;&lt;br /&gt;
* Change &amp;lt;b&amp;gt;NEWPASSWORD&amp;lt;/b&amp;gt; to your new password&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt; to leave the container&#039;s shell.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The admin password has now been reset. You can log in to your wiki with the new password.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.2: Test Extensions ===&lt;br /&gt;
* &#039;&#039;&#039;YouTube&#039;&#039;&#039;: Edit any page, add &amp;lt;code&amp;gt;&amp;lt;youtube&amp;gt;N9qYF9DZPdw&amp;lt;/youtube&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;PageNotice&#039;&#039;&#039;: Should already be visible at the top&lt;br /&gt;
* &#039;&#039;&#039;SyntaxHighlight&#039;&#039;&#039;: Add code blocks with &amp;lt;nowiki&amp;gt;&amp;lt;code&amp;gt;&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;code here&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.3: Check Status ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/check_status.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 5: XML Update Operations ==&lt;br /&gt;
*NOTE: update_xml.sh still needs alot of work, this just idea placeholder for now.&lt;br /&gt;
=== 5.1: Check for Updates (Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will:&lt;br /&gt;
* Check the CompleteNoobs XML repository for new dumps&lt;br /&gt;
* Compare with your current version&lt;br /&gt;
* Ask for confirmation before updating&lt;br /&gt;
* Import ONLY new pages (preserves your edits)&lt;br /&gt;
* Create a backup before making changes&lt;br /&gt;
&lt;br /&gt;
=== 5.2: Force Update (Non-Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki bash -c &amp;quot;echo &#039;y&#039; | /var/www/html/update_xml.sh&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5.3: Manual Update Process ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# 1. Enter container&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&lt;br /&gt;
# 2. Check current version&lt;br /&gt;
cat /var/www/html/.last_import&lt;br /&gt;
&lt;br /&gt;
# 3. Run update&lt;br /&gt;
/var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# 4. Exit container&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 6: Troubleshooting Commands ==&lt;br /&gt;
=== Access container shell: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Edit configuration: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki nano /var/www/html/LocalSettings.php&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Check logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== View update logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki tail -f /tmp/mediawiki-debug.log&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Complete restart: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Expected Results ==&lt;br /&gt;
* Working wiki with imported CompleteNoobs content&lt;br /&gt;
* PageNotice visible at top of all pages&lt;br /&gt;
* All extensions functional&lt;br /&gt;
* Text editors (nano/vim) available in container&lt;br /&gt;
* Utility scripts for maintenance&lt;br /&gt;
* XML update system that preserves local edits&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=660</id>
		<title>CompleteNoobs Docker Image Creation</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=660"/>
		<updated>2025-09-01T20:04:02Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* Update System Features */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Complete Noobs Docker Wiki Tutorial =&lt;br /&gt;
* [[Docker_Install_Guide| Docker install guide]]&lt;br /&gt;
==errors==&lt;br /&gt;
* This mainly works - just need to fix the extensions popular pages and contrubtion scores&lt;br /&gt;
* The XML updater requires more work - currently idea placeholder&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
* Ubuntu 24.04&lt;br /&gt;
* Docker installed and running&lt;br /&gt;
* Your user in docker group: &amp;lt;code&amp;gt;sudo usermod -aG docker $USER&amp;lt;/code&amp;gt; (then logout/login)&lt;br /&gt;
&lt;br /&gt;
== Step 2: Create All Files ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1: Dockerfile ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Copy this exactly:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM mediawiki:1.44&lt;br /&gt;
# Mediawiki 1.44 used over latest because can confirm extensions youtube and pagenotice works&lt;br /&gt;
# Install dependencies&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \&lt;br /&gt;
    mariadb-server \&lt;br /&gt;
    python3 \&lt;br /&gt;
    python3-requests \&lt;br /&gt;
    python3-bs4 \&lt;br /&gt;
    python3-pygments \&lt;br /&gt;
    curl \&lt;br /&gt;
    wget \&lt;br /&gt;
    unzip \&lt;br /&gt;
    nano \&lt;br /&gt;
    git \&lt;br /&gt;
    &amp;amp;&amp;amp; apt-get clean&lt;br /&gt;
&lt;br /&gt;
# Copy scripts&lt;br /&gt;
COPY download_latest_xml.py /usr/src/download_latest_xml.py&lt;br /&gt;
COPY setup_wiki.sh /usr/src/setup_wiki.sh&lt;br /&gt;
COPY update_xml.sh /usr/src/update_xml.sh&lt;br /&gt;
COPY entrypoint.sh /entrypoint.sh&lt;br /&gt;
&lt;br /&gt;
# Make executable&lt;br /&gt;
RUN chmod +x /usr/src/setup_wiki.sh /entrypoint.sh /usr/src/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Download XML&lt;br /&gt;
RUN python3 /usr/src/download_latest_xml.py&lt;br /&gt;
&lt;br /&gt;
# Setup wiki&lt;br /&gt;
RUN /usr/src/setup_wiki.sh&lt;br /&gt;
&lt;br /&gt;
EXPOSE 80&lt;br /&gt;
VOLUME /var/lib/mysql&lt;br /&gt;
VOLUME /var/www/html/images&lt;br /&gt;
&lt;br /&gt;
ENTRYPOINT [&amp;quot;/entrypoint.sh&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.2: XML Download Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano download_latest_xml.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_available_dumps():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        return sorted(dumps, key=parse_date_from_dump, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dumps: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def get_dump_files(dump):&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(f&amp;quot;{BASE_URL}{dump}/&amp;quot;, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
        return sorted(files, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dump files: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def download_file(url, filename):&lt;br /&gt;
    try:&lt;br /&gt;
        print(f&amp;quot;Downloading {filename}...&amp;quot;)&lt;br /&gt;
        response = requests.get(url, stream=True, timeout=60)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        &lt;br /&gt;
        total_size = int(response.headers.get(&#039;content-length&#039;, 0))&lt;br /&gt;
        downloaded = 0&lt;br /&gt;
        &lt;br /&gt;
        with open(filename, &#039;wb&#039;) as f:&lt;br /&gt;
            for chunk in response.iter_content(chunk_size=8192):&lt;br /&gt;
                if chunk:&lt;br /&gt;
                    f.write(chunk)&lt;br /&gt;
                    downloaded += len(chunk)&lt;br /&gt;
                    if total_size &amp;gt; 0:&lt;br /&gt;
                        progress = (downloaded / total_size) * 100&lt;br /&gt;
                        print(f&amp;quot;\rProgress: {progress:.1f}%&amp;quot;, end=&#039;&#039;, flush=True)&lt;br /&gt;
        print()&lt;br /&gt;
        return True&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error downloading {filename}: {e}&amp;quot;)&lt;br /&gt;
        return False&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    print(&amp;quot;Fetching available XML dumps...&amp;quot;)&lt;br /&gt;
    dumps = get_available_dumps()&lt;br /&gt;
    &lt;br /&gt;
    if not dumps:&lt;br /&gt;
        print(&amp;quot;No dumps found!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_dump = dumps[0]&lt;br /&gt;
    print(f&amp;quot;Latest dump: {newest_dump}&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    files = get_dump_files(newest_dump)&lt;br /&gt;
    if not files:&lt;br /&gt;
        print(&amp;quot;No XML files found in latest dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_xml = files[0]&lt;br /&gt;
    xml_url = f&amp;quot;{BASE_URL}{newest_dump}/{newest_xml}&amp;quot;&lt;br /&gt;
    local_filename = &amp;quot;/tmp/completenoobs_dump.xml&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if download_file(xml_url, local_filename):&lt;br /&gt;
        print(f&amp;quot;Successfully downloaded {newest_xml}&amp;quot;)&lt;br /&gt;
        with open(&amp;quot;/tmp/dump_info.txt&amp;quot;, &amp;quot;w&amp;quot;) as f:&lt;br /&gt;
            f.write(f&amp;quot;{newest_dump}/{newest_xml}&amp;quot;)&lt;br /&gt;
    else:&lt;br /&gt;
        print(&amp;quot;Failed to download XML dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.3: Main Setup Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano setup_wiki.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Setting up CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Initialize MariaDB&lt;br /&gt;
if [ ! -d &amp;quot;/var/lib/mysql/mysql&amp;quot; ]; then&lt;br /&gt;
    mysql_install_db --user=mysql --datadir=/var/lib/mysql&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 2&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
# Setup database&lt;br /&gt;
mysql -e &amp;quot;CREATE DATABASE IF NOT EXISTS completenoobs_wiki CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;127.0.0.1&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;127.0.0.1&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;localhost&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;localhost&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;FLUSH PRIVILEGES;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Install MediaWiki&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
php maintenance/install.php \&lt;br /&gt;
    --dbtype=mysql \&lt;br /&gt;
    --dbserver=127.0.0.1 \&lt;br /&gt;
    --dbname=completenoobs_wiki \&lt;br /&gt;
    --dbuser=wikiuser \&lt;br /&gt;
    --dbpass=wikipass \&lt;br /&gt;
    --server=&amp;quot;http://localhost:8080&amp;quot; \&lt;br /&gt;
    --scriptpath=&amp;quot;&amp;quot; \&lt;br /&gt;
    --lang=en \&lt;br /&gt;
    --pass=AdminPass123! \&lt;br /&gt;
    &amp;quot;CompleteNoobs Wiki&amp;quot; \&lt;br /&gt;
    &amp;quot;admin&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Download and install extensions&lt;br /&gt;
cd extensions/&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/PageNotice --branch REL1_44 || echo &amp;quot;PageNotice download failed, continuing...&amp;quot;&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/YouTube --branch REL1_44 || echo &amp;quot;YouTube download failed, continuing...&amp;quot;&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
&lt;br /&gt;
# Configure LocalSettings.php&lt;br /&gt;
cat &amp;gt;&amp;gt; LocalSettings.php &amp;lt;&amp;lt; &#039;EOF&#039;&lt;br /&gt;
# Basic settings&lt;br /&gt;
$wgEnableUploads = true;&lt;br /&gt;
$wgUseImageMagick = true;&lt;br /&gt;
$wgImageMagickConvertCommand = &amp;quot;/usr/bin/convert&amp;quot;;&lt;br /&gt;
$wgDefaultSkin = &amp;quot;vector-2022&amp;quot;;&lt;br /&gt;
$wgAllowExternalImages = true;&lt;br /&gt;
&lt;br /&gt;
# Debug (can be removed later)&lt;br /&gt;
$wgShowExceptionDetails = true;&lt;br /&gt;
$wgDebugLogFile = &amp;quot;/tmp/mediawiki-debug.log&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
# PageNotice extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/PageNotice/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;PageNotice&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# YouTube extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/YouTube/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;YouTube&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# SyntaxHighlight (usually bundled)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/SyntaxHighlight_GeSHi/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;SyntaxHighlight_GeSHi&#039; );&lt;br /&gt;
    $wgPygmentizePath = &#039;/usr/bin/pygmentize&#039;;&lt;br /&gt;
}&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
# Import XML dump if available&lt;br /&gt;
if [ -f &amp;quot;/tmp/completenoobs_dump.xml&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Importing XML dump...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if php maintenance/importDump.php --uploads &amp;lt; /tmp/completenoobs_dump.xml; then&lt;br /&gt;
        echo &amp;quot;XML import completed!&amp;quot;&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;XML import had warnings&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Basic maintenance&lt;br /&gt;
    php maintenance/update.php --quick || echo &amp;quot;Update completed with warnings&amp;quot;&lt;br /&gt;
    php maintenance/rebuildrecentchanges.php || echo &amp;quot;RecentChanges rebuilt with warnings&amp;quot;&lt;br /&gt;
    php maintenance/initSiteStats.php || echo &amp;quot;SiteStats initialized with warnings&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if [ -f &amp;quot;/tmp/dump_info.txt&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Import: $(cat /tmp/dump_info.txt)&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
        echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
    fi&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No XML dump found - starting with empty wiki&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
# Copy update script to accessible location&lt;br /&gt;
cp /usr/src/update_xml.sh /var/www/html/update_xml.sh&lt;br /&gt;
chmod +x /var/www/html/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Create user-friendly update wrapper&lt;br /&gt;
cat &amp;gt; /var/www/html/check_updates.sh &amp;lt;&amp;lt; &#039;UPDATE_WRAPPER_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki Update Checker ===&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;This tool checks for new content from the CompleteNoobs XML repository&amp;quot;&lt;br /&gt;
echo &amp;quot;and imports ONLY new pages, preserving all your local edits.&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
/var/www/html/update_xml.sh&lt;br /&gt;
UPDATE_WRAPPER_EOF&lt;br /&gt;
chmod +x /var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# Create simple status script&lt;br /&gt;
cat &amp;gt; /var/www/html/check_status.sh &amp;lt;&amp;lt; &#039;STATUS_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
echo &amp;quot;=== Wiki Status ===&amp;quot;&lt;br /&gt;
echo &amp;quot;Pages: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;Users: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM user WHERE user_id &amp;gt; 0;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Extensions ===&amp;quot;&lt;br /&gt;
if [ -d &amp;quot;extensions/PageNotice&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;PageNotice: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;PageNotice: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/YouTube&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;YouTube: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;YouTube: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/ContributionScores&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;ContributionScores: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;ContributionScores: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Update System ===&amp;quot;&lt;br /&gt;
if [ -f &amp;quot;.last_import&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Current version: $(grep &#039;Import:&#039; .last_import | cut -d&#039; &#039; -f2)&amp;quot;&lt;br /&gt;
    echo &amp;quot;Import date: $(grep &#039;Date:&#039; .last_import | cut -d&#039; &#039; -f2-)&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No version info available&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
STATUS_EOF&lt;br /&gt;
chmod +x /var/www/html/check_status.sh&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Setup completed!&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;Update scripts installed:&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/check_updates.sh (user-friendly)&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/update_xml.sh (direct)&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Final counts&lt;br /&gt;
PAGES=$(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;0&amp;quot;)&lt;br /&gt;
echo &amp;quot;Pages imported: $PAGES&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb stop&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.4: XML Update Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano update_xml.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki XML Updater ===&amp;quot;&lt;br /&gt;
echo &amp;quot;This will check for new and updated pages&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Function to check if running in container&lt;br /&gt;
check_environment() {&lt;br /&gt;
    if [ ! -f &amp;quot;/var/www/html/LocalSettings.php&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Error: This script must be run inside the wiki container&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to get current XML version&lt;br /&gt;
get_current_version() {&lt;br /&gt;
    if [ -f &amp;quot;/var/www/html/.last_import&amp;quot; ]; then&lt;br /&gt;
        grep &amp;quot;Import:&amp;quot; /var/www/html/.last_import | cut -d&#039; &#039; -f2&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;none&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to check for updates&lt;br /&gt;
check_for_updates() {&lt;br /&gt;
    python3 - &amp;lt;&amp;lt; &#039;PYTHON_EOF&#039;&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_latest_dump():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        if dumps:&lt;br /&gt;
            latest = sorted(dumps, key=parse_date_from_dump, reverse=True)[0]&lt;br /&gt;
            &lt;br /&gt;
            # Get XML files from latest dump&lt;br /&gt;
            response = requests.get(f&amp;quot;{BASE_URL}{latest}/&amp;quot;, timeout=30)&lt;br /&gt;
            response.raise_for_status()&lt;br /&gt;
            soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
            files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                    if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
            &lt;br /&gt;
            if files:&lt;br /&gt;
                newest_xml = sorted(files, reverse=True)[0]&lt;br /&gt;
                print(f&amp;quot;{latest}/{newest_xml}&amp;quot;)&lt;br /&gt;
                sys.exit(0)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;ERROR: {e}&amp;quot;, file=sys.stderr)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
    &lt;br /&gt;
    print(&amp;quot;ERROR: No dumps found&amp;quot;, file=sys.stderr)&lt;br /&gt;
    sys.exit(1)&lt;br /&gt;
&lt;br /&gt;
get_latest_dump()&lt;br /&gt;
PYTHON_EOF&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to download new XML&lt;br /&gt;
download_xml() {&lt;br /&gt;
    local dump_info=&amp;quot;$1&amp;quot;&lt;br /&gt;
    local dump_dir=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f1)&lt;br /&gt;
    local xml_file=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f2)&lt;br /&gt;
    local url=&amp;quot;https://xml.completenoobs.com/xmlDumps/${dump_info}&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Downloading: $xml_file&amp;quot;&lt;br /&gt;
    echo &amp;quot;From: $dump_dir&amp;quot;&lt;br /&gt;
    echo &amp;quot;URL: $url&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if wget -O /tmp/new_dump.xml &amp;quot;$url&amp;quot; --progress=bar:force 2&amp;gt;&amp;amp;1; then&lt;br /&gt;
        echo &amp;quot;Download successful!&amp;quot;&lt;br /&gt;
        return 0&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;Download failed!&amp;quot;&lt;br /&gt;
        return 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to backup current database&lt;br /&gt;
backup_database() {&lt;br /&gt;
    echo &amp;quot;Creating database backup...&amp;quot;&lt;br /&gt;
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)&lt;br /&gt;
    mysqldump --user=wikiuser --password=wikipass completenoobs_wiki &amp;gt; /tmp/wiki_backup_${TIMESTAMP}.sql&lt;br /&gt;
    echo &amp;quot;Backup created: /tmp/wiki_backup_${TIMESTAMP}.sql&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to analyze and import changes&lt;br /&gt;
analyze_and_import() {&lt;br /&gt;
    echo &amp;quot;Analyzing differences between XML and local wiki...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Create analysis and import script for MediaWiki 1.44+&lt;br /&gt;
    cat &amp;gt; /tmp/analyze_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class AnalyzeAndImport extends Maintenance {&lt;br /&gt;
    private $db;&lt;br /&gt;
    private $new_pages = [];&lt;br /&gt;
    private $changed_pages = [];&lt;br /&gt;
    private $unchanged_pages = [];&lt;br /&gt;
    &lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addDescription(&#039;Analyze and selectively import from XML dump&#039;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $this-&amp;gt;db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // MediaWiki 1.35+ uses slots and content tables&lt;br /&gt;
        // Get existing pages with their content&lt;br /&gt;
        $query = &amp;quot;&lt;br /&gt;
            SELECT p.page_title, p.page_id, c.content_address, c.content_sha1&lt;br /&gt;
            FROM page p&lt;br /&gt;
            JOIN revision r ON p.page_latest = r.rev_id&lt;br /&gt;
            JOIN slots s ON r.rev_id = s.slot_revision_id&lt;br /&gt;
            JOIN slot_roles sr ON s.slot_role_id = sr.role_id&lt;br /&gt;
            JOIN content c ON s.slot_content_id = c.content_id&lt;br /&gt;
            WHERE p.page_namespace = 0 AND sr.role_name = &#039;main&#039;&lt;br /&gt;
        &amp;quot;;&lt;br /&gt;
        &lt;br /&gt;
        $result = $this-&amp;gt;db-&amp;gt;query($query);&lt;br /&gt;
        if (!$result) {&lt;br /&gt;
            $this-&amp;gt;error(&amp;quot;Database query failed: &amp;quot; . $this-&amp;gt;db-&amp;gt;error);&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $existing = [];&lt;br /&gt;
        while ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
            // Get actual text content&lt;br /&gt;
            $text_content = $this-&amp;gt;getTextContent($row[&#039;content_address&#039;]);&lt;br /&gt;
            $existing[$row[&#039;page_title&#039;]] = [&lt;br /&gt;
                &#039;id&#039; =&amp;gt; $row[&#039;page_id&#039;],&lt;br /&gt;
                &#039;content&#039; =&amp;gt; $text_content,&lt;br /&gt;
                &#039;sha1&#039; =&amp;gt; $row[&#039;content_sha1&#039;]&lt;br /&gt;
            ];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Parse XML and compare&lt;br /&gt;
        $xml = simplexml_load_file(&#039;/tmp/new_dump.xml&#039;);&lt;br /&gt;
        &lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            $title = str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title);&lt;br /&gt;
            $xml_content = (string)$page-&amp;gt;revision-&amp;gt;text;&lt;br /&gt;
            &lt;br /&gt;
            if (!isset($existing[$title])) {&lt;br /&gt;
                // New page&lt;br /&gt;
                $this-&amp;gt;new_pages[$title] = $xml_content;&lt;br /&gt;
            } else {&lt;br /&gt;
                // Compare content using SHA1 for efficiency&lt;br /&gt;
                $xml_sha1 = sha1($xml_content);&lt;br /&gt;
                &lt;br /&gt;
                if ($existing[$title][&#039;sha1&#039;] !== $xml_sha1) {&lt;br /&gt;
                    // Content is different&lt;br /&gt;
                    $this-&amp;gt;changed_pages[$title] = [&lt;br /&gt;
                        &#039;local&#039; =&amp;gt; $existing[$title][&#039;content&#039;],&lt;br /&gt;
                        &#039;xml&#039; =&amp;gt; $xml_content,&lt;br /&gt;
                        &#039;page_id&#039; =&amp;gt; $existing[$title][&#039;id&#039;]&lt;br /&gt;
                    ];&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;unchanged_pages[] = $title;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Display summary&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Update Analysis ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages to import: &amp;quot; . count($this-&amp;gt;new_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Changed pages found: &amp;quot; . count($this-&amp;gt;changed_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Unchanged pages: &amp;quot; . count($this-&amp;gt;unchanged_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Save analysis for review&lt;br /&gt;
        file_put_contents(&#039;/tmp/update_analysis.json&#039;, json_encode([&lt;br /&gt;
            &#039;new&#039; =&amp;gt; array_keys($this-&amp;gt;new_pages),&lt;br /&gt;
            &#039;changed&#039; =&amp;gt; array_keys($this-&amp;gt;changed_pages),&lt;br /&gt;
            &#039;unchanged&#039; =&amp;gt; $this-&amp;gt;unchanged_pages&lt;br /&gt;
        ], JSON_PRETTY_PRINT));&lt;br /&gt;
        &lt;br /&gt;
        // Show changed pages with preview (limit to first 20 for readability)&lt;br /&gt;
        if (count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Changed Pages ===\n&amp;quot;);&lt;br /&gt;
            $count = 0;&lt;br /&gt;
            $total_changed = count($this-&amp;gt;changed_pages);&lt;br /&gt;
            &lt;br /&gt;
            foreach ($this-&amp;gt;changed_pages as $title =&amp;gt; $data) {&lt;br /&gt;
                $count++;&lt;br /&gt;
                if ($count &amp;lt;= 20) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;\n$count. $title\n&amp;quot;);&lt;br /&gt;
                    &lt;br /&gt;
                    // Create a simple diff preview (first 300 chars)&lt;br /&gt;
                    $xml_preview = substr($data[&#039;xml&#039;], 0, 100);&lt;br /&gt;
                    &lt;br /&gt;
                    // Save full diff to file&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;=== FULL DIFF FOR: $title ===\n\n&amp;quot;);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- LOCAL VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;local&#039;] . &amp;quot;\n\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- XML VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;xml&#039;], FILE_APPEND);&lt;br /&gt;
                    &lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;   Preview: &amp;quot; . $xml_preview . &amp;quot;...\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ($total_changed &amp;gt; 20) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\n... and &amp;quot; . ($total_changed - 20) . &amp;quot; more changed pages.\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;All diff files saved to /tmp/diff_*.txt\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Interactive selection&lt;br /&gt;
        if (count($this-&amp;gt;new_pages) &amp;gt; 0 || count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Import Options ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;1. Import new pages only (preserve all local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;2. Import new pages + update ALL changed pages (overwrites local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;3. Selective import (choose which updates to apply)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;4. Cancel (no changes)\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            // Save state for import script WITHOUT the XML object&lt;br /&gt;
            $import_data = [&lt;br /&gt;
                &#039;new_pages&#039; =&amp;gt; $this-&amp;gt;new_pages,&lt;br /&gt;
                &#039;changed_pages&#039; =&amp;gt; $this-&amp;gt;changed_pages,&lt;br /&gt;
                &#039;xml_file&#039; =&amp;gt; &#039;/tmp/new_dump.xml&#039;  // Save path instead of object&lt;br /&gt;
            ];&lt;br /&gt;
            &lt;br /&gt;
            file_put_contents(&#039;/tmp/import_data.ser&#039;, serialize($import_data));&lt;br /&gt;
        } else {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\nNo changes detected. Your wiki is up to date!\n&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function getTextContent($address) {&lt;br /&gt;
        // Handle different content storage formats in MW 1.35+&lt;br /&gt;
        if (strpos($address, &#039;tt:&#039;) === 0) {&lt;br /&gt;
            // Text table reference&lt;br /&gt;
            $text_id = substr($address, 3);&lt;br /&gt;
            $result = $this-&amp;gt;db-&amp;gt;query(&amp;quot;SELECT old_text FROM text WHERE old_id = $text_id&amp;quot;);&lt;br /&gt;
            if ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
                return $row[&#039;old_text&#039;];&lt;br /&gt;
            }&lt;br /&gt;
        } elseif (strpos($address, &#039;es:&#039;) === 0) {&lt;br /&gt;
            // External storage - would need special handling&lt;br /&gt;
            return &amp;quot;[External storage content]&amp;quot;;&lt;br /&gt;
        }&lt;br /&gt;
        // Direct content&lt;br /&gt;
        return $address;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = AnalyzeAndImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/analyze_import.php&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to perform selected import&lt;br /&gt;
perform_import() {&lt;br /&gt;
    local choice=$1&lt;br /&gt;
    &lt;br /&gt;
    cat &amp;gt; /tmp/do_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class DoImport extends Maintenance {&lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addOption(&#039;mode&#039;, &#039;Import mode&#039;, true, true);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $mode = $this-&amp;gt;getOption(&#039;mode&#039;);&lt;br /&gt;
        $data = unserialize(file_get_contents(&#039;/tmp/import_data.ser&#039;));&lt;br /&gt;
        &lt;br /&gt;
        // Load XML file&lt;br /&gt;
        $xml = simplexml_load_file($data[&#039;xml_file&#039;]);&lt;br /&gt;
        &lt;br /&gt;
        $new_imported = 0;&lt;br /&gt;
        $updated = 0;&lt;br /&gt;
        &lt;br /&gt;
        // Import new pages (always for modes 1-3)&lt;br /&gt;
        if ($mode != &#039;4&#039;) {&lt;br /&gt;
            foreach ($data[&#039;new_pages&#039;] as $title =&amp;gt; $content) {&lt;br /&gt;
                $this-&amp;gt;importPage($title, $content, $xml);&lt;br /&gt;
                $new_imported++;&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Imported new page: $title\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Handle changed pages based on mode&lt;br /&gt;
        if ($mode == &#039;2&#039;) {&lt;br /&gt;
            // Update all changed pages&lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Updating: $title\n&amp;quot;);&lt;br /&gt;
                if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                    $updated++;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Updated page: $title\n&amp;quot;);&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        } elseif ($mode == &#039;3&#039;) {&lt;br /&gt;
            // Selective update&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Selective Import Mode ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;For each changed page, choose:\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  y = yes, update this page\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  n = no, keep local version\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  d = show diff file\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  a = update all remaining pages\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  s = skip all remaining pages\n\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            $update_all = false;&lt;br /&gt;
            $skip_all = false;&lt;br /&gt;
            &lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                if ($skip_all) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($update_all) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\nPage: $title\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Action (y/n/d/a/s): &amp;quot;);&lt;br /&gt;
                $handle = fopen(&amp;quot;php://stdin&amp;quot;, &amp;quot;r&amp;quot;);&lt;br /&gt;
                $line = trim(fgets($handle));&lt;br /&gt;
                &lt;br /&gt;
                while ($line == &#039;d&#039;) {&lt;br /&gt;
                    // Show diff&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    if (file_exists($diff_file)) {&lt;br /&gt;
                        system(&amp;quot;head -50 $diff_file&amp;quot;);&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;\n[Showing first 50 lines - full file at $diff_file]\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Action (y/n/a/s): &amp;quot;);&lt;br /&gt;
                    $line = trim(fgets($handle));&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($line == &#039;a&#039;) {&lt;br /&gt;
                    $update_all = true;&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } elseif ($line == &#039;s&#039;) {&lt;br /&gt;
                    $skip_all = true;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                } elseif ($line == &#039;y&#039;) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Import Complete ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages imported: $new_imported\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Pages updated: $updated\n&amp;quot;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function importPage($title, $content, $xml) {&lt;br /&gt;
        // Create single page XML for import&lt;br /&gt;
        $tempFile = &#039;/tmp/single_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
        $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
        &lt;br /&gt;
        if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
            $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
            foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Find and add the page&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                break;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
        exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;);&lt;br /&gt;
        unlink($tempFile);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function reimportPage($title, $xml) {&lt;br /&gt;
        // For updating existing pages, delete then reimport&lt;br /&gt;
        $db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Delete the existing page&lt;br /&gt;
        $safe_title = $db-&amp;gt;real_escape_string(str_replace(&#039; &#039;, &#039;_&#039;, $title));&lt;br /&gt;
        $db-&amp;gt;query(&amp;quot;DELETE FROM page WHERE page_title = &#039;$safe_title&#039; AND page_namespace = 0&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        $db-&amp;gt;close();&lt;br /&gt;
        &lt;br /&gt;
        // Now import the new version&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $tempFile = &#039;/tmp/update_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
                $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
                &lt;br /&gt;
                if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
                    $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
                    foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                        $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
                $result = exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;, $output, $return);&lt;br /&gt;
                unlink($tempFile);&lt;br /&gt;
                &lt;br /&gt;
                return ($return === 0);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = DoImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/do_import.php --mode=&amp;quot;$choice&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Main execution&lt;br /&gt;
main() {&lt;br /&gt;
    check_environment&lt;br /&gt;
    &lt;br /&gt;
    # Start MariaDB if not running&lt;br /&gt;
    service mariadb status &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 || service mariadb start&lt;br /&gt;
    &lt;br /&gt;
    # Wait for MariaDB&lt;br /&gt;
    for i in {1..30}; do&lt;br /&gt;
        if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
            break&lt;br /&gt;
        fi&lt;br /&gt;
        sleep 1&lt;br /&gt;
    done&lt;br /&gt;
    &lt;br /&gt;
    CURRENT=$(get_current_version)&lt;br /&gt;
    echo &amp;quot;Current version: $CURRENT&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Checking for updates...&amp;quot;&lt;br /&gt;
    LATEST=$(check_for_updates 2&amp;gt;/dev/null)&lt;br /&gt;
    if [ $? -ne 0 ] || [ -z &amp;quot;$LATEST&amp;quot; ] || [[ &amp;quot;$LATEST&amp;quot; == *&amp;quot;ERROR&amp;quot;* ]]; then&lt;br /&gt;
        echo &amp;quot;Failed to check for updates&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Latest available: $LATEST&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Always proceed to analysis even if versions match&lt;br /&gt;
    # (there might be content updates in the same version)&lt;br /&gt;
    echo &amp;quot;Proceeding with content analysis...&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Backup database&lt;br /&gt;
    backup_database&lt;br /&gt;
    &lt;br /&gt;
    # Download new XML&lt;br /&gt;
    if ! download_xml &amp;quot;$LATEST&amp;quot;; then&lt;br /&gt;
        echo &amp;quot;Failed to download new XML&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Analyze differences&lt;br /&gt;
    analyze_and_import&lt;br /&gt;
    &lt;br /&gt;
    # Check if there are changes to import&lt;br /&gt;
    if [ -f &amp;quot;/tmp/import_data.ser&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;&amp;quot;&lt;br /&gt;
        read -p &amp;quot;Choose option (1-4): &amp;quot; -n 1 -r&lt;br /&gt;
        echo&lt;br /&gt;
        &lt;br /&gt;
        if [[ $REPLY =~ ^[1-4]$ ]]; then&lt;br /&gt;
            if [ &amp;quot;$REPLY&amp;quot; != &amp;quot;4&amp;quot; ]; then&lt;br /&gt;
                perform_import &amp;quot;$REPLY&amp;quot;&lt;br /&gt;
                &lt;br /&gt;
                # Update version info&lt;br /&gt;
                echo &amp;quot;Import: $LATEST&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
                echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
                &lt;br /&gt;
                # Rebuild indices&lt;br /&gt;
                echo &amp;quot;Rebuilding indices...&amp;quot;&lt;br /&gt;
                php maintenance/rebuildrecentchanges.php&lt;br /&gt;
                php maintenance/initSiteStats.php&lt;br /&gt;
            else&lt;br /&gt;
                echo &amp;quot;Update cancelled&amp;quot;&lt;br /&gt;
            fi&lt;br /&gt;
        else&lt;br /&gt;
            echo &amp;quot;Invalid option. Update cancelled&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Clean up temp files&lt;br /&gt;
    rm -f /tmp/import_data.ser /tmp/update_analysis.json /tmp/diff_*.txt /tmp/analyze_import.php /tmp/do_import.php 2&amp;gt;/dev/null&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    echo &amp;quot;Done!&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Run main function&lt;br /&gt;
main &amp;quot;$@&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.5: Entrypoint Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano entrypoint.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Starting CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 1&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;CompleteNoobs Wiki ready at: http://localhost:8080&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin login: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Features:&amp;quot;&lt;br /&gt;
echo &amp;quot;- Complete wiki content imported from XML&amp;quot;&lt;br /&gt;
echo &amp;quot;- License notices on all pages (via PageNotice)&amp;quot;&lt;br /&gt;
echo &amp;quot;- SyntaxHighlight for code blocks&amp;quot;&lt;br /&gt;
echo &amp;quot;- YouTube video embedding&amp;quot;&lt;br /&gt;
echo &amp;quot;- Contribution Scores special page&amp;quot;&lt;br /&gt;
echo &amp;quot;- XML update system (preserves local edits)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
apache2-foreground&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 3: Build and Run ==&lt;br /&gt;
&lt;br /&gt;
=== 3.1: Build the Image ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t completenoobs/wiki:latest .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will take several minutes.&lt;br /&gt;
&lt;br /&gt;
=== 3.2: Run the Container ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 4: Test Everything ==&lt;br /&gt;
&lt;br /&gt;
=== 4.1: Check the Wiki ===&lt;br /&gt;
* Visit: http://localhost:8080&lt;br /&gt;
* You should see the PageNotice at the top&lt;br /&gt;
* Login with: admin / AdminPass123!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Change Admin Password:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By default, the admin user&#039;s password is &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;. It&#039;s highly recommended to change this immediately. You can do this either through the wiki&#039;s web interface or directly in the Docker terminal.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 1: Change Password via Web Interface&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
This is the easiest method. You can change your password directly from the wiki itself.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Log in to your wiki with the default credentials: &amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
2. Once logged in, click your username (&amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt;) in the top-right corner of the page.&amp;lt;br&amp;gt;&lt;br /&gt;
3. From the drop-down menu, select &amp;lt;b&amp;gt;Preferences&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
4. On the Preferences page, go to the &amp;lt;b&amp;gt;Password&amp;lt;/b&amp;gt; tab.&amp;lt;br&amp;gt;&lt;br /&gt;
5. Enter the current password (&amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;), then enter your new password twice.&amp;lt;br&amp;gt;&lt;br /&gt;
6. Click &amp;lt;b&amp;gt;Change password&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Your password is now changed, and you will need to use the new one for future logins.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 2: Change Password via Terminal (No-Email Reset)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you have forgotten the password or prefer to use the command line, you can reset it directly inside the Docker container using a MediaWiki maintenance script.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;b&amp;gt;Access the container&#039;s shell&amp;lt;/b&amp;gt; with the following command from your host machine:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Once inside the container, use the &amp;lt;code&amp;gt;changePassword.php&amp;lt;/code&amp;gt; maintenance script to change the password. This is the modern, recommended way to run MediaWiki maintenance scripts.&amp;lt;br&amp;gt;&lt;br /&gt;
* Change &amp;lt;b&amp;gt;NEWPASSWORD&amp;lt;/b&amp;gt; to your new password&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt; to leave the container&#039;s shell.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The admin password has now been reset. You can log in to your wiki with the new password.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.2: Test Extensions ===&lt;br /&gt;
* &#039;&#039;&#039;YouTube&#039;&#039;&#039;: Edit any page, add &amp;lt;code&amp;gt;&amp;lt;youtube&amp;gt;N9qYF9DZPdw&amp;lt;/youtube&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;PageNotice&#039;&#039;&#039;: Should already be visible at the top&lt;br /&gt;
* &#039;&#039;&#039;SyntaxHighlight&#039;&#039;&#039;: Add code blocks with &amp;lt;nowiki&amp;gt;&amp;lt;code&amp;gt;&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;code here&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.3: Check Status ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/check_status.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 5: XML Update Operations ==&lt;br /&gt;
*NOTE: update_xml.sh still needs alot of work, this just idea placeholder for now.&lt;br /&gt;
=== 5.1: Check for Updates (Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will:&lt;br /&gt;
* Check the CompleteNoobs XML repository for new dumps&lt;br /&gt;
* Compare with your current version&lt;br /&gt;
* Ask for confirmation before updating&lt;br /&gt;
* Import ONLY new pages (preserves your edits)&lt;br /&gt;
* Create a backup before making changes&lt;br /&gt;
&lt;br /&gt;
=== 5.2: Force Update (Non-Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki bash -c &amp;quot;echo &#039;y&#039; | /var/www/html/update_xml.sh&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5.3: Manual Update Process ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# 1. Enter container&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&lt;br /&gt;
# 2. Check current version&lt;br /&gt;
cat /var/www/html/.last_import&lt;br /&gt;
&lt;br /&gt;
# 3. Run update&lt;br /&gt;
/var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# 4. Exit container&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 6: Troubleshooting Commands ==&lt;br /&gt;
=== Access container shell: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Edit configuration: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki nano /var/www/html/LocalSettings.php&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Check logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== View update logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki tail -f /tmp/mediawiki-debug.log&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Complete restart: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Expected Results ==&lt;br /&gt;
* Working wiki with imported CompleteNoobs content&lt;br /&gt;
* PageNotice visible at top of all pages&lt;br /&gt;
* All extensions functional&lt;br /&gt;
* Text editors (nano/vim) available in container&lt;br /&gt;
* Utility scripts for maintenance&lt;br /&gt;
* XML update system that preserves local edits&lt;br /&gt;
&lt;br /&gt;
== Important Notes ==&lt;br /&gt;
* First Import: Initial build imports ALL content from XML&lt;br /&gt;
* Subsequent Updates: Only import NEW pages, preserving your edits&lt;br /&gt;
* Conflict Resolution: To force-update a specific page with XML version, delete it first through wiki interface&lt;br /&gt;
* Backups: Stored in /tmp/ (cleared on container restart)&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=659</id>
		<title>CompleteNoobs Docker Image Creation</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=659"/>
		<updated>2025-09-01T20:00:59Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* Step 1: Clean Start */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Complete Noobs Docker Wiki Tutorial =&lt;br /&gt;
* [[Docker_Install_Guide| Docker install guide]]&lt;br /&gt;
==errors==&lt;br /&gt;
* This mainly works - just need to fix the extensions popular pages and contrubtion scores&lt;br /&gt;
* The XML updater requires more work - currently idea placeholder&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
* Ubuntu 24.04&lt;br /&gt;
* Docker installed and running&lt;br /&gt;
* Your user in docker group: &amp;lt;code&amp;gt;sudo usermod -aG docker $USER&amp;lt;/code&amp;gt; (then logout/login)&lt;br /&gt;
&lt;br /&gt;
== Step 2: Create All Files ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1: Dockerfile ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Copy this exactly:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM mediawiki:1.44&lt;br /&gt;
# Mediawiki 1.44 used over latest because can confirm extensions youtube and pagenotice works&lt;br /&gt;
# Install dependencies&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \&lt;br /&gt;
    mariadb-server \&lt;br /&gt;
    python3 \&lt;br /&gt;
    python3-requests \&lt;br /&gt;
    python3-bs4 \&lt;br /&gt;
    python3-pygments \&lt;br /&gt;
    curl \&lt;br /&gt;
    wget \&lt;br /&gt;
    unzip \&lt;br /&gt;
    nano \&lt;br /&gt;
    git \&lt;br /&gt;
    &amp;amp;&amp;amp; apt-get clean&lt;br /&gt;
&lt;br /&gt;
# Copy scripts&lt;br /&gt;
COPY download_latest_xml.py /usr/src/download_latest_xml.py&lt;br /&gt;
COPY setup_wiki.sh /usr/src/setup_wiki.sh&lt;br /&gt;
COPY update_xml.sh /usr/src/update_xml.sh&lt;br /&gt;
COPY entrypoint.sh /entrypoint.sh&lt;br /&gt;
&lt;br /&gt;
# Make executable&lt;br /&gt;
RUN chmod +x /usr/src/setup_wiki.sh /entrypoint.sh /usr/src/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Download XML&lt;br /&gt;
RUN python3 /usr/src/download_latest_xml.py&lt;br /&gt;
&lt;br /&gt;
# Setup wiki&lt;br /&gt;
RUN /usr/src/setup_wiki.sh&lt;br /&gt;
&lt;br /&gt;
EXPOSE 80&lt;br /&gt;
VOLUME /var/lib/mysql&lt;br /&gt;
VOLUME /var/www/html/images&lt;br /&gt;
&lt;br /&gt;
ENTRYPOINT [&amp;quot;/entrypoint.sh&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.2: XML Download Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano download_latest_xml.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_available_dumps():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        return sorted(dumps, key=parse_date_from_dump, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dumps: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def get_dump_files(dump):&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(f&amp;quot;{BASE_URL}{dump}/&amp;quot;, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
        return sorted(files, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dump files: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def download_file(url, filename):&lt;br /&gt;
    try:&lt;br /&gt;
        print(f&amp;quot;Downloading {filename}...&amp;quot;)&lt;br /&gt;
        response = requests.get(url, stream=True, timeout=60)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        &lt;br /&gt;
        total_size = int(response.headers.get(&#039;content-length&#039;, 0))&lt;br /&gt;
        downloaded = 0&lt;br /&gt;
        &lt;br /&gt;
        with open(filename, &#039;wb&#039;) as f:&lt;br /&gt;
            for chunk in response.iter_content(chunk_size=8192):&lt;br /&gt;
                if chunk:&lt;br /&gt;
                    f.write(chunk)&lt;br /&gt;
                    downloaded += len(chunk)&lt;br /&gt;
                    if total_size &amp;gt; 0:&lt;br /&gt;
                        progress = (downloaded / total_size) * 100&lt;br /&gt;
                        print(f&amp;quot;\rProgress: {progress:.1f}%&amp;quot;, end=&#039;&#039;, flush=True)&lt;br /&gt;
        print()&lt;br /&gt;
        return True&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error downloading {filename}: {e}&amp;quot;)&lt;br /&gt;
        return False&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    print(&amp;quot;Fetching available XML dumps...&amp;quot;)&lt;br /&gt;
    dumps = get_available_dumps()&lt;br /&gt;
    &lt;br /&gt;
    if not dumps:&lt;br /&gt;
        print(&amp;quot;No dumps found!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_dump = dumps[0]&lt;br /&gt;
    print(f&amp;quot;Latest dump: {newest_dump}&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    files = get_dump_files(newest_dump)&lt;br /&gt;
    if not files:&lt;br /&gt;
        print(&amp;quot;No XML files found in latest dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_xml = files[0]&lt;br /&gt;
    xml_url = f&amp;quot;{BASE_URL}{newest_dump}/{newest_xml}&amp;quot;&lt;br /&gt;
    local_filename = &amp;quot;/tmp/completenoobs_dump.xml&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if download_file(xml_url, local_filename):&lt;br /&gt;
        print(f&amp;quot;Successfully downloaded {newest_xml}&amp;quot;)&lt;br /&gt;
        with open(&amp;quot;/tmp/dump_info.txt&amp;quot;, &amp;quot;w&amp;quot;) as f:&lt;br /&gt;
            f.write(f&amp;quot;{newest_dump}/{newest_xml}&amp;quot;)&lt;br /&gt;
    else:&lt;br /&gt;
        print(&amp;quot;Failed to download XML dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.3: Main Setup Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano setup_wiki.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Setting up CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Initialize MariaDB&lt;br /&gt;
if [ ! -d &amp;quot;/var/lib/mysql/mysql&amp;quot; ]; then&lt;br /&gt;
    mysql_install_db --user=mysql --datadir=/var/lib/mysql&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 2&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
# Setup database&lt;br /&gt;
mysql -e &amp;quot;CREATE DATABASE IF NOT EXISTS completenoobs_wiki CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;127.0.0.1&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;127.0.0.1&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;localhost&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;localhost&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;FLUSH PRIVILEGES;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Install MediaWiki&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
php maintenance/install.php \&lt;br /&gt;
    --dbtype=mysql \&lt;br /&gt;
    --dbserver=127.0.0.1 \&lt;br /&gt;
    --dbname=completenoobs_wiki \&lt;br /&gt;
    --dbuser=wikiuser \&lt;br /&gt;
    --dbpass=wikipass \&lt;br /&gt;
    --server=&amp;quot;http://localhost:8080&amp;quot; \&lt;br /&gt;
    --scriptpath=&amp;quot;&amp;quot; \&lt;br /&gt;
    --lang=en \&lt;br /&gt;
    --pass=AdminPass123! \&lt;br /&gt;
    &amp;quot;CompleteNoobs Wiki&amp;quot; \&lt;br /&gt;
    &amp;quot;admin&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Download and install extensions&lt;br /&gt;
cd extensions/&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/PageNotice --branch REL1_44 || echo &amp;quot;PageNotice download failed, continuing...&amp;quot;&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/YouTube --branch REL1_44 || echo &amp;quot;YouTube download failed, continuing...&amp;quot;&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
&lt;br /&gt;
# Configure LocalSettings.php&lt;br /&gt;
cat &amp;gt;&amp;gt; LocalSettings.php &amp;lt;&amp;lt; &#039;EOF&#039;&lt;br /&gt;
# Basic settings&lt;br /&gt;
$wgEnableUploads = true;&lt;br /&gt;
$wgUseImageMagick = true;&lt;br /&gt;
$wgImageMagickConvertCommand = &amp;quot;/usr/bin/convert&amp;quot;;&lt;br /&gt;
$wgDefaultSkin = &amp;quot;vector-2022&amp;quot;;&lt;br /&gt;
$wgAllowExternalImages = true;&lt;br /&gt;
&lt;br /&gt;
# Debug (can be removed later)&lt;br /&gt;
$wgShowExceptionDetails = true;&lt;br /&gt;
$wgDebugLogFile = &amp;quot;/tmp/mediawiki-debug.log&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
# PageNotice extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/PageNotice/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;PageNotice&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# YouTube extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/YouTube/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;YouTube&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# SyntaxHighlight (usually bundled)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/SyntaxHighlight_GeSHi/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;SyntaxHighlight_GeSHi&#039; );&lt;br /&gt;
    $wgPygmentizePath = &#039;/usr/bin/pygmentize&#039;;&lt;br /&gt;
}&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
# Import XML dump if available&lt;br /&gt;
if [ -f &amp;quot;/tmp/completenoobs_dump.xml&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Importing XML dump...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if php maintenance/importDump.php --uploads &amp;lt; /tmp/completenoobs_dump.xml; then&lt;br /&gt;
        echo &amp;quot;XML import completed!&amp;quot;&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;XML import had warnings&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Basic maintenance&lt;br /&gt;
    php maintenance/update.php --quick || echo &amp;quot;Update completed with warnings&amp;quot;&lt;br /&gt;
    php maintenance/rebuildrecentchanges.php || echo &amp;quot;RecentChanges rebuilt with warnings&amp;quot;&lt;br /&gt;
    php maintenance/initSiteStats.php || echo &amp;quot;SiteStats initialized with warnings&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if [ -f &amp;quot;/tmp/dump_info.txt&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Import: $(cat /tmp/dump_info.txt)&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
        echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
    fi&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No XML dump found - starting with empty wiki&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
# Copy update script to accessible location&lt;br /&gt;
cp /usr/src/update_xml.sh /var/www/html/update_xml.sh&lt;br /&gt;
chmod +x /var/www/html/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Create user-friendly update wrapper&lt;br /&gt;
cat &amp;gt; /var/www/html/check_updates.sh &amp;lt;&amp;lt; &#039;UPDATE_WRAPPER_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki Update Checker ===&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;This tool checks for new content from the CompleteNoobs XML repository&amp;quot;&lt;br /&gt;
echo &amp;quot;and imports ONLY new pages, preserving all your local edits.&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
/var/www/html/update_xml.sh&lt;br /&gt;
UPDATE_WRAPPER_EOF&lt;br /&gt;
chmod +x /var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# Create simple status script&lt;br /&gt;
cat &amp;gt; /var/www/html/check_status.sh &amp;lt;&amp;lt; &#039;STATUS_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
echo &amp;quot;=== Wiki Status ===&amp;quot;&lt;br /&gt;
echo &amp;quot;Pages: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;Users: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM user WHERE user_id &amp;gt; 0;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Extensions ===&amp;quot;&lt;br /&gt;
if [ -d &amp;quot;extensions/PageNotice&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;PageNotice: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;PageNotice: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/YouTube&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;YouTube: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;YouTube: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/ContributionScores&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;ContributionScores: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;ContributionScores: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Update System ===&amp;quot;&lt;br /&gt;
if [ -f &amp;quot;.last_import&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Current version: $(grep &#039;Import:&#039; .last_import | cut -d&#039; &#039; -f2)&amp;quot;&lt;br /&gt;
    echo &amp;quot;Import date: $(grep &#039;Date:&#039; .last_import | cut -d&#039; &#039; -f2-)&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No version info available&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
STATUS_EOF&lt;br /&gt;
chmod +x /var/www/html/check_status.sh&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Setup completed!&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;Update scripts installed:&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/check_updates.sh (user-friendly)&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/update_xml.sh (direct)&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Final counts&lt;br /&gt;
PAGES=$(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;0&amp;quot;)&lt;br /&gt;
echo &amp;quot;Pages imported: $PAGES&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb stop&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.4: XML Update Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano update_xml.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki XML Updater ===&amp;quot;&lt;br /&gt;
echo &amp;quot;This will check for new and updated pages&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Function to check if running in container&lt;br /&gt;
check_environment() {&lt;br /&gt;
    if [ ! -f &amp;quot;/var/www/html/LocalSettings.php&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Error: This script must be run inside the wiki container&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to get current XML version&lt;br /&gt;
get_current_version() {&lt;br /&gt;
    if [ -f &amp;quot;/var/www/html/.last_import&amp;quot; ]; then&lt;br /&gt;
        grep &amp;quot;Import:&amp;quot; /var/www/html/.last_import | cut -d&#039; &#039; -f2&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;none&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to check for updates&lt;br /&gt;
check_for_updates() {&lt;br /&gt;
    python3 - &amp;lt;&amp;lt; &#039;PYTHON_EOF&#039;&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_latest_dump():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        if dumps:&lt;br /&gt;
            latest = sorted(dumps, key=parse_date_from_dump, reverse=True)[0]&lt;br /&gt;
            &lt;br /&gt;
            # Get XML files from latest dump&lt;br /&gt;
            response = requests.get(f&amp;quot;{BASE_URL}{latest}/&amp;quot;, timeout=30)&lt;br /&gt;
            response.raise_for_status()&lt;br /&gt;
            soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
            files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                    if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
            &lt;br /&gt;
            if files:&lt;br /&gt;
                newest_xml = sorted(files, reverse=True)[0]&lt;br /&gt;
                print(f&amp;quot;{latest}/{newest_xml}&amp;quot;)&lt;br /&gt;
                sys.exit(0)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;ERROR: {e}&amp;quot;, file=sys.stderr)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
    &lt;br /&gt;
    print(&amp;quot;ERROR: No dumps found&amp;quot;, file=sys.stderr)&lt;br /&gt;
    sys.exit(1)&lt;br /&gt;
&lt;br /&gt;
get_latest_dump()&lt;br /&gt;
PYTHON_EOF&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to download new XML&lt;br /&gt;
download_xml() {&lt;br /&gt;
    local dump_info=&amp;quot;$1&amp;quot;&lt;br /&gt;
    local dump_dir=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f1)&lt;br /&gt;
    local xml_file=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f2)&lt;br /&gt;
    local url=&amp;quot;https://xml.completenoobs.com/xmlDumps/${dump_info}&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Downloading: $xml_file&amp;quot;&lt;br /&gt;
    echo &amp;quot;From: $dump_dir&amp;quot;&lt;br /&gt;
    echo &amp;quot;URL: $url&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if wget -O /tmp/new_dump.xml &amp;quot;$url&amp;quot; --progress=bar:force 2&amp;gt;&amp;amp;1; then&lt;br /&gt;
        echo &amp;quot;Download successful!&amp;quot;&lt;br /&gt;
        return 0&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;Download failed!&amp;quot;&lt;br /&gt;
        return 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to backup current database&lt;br /&gt;
backup_database() {&lt;br /&gt;
    echo &amp;quot;Creating database backup...&amp;quot;&lt;br /&gt;
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)&lt;br /&gt;
    mysqldump --user=wikiuser --password=wikipass completenoobs_wiki &amp;gt; /tmp/wiki_backup_${TIMESTAMP}.sql&lt;br /&gt;
    echo &amp;quot;Backup created: /tmp/wiki_backup_${TIMESTAMP}.sql&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to analyze and import changes&lt;br /&gt;
analyze_and_import() {&lt;br /&gt;
    echo &amp;quot;Analyzing differences between XML and local wiki...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Create analysis and import script for MediaWiki 1.44+&lt;br /&gt;
    cat &amp;gt; /tmp/analyze_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class AnalyzeAndImport extends Maintenance {&lt;br /&gt;
    private $db;&lt;br /&gt;
    private $new_pages = [];&lt;br /&gt;
    private $changed_pages = [];&lt;br /&gt;
    private $unchanged_pages = [];&lt;br /&gt;
    &lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addDescription(&#039;Analyze and selectively import from XML dump&#039;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $this-&amp;gt;db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // MediaWiki 1.35+ uses slots and content tables&lt;br /&gt;
        // Get existing pages with their content&lt;br /&gt;
        $query = &amp;quot;&lt;br /&gt;
            SELECT p.page_title, p.page_id, c.content_address, c.content_sha1&lt;br /&gt;
            FROM page p&lt;br /&gt;
            JOIN revision r ON p.page_latest = r.rev_id&lt;br /&gt;
            JOIN slots s ON r.rev_id = s.slot_revision_id&lt;br /&gt;
            JOIN slot_roles sr ON s.slot_role_id = sr.role_id&lt;br /&gt;
            JOIN content c ON s.slot_content_id = c.content_id&lt;br /&gt;
            WHERE p.page_namespace = 0 AND sr.role_name = &#039;main&#039;&lt;br /&gt;
        &amp;quot;;&lt;br /&gt;
        &lt;br /&gt;
        $result = $this-&amp;gt;db-&amp;gt;query($query);&lt;br /&gt;
        if (!$result) {&lt;br /&gt;
            $this-&amp;gt;error(&amp;quot;Database query failed: &amp;quot; . $this-&amp;gt;db-&amp;gt;error);&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $existing = [];&lt;br /&gt;
        while ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
            // Get actual text content&lt;br /&gt;
            $text_content = $this-&amp;gt;getTextContent($row[&#039;content_address&#039;]);&lt;br /&gt;
            $existing[$row[&#039;page_title&#039;]] = [&lt;br /&gt;
                &#039;id&#039; =&amp;gt; $row[&#039;page_id&#039;],&lt;br /&gt;
                &#039;content&#039; =&amp;gt; $text_content,&lt;br /&gt;
                &#039;sha1&#039; =&amp;gt; $row[&#039;content_sha1&#039;]&lt;br /&gt;
            ];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Parse XML and compare&lt;br /&gt;
        $xml = simplexml_load_file(&#039;/tmp/new_dump.xml&#039;);&lt;br /&gt;
        &lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            $title = str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title);&lt;br /&gt;
            $xml_content = (string)$page-&amp;gt;revision-&amp;gt;text;&lt;br /&gt;
            &lt;br /&gt;
            if (!isset($existing[$title])) {&lt;br /&gt;
                // New page&lt;br /&gt;
                $this-&amp;gt;new_pages[$title] = $xml_content;&lt;br /&gt;
            } else {&lt;br /&gt;
                // Compare content using SHA1 for efficiency&lt;br /&gt;
                $xml_sha1 = sha1($xml_content);&lt;br /&gt;
                &lt;br /&gt;
                if ($existing[$title][&#039;sha1&#039;] !== $xml_sha1) {&lt;br /&gt;
                    // Content is different&lt;br /&gt;
                    $this-&amp;gt;changed_pages[$title] = [&lt;br /&gt;
                        &#039;local&#039; =&amp;gt; $existing[$title][&#039;content&#039;],&lt;br /&gt;
                        &#039;xml&#039; =&amp;gt; $xml_content,&lt;br /&gt;
                        &#039;page_id&#039; =&amp;gt; $existing[$title][&#039;id&#039;]&lt;br /&gt;
                    ];&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;unchanged_pages[] = $title;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Display summary&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Update Analysis ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages to import: &amp;quot; . count($this-&amp;gt;new_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Changed pages found: &amp;quot; . count($this-&amp;gt;changed_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Unchanged pages: &amp;quot; . count($this-&amp;gt;unchanged_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Save analysis for review&lt;br /&gt;
        file_put_contents(&#039;/tmp/update_analysis.json&#039;, json_encode([&lt;br /&gt;
            &#039;new&#039; =&amp;gt; array_keys($this-&amp;gt;new_pages),&lt;br /&gt;
            &#039;changed&#039; =&amp;gt; array_keys($this-&amp;gt;changed_pages),&lt;br /&gt;
            &#039;unchanged&#039; =&amp;gt; $this-&amp;gt;unchanged_pages&lt;br /&gt;
        ], JSON_PRETTY_PRINT));&lt;br /&gt;
        &lt;br /&gt;
        // Show changed pages with preview (limit to first 20 for readability)&lt;br /&gt;
        if (count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Changed Pages ===\n&amp;quot;);&lt;br /&gt;
            $count = 0;&lt;br /&gt;
            $total_changed = count($this-&amp;gt;changed_pages);&lt;br /&gt;
            &lt;br /&gt;
            foreach ($this-&amp;gt;changed_pages as $title =&amp;gt; $data) {&lt;br /&gt;
                $count++;&lt;br /&gt;
                if ($count &amp;lt;= 20) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;\n$count. $title\n&amp;quot;);&lt;br /&gt;
                    &lt;br /&gt;
                    // Create a simple diff preview (first 300 chars)&lt;br /&gt;
                    $xml_preview = substr($data[&#039;xml&#039;], 0, 100);&lt;br /&gt;
                    &lt;br /&gt;
                    // Save full diff to file&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;=== FULL DIFF FOR: $title ===\n\n&amp;quot;);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- LOCAL VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;local&#039;] . &amp;quot;\n\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- XML VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;xml&#039;], FILE_APPEND);&lt;br /&gt;
                    &lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;   Preview: &amp;quot; . $xml_preview . &amp;quot;...\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ($total_changed &amp;gt; 20) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\n... and &amp;quot; . ($total_changed - 20) . &amp;quot; more changed pages.\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;All diff files saved to /tmp/diff_*.txt\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Interactive selection&lt;br /&gt;
        if (count($this-&amp;gt;new_pages) &amp;gt; 0 || count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Import Options ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;1. Import new pages only (preserve all local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;2. Import new pages + update ALL changed pages (overwrites local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;3. Selective import (choose which updates to apply)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;4. Cancel (no changes)\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            // Save state for import script WITHOUT the XML object&lt;br /&gt;
            $import_data = [&lt;br /&gt;
                &#039;new_pages&#039; =&amp;gt; $this-&amp;gt;new_pages,&lt;br /&gt;
                &#039;changed_pages&#039; =&amp;gt; $this-&amp;gt;changed_pages,&lt;br /&gt;
                &#039;xml_file&#039; =&amp;gt; &#039;/tmp/new_dump.xml&#039;  // Save path instead of object&lt;br /&gt;
            ];&lt;br /&gt;
            &lt;br /&gt;
            file_put_contents(&#039;/tmp/import_data.ser&#039;, serialize($import_data));&lt;br /&gt;
        } else {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\nNo changes detected. Your wiki is up to date!\n&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function getTextContent($address) {&lt;br /&gt;
        // Handle different content storage formats in MW 1.35+&lt;br /&gt;
        if (strpos($address, &#039;tt:&#039;) === 0) {&lt;br /&gt;
            // Text table reference&lt;br /&gt;
            $text_id = substr($address, 3);&lt;br /&gt;
            $result = $this-&amp;gt;db-&amp;gt;query(&amp;quot;SELECT old_text FROM text WHERE old_id = $text_id&amp;quot;);&lt;br /&gt;
            if ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
                return $row[&#039;old_text&#039;];&lt;br /&gt;
            }&lt;br /&gt;
        } elseif (strpos($address, &#039;es:&#039;) === 0) {&lt;br /&gt;
            // External storage - would need special handling&lt;br /&gt;
            return &amp;quot;[External storage content]&amp;quot;;&lt;br /&gt;
        }&lt;br /&gt;
        // Direct content&lt;br /&gt;
        return $address;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = AnalyzeAndImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/analyze_import.php&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to perform selected import&lt;br /&gt;
perform_import() {&lt;br /&gt;
    local choice=$1&lt;br /&gt;
    &lt;br /&gt;
    cat &amp;gt; /tmp/do_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class DoImport extends Maintenance {&lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addOption(&#039;mode&#039;, &#039;Import mode&#039;, true, true);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $mode = $this-&amp;gt;getOption(&#039;mode&#039;);&lt;br /&gt;
        $data = unserialize(file_get_contents(&#039;/tmp/import_data.ser&#039;));&lt;br /&gt;
        &lt;br /&gt;
        // Load XML file&lt;br /&gt;
        $xml = simplexml_load_file($data[&#039;xml_file&#039;]);&lt;br /&gt;
        &lt;br /&gt;
        $new_imported = 0;&lt;br /&gt;
        $updated = 0;&lt;br /&gt;
        &lt;br /&gt;
        // Import new pages (always for modes 1-3)&lt;br /&gt;
        if ($mode != &#039;4&#039;) {&lt;br /&gt;
            foreach ($data[&#039;new_pages&#039;] as $title =&amp;gt; $content) {&lt;br /&gt;
                $this-&amp;gt;importPage($title, $content, $xml);&lt;br /&gt;
                $new_imported++;&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Imported new page: $title\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Handle changed pages based on mode&lt;br /&gt;
        if ($mode == &#039;2&#039;) {&lt;br /&gt;
            // Update all changed pages&lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Updating: $title\n&amp;quot;);&lt;br /&gt;
                if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                    $updated++;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Updated page: $title\n&amp;quot;);&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        } elseif ($mode == &#039;3&#039;) {&lt;br /&gt;
            // Selective update&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Selective Import Mode ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;For each changed page, choose:\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  y = yes, update this page\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  n = no, keep local version\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  d = show diff file\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  a = update all remaining pages\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  s = skip all remaining pages\n\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            $update_all = false;&lt;br /&gt;
            $skip_all = false;&lt;br /&gt;
            &lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                if ($skip_all) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($update_all) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\nPage: $title\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Action (y/n/d/a/s): &amp;quot;);&lt;br /&gt;
                $handle = fopen(&amp;quot;php://stdin&amp;quot;, &amp;quot;r&amp;quot;);&lt;br /&gt;
                $line = trim(fgets($handle));&lt;br /&gt;
                &lt;br /&gt;
                while ($line == &#039;d&#039;) {&lt;br /&gt;
                    // Show diff&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    if (file_exists($diff_file)) {&lt;br /&gt;
                        system(&amp;quot;head -50 $diff_file&amp;quot;);&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;\n[Showing first 50 lines - full file at $diff_file]\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Action (y/n/a/s): &amp;quot;);&lt;br /&gt;
                    $line = trim(fgets($handle));&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($line == &#039;a&#039;) {&lt;br /&gt;
                    $update_all = true;&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } elseif ($line == &#039;s&#039;) {&lt;br /&gt;
                    $skip_all = true;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                } elseif ($line == &#039;y&#039;) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Import Complete ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages imported: $new_imported\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Pages updated: $updated\n&amp;quot;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function importPage($title, $content, $xml) {&lt;br /&gt;
        // Create single page XML for import&lt;br /&gt;
        $tempFile = &#039;/tmp/single_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
        $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
        &lt;br /&gt;
        if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
            $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
            foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Find and add the page&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                break;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
        exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;);&lt;br /&gt;
        unlink($tempFile);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function reimportPage($title, $xml) {&lt;br /&gt;
        // For updating existing pages, delete then reimport&lt;br /&gt;
        $db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Delete the existing page&lt;br /&gt;
        $safe_title = $db-&amp;gt;real_escape_string(str_replace(&#039; &#039;, &#039;_&#039;, $title));&lt;br /&gt;
        $db-&amp;gt;query(&amp;quot;DELETE FROM page WHERE page_title = &#039;$safe_title&#039; AND page_namespace = 0&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        $db-&amp;gt;close();&lt;br /&gt;
        &lt;br /&gt;
        // Now import the new version&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $tempFile = &#039;/tmp/update_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
                $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
                &lt;br /&gt;
                if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
                    $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
                    foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                        $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
                $result = exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;, $output, $return);&lt;br /&gt;
                unlink($tempFile);&lt;br /&gt;
                &lt;br /&gt;
                return ($return === 0);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = DoImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/do_import.php --mode=&amp;quot;$choice&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Main execution&lt;br /&gt;
main() {&lt;br /&gt;
    check_environment&lt;br /&gt;
    &lt;br /&gt;
    # Start MariaDB if not running&lt;br /&gt;
    service mariadb status &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 || service mariadb start&lt;br /&gt;
    &lt;br /&gt;
    # Wait for MariaDB&lt;br /&gt;
    for i in {1..30}; do&lt;br /&gt;
        if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
            break&lt;br /&gt;
        fi&lt;br /&gt;
        sleep 1&lt;br /&gt;
    done&lt;br /&gt;
    &lt;br /&gt;
    CURRENT=$(get_current_version)&lt;br /&gt;
    echo &amp;quot;Current version: $CURRENT&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Checking for updates...&amp;quot;&lt;br /&gt;
    LATEST=$(check_for_updates 2&amp;gt;/dev/null)&lt;br /&gt;
    if [ $? -ne 0 ] || [ -z &amp;quot;$LATEST&amp;quot; ] || [[ &amp;quot;$LATEST&amp;quot; == *&amp;quot;ERROR&amp;quot;* ]]; then&lt;br /&gt;
        echo &amp;quot;Failed to check for updates&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Latest available: $LATEST&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Always proceed to analysis even if versions match&lt;br /&gt;
    # (there might be content updates in the same version)&lt;br /&gt;
    echo &amp;quot;Proceeding with content analysis...&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Backup database&lt;br /&gt;
    backup_database&lt;br /&gt;
    &lt;br /&gt;
    # Download new XML&lt;br /&gt;
    if ! download_xml &amp;quot;$LATEST&amp;quot;; then&lt;br /&gt;
        echo &amp;quot;Failed to download new XML&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Analyze differences&lt;br /&gt;
    analyze_and_import&lt;br /&gt;
    &lt;br /&gt;
    # Check if there are changes to import&lt;br /&gt;
    if [ -f &amp;quot;/tmp/import_data.ser&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;&amp;quot;&lt;br /&gt;
        read -p &amp;quot;Choose option (1-4): &amp;quot; -n 1 -r&lt;br /&gt;
        echo&lt;br /&gt;
        &lt;br /&gt;
        if [[ $REPLY =~ ^[1-4]$ ]]; then&lt;br /&gt;
            if [ &amp;quot;$REPLY&amp;quot; != &amp;quot;4&amp;quot; ]; then&lt;br /&gt;
                perform_import &amp;quot;$REPLY&amp;quot;&lt;br /&gt;
                &lt;br /&gt;
                # Update version info&lt;br /&gt;
                echo &amp;quot;Import: $LATEST&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
                echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
                &lt;br /&gt;
                # Rebuild indices&lt;br /&gt;
                echo &amp;quot;Rebuilding indices...&amp;quot;&lt;br /&gt;
                php maintenance/rebuildrecentchanges.php&lt;br /&gt;
                php maintenance/initSiteStats.php&lt;br /&gt;
            else&lt;br /&gt;
                echo &amp;quot;Update cancelled&amp;quot;&lt;br /&gt;
            fi&lt;br /&gt;
        else&lt;br /&gt;
            echo &amp;quot;Invalid option. Update cancelled&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Clean up temp files&lt;br /&gt;
    rm -f /tmp/import_data.ser /tmp/update_analysis.json /tmp/diff_*.txt /tmp/analyze_import.php /tmp/do_import.php 2&amp;gt;/dev/null&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    echo &amp;quot;Done!&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Run main function&lt;br /&gt;
main &amp;quot;$@&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.5: Entrypoint Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano entrypoint.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Starting CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 1&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;CompleteNoobs Wiki ready at: http://localhost:8080&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin login: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Features:&amp;quot;&lt;br /&gt;
echo &amp;quot;- Complete wiki content imported from XML&amp;quot;&lt;br /&gt;
echo &amp;quot;- License notices on all pages (via PageNotice)&amp;quot;&lt;br /&gt;
echo &amp;quot;- SyntaxHighlight for code blocks&amp;quot;&lt;br /&gt;
echo &amp;quot;- YouTube video embedding&amp;quot;&lt;br /&gt;
echo &amp;quot;- Contribution Scores special page&amp;quot;&lt;br /&gt;
echo &amp;quot;- XML update system (preserves local edits)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
apache2-foreground&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 3: Build and Run ==&lt;br /&gt;
&lt;br /&gt;
=== 3.1: Build the Image ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t completenoobs/wiki:latest .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will take several minutes.&lt;br /&gt;
&lt;br /&gt;
=== 3.2: Run the Container ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 4: Test Everything ==&lt;br /&gt;
&lt;br /&gt;
=== 4.1: Check the Wiki ===&lt;br /&gt;
* Visit: http://localhost:8080&lt;br /&gt;
* You should see the PageNotice at the top&lt;br /&gt;
* Login with: admin / AdminPass123!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Change Admin Password:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By default, the admin user&#039;s password is &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;. It&#039;s highly recommended to change this immediately. You can do this either through the wiki&#039;s web interface or directly in the Docker terminal.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 1: Change Password via Web Interface&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
This is the easiest method. You can change your password directly from the wiki itself.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Log in to your wiki with the default credentials: &amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
2. Once logged in, click your username (&amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt;) in the top-right corner of the page.&amp;lt;br&amp;gt;&lt;br /&gt;
3. From the drop-down menu, select &amp;lt;b&amp;gt;Preferences&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
4. On the Preferences page, go to the &amp;lt;b&amp;gt;Password&amp;lt;/b&amp;gt; tab.&amp;lt;br&amp;gt;&lt;br /&gt;
5. Enter the current password (&amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;), then enter your new password twice.&amp;lt;br&amp;gt;&lt;br /&gt;
6. Click &amp;lt;b&amp;gt;Change password&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Your password is now changed, and you will need to use the new one for future logins.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 2: Change Password via Terminal (No-Email Reset)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you have forgotten the password or prefer to use the command line, you can reset it directly inside the Docker container using a MediaWiki maintenance script.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;b&amp;gt;Access the container&#039;s shell&amp;lt;/b&amp;gt; with the following command from your host machine:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Once inside the container, use the &amp;lt;code&amp;gt;changePassword.php&amp;lt;/code&amp;gt; maintenance script to change the password. This is the modern, recommended way to run MediaWiki maintenance scripts.&amp;lt;br&amp;gt;&lt;br /&gt;
* Change &amp;lt;b&amp;gt;NEWPASSWORD&amp;lt;/b&amp;gt; to your new password&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt; to leave the container&#039;s shell.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The admin password has now been reset. You can log in to your wiki with the new password.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.2: Test Extensions ===&lt;br /&gt;
* &#039;&#039;&#039;YouTube&#039;&#039;&#039;: Edit any page, add &amp;lt;code&amp;gt;&amp;lt;youtube&amp;gt;N9qYF9DZPdw&amp;lt;/youtube&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;PageNotice&#039;&#039;&#039;: Should already be visible at the top&lt;br /&gt;
* &#039;&#039;&#039;SyntaxHighlight&#039;&#039;&#039;: Add code blocks with &amp;lt;nowiki&amp;gt;&amp;lt;code&amp;gt;&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;code here&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.3: Check Status ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/check_status.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 5: XML Update Operations ==&lt;br /&gt;
*NOTE: update_xml.sh still needs alot of work, this just idea placeholder for now.&lt;br /&gt;
=== 5.1: Check for Updates (Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will:&lt;br /&gt;
* Check the CompleteNoobs XML repository for new dumps&lt;br /&gt;
* Compare with your current version&lt;br /&gt;
* Ask for confirmation before updating&lt;br /&gt;
* Import ONLY new pages (preserves your edits)&lt;br /&gt;
* Create a backup before making changes&lt;br /&gt;
&lt;br /&gt;
=== 5.2: Force Update (Non-Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki bash -c &amp;quot;echo &#039;y&#039; | /var/www/html/update_xml.sh&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5.3: Manual Update Process ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# 1. Enter container&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&lt;br /&gt;
# 2. Check current version&lt;br /&gt;
cat /var/www/html/.last_import&lt;br /&gt;
&lt;br /&gt;
# 3. Run update&lt;br /&gt;
/var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# 4. Exit container&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 6: Troubleshooting Commands ==&lt;br /&gt;
=== Access container shell: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Edit configuration: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki nano /var/www/html/LocalSettings.php&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Check logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== View update logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki tail -f /tmp/mediawiki-debug.log&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Complete restart: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Expected Results ==&lt;br /&gt;
* Working wiki with imported CompleteNoobs content&lt;br /&gt;
* PageNotice visible at top of all pages&lt;br /&gt;
* All extensions functional&lt;br /&gt;
* Text editors (nano/vim) available in container&lt;br /&gt;
* Utility scripts for maintenance&lt;br /&gt;
* XML update system that preserves local edits&lt;br /&gt;
&lt;br /&gt;
== Update System Features ==&lt;br /&gt;
&lt;br /&gt;
=== How Updates Work ===&lt;br /&gt;
# Version Tracking: System tracks which XML dump version you have&lt;br /&gt;
# Smart Import: Only imports pages that don&#039;t exist locally&lt;br /&gt;
# Edit Preservation: Never overwrites pages you&#039;ve edited&lt;br /&gt;
# Automatic Backup: Creates SQL backup before any updates&lt;br /&gt;
# User Confirmation: Asks before making changes&lt;br /&gt;
# Progress Feedback: Shows what&#039;s being imported/skipped&lt;br /&gt;
&lt;br /&gt;
== Important Notes ==&lt;br /&gt;
* First Import: Initial build imports ALL content from XML&lt;br /&gt;
* Subsequent Updates: Only import NEW pages, preserving your edits&lt;br /&gt;
* Conflict Resolution: To force-update a specific page with XML version, delete it first through wiki interface&lt;br /&gt;
* Backups: Stored in /tmp/ (cleared on container restart)&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Local_Wiki_In_Docker&amp;diff=658</id>
		<title>CompleteNoobs Local Wiki In Docker</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Local_Wiki_In_Docker&amp;diff=658"/>
		<updated>2025-09-01T19:13:31Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* Starting Environment */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
&lt;br /&gt;
==Docker Image==&lt;br /&gt;
&lt;br /&gt;
* [[Ubuntu2404_Install_Docker_and_Docker_Compose| Download the wiki as a &#039;&#039;&#039;Docker image&#039;&#039;&#039; to run locally on your computer or fork it to contribute and share knowledge.]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Starting Environment==&lt;br /&gt;
* &#039;&#039;&#039;Hardware&#039;&#039;&#039;: HP EliteDesk 800 G1&lt;br /&gt;
* &#039;&#039;&#039;Operating System&#039;&#039;&#039;: Ubuntu-Mate 24.04&lt;br /&gt;
* &#039;&#039;&#039;Installation Type&#039;&#039;&#039;: Fresh Install&lt;br /&gt;
* &#039;&#039;&#039;Initial Setup Post-Install&#039;&#039;&#039;: Package Update: &amp;lt;code&amp;gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;Additional Software&#039;&#039;&#039;: Installed just to record screen for this tut and not needed &amp;lt;code&amp;gt;sudo apt install simplescreenrecorder&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Docker Installation Guide ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;youtube&amp;gt;https://www.youtube.com/watch?v=MheOIG2KiTI&amp;lt;/youtube&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Preparation ===&lt;br /&gt;
&lt;br /&gt;
Before we begin, make sure you&#039;re logged in with a user account that has sudo privileges.&lt;br /&gt;
&lt;br /&gt;
=== Update System Packages ===&lt;br /&gt;
&lt;br /&gt;
Update your package list to ensure you have the latest versions of packages:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt update&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Install Docker Prerequisites ===&lt;br /&gt;
&lt;br /&gt;
Install the necessary packages for Docker setup:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Setup Docker Repository ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Add Docker&#039;s Official GPG Key&#039;&#039;&#039;:&lt;br /&gt;
 &lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Add the Docker Repository&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  echo &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&amp;quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Install Docker and Docker Compose ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Update Package List Again&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo apt update&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Install Docker Engine, CLI, Containerd, and Additional Tools&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo apt install -y docker-ce docker-ce-cli containerd.io python3-bs4 python3-requests docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Install Docker Compose&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
Here we&#039;re downloading the latest version of Docker Compose:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo curl -L &amp;quot;https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)&amp;quot; -o /usr/local/bin/docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Make the Docker Compose binary executable:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo chmod +x /usr/local/bin/docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Verify Installation ===&lt;br /&gt;
&lt;br /&gt;
Check if Docker and Docker Compose are installed correctly:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker --version&lt;br /&gt;
docker-compose --version&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Configure User Permissions ===&lt;br /&gt;
&lt;br /&gt;
To run Docker commands without &amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt;, add your user to the &amp;lt;code&amp;gt;docker&amp;lt;/code&amp;gt; group:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo usermod -aG docker $USER&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note&#039;&#039;&#039;: After adding your user to the docker group, you&#039;ll need to &#039;&#039;&#039;log out and log back in&#039;&#039;&#039; for the changes to take effect.&lt;br /&gt;
If you do not log out and back in, Or you do not add your $USER to the docker group, you will be required to use sudo in some cases. such as ..&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
a way to apply group changes without logging out and back in - tip:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
exec sudo su -l $USER&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will replace your current shell with a new login shell for your user, which will have the updated group memberships. Both of these methods will apply the group changes immediately, allowing you to use LXD commands without having to log out and back in. Remember, these changes only apply to the current terminal session. If you open a new terminal window, you might need to run the command again or log out and back in for the changes to take effect system-wide.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
= Creating a Wiki and Importing an XML Dump =&lt;br /&gt;
&lt;br /&gt;
== Setup the Wiki Environment ==&lt;br /&gt;
&lt;br /&gt;
=== Create Directory for the Wiki ===&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
mkdir noobwiki&lt;br /&gt;
cd noobwiki&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Create Docker Compose File ===&lt;br /&gt;
Edit your &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt; file:&lt;br /&gt;
&lt;br /&gt;
* [[SET$EDITOR|set editor]]&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$EDITOR docker-compose.yml&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Contents of &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt;&#039;&#039;&#039;:&lt;br /&gt;
&amp;lt;source lang=&amp;quot;yaml&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  database:&lt;br /&gt;
    image: mysql:5.7&lt;br /&gt;
    environment:&lt;br /&gt;
      MYSQL_DATABASE: my_wiki&lt;br /&gt;
      MYSQL_USER: wikiuser&lt;br /&gt;
      MYSQL_PASSWORD: wikipass&lt;br /&gt;
      MYSQL_RANDOM_ROOT_PASSWORD: &#039;yes&#039;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ./mysql_data:/var/lib/mysql&lt;br /&gt;
&lt;br /&gt;
  mediawiki:&lt;br /&gt;
    image: mediawiki&lt;br /&gt;
    ports:&lt;br /&gt;
      - 8080:80&lt;br /&gt;
    links:&lt;br /&gt;
      - database&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ./mediawiki_data:/var/www/html/images&lt;br /&gt;
#      - ./LocalSettings.php:/var/www/html/LocalSettings.php # This line is commented out initially&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note:&#039;&#039;&#039; The &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt; volume mapping is commented out to prevent automatic creation during initial setup.&lt;br /&gt;
&lt;br /&gt;
== Python Script for Setup and XML Import ==&lt;br /&gt;
&lt;br /&gt;
=== Script Details ===&lt;br /&gt;
Create a Python script to automate the setup and import process:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$EDITOR setup_and_import.py&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Newer Script that will list newest first - list from new to old in order:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
* Passed early testing&lt;br /&gt;
&amp;lt;source lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import os&lt;br /&gt;
import time&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
import subprocess&lt;br /&gt;
import sys&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Parse DD_MM_YY format and return a sortable date tuple.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        # Convert 2-digit year to 4-digit (assuming 00-49 = 2000-2049, 50-99 = 1950-1999)&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        if year_int &amp;lt;= 49:&lt;br /&gt;
            full_year = 2000 + year_int&lt;br /&gt;
        else:&lt;br /&gt;
            full_year = 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)  # fallback for unparseable names&lt;br /&gt;
&lt;br /&gt;
def get_available_dumps():&lt;br /&gt;
    response = requests.get(BASE_URL)&lt;br /&gt;
    soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
    dumps = []&lt;br /&gt;
    for link in soup.find_all(&#039;a&#039;):&lt;br /&gt;
        href = link.get(&#039;href&#039;)&lt;br /&gt;
        if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, href):&lt;br /&gt;
            dumps.append(href.rstrip(&#039;/&#039;))&lt;br /&gt;
    &lt;br /&gt;
    # Sort by actual date (newest first)&lt;br /&gt;
    return sorted(dumps, key=parse_date_from_dump, reverse=True)&lt;br /&gt;
&lt;br /&gt;
def get_dump_files(dump):&lt;br /&gt;
    response = requests.get(BASE_URL + dump)&lt;br /&gt;
    soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
    files = {}&lt;br /&gt;
    for link in soup.find_all(&#039;a&#039;):&lt;br /&gt;
        href = link.get(&#039;href&#039;)&lt;br /&gt;
        if href.endswith(&#039;.xml&#039;):&lt;br /&gt;
            # Extract file info (size, date) from the next sibling text&lt;br /&gt;
            file_info = &amp;quot;&amp;quot;&lt;br /&gt;
            next_sibling = link.next_sibling&lt;br /&gt;
            if next_sibling:&lt;br /&gt;
                file_info = next_sibling.strip()&lt;br /&gt;
            files[href] = file_info&lt;br /&gt;
    &lt;br /&gt;
    # Sort XML files by name (newest first, assuming filename contains date/time info)&lt;br /&gt;
    sorted_files = dict(sorted(files.items(), key=lambda x: x[0], reverse=True))&lt;br /&gt;
    return sorted_files&lt;br /&gt;
&lt;br /&gt;
def download_file(url, filename):&lt;br /&gt;
    print(f&amp;quot;Downloading from: {url}&amp;quot;)&lt;br /&gt;
    response = requests.get(url, stream=True)&lt;br /&gt;
    response.raise_for_status()&lt;br /&gt;
    &lt;br /&gt;
    total_size = int(response.headers.get(&#039;content-length&#039;, 0))&lt;br /&gt;
    downloaded = 0&lt;br /&gt;
    &lt;br /&gt;
    with open(filename, &#039;wb&#039;) as f:&lt;br /&gt;
        for chunk in response.iter_content(chunk_size=8192):&lt;br /&gt;
            if chunk:&lt;br /&gt;
                f.write(chunk)&lt;br /&gt;
                downloaded += len(chunk)&lt;br /&gt;
                if total_size &amp;gt; 0:&lt;br /&gt;
                    progress = (downloaded / total_size) * 100&lt;br /&gt;
                    print(f&amp;quot;\rProgress: {progress:.1f}%&amp;quot;, end=&#039;&#039;, flush=True)&lt;br /&gt;
    print()  # New line after progress&lt;br /&gt;
&lt;br /&gt;
def run_command(command):&lt;br /&gt;
    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)&lt;br /&gt;
    output, error = process.communicate()&lt;br /&gt;
    return output.decode(), error.decode(), process.returncode&lt;br /&gt;
&lt;br /&gt;
def check_docker_permissions():&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Check if Docker is accessible and provide helpful error messages.&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        output, error, return_code = run_command(&amp;quot;docker info&amp;quot;)&lt;br /&gt;
        if return_code != 0:&lt;br /&gt;
            if &amp;quot;permission denied&amp;quot; in error.lower():&lt;br /&gt;
                print(&amp;quot;ERROR: Docker permission denied!&amp;quot;)&lt;br /&gt;
                print(&amp;quot;Solutions:&amp;quot;)&lt;br /&gt;
                print(&amp;quot;1. Run this script with sudo: sudo python3 setup_and_import.py&amp;quot;)&lt;br /&gt;
                print(&amp;quot;2. Or add your user to docker group: sudo usermod -aG docker $USER&amp;quot;)&lt;br /&gt;
                print(&amp;quot;   Then log out and log back in.&amp;quot;)&lt;br /&gt;
                return False&lt;br /&gt;
            else:&lt;br /&gt;
                print(f&amp;quot;Docker error: {error}&amp;quot;)&lt;br /&gt;
                return False&lt;br /&gt;
        return True&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error checking Docker: {e}&amp;quot;)&lt;br /&gt;
        return False&lt;br /&gt;
&lt;br /&gt;
def setup_mediawiki():&lt;br /&gt;
    print(&amp;quot;Checking Docker permissions...&amp;quot;)&lt;br /&gt;
    if not check_docker_permissions():&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
    &lt;br /&gt;
    print(&amp;quot;Setting up MediaWiki...&amp;quot;)&lt;br /&gt;
    output, error, return_code = run_command(&amp;quot;docker-compose up -d&amp;quot;)&lt;br /&gt;
    if return_code != 0:&lt;br /&gt;
        print(f&amp;quot;Error starting Docker containers: {error}&amp;quot;)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
    &lt;br /&gt;
    time.sleep(10)  # Wait for services to start&lt;br /&gt;
&lt;br /&gt;
    print(&amp;quot;Please complete the MediaWiki installation by visiting http://localhost:8080&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Use the following database settings:&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Database host: database&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Database name: my_wiki&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Database username: wikiuser&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Database password: wikipass&amp;quot;)&lt;br /&gt;
    input(&amp;quot;Press Enter when you have completed the installation and downloaded LocalSettings.php...&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    if not os.path.exists(&#039;LocalSettings.php&#039;):&lt;br /&gt;
        print(&amp;quot;LocalSettings.php not found. Please place it in the current directory.&amp;quot;)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
&lt;br /&gt;
    output, error, return_code = run_command(&amp;quot;docker-compose restart mediawiki&amp;quot;)&lt;br /&gt;
    if return_code != 0:&lt;br /&gt;
        print(f&amp;quot;Error restarting MediaWiki: {error}&amp;quot;)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
    &lt;br /&gt;
    print(&amp;quot;MediaWiki setup completed.&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def import_xml_to_mediawiki(xml_file):&lt;br /&gt;
    command = f&amp;quot;docker-compose exec -T mediawiki php maintenance/importDump.php &amp;lt; {xml_file}&amp;quot;&lt;br /&gt;
    print(f&amp;quot;Importing {xml_file} into MediaWiki...&amp;quot;)&lt;br /&gt;
    output, error, return_code = run_command(command)&lt;br /&gt;
    if return_code != 0:&lt;br /&gt;
        print(f&amp;quot;Error importing XML: {error}&amp;quot;)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
    print(&amp;quot;Import completed successfully.&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    # Check Docker permissions early&lt;br /&gt;
    if not os.path.exists(&#039;LocalSettings.php&#039;):&lt;br /&gt;
        setup_mediawiki()&lt;br /&gt;
    else:&lt;br /&gt;
        # Even if LocalSettings.php exists, check Docker permissions for import&lt;br /&gt;
        print(&amp;quot;Checking Docker permissions...&amp;quot;)&lt;br /&gt;
        if not check_docker_permissions():&lt;br /&gt;
            sys.exit(1)&lt;br /&gt;
&lt;br /&gt;
    print(&amp;quot;Fetching available dumps...&amp;quot;)&lt;br /&gt;
    dumps = get_available_dumps()&lt;br /&gt;
    if not dumps:&lt;br /&gt;
        print(&amp;quot;No dumps found!&amp;quot;)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
    &lt;br /&gt;
    print(&amp;quot;\nAvailable dumps (newest first):&amp;quot;)&lt;br /&gt;
    for i, dump in enumerate(dumps):&lt;br /&gt;
        marker = &amp;quot; (NEWEST)&amp;quot; if i == 0 else &amp;quot;&amp;quot;&lt;br /&gt;
        print(f&amp;quot;{i + 1}. {dump}{marker}&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    # Default to newest dump (index 0)&lt;br /&gt;
    default_choice = 1&lt;br /&gt;
    choice_input = input(f&amp;quot;\nEnter the number of the dump you want to download (default: {default_choice} - newest): &amp;quot;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if choice_input == &amp;quot;&amp;quot;:&lt;br /&gt;
        choice = default_choice - 1&lt;br /&gt;
    else:&lt;br /&gt;
        try:&lt;br /&gt;
            choice = int(choice_input) - 1&lt;br /&gt;
            if choice &amp;lt; 0 or choice &amp;gt;= len(dumps):&lt;br /&gt;
                print(&amp;quot;Invalid choice. Using newest dump.&amp;quot;)&lt;br /&gt;
                choice = 0&lt;br /&gt;
        except ValueError:&lt;br /&gt;
            print(&amp;quot;Invalid input. Using newest dump.&amp;quot;)&lt;br /&gt;
            choice = 0&lt;br /&gt;
    &lt;br /&gt;
    selected_dump = dumps[choice]&lt;br /&gt;
    print(f&amp;quot;\nSelected dump: {selected_dump}&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    print(&amp;quot;Fetching XML files from selected dump...&amp;quot;)&lt;br /&gt;
    files = get_dump_files(selected_dump)&lt;br /&gt;
    &lt;br /&gt;
    if not files:&lt;br /&gt;
        print(&amp;quot;No XML files found in the selected dump!&amp;quot;)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
    &lt;br /&gt;
    xml_files = list(files.keys())&lt;br /&gt;
    &lt;br /&gt;
    print(f&amp;quot;\nAvailable XML files in {selected_dump} (newest first):&amp;quot;)&lt;br /&gt;
    for i, xml_file in enumerate(xml_files):&lt;br /&gt;
        file_info = files[xml_file]&lt;br /&gt;
        marker = &amp;quot; (RECOMMENDED)&amp;quot; if i == 0 else &amp;quot;&amp;quot;&lt;br /&gt;
        print(f&amp;quot;{i + 1}. {xml_file}{marker}&amp;quot;)&lt;br /&gt;
        if file_info:&lt;br /&gt;
            print(f&amp;quot;    {file_info}&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    # Default to newest XML file (index 0)&lt;br /&gt;
    default_xml_choice = 1&lt;br /&gt;
    xml_choice_input = input(f&amp;quot;\nEnter the number of the XML file you want to download (default: {default_xml_choice} - newest): &amp;quot;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if xml_choice_input == &amp;quot;&amp;quot;:&lt;br /&gt;
        xml_choice = default_xml_choice - 1&lt;br /&gt;
    else:&lt;br /&gt;
        try:&lt;br /&gt;
            xml_choice = int(xml_choice_input) - 1&lt;br /&gt;
            if xml_choice &amp;lt; 0 or xml_choice &amp;gt;= len(xml_files):&lt;br /&gt;
                print(&amp;quot;Invalid choice. Using newest XML file.&amp;quot;)&lt;br /&gt;
                xml_choice = 0&lt;br /&gt;
        except ValueError:&lt;br /&gt;
            print(&amp;quot;Invalid input. Using newest XML file.&amp;quot;)&lt;br /&gt;
            xml_choice = 0&lt;br /&gt;
    &lt;br /&gt;
    selected_xml = xml_files[xml_choice]&lt;br /&gt;
    print(f&amp;quot;\nSelected XML file: {selected_xml}&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    # Check if file already exists&lt;br /&gt;
    if os.path.exists(selected_xml):&lt;br /&gt;
        overwrite = input(f&amp;quot;\n{selected_xml} already exists. Overwrite? (y/N): &amp;quot;).strip().lower()&lt;br /&gt;
        if overwrite not in [&#039;y&#039;, &#039;yes&#039;]:&lt;br /&gt;
            print(&amp;quot;Using existing file.&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            print(f&amp;quot;Downloading {selected_xml}...&amp;quot;)&lt;br /&gt;
            download_file(BASE_URL + selected_dump + &#039;/&#039; + selected_xml, selected_xml)&lt;br /&gt;
    else:&lt;br /&gt;
        print(f&amp;quot;Downloading {selected_xml}...&amp;quot;)&lt;br /&gt;
        download_file(BASE_URL + selected_dump + &#039;/&#039; + selected_xml, selected_xml)&lt;br /&gt;
    &lt;br /&gt;
    print(&amp;quot;Importing XML dump into MediaWiki...&amp;quot;)&lt;br /&gt;
    import_xml_to_mediawiki(selected_xml)&lt;br /&gt;
    print(&amp;quot;\nImport completed successfully!&amp;quot;)&lt;br /&gt;
    print(f&amp;quot;Your MediaWiki instance should now be available at: http://localhost:8080&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Contents of &amp;lt;code&amp;gt;setup_and_import.py&amp;lt;/code&amp;gt;&#039;&#039;&#039;:&lt;br /&gt;
&amp;lt;source lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
import os&lt;br /&gt;
import time&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
import subprocess&lt;br /&gt;
import sys&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_available_dumps():&lt;br /&gt;
    response = requests.get(BASE_URL)&lt;br /&gt;
    soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
    dumps = []&lt;br /&gt;
    for link in soup.find_all(&#039;a&#039;):&lt;br /&gt;
        href = link.get(&#039;href&#039;)&lt;br /&gt;
        if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, href):&lt;br /&gt;
            dumps.append(href.rstrip(&#039;/&#039;))&lt;br /&gt;
    return sorted(dumps, reverse=True)&lt;br /&gt;
&lt;br /&gt;
def get_dump_files(dump):&lt;br /&gt;
    response = requests.get(BASE_URL + dump)&lt;br /&gt;
    soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
    files = {}&lt;br /&gt;
    for link in soup.find_all(&#039;a&#039;):&lt;br /&gt;
        href = link.get(&#039;href&#039;)&lt;br /&gt;
        if href.endswith(&#039;.xml&#039;):&lt;br /&gt;
            files[href] = link.next_sibling.strip()&lt;br /&gt;
    return files&lt;br /&gt;
&lt;br /&gt;
def download_file(url, filename):&lt;br /&gt;
    response = requests.get(url)&lt;br /&gt;
    with open(filename, &#039;wb&#039;) as f:&lt;br /&gt;
        f.write(response.content)&lt;br /&gt;
&lt;br /&gt;
def run_command(command):&lt;br /&gt;
    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)&lt;br /&gt;
    output, error = process.communicate()&lt;br /&gt;
    return output.decode(), error.decode(), process.returncode&lt;br /&gt;
&lt;br /&gt;
def setup_mediawiki():&lt;br /&gt;
    print(&amp;quot;Setting up MediaWiki...&amp;quot;)&lt;br /&gt;
    os.system(&amp;quot;docker-compose up -d&amp;quot;)&lt;br /&gt;
    time.sleep(10)  # Wait for services to start&lt;br /&gt;
&lt;br /&gt;
    print(&amp;quot;Please complete the MediaWiki installation by visiting http://localhost:8080&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Use the following database settings:&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Database host: database&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Database name: my_wiki&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Database username: wikiuser&amp;quot;)&lt;br /&gt;
    print(&amp;quot;Database password: wikipass&amp;quot;)&lt;br /&gt;
    input(&amp;quot;Press Enter when you have completed the installation and downloaded LocalSettings.php...&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
    if not os.path.exists(&#039;LocalSettings.php&#039;):&lt;br /&gt;
        print(&amp;quot;LocalSettings.php not found. Please place it in the current directory.&amp;quot;)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
&lt;br /&gt;
    os.system(&amp;quot;docker-compose restart mediawiki&amp;quot;)&lt;br /&gt;
    print(&amp;quot;MediaWiki setup completed.&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
def import_xml_to_mediawiki(xml_file):&lt;br /&gt;
    command = f&amp;quot;docker-compose exec -T mediawiki php maintenance/importDump.php &amp;lt; {xml_file}&amp;quot;&lt;br /&gt;
    output, error, return_code = run_command(command)&lt;br /&gt;
    if return_code != 0:&lt;br /&gt;
        print(f&amp;quot;Error importing XML: {error}&amp;quot;)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
    print(output)&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    if not os.path.exists(&#039;LocalSettings.php&#039;):&lt;br /&gt;
        setup_mediawiki()&lt;br /&gt;
&lt;br /&gt;
    dumps = get_available_dumps()&lt;br /&gt;
    print(&amp;quot;Available dumps:&amp;quot;)&lt;br /&gt;
    for i, dump in enumerate(dumps):&lt;br /&gt;
        print(f&amp;quot;{i + 1}. {dump}&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    choice = int(input(&amp;quot;Enter the number of the dump you want to download: &amp;quot;)) - 1&lt;br /&gt;
    selected_dump = dumps[choice]&lt;br /&gt;
    &lt;br /&gt;
    files = get_dump_files(selected_dump)&lt;br /&gt;
    xml_file = next(iter(files))&lt;br /&gt;
    &lt;br /&gt;
    print(f&amp;quot;Downloading {xml_file}...&amp;quot;)&lt;br /&gt;
    download_file(BASE_URL + selected_dump + &#039;/&#039; + xml_file, xml_file)&lt;br /&gt;
    &lt;br /&gt;
    print(&amp;quot;Importing XML dump into MediaWiki...&amp;quot;)&lt;br /&gt;
    import_xml_to_mediawiki(xml_file)&lt;br /&gt;
    print(&amp;quot;Import completed successfully.&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Running the Script ===&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
python3 setup_and_import.py&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Terminal Output: when script running at this stage:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
noob@noob-elite:~/wiki$ python3 setup_and_import.py &lt;br /&gt;
Setting up MediaWiki...&lt;br /&gt;
WARN[0000] /home/noob/wiki/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion &lt;br /&gt;
[+] Running 33/2&lt;br /&gt;
 ✔ database Pulled                                                                                                                                                                      54.0s &lt;br /&gt;
 ✔ mediawiki Pulled                                                                                                                                                                     95.0s &lt;br /&gt;
[+] Running 3/3&lt;br /&gt;
 ✔ Network wiki_default        Created                                                                                                                                                   0.1s &lt;br /&gt;
 ✔ Container wiki-database-1   Started                                                                                                                                                   0.3s &lt;br /&gt;
 ✔ Container wiki-mediawiki-1  Started                                                                                                                                                   0.5s &lt;br /&gt;
Please complete the MediaWiki installation by visiting http://localhost:8080&lt;br /&gt;
Use the following database settings:&lt;br /&gt;
Database host: database&lt;br /&gt;
Database name: my_wiki&lt;br /&gt;
Database username: wikiuser&lt;br /&gt;
Database password: wikipass&lt;br /&gt;
Press Enter when you have completed the installation and downloaded LocalSettings.php...&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
=== Script Execution Steps ===&lt;br /&gt;
* &#039;&#039;&#039;Initial Setup:&#039;&#039;&#039; The script starts Docker containers and waits for MediaWiki installation.&lt;br /&gt;
* &#039;&#039;&#039;Manual Steps:&#039;&#039;&#039; &lt;br /&gt;
** Leave the script terminal running. &lt;br /&gt;
** Open a new terminal for further commands.&lt;br /&gt;
** Visit &amp;lt;code&amp;gt;http://localhost:8080&amp;lt;/code&amp;gt; in your browser to complete the MediaWiki setup using:&lt;br /&gt;
*** Database host: &amp;lt;code&amp;gt;database&amp;lt;/code&amp;gt;&lt;br /&gt;
*** Database name: &amp;lt;code&amp;gt;my_wiki&amp;lt;/code&amp;gt;&lt;br /&gt;
*** Database username: &amp;lt;code&amp;gt;wikiuser&amp;lt;/code&amp;gt;&lt;br /&gt;
*** Database password: &amp;lt;code&amp;gt;wikipass&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;Post Installation:&#039;&#039;&#039;&lt;br /&gt;
** Download &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt; from the browser. &lt;br /&gt;
** This Downloads to your &amp;lt;code&amp;gt;Downloads&amp;lt;/code&amp;gt; directory, move/copy to your &amp;lt;code&amp;gt;noobwiki&amp;lt;/code&amp;gt; directory.&lt;br /&gt;
&lt;br /&gt;
** Stop the containers:&lt;br /&gt;
*** NOTE: &amp;lt;code&amp;gt;docker-compose&amp;lt;/code&amp;gt; commands are ment to be run in direcotry containing the docker-compose.yml file Or you need to use &amp;lt;code&amp;gt;docker-compose -f /path/to/docker-compose.yml down -v&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker-compose down -v&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
** Uncomment the &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt; volume mapping in &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt;.&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
$EDITOR docker-compose.yml&amp;lt;/source&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt; After uncommenting:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  database:&lt;br /&gt;
    image: mysql:5.7&lt;br /&gt;
    environment:&lt;br /&gt;
      MYSQL_DATABASE: my_wiki&lt;br /&gt;
      MYSQL_USER: wikiuser&lt;br /&gt;
      MYSQL_PASSWORD: wikipass&lt;br /&gt;
      MYSQL_RANDOM_ROOT_PASSWORD: &#039;yes&#039;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ./mysql_data:/var/lib/mysql&lt;br /&gt;
&lt;br /&gt;
  mediawiki:&lt;br /&gt;
    image: mediawiki&lt;br /&gt;
    ports:&lt;br /&gt;
      - 8080:80&lt;br /&gt;
    links:&lt;br /&gt;
      - database&lt;br /&gt;
    volumes:&lt;br /&gt;
      - ./mediawiki_data:/var/www/html/images&lt;br /&gt;
      - ./LocalSettings.php:/var/www/html/LocalSettings.php # This line is commented out initially&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
** Restart Docker:&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker-compose up -d&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;Return to Script Terminal:&#039;&#039;&#039; &lt;br /&gt;
** Press Enter to continue with XML dump selection and import.&lt;br /&gt;
&lt;br /&gt;
===Post LocalSettings.php script===&lt;br /&gt;
&lt;br /&gt;
After returning to script and pressing Enter, the script will scan xml.completenoobs.com and give you a list of wiki dumps with a Number on the left and a date.&lt;br /&gt;
* Enter number to select a dated xml&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Press Enter when you have completed the installation and downloaded LocalSettings.php...&lt;br /&gt;
[+] Restarting 1/1&lt;br /&gt;
 ✔ Container wiki-mediawiki-1  Started                                                   1.3s &lt;br /&gt;
MediaWiki setup completed.&lt;br /&gt;
Available dumps:&lt;br /&gt;
1. 31_05_23.Noobs&lt;br /&gt;
2. 30_03_23.Noobs&lt;br /&gt;
3. 28_04_24.Noobs&lt;br /&gt;
4. 28_04_23.Noobs&lt;br /&gt;
5. 26_06_23.Noobs&lt;br /&gt;
6. 26_05_23.Noobs&lt;br /&gt;
7. 26_04_23.Noobs&lt;br /&gt;
8. 23_04_23.Noobs&lt;br /&gt;
9. 23_03_23.Noobs&lt;br /&gt;
10. 21_06_23.Noobs&lt;br /&gt;
11. 21_05_23.Noobs&lt;br /&gt;
12. 21_04_23.Noobs&lt;br /&gt;
13. 20_03_23.Noobs&lt;br /&gt;
14. 17_04_23.Noobs&lt;br /&gt;
15. 16_06_23.Noobs&lt;br /&gt;
16. 16_05_23.Noobs&lt;br /&gt;
17. 11_07_23.Noobs&lt;br /&gt;
18. 11_06_23.Noobs&lt;br /&gt;
19. 11_05_23.Noobs&lt;br /&gt;
20. 06_07_23.Noobs&lt;br /&gt;
21. 06_06_23.Noobs&lt;br /&gt;
22. 06_05_23.Noobs&lt;br /&gt;
23. 04_05_23.Noobs&lt;br /&gt;
24. 01_07_23.Noobs&lt;br /&gt;
25. 01_06_23.Noobs&lt;br /&gt;
26. 01_05_23.Noobs&lt;br /&gt;
Enter the number of the dump you want to download: 3&lt;br /&gt;
Downloading 28_04_24.Noobs.xml...&lt;br /&gt;
Importing XML dump into MediaWiki...&lt;br /&gt;
Done!&lt;br /&gt;
You might want to run rebuildrecentchanges.php to regenerate RecentChanges,&lt;br /&gt;
and initSiteStats.php to update page and revision counts&lt;br /&gt;
&lt;br /&gt;
Import completed successfully.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Post-Setup Maintenance ==&lt;br /&gt;
&lt;br /&gt;
Now, we need to rebuild our wiki so that the database reflects our imported content.&lt;br /&gt;
&lt;br /&gt;
=== Login to Container for Maintenance ===&lt;br /&gt;
* Syntax:&amp;lt;code&amp;gt;docker exec -it &amp;lt;container_name_and_service&amp;gt;-1 /bin/bash&amp;lt;/code&amp;gt;&lt;br /&gt;
** use:&amp;lt;code&amp;gt;docker container ls&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;docker ps&amp;lt;/code&amp;gt; to see container and service names.&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it noobwiki-mediawiki-1 /bin/bash&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Run maintenance scripts:&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php maintenance/initSiteStats.php&lt;br /&gt;
php maintenance/rebuildrecentchanges.php&lt;br /&gt;
php maintenance/rebuildall.php&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Exit the container:&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Verify Wiki Content ===&lt;br /&gt;
* Check all pages have loaded by visiting:&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
http://127.0.0.1:8080/index.php/Special:AllPages&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Here&#039;s the section in raw MediaWiki syntax:&lt;br /&gt;
&lt;br /&gt;
== Network Configuration ==&lt;br /&gt;
&lt;br /&gt;
By default, your wiki might only be accessible from the host machine where Docker is running, using &amp;lt;code&amp;gt;localhost&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;127.0.0.1&amp;lt;/code&amp;gt;. However, if you want others on your network to access your wiki, you need to make some adjustments.&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Current Setup:&#039;&#039;&#039; The computer running Docker Compose has an IP address of &amp;lt;code&amp;gt;192.168.0.44&amp;lt;/code&amp;gt;. &lt;br /&gt;
&lt;br /&gt;
* You can find the IP address of your computer running Docker using the command:&lt;br /&gt;
&amp;lt;code&amp;gt;ip addr&amp;lt;/code&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
OutPut from &amp;lt;code&amp;gt;ip addr&amp;lt;/code&amp;gt;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
noob@noob-HP-EliteDesk-800-G1-DM:~$ ip addr&lt;br /&gt;
1: lo: &amp;lt;LOOPBACK,UP,LOWER_UP&amp;gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000&lt;br /&gt;
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00&lt;br /&gt;
    inet 127.0.0.1/8 scope host lo&lt;br /&gt;
       valid_lft forever preferred_lft forever&lt;br /&gt;
    inet6 ::1/128 scope host noprefixroute &lt;br /&gt;
       valid_lft forever preferred_lft forever&lt;br /&gt;
2: eno1: &amp;lt;NO-CARRIER,BROADCAST,MULTICAST,UP&amp;gt; mtu 1500 qdisc fq_codel state DOWN group default qlen 1000&lt;br /&gt;
    link/ether 8c:dc:d4:3d:93:49 brd ff:ff:ff:ff:ff:ff&lt;br /&gt;
    altname enp0s25&lt;br /&gt;
3: wlxe8de27142be2: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc noqueue state UP group default qlen 1000&lt;br /&gt;
    link/ether e8:de:27:14:2b:e2 brd ff:ff:ff:ff:ff:ff&lt;br /&gt;
    inet 192.168.0.44/24 brd 192.168.0.255 scope global dynamic noprefixroute wlxe8de27142be2&lt;br /&gt;
       valid_lft 86357sec preferred_lft 86357sec&lt;br /&gt;
    inet6 fe80::afbe:cc73:73a2:fcdf/64 scope link noprefixroute &lt;br /&gt;
       valid_lft forever preferred_lft forever&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
My IP is &amp;lt;code&amp;gt;192.168.0.44&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
If someone from another computer on the network tries to visit &amp;lt;code&amp;gt;192.168.0.44:8080&amp;lt;/code&amp;gt;, they might encounter a &amp;quot;cannot connect&amp;quot; error. This happens because MediaWiki, by default, redirects to &amp;lt;code&amp;gt;127.0.0.1:8080&amp;lt;/code&amp;gt;, which is only accessible from the host machine itself. &lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Solution:&#039;&#039;&#039; To allow access from other devices on the same network, you need to:&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Edit the &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt; File:&#039;&#039;&#039; This isn&#039;t done inside the Docker container but rather in the directory where your &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt; file is located. Here, you need to change the server URL configuration.&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Modify URL Configuration:&#039;&#039;&#039; On lines 34-35 of &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt;, you&#039;ll find the following:&lt;br /&gt;
&lt;br /&gt;
=== Allowing Access from Other Network Devices ===&lt;br /&gt;
* Edit &amp;lt;code&amp;gt;LocalSettings.php&amp;lt;/code&amp;gt; in the Docker Compose directory to change the server URL:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;php&amp;quot;&amp;gt;&lt;br /&gt;
## The protocol and server name to use in fully-qualified URLs&lt;br /&gt;
$wgServer = &#039;http://192.168.0.44:8080&#039;;&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Restart Docker to apply changes:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker-compose restart&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This allows access to your wiki from other devices on the network using &amp;lt;code&amp;gt;192.168.0.44:8080&amp;lt;/code&amp;gt;.&lt;br /&gt;
This adjustment tells MediaWiki to use the network IP of the host (&amp;lt;code&amp;gt;192.168.0.44&amp;lt;/code&amp;gt;) instead of the local loopback (&amp;lt;code&amp;gt;127.0.0.1&amp;lt;/code&amp;gt;), allowing other devices on the network to access the wiki through &amp;lt;code&amp;gt;192.168.0.44:8080&amp;lt;/code&amp;gt;. Even after this change, &amp;lt;code&amp;gt;localhost:8080&amp;lt;/code&amp;gt; or &amp;lt;code&amp;gt;127.0.0.1:8080&amp;lt;/code&amp;gt; will still work on the host machine, but now the wiki is also accessible via the network IP from other devices.&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Main_Page&amp;diff=657</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Main_Page&amp;diff=657"/>
		<updated>2025-09-01T18:37:56Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* CompleteNoobs: A Downloadable Wiki for Reproducible Computer Tutorials */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
=In Concept development Mode - a wiki you can download=&lt;br /&gt;
= CompleteNoobs: A Downloadable Wiki for Reproducible Computer Tutorials =&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;CompleteNoobs&#039;&#039;&#039; is a open-source wiki offering free, reproducible computer science tutorials. &lt;br /&gt;
&lt;br /&gt;
* [[Ubuntu2404_Install_Docker_and_Docker_Compose| Download the wiki as a &#039;&#039;&#039;Docker image&#039;&#039;&#039; to run locally on your computer or fork it to contribute and share knowledge.]]&lt;br /&gt;
&lt;br /&gt;
== About CompleteNoobs ==&lt;br /&gt;
&lt;br /&gt;
Our mission is to provide accessible, libre-licensed resources for hobbyists, sysadmins, students, teachers, and computer science enthusiasts. All content is available under a &#039;&#039;&#039;Creative Commons BY-NC-SA&#039;&#039;&#039; license, ensuring the freedoms to:&lt;br /&gt;
* Read&lt;br /&gt;
* Edit/Modify&lt;br /&gt;
* Copy&lt;br /&gt;
* Share freely&lt;br /&gt;
&lt;br /&gt;
Download the wiki as an XML file at [https://xml.completenoobs.com xml.completenoobs.com] or access data-heavy content (images, videos, audio) via &#039;&#039;&#039;IPFS&#039;&#039;&#039; hashes.&lt;br /&gt;
&lt;br /&gt;
== Get Started ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Download the Wiki&#039;&#039;&#039;: Run CompleteNoobs locally using our [[Local_CompleteNoobs_Wiki|Manually install guides or Docker image]].&lt;br /&gt;
* &#039;&#039;&#039;Host Your Own&#039;&#039;&#039;: Set up your own MediaWiki instance with our guide: [[Host_Your_Own_Mediawiki_Online|Host Your Own MediaWiki Online]].&lt;br /&gt;
&lt;br /&gt;
== CompleteNoobs Blockchain Project ==&lt;br /&gt;
&lt;br /&gt;
As Noobs we want to learn more about bitcoin, without losing any real bitcoin&lt;br /&gt;
&lt;br /&gt;
First we are learning how to fork bitcoin version 0.14.3, best way to learn is by tinkering with it.&lt;br /&gt;
* [[N33Bcoin| n33b.com]]&lt;br /&gt;
&lt;br /&gt;
== Essential Links ==&lt;br /&gt;
&lt;br /&gt;
* [[Main_Index|Main Index Page]]&lt;br /&gt;
* [[Special:AllPages|All Pages]]&lt;br /&gt;
* [[Wiki_Basic_Syntax|Wiki Basic Syntax]]&lt;br /&gt;
* [[COMPLETENOOBS_FUNDING|Support Us]]&lt;br /&gt;
&lt;br /&gt;
== Licenses ==&lt;br /&gt;
&lt;br /&gt;
All content is libre-licensed for free copying, modification, and distribution. Add a license header to your contributions using:&lt;br /&gt;
&amp;lt;pre&amp;gt;{{:LICENCE_HEADER_CC0}}&amp;lt;/pre&amp;gt;&lt;br /&gt;
{{:LICENCE_HEADER_CC0}}&lt;br /&gt;
&lt;br /&gt;
== Disclaimer ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;&#039;&#039;DISCLAIMER:&#039;&#039;&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
Content on CompleteNoobs is for informational and educational purposes only. We make no warranties about accuracy or reliability. Use at your own risk. Links to external sites are not endorsements, and we are not responsible for their content or availability. The site may be temporarily unavailable due to technical issues beyond our control.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Special:ContributionScores/10/5}}&lt;br /&gt;
{{Special:PopularPages/10}}&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Main_Page&amp;diff=656</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Main_Page&amp;diff=656"/>
		<updated>2025-09-01T18:00:22Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* Get Started */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
=In Concept development Mode - a wiki you can download=&lt;br /&gt;
= CompleteNoobs: A Downloadable Wiki for Reproducible Computer Tutorials =&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;CompleteNoobs&#039;&#039;&#039; is a open-source wiki offering free, reproducible computer science tutorials. &lt;br /&gt;
&lt;br /&gt;
* [[https://www.completenoobs.com/noobs/Ubuntu2404_Install_Docker_and_Docker_Compose| Download the wiki as a &#039;&#039;&#039;Docker image&#039;&#039;&#039; to run locally on your computer or fork it to contribute and share knowledge.]]&lt;br /&gt;
&lt;br /&gt;
== About CompleteNoobs ==&lt;br /&gt;
&lt;br /&gt;
Our mission is to provide accessible, libre-licensed resources for hobbyists, sysadmins, students, teachers, and computer science enthusiasts. All content is available under a &#039;&#039;&#039;Creative Commons BY-NC-SA&#039;&#039;&#039; license, ensuring the freedoms to:&lt;br /&gt;
* Read&lt;br /&gt;
* Edit/Modify&lt;br /&gt;
* Copy&lt;br /&gt;
* Share freely&lt;br /&gt;
&lt;br /&gt;
Download the wiki as an XML file at [https://xml.completenoobs.com xml.completenoobs.com] or access data-heavy content (images, videos, audio) via &#039;&#039;&#039;IPFS&#039;&#039;&#039; hashes.&lt;br /&gt;
&lt;br /&gt;
== Get Started ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Download the Wiki&#039;&#039;&#039;: Run CompleteNoobs locally using our [[Local_CompleteNoobs_Wiki|Manually install guides or Docker image]].&lt;br /&gt;
* &#039;&#039;&#039;Host Your Own&#039;&#039;&#039;: Set up your own MediaWiki instance with our guide: [[Host_Your_Own_Mediawiki_Online|Host Your Own MediaWiki Online]].&lt;br /&gt;
&lt;br /&gt;
== CompleteNoobs Blockchain Project ==&lt;br /&gt;
&lt;br /&gt;
As Noobs we want to learn more about bitcoin, without losing any real bitcoin&lt;br /&gt;
&lt;br /&gt;
First we are learning how to fork bitcoin version 0.14.3, best way to learn is by tinkering with it.&lt;br /&gt;
* [[N33Bcoin| n33b.com]]&lt;br /&gt;
&lt;br /&gt;
== Essential Links ==&lt;br /&gt;
&lt;br /&gt;
* [[Main_Index|Main Index Page]]&lt;br /&gt;
* [[Special:AllPages|All Pages]]&lt;br /&gt;
* [[Wiki_Basic_Syntax|Wiki Basic Syntax]]&lt;br /&gt;
* [[COMPLETENOOBS_FUNDING|Support Us]]&lt;br /&gt;
&lt;br /&gt;
== Licenses ==&lt;br /&gt;
&lt;br /&gt;
All content is libre-licensed for free copying, modification, and distribution. Add a license header to your contributions using:&lt;br /&gt;
&amp;lt;pre&amp;gt;{{:LICENCE_HEADER_CC0}}&amp;lt;/pre&amp;gt;&lt;br /&gt;
{{:LICENCE_HEADER_CC0}}&lt;br /&gt;
&lt;br /&gt;
== Disclaimer ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;&#039;&#039;DISCLAIMER:&#039;&#039;&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
Content on CompleteNoobs is for informational and educational purposes only. We make no warranties about accuracy or reliability. Use at your own risk. Links to external sites are not endorsements, and we are not responsible for their content or availability. The site may be temporarily unavailable due to technical issues beyond our control.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Special:ContributionScores/10/5}}&lt;br /&gt;
{{Special:PopularPages/10}}&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Main_Page&amp;diff=655</id>
		<title>Main Page</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Main_Page&amp;diff=655"/>
		<updated>2025-09-01T17:59:32Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;&lt;br /&gt;
=In Concept development Mode - a wiki you can download=&lt;br /&gt;
= CompleteNoobs: A Downloadable Wiki for Reproducible Computer Tutorials =&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;CompleteNoobs&#039;&#039;&#039; is a open-source wiki offering free, reproducible computer science tutorials. &lt;br /&gt;
&lt;br /&gt;
* [[https://www.completenoobs.com/noobs/Ubuntu2404_Install_Docker_and_Docker_Compose| Download the wiki as a &#039;&#039;&#039;Docker image&#039;&#039;&#039; to run locally on your computer or fork it to contribute and share knowledge.]]&lt;br /&gt;
&lt;br /&gt;
== About CompleteNoobs ==&lt;br /&gt;
&lt;br /&gt;
Our mission is to provide accessible, libre-licensed resources for hobbyists, sysadmins, students, teachers, and computer science enthusiasts. All content is available under a &#039;&#039;&#039;Creative Commons BY-NC-SA&#039;&#039;&#039; license, ensuring the freedoms to:&lt;br /&gt;
* Read&lt;br /&gt;
* Edit/Modify&lt;br /&gt;
* Copy&lt;br /&gt;
* Share freely&lt;br /&gt;
&lt;br /&gt;
Download the wiki as an XML file at [https://xml.completenoobs.com xml.completenoobs.com] or access data-heavy content (images, videos, audio) via &#039;&#039;&#039;IPFS&#039;&#039;&#039; hashes.&lt;br /&gt;
&lt;br /&gt;
== Get Started ==&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Download the Wiki&#039;&#039;&#039;: Run CompleteNoobs locally using our [[/Local_CompleteNoobs_Wiki|Manually install guides or Docker image]].&lt;br /&gt;
* &#039;&#039;&#039;Host Your Own&#039;&#039;&#039;: Set up your own MediaWiki instance with our guide: [[Host_Your_Own_Mediawiki_Online|Host Your Own MediaWiki Online]].&lt;br /&gt;
&lt;br /&gt;
== CompleteNoobs Blockchain Project ==&lt;br /&gt;
&lt;br /&gt;
As Noobs we want to learn more about bitcoin, without losing any real bitcoin&lt;br /&gt;
&lt;br /&gt;
First we are learning how to fork bitcoin version 0.14.3, best way to learn is by tinkering with it.&lt;br /&gt;
* [[N33Bcoin| n33b.com]]&lt;br /&gt;
&lt;br /&gt;
== Essential Links ==&lt;br /&gt;
&lt;br /&gt;
* [[Main_Index|Main Index Page]]&lt;br /&gt;
* [[Special:AllPages|All Pages]]&lt;br /&gt;
* [[Wiki_Basic_Syntax|Wiki Basic Syntax]]&lt;br /&gt;
* [[COMPLETENOOBS_FUNDING|Support Us]]&lt;br /&gt;
&lt;br /&gt;
== Licenses ==&lt;br /&gt;
&lt;br /&gt;
All content is libre-licensed for free copying, modification, and distribution. Add a license header to your contributions using:&lt;br /&gt;
&amp;lt;pre&amp;gt;{{:LICENCE_HEADER_CC0}}&amp;lt;/pre&amp;gt;&lt;br /&gt;
{{:LICENCE_HEADER_CC0}}&lt;br /&gt;
&lt;br /&gt;
== Disclaimer ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;&#039;&#039;DISCLAIMER:&#039;&#039;&#039;&#039;&#039;&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
Content on CompleteNoobs is for informational and educational purposes only. We make no warranties about accuracy or reliability. Use at your own risk. Links to external sites are not endorsements, and we are not responsible for their content or availability. The site may be temporarily unavailable due to technical issues beyond our control.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{{Special:ContributionScores/10/5}}&lt;br /&gt;
{{Special:PopularPages/10}}&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Local_CompleteNoobs_Wiki&amp;diff=654</id>
		<title>Local CompleteNoobs Wiki</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Local_CompleteNoobs_Wiki&amp;diff=654"/>
		<updated>2025-09-01T17:55:51Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;CompleteNoobs Wiki [[Nginx_Server_For_Hosting_Files_Ubuntu_22.04|XML Dumps]] can be found at &amp;lt;nowiki&amp;gt;https://xml.completenoobs.com&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
[[Ubuntu_Local_Wiki_Import | Ubuntu Import Wiki to Local]]&lt;br /&gt;
&lt;br /&gt;
[[Windows_10_Local_Wiki_Import | Windows 10 Import Local Wiki]]&lt;br /&gt;
&lt;br /&gt;
[[FreeBSD_13.2_Jail_Local_Mediawiki_Nginx_MySQL | FreeBSD Jail]]&lt;br /&gt;
&lt;br /&gt;
[[CompleteNoobs_Local_Wiki_In_Docker | Docker ]]&lt;br /&gt;
&lt;br /&gt;
[[Ubuntu2404_Install_Docker_and_Docker_Compose| Download are Docker Image]]&lt;br /&gt;
&lt;br /&gt;
[[Docker_Mediawiki_Local_Install| vanilla mediawiki docker install]]&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Docker_Image_Sharing_-_Export_Import&amp;diff=653</id>
		<title>Docker Image Sharing - Export Import</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Docker_Image_Sharing_-_Export_Import&amp;diff=653"/>
		<updated>2025-09-01T17:41:45Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot;= Upload Your Docker Image to Docker Hub on Ubuntu 24.04 - Complete Beginner&amp;#039;s Guide =  This tutorial will walk you through uploading your first Docker image to Docker Hub, making it available for anyone to download and run. We&amp;#039;ll use the CompleteNoobs Wiki image as our example.  == What is Docker Hub? == Docker Hub is like GitHub but for Docker images. It&amp;#039;s a cloud-based registry where you can: * Store your Docker images publicly (free) or privately (paid) * Share conta...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Upload Your Docker Image to Docker Hub on Ubuntu 24.04 - Complete Beginner&#039;s Guide =&lt;br /&gt;
&lt;br /&gt;
This tutorial will walk you through uploading your first Docker image to Docker Hub, making it available for anyone to download and run. We&#039;ll use the CompleteNoobs Wiki image as our example.&lt;br /&gt;
&lt;br /&gt;
== What is Docker Hub? ==&lt;br /&gt;
Docker Hub is like GitHub but for Docker images. It&#039;s a cloud-based registry where you can:&lt;br /&gt;
* Store your Docker images publicly (free) or privately (paid)&lt;br /&gt;
* Share containers with the community&lt;br /&gt;
* Download images others have created&lt;br /&gt;
* Automate builds from GitHub repositories&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
* Ubuntu 24.04 with Docker installed&lt;br /&gt;
* A working Docker image (we&#039;ll use completenoobs/wiki:latest from the previous tutorial)&lt;br /&gt;
* Internet connection&lt;br /&gt;
* Email address for Docker Hub account&lt;br /&gt;
&lt;br /&gt;
== Step 1: Create Docker Hub Account ==&lt;br /&gt;
&lt;br /&gt;
=== 1.1: Sign Up ===&lt;br /&gt;
# Open your web browser&lt;br /&gt;
# Navigate to: https://hub.docker.com&lt;br /&gt;
# Click &#039;&#039;&#039;Sign Up&#039;&#039;&#039; button (top right)&lt;br /&gt;
# Fill in the registration form:&lt;br /&gt;
#* &#039;&#039;&#039;Docker ID&#039;&#039;&#039;: Choose a unique username (e.g., &amp;quot;yourname&amp;quot; or &amp;quot;completenoobs&amp;quot;)&lt;br /&gt;
#* &#039;&#039;&#039;Email&#039;&#039;&#039;: Your email address&lt;br /&gt;
#* &#039;&#039;&#039;Password&#039;&#039;&#039;: Strong password (mix of letters, numbers, symbols)&lt;br /&gt;
# Complete the CAPTCHA&lt;br /&gt;
# Click &#039;&#039;&#039;Sign Up&#039;&#039;&#039;&lt;br /&gt;
# Verify your email (check inbox for Docker confirmation email)&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Important&#039;&#039;&#039;: Your Docker ID becomes part of your image name (e.g., yourname/wiki:latest)&lt;br /&gt;
&lt;br /&gt;
=== 1.2: Sign In to Docker Hub ===&lt;br /&gt;
# Return to https://hub.docker.com&lt;br /&gt;
# Click &#039;&#039;&#039;Sign In&#039;&#039;&#039;&lt;br /&gt;
# Enter your Docker ID and password&lt;br /&gt;
# You&#039;ll see your Docker Hub dashboard&lt;br /&gt;
&lt;br /&gt;
== Step 2: Create Repository on Docker Hub ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1: Create New Repository ===&lt;br /&gt;
# On Docker Hub dashboard, click &#039;&#039;&#039;Create Repository&#039;&#039;&#039;&lt;br /&gt;
# Fill in repository details:&lt;br /&gt;
#* &#039;&#039;&#039;Name&#039;&#039;&#039;: &amp;lt;code&amp;gt;wiki&amp;lt;/code&amp;gt; (or &amp;lt;code&amp;gt;completenoobs-wiki&amp;lt;/code&amp;gt;)&lt;br /&gt;
#* &#039;&#039;&#039;Description&#039;&#039;&#039;: &amp;lt;code&amp;gt;CompleteNoobs Wiki - MediaWiki with pre-imported content from CompleteNoobs.com&amp;lt;/code&amp;gt;&lt;br /&gt;
#* &#039;&#039;&#039;Visibility&#039;&#039;&#039;: Select &#039;&#039;&#039;Public&#039;&#039;&#039; (free) or &#039;&#039;&#039;Private&#039;&#039;&#039; (requires subscription)&lt;br /&gt;
# Click &#039;&#039;&#039;Create&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
You now have a repository at: &amp;lt;code&amp;gt;docker.io/yourdockerid/wiki&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 3: Login to Docker Hub from Terminal ==&lt;br /&gt;
&lt;br /&gt;
=== 3.1: Open Terminal ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Open terminal on Ubuntu 24.04&lt;br /&gt;
# You can use Ctrl+Alt+T or search for &amp;quot;Terminal&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 3.2: Login to Docker Hub ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker login&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You will see:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
USING WEB-BASED LOGIN&lt;br /&gt;
&lt;br /&gt;
i Info → To sign in with credentials on the command line, use &#039;docker login -u &amp;lt;username&amp;gt;&#039;&lt;br /&gt;
         &lt;br /&gt;
&lt;br /&gt;
Your one-time device confirmation code is: MPMR-BKUH&lt;br /&gt;
Press ENTER to open your browser or submit your device code here: https://login.docker.com/activate&lt;br /&gt;
&lt;br /&gt;
Waiting for authentication in the browser…&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
go to https://login.docker.com/activate and enter the &amp;lt;b&amp;gt;confirmation code&amp;lt;/b&amp;gt;, after which you will be asked to sign into docker, after that your set.&lt;br /&gt;
&lt;br /&gt;
the terminal will change to include:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
WARNING! Your credentials are stored unencrypted in &#039;/home/noob/.docker/config.json&#039;.&lt;br /&gt;
Configure a credential helper to remove this warning. See&lt;br /&gt;
https://docs.docker.com/go/credential-store/&lt;br /&gt;
&lt;br /&gt;
Login Succeeded&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 4: Prepare Your Image ==&lt;br /&gt;
&lt;br /&gt;
=== 4.1: Check Your Local Images ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You should see:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
REPOSITORY            TAG       IMAGE ID       CREATED         SIZE&lt;br /&gt;
completenoobs/wiki    latest    abc123def456   2 hours ago     1.5GB&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.2: Tag Your Image for Docker Hub ===&lt;br /&gt;
Replace &amp;lt;code&amp;gt;wiki&amp;lt;/code&amp;gt; with your repo name&lt;br /&gt;
Replace &amp;lt;code&amp;gt;yourdockerid&amp;lt;/code&amp;gt; with your actual Docker ID:&lt;br /&gt;
* Syntax:&amp;lt;code&amp;gt;docker tag YOURDOCKERID/YOUR_REPO_NAME:VERSION&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Tag your image with your Docker Hub username&lt;br /&gt;
docker tag completenoobs/wiki:latest yourdockerid/wiki:latest&lt;br /&gt;
&lt;br /&gt;
# Also create a version tag for better practice&lt;br /&gt;
docker tag completenoobs/wiki:latest yourdockerid/wiki:1.0&lt;br /&gt;
&lt;br /&gt;
# Verify the tags&lt;br /&gt;
docker images&lt;br /&gt;
&lt;br /&gt;
docker images | grep wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You should now see multiple tags for the same image ID.&lt;br /&gt;
&lt;br /&gt;
== Step 5: Push Image to Docker Hub ==&lt;br /&gt;
&lt;br /&gt;
=== 5.1: Push the Image ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Push the latest tag&lt;br /&gt;
docker push yourdockerid/wiki:latest&lt;br /&gt;
&lt;br /&gt;
# Push the version tag&lt;br /&gt;
docker push yourdockerid/wiki:1.0&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
You&#039;ll see upload progress:&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
The push refers to repository [docker.io/yourdockerid/wiki]&lt;br /&gt;
a1b2c3d4e5f6: Pushing [==============&amp;gt;                     ]  45.3MB/150MB&lt;br /&gt;
7g8h9i0j1k2l: Pushed&lt;br /&gt;
3m4n5o6p7q8r: Pushing [========&amp;gt;                           ]  25.6MB/100MB&lt;br /&gt;
...&lt;br /&gt;
latest: digest: sha256:abc123... size: 3245&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note&#039;&#039;&#039;: First push takes longer (uploads all layers). Subsequent pushes only upload changes.&lt;br /&gt;
&lt;br /&gt;
== Step 6: Verify Upload ==&lt;br /&gt;
&lt;br /&gt;
=== 6.1: Check on Docker Hub ===&lt;br /&gt;
# Go to: https://hub.docker.com/r/yourdockerid/wiki&lt;br /&gt;
# You should see:&lt;br /&gt;
#* Your repository&lt;br /&gt;
#* Tags (latest, 1.0)&lt;br /&gt;
#* Last pushed timestamp&lt;br /&gt;
#* Image size&lt;br /&gt;
#* Pull count (starts at 0)&lt;br /&gt;
&lt;br /&gt;
=== 6.2: Update Repository Description ===&lt;br /&gt;
# Click on your repository&lt;br /&gt;
# Click &#039;&#039;&#039;Manage Repository&#039;&#039;&#039; (gear icon)&lt;br /&gt;
# Add a detailed description&lt;br /&gt;
# Add README content (supports Markdown):&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;markdown&amp;quot;&amp;gt;&lt;br /&gt;
 CompleteNoobs Wiki Docker Image&lt;br /&gt;
&lt;br /&gt;
Pre-configured MediaWiki with CompleteNoobs.com content - For Local Use.&lt;br /&gt;
May require an update after first install - as for some reason some pages missing from first xml import&lt;br /&gt;
```bash&lt;br /&gt;
docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
## Quick Start&lt;br /&gt;
```bash&lt;br /&gt;
docker run -d -p 8080:80 yourdockerid/wiki:latest&lt;br /&gt;
```&lt;br /&gt;
##Quick Start with Persistent Storage&lt;br /&gt;
```bash&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/cnoobs-wiki:latest&lt;br /&gt;
```&lt;br /&gt;
## Features&lt;br /&gt;
- MediaWiki 1.44&lt;br /&gt;
- Pre-imported CompleteNoobs content&lt;br /&gt;
- YouTube extension&lt;br /&gt;
- PageNotice extension&lt;br /&gt;
- SyntaxHighlight support&lt;br /&gt;
- XML update system&lt;br /&gt;
&lt;br /&gt;
## Default Credentials&lt;br /&gt;
- Username: admin&lt;br /&gt;
- Password: AdminPass123!&lt;br /&gt;
&lt;br /&gt;
## Volumes&lt;br /&gt;
- `/var/lib/mysql` - Database storage&lt;br /&gt;
- `/var/www/html/images` - Uploaded images&lt;br /&gt;
&lt;br /&gt;
## More Information&lt;br /&gt;
Visit: https://completenoobs.com&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 7: Test Download (Verify It Works) ==&lt;br /&gt;
&lt;br /&gt;
=== 7.1: Remove Local Images (Optional) ===&lt;br /&gt;
To truly test downloading from Docker Hub:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Stop and remove container if running&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
&lt;br /&gt;
# Remove local images&lt;br /&gt;
docker rmi yourdockerid/wiki:latest&lt;br /&gt;
docker rmi yourdockerid/wiki:1.0&lt;br /&gt;
docker rmi completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 7.2: Pull From Docker Hub ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Pull your image from Docker Hub&lt;br /&gt;
docker pull yourdockerid/wiki:latest&lt;br /&gt;
&lt;br /&gt;
# Run it&lt;br /&gt;
docker run -d -p 8080:80 --name test_wiki yourdockerid/wiki:latest&lt;br /&gt;
&lt;br /&gt;
# Check it&#039;s working&lt;br /&gt;
docker logs test_wiki&lt;br /&gt;
curl http://localhost:8080&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 8: Share With Others ==&lt;br /&gt;
&lt;br /&gt;
=== 8.1: Basic Run Command ===&lt;br /&gt;
Share this simple command for others to use your image:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 yourdockerid/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 8.2: Docker Compose File ===&lt;br /&gt;
Create a &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt; for easier deployment:&lt;br /&gt;
&lt;br /&gt;
* With version 0.1 its all in one container so these commands do not really apply - just placeholder do now - will fix for 0.2&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot;&amp;gt;&lt;br /&gt;
version: &#039;3.8&#039;&lt;br /&gt;
&lt;br /&gt;
services:&lt;br /&gt;
  wiki:&lt;br /&gt;
    image: yourdockerid/wiki:latest&lt;br /&gt;
    container_name: completenoobs_wiki&lt;br /&gt;
    ports:&lt;br /&gt;
      - &amp;quot;8080:80&amp;quot;&lt;br /&gt;
    volumes:&lt;br /&gt;
      - mysql_data:/var/lib/mysql&lt;br /&gt;
      - wiki_images:/var/www/html/images&lt;br /&gt;
    restart: unless-stopped&lt;br /&gt;
    environment:&lt;br /&gt;
      - TZ=America/New_York  # Adjust timezone&lt;br /&gt;
&lt;br /&gt;
volumes:&lt;br /&gt;
  mysql_data:&lt;br /&gt;
  wiki_images:&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 9: Best Practices ==&lt;br /&gt;
&lt;br /&gt;
=== 9.1: Versioning ===&lt;br /&gt;
Always use version tags alongside &#039;latest&#039;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Bad (only latest)&lt;br /&gt;
docker push yourdockerid/wiki:latest&lt;br /&gt;
&lt;br /&gt;
# Good (version + latest)&lt;br /&gt;
docker push yourdockerid/wiki:1.0&lt;br /&gt;
docker push yourdockerid/wiki:latest&lt;br /&gt;
&lt;br /&gt;
# When updating&lt;br /&gt;
docker tag yourdockerid/wiki:latest yourdockerid/wiki:1.1&lt;br /&gt;
docker push yourdockerid/wiki:1.1&lt;br /&gt;
docker push yourdockerid/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 9.2: Image Size Optimization ===&lt;br /&gt;
Before pushing, consider reducing image size:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Check image size&lt;br /&gt;
docker images yourdockerid/wiki&lt;br /&gt;
&lt;br /&gt;
# Remove unnecessary files in Dockerfile&lt;br /&gt;
# Use multi-stage builds if possible&lt;br /&gt;
# Clean package manager cache&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 9.3: Security ===&lt;br /&gt;
* Never include passwords in image (use environment variables)&lt;br /&gt;
* Regularly update base images&lt;br /&gt;
* Scan for vulnerabilities:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Docker Scout (built-in to Docker Desktop)&lt;br /&gt;
docker scout cves yourdockerid/wiki:latest&lt;br /&gt;
&lt;br /&gt;
# Or use Trivy&lt;br /&gt;
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \&lt;br /&gt;
  aquasec/trivy image yourdockerid/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 10: Maintain Your Image ==&lt;br /&gt;
&lt;br /&gt;
=== 10.1: Update Your Image ===&lt;br /&gt;
When you need to update:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# 1. Make changes to Dockerfile or scripts&lt;br /&gt;
cd ~/completenoobs-docker-image&lt;br /&gt;
&lt;br /&gt;
# 2. Rebuild with new version&lt;br /&gt;
docker build -t yourdockerid/wiki:1.1 .&lt;br /&gt;
&lt;br /&gt;
# 3. Test locally&lt;br /&gt;
docker run -d -p 8080:80 --name test yourdockerid/wiki:1.1&lt;br /&gt;
# ... test everything works ...&lt;br /&gt;
docker stop test &amp;amp;&amp;amp; docker rm test&lt;br /&gt;
&lt;br /&gt;
# 4. Tag as latest&lt;br /&gt;
docker tag yourdockerid/wiki:1.1 yourdockerid/wiki:latest&lt;br /&gt;
&lt;br /&gt;
# 5. Push both tags&lt;br /&gt;
docker push yourdockerid/wiki:1.1&lt;br /&gt;
docker push yourdockerid/wiki:latest&lt;br /&gt;
&lt;br /&gt;
# 6. Update Docker Hub description with changelog&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 10.2: Monitor Usage ===&lt;br /&gt;
Check Docker Hub regularly for:&lt;br /&gt;
* Pull count (how many times downloaded)&lt;br /&gt;
* User issues (check if you&#039;ve linked a GitHub repo)&lt;br /&gt;
* Security scanning results&lt;br /&gt;
&lt;br /&gt;
= Backup Docker Image Locally =&lt;br /&gt;
&lt;br /&gt;
To back up a Docker image locally as a .tar file, which can then be stored on an external drive or transferred to another server, follow these steps.&lt;br /&gt;
&lt;br /&gt;
== Steps ==&lt;br /&gt;
&lt;br /&gt;
=== Save the Docker Image to a .tar File ===&lt;br /&gt;
Use the &amp;lt;code&amp;gt;docker save&amp;lt;/code&amp;gt; command to export the image to a tarball:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker save -o /path/to/backup/my-image.tar my-image:latest&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;-o&amp;lt;/code&amp;gt;: Specifies the output file path (e.g., &amp;lt;code&amp;gt;/path/to/backup/my-image.tar&amp;lt;/code&amp;gt;).&lt;br /&gt;
* &amp;lt;code&amp;gt;my-image:latest&amp;lt;/code&amp;gt;: The name and tag of the image to back up.&lt;br /&gt;
&lt;br /&gt;
Example:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker save -o /home/user/backups/my-image.tar my-image:latest&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Verify the Tarball ===&lt;br /&gt;
Ensure the .tar file was created:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
ls -lh /path/to/backup/my-image.tar&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Store on an External Drive ===&lt;br /&gt;
To store the image on an external drive (e.g., mounted at &amp;lt;code&amp;gt;/mnt/external&amp;lt;/code&amp;gt;):&lt;br /&gt;
&lt;br /&gt;
# Copy the .tar file to the external drive:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
cp /path/to/backup/my-image.tar /mnt/external/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Verify the file on the external drive:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
ls /mnt/external/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
# Safely unmount the external drive after copying:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
sudo umount /mnt/external&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
= Send Docker Image to Another Server =&lt;br /&gt;
&lt;br /&gt;
To transfer the saved Docker image to another server, use tools like &amp;lt;code&amp;gt;scp&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;rsync&amp;lt;/code&amp;gt;, or &amp;lt;code&amp;gt;sftp&amp;lt;/code&amp;gt; to securely copy the .tar file.&lt;br /&gt;
&lt;br /&gt;
== Steps ==&lt;br /&gt;
&lt;br /&gt;
=== Save the Docker Image ===&lt;br /&gt;
Follow the same &amp;lt;code&amp;gt;docker save&amp;lt;/code&amp;gt; step as above to create the .tar file:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker save -o /path/to/backup/my-image.tar my-image:latest&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Transfer the Image to Another Server ===&lt;br /&gt;
Use &amp;lt;code&amp;gt;scp&amp;lt;/code&amp;gt; to copy the .tar file to the remote server:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
scp /path/to/backup/my-image.tar user@remote-server:/path/to/destination/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Replace &amp;lt;code&amp;gt;user&amp;lt;/code&amp;gt; with the remote server&#039;s username.&lt;br /&gt;
* Replace &amp;lt;code&amp;gt;remote-server&amp;lt;/code&amp;gt; with the server&#039;s IP address or hostname.&lt;br /&gt;
* Replace &amp;lt;code&amp;gt;/path/to/destination/&amp;lt;/code&amp;gt; with the destination path on the remote server.&lt;br /&gt;
&lt;br /&gt;
Example:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
scp /home/user/backups/my-image.tar admin@192.168.1.100:/home/admin/backups/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Load the Image on the Remote Server ===&lt;br /&gt;
On the remote server, use &amp;lt;code&amp;gt;docker load&amp;lt;/code&amp;gt; to import the .tar file into Docker:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker load -i /path/to/destination/my-image.tar&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Example:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker load -i /home/admin/backups/my-image.tar&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Verify the image is loaded:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker images&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
= Additional Tips =&lt;br /&gt;
&lt;br /&gt;
== Compress the Image (Optional) ==&lt;br /&gt;
To save space, compress the .tar file before storing or transferring:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
gzip /path/to/backup/my-image.tar&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This creates &amp;lt;code&amp;gt;my-image.tar.gz&amp;lt;/code&amp;gt;. To decompress on the destination:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
gunzip /path/to/backup/my-image.tar.gz&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Use rsync for Large Files ==&lt;br /&gt;
For large .tar files, &amp;lt;code&amp;gt;rsync&amp;lt;/code&amp;gt; can be more reliable than &amp;lt;code&amp;gt;scp&amp;lt;/code&amp;gt;:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
rsync -avz /path/to/backup/my-image.tar user@remote-server:/path/to/destination/&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Automate Backups ==&lt;br /&gt;
Create a script (e.g., &amp;lt;code&amp;gt;backup_docker.sh&amp;lt;/code&amp;gt;) to automate backups:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
IMAGE_NAME=&amp;quot;my-image:latest&amp;quot;&lt;br /&gt;
BACKUP_PATH=&amp;quot;/home/user/backups&amp;quot;&lt;br /&gt;
TIMESTAMP=$(date +%F_%H-%M-%S)&lt;br /&gt;
docker save -o &amp;quot;$BACKUP_PATH/my-image-$TIMESTAMP.tar&amp;quot; $IMAGE_NAME&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Make it executable:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
chmod +x backup_docker.sh&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Schedule it with cron:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
crontab -e&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Add a line to run daily at midnight:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
0 0 * * * /path/to/backup_docker.sh&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Check Disk Space ==&lt;br /&gt;
Before saving or copying, ensure there&#039;s enough disk space:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
df -h /path/to/backup&lt;br /&gt;
df -h /mnt/external&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
= Restoring the Image Locally =&lt;br /&gt;
&lt;br /&gt;
To restore the image from an external drive or a local .tar file:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker load -i /path/to/backup/my-image.tar&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Verify:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker images&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
== Common Issues and Solutions ==&lt;br /&gt;
&lt;br /&gt;
=== Issue: &amp;quot;denied: requested access to the resource is denied&amp;quot; ===&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: You&#039;re not logged in or using wrong username&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logout&lt;br /&gt;
docker login&lt;br /&gt;
# Make sure to use YOUR Docker ID in the tag&lt;br /&gt;
docker tag completenoobs/wiki:latest YOURDOCKERID/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Issue: &amp;quot;name unknown: repository name must be lowercase&amp;quot; ===&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Docker Hub requires lowercase names&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Wrong&lt;br /&gt;
docker tag image:latest YourName/Wiki:latest&lt;br /&gt;
&lt;br /&gt;
# Correct&lt;br /&gt;
docker tag image:latest yourname/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Issue: Push takes forever ===&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Large images take time on first push&lt;br /&gt;
* Be patient (can take 30+ minutes for large images)&lt;br /&gt;
* Ensure stable internet connection&lt;br /&gt;
* Consider optimizing image size&lt;br /&gt;
* Use &amp;lt;code&amp;gt;.dockerignore&amp;lt;/code&amp;gt; file to exclude unnecessary files&lt;br /&gt;
&lt;br /&gt;
=== Issue: &amp;quot;filesystem layer verification failed&amp;quot; ===&lt;br /&gt;
&#039;&#039;&#039;Solution&#039;&#039;&#039;: Corrupted layer, rebuild and repush&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build --no-cache -t yourdockerid/wiki:latest .&lt;br /&gt;
docker push yourdockerid/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Issue: Error response from daemon: Get &amp;quot;https://registry-1.docker.io/v2/completenoobs/cnoobs-wiki/manifests/sha256:d6cc&amp;quot;: net/http: TLS handshake timeout===&lt;br /&gt;
&lt;br /&gt;
If then pulling a container &amp;lt;code&amp;gt;docker pull completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; you see this error:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
Error response from daemon: Get &amp;quot;https://registry-1.docker.io/v2/completenoobs/cnoobs-wiki/manifests/sha256:d6cc65ee8f9716986c6b406a9c037b18ab3cf39326b21b42c6ad0d84c80693f4&amp;quot;: net/http: TLS handshake timeout&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Then restart docker &lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
sudo systemctl restart docker&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Example Commands Summary ==&lt;br /&gt;
&lt;br /&gt;
For quick reference, here are all the essential commands:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Login to Docker Hub&lt;br /&gt;
docker login&lt;br /&gt;
&lt;br /&gt;
# Tag your image&lt;br /&gt;
docker tag completenoobs/wiki:latest yourdockerid/wiki:latest&lt;br /&gt;
docker tag completenoobs/wiki:latest yourdockerid/wiki:1.0&lt;br /&gt;
&lt;br /&gt;
# Push to Docker Hub&lt;br /&gt;
docker push yourdockerid/wiki:latest&lt;br /&gt;
docker push yourdockerid/wiki:1.0&lt;br /&gt;
&lt;br /&gt;
# Others can now pull and run&lt;br /&gt;
docker pull yourdockerid/wiki:latest&lt;br /&gt;
docker run -d -p 8080:80 yourdockerid/wiki:latest&lt;br /&gt;
&lt;br /&gt;
# Logout when done&lt;br /&gt;
docker logout&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=652</id>
		<title>CompleteNoobs Docker Image Creation</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=652"/>
		<updated>2025-09-01T17:39:54Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* Complete Noobs Docker Wiki Tutorial */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Complete Noobs Docker Wiki Tutorial =&lt;br /&gt;
* [[Docker_Install_Guide| Docker install guide]]&lt;br /&gt;
==errors==&lt;br /&gt;
* This mainly works - just need to fix the extensions popular pages and contrubtion scores&lt;br /&gt;
* The XML updater requires more work - currently idea placeholder&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
* Ubuntu 24.04&lt;br /&gt;
* Docker installed and running&lt;br /&gt;
* Your user in docker group: &amp;lt;code&amp;gt;sudo usermod -aG docker $USER&amp;lt;/code&amp;gt; (then logout/login)&lt;br /&gt;
&lt;br /&gt;
== Step 1: Clean Start ==&lt;br /&gt;
Remove any previous attempts:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Stop and remove existing containers/images&lt;br /&gt;
docker stop completenoobs_wiki 2&amp;gt;/dev/null || true&lt;br /&gt;
docker rm completenoobs_wiki 2&amp;gt;/dev/null || true&lt;br /&gt;
docker rmi completenoobs/wiki:latest 2&amp;gt;/dev/null || true&lt;br /&gt;
&lt;br /&gt;
# Remove old build directory&lt;br /&gt;
cd ~&lt;br /&gt;
rm -rf completenoobs-docker-image&lt;br /&gt;
&lt;br /&gt;
# Create fresh directory&lt;br /&gt;
mkdir completenoobs-docker-image&lt;br /&gt;
cd completenoobs-docker-image&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 2: Create All Files ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1: Dockerfile ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Copy this exactly:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM mediawiki:1.44&lt;br /&gt;
# Mediawiki 1.44 used over latest because can confirm extensions youtube and pagenotice works&lt;br /&gt;
# Install dependencies&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \&lt;br /&gt;
    mariadb-server \&lt;br /&gt;
    python3 \&lt;br /&gt;
    python3-requests \&lt;br /&gt;
    python3-bs4 \&lt;br /&gt;
    python3-pygments \&lt;br /&gt;
    curl \&lt;br /&gt;
    wget \&lt;br /&gt;
    unzip \&lt;br /&gt;
    nano \&lt;br /&gt;
    git \&lt;br /&gt;
    &amp;amp;&amp;amp; apt-get clean&lt;br /&gt;
&lt;br /&gt;
# Copy scripts&lt;br /&gt;
COPY download_latest_xml.py /usr/src/download_latest_xml.py&lt;br /&gt;
COPY setup_wiki.sh /usr/src/setup_wiki.sh&lt;br /&gt;
COPY update_xml.sh /usr/src/update_xml.sh&lt;br /&gt;
COPY entrypoint.sh /entrypoint.sh&lt;br /&gt;
&lt;br /&gt;
# Make executable&lt;br /&gt;
RUN chmod +x /usr/src/setup_wiki.sh /entrypoint.sh /usr/src/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Download XML&lt;br /&gt;
RUN python3 /usr/src/download_latest_xml.py&lt;br /&gt;
&lt;br /&gt;
# Setup wiki&lt;br /&gt;
RUN /usr/src/setup_wiki.sh&lt;br /&gt;
&lt;br /&gt;
EXPOSE 80&lt;br /&gt;
VOLUME /var/lib/mysql&lt;br /&gt;
VOLUME /var/www/html/images&lt;br /&gt;
&lt;br /&gt;
ENTRYPOINT [&amp;quot;/entrypoint.sh&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.2: XML Download Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano download_latest_xml.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_available_dumps():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        return sorted(dumps, key=parse_date_from_dump, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dumps: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def get_dump_files(dump):&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(f&amp;quot;{BASE_URL}{dump}/&amp;quot;, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
        return sorted(files, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dump files: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def download_file(url, filename):&lt;br /&gt;
    try:&lt;br /&gt;
        print(f&amp;quot;Downloading {filename}...&amp;quot;)&lt;br /&gt;
        response = requests.get(url, stream=True, timeout=60)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        &lt;br /&gt;
        total_size = int(response.headers.get(&#039;content-length&#039;, 0))&lt;br /&gt;
        downloaded = 0&lt;br /&gt;
        &lt;br /&gt;
        with open(filename, &#039;wb&#039;) as f:&lt;br /&gt;
            for chunk in response.iter_content(chunk_size=8192):&lt;br /&gt;
                if chunk:&lt;br /&gt;
                    f.write(chunk)&lt;br /&gt;
                    downloaded += len(chunk)&lt;br /&gt;
                    if total_size &amp;gt; 0:&lt;br /&gt;
                        progress = (downloaded / total_size) * 100&lt;br /&gt;
                        print(f&amp;quot;\rProgress: {progress:.1f}%&amp;quot;, end=&#039;&#039;, flush=True)&lt;br /&gt;
        print()&lt;br /&gt;
        return True&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error downloading {filename}: {e}&amp;quot;)&lt;br /&gt;
        return False&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    print(&amp;quot;Fetching available XML dumps...&amp;quot;)&lt;br /&gt;
    dumps = get_available_dumps()&lt;br /&gt;
    &lt;br /&gt;
    if not dumps:&lt;br /&gt;
        print(&amp;quot;No dumps found!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_dump = dumps[0]&lt;br /&gt;
    print(f&amp;quot;Latest dump: {newest_dump}&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    files = get_dump_files(newest_dump)&lt;br /&gt;
    if not files:&lt;br /&gt;
        print(&amp;quot;No XML files found in latest dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_xml = files[0]&lt;br /&gt;
    xml_url = f&amp;quot;{BASE_URL}{newest_dump}/{newest_xml}&amp;quot;&lt;br /&gt;
    local_filename = &amp;quot;/tmp/completenoobs_dump.xml&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if download_file(xml_url, local_filename):&lt;br /&gt;
        print(f&amp;quot;Successfully downloaded {newest_xml}&amp;quot;)&lt;br /&gt;
        with open(&amp;quot;/tmp/dump_info.txt&amp;quot;, &amp;quot;w&amp;quot;) as f:&lt;br /&gt;
            f.write(f&amp;quot;{newest_dump}/{newest_xml}&amp;quot;)&lt;br /&gt;
    else:&lt;br /&gt;
        print(&amp;quot;Failed to download XML dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.3: Main Setup Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano setup_wiki.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Setting up CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Initialize MariaDB&lt;br /&gt;
if [ ! -d &amp;quot;/var/lib/mysql/mysql&amp;quot; ]; then&lt;br /&gt;
    mysql_install_db --user=mysql --datadir=/var/lib/mysql&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 2&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
# Setup database&lt;br /&gt;
mysql -e &amp;quot;CREATE DATABASE IF NOT EXISTS completenoobs_wiki CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;127.0.0.1&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;127.0.0.1&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;localhost&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;localhost&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;FLUSH PRIVILEGES;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Install MediaWiki&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
php maintenance/install.php \&lt;br /&gt;
    --dbtype=mysql \&lt;br /&gt;
    --dbserver=127.0.0.1 \&lt;br /&gt;
    --dbname=completenoobs_wiki \&lt;br /&gt;
    --dbuser=wikiuser \&lt;br /&gt;
    --dbpass=wikipass \&lt;br /&gt;
    --server=&amp;quot;http://localhost:8080&amp;quot; \&lt;br /&gt;
    --scriptpath=&amp;quot;&amp;quot; \&lt;br /&gt;
    --lang=en \&lt;br /&gt;
    --pass=AdminPass123! \&lt;br /&gt;
    &amp;quot;CompleteNoobs Wiki&amp;quot; \&lt;br /&gt;
    &amp;quot;admin&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Download and install extensions&lt;br /&gt;
cd extensions/&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/PageNotice --branch REL1_44 || echo &amp;quot;PageNotice download failed, continuing...&amp;quot;&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/YouTube --branch REL1_44 || echo &amp;quot;YouTube download failed, continuing...&amp;quot;&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
&lt;br /&gt;
# Configure LocalSettings.php&lt;br /&gt;
cat &amp;gt;&amp;gt; LocalSettings.php &amp;lt;&amp;lt; &#039;EOF&#039;&lt;br /&gt;
# Basic settings&lt;br /&gt;
$wgEnableUploads = true;&lt;br /&gt;
$wgUseImageMagick = true;&lt;br /&gt;
$wgImageMagickConvertCommand = &amp;quot;/usr/bin/convert&amp;quot;;&lt;br /&gt;
$wgDefaultSkin = &amp;quot;vector-2022&amp;quot;;&lt;br /&gt;
$wgAllowExternalImages = true;&lt;br /&gt;
&lt;br /&gt;
# Debug (can be removed later)&lt;br /&gt;
$wgShowExceptionDetails = true;&lt;br /&gt;
$wgDebugLogFile = &amp;quot;/tmp/mediawiki-debug.log&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
# PageNotice extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/PageNotice/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;PageNotice&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# YouTube extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/YouTube/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;YouTube&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# SyntaxHighlight (usually bundled)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/SyntaxHighlight_GeSHi/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;SyntaxHighlight_GeSHi&#039; );&lt;br /&gt;
    $wgPygmentizePath = &#039;/usr/bin/pygmentize&#039;;&lt;br /&gt;
}&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
# Import XML dump if available&lt;br /&gt;
if [ -f &amp;quot;/tmp/completenoobs_dump.xml&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Importing XML dump...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if php maintenance/importDump.php --uploads &amp;lt; /tmp/completenoobs_dump.xml; then&lt;br /&gt;
        echo &amp;quot;XML import completed!&amp;quot;&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;XML import had warnings&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Basic maintenance&lt;br /&gt;
    php maintenance/update.php --quick || echo &amp;quot;Update completed with warnings&amp;quot;&lt;br /&gt;
    php maintenance/rebuildrecentchanges.php || echo &amp;quot;RecentChanges rebuilt with warnings&amp;quot;&lt;br /&gt;
    php maintenance/initSiteStats.php || echo &amp;quot;SiteStats initialized with warnings&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if [ -f &amp;quot;/tmp/dump_info.txt&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Import: $(cat /tmp/dump_info.txt)&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
        echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
    fi&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No XML dump found - starting with empty wiki&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
# Copy update script to accessible location&lt;br /&gt;
cp /usr/src/update_xml.sh /var/www/html/update_xml.sh&lt;br /&gt;
chmod +x /var/www/html/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Create user-friendly update wrapper&lt;br /&gt;
cat &amp;gt; /var/www/html/check_updates.sh &amp;lt;&amp;lt; &#039;UPDATE_WRAPPER_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki Update Checker ===&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;This tool checks for new content from the CompleteNoobs XML repository&amp;quot;&lt;br /&gt;
echo &amp;quot;and imports ONLY new pages, preserving all your local edits.&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
/var/www/html/update_xml.sh&lt;br /&gt;
UPDATE_WRAPPER_EOF&lt;br /&gt;
chmod +x /var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# Create simple status script&lt;br /&gt;
cat &amp;gt; /var/www/html/check_status.sh &amp;lt;&amp;lt; &#039;STATUS_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
echo &amp;quot;=== Wiki Status ===&amp;quot;&lt;br /&gt;
echo &amp;quot;Pages: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;Users: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM user WHERE user_id &amp;gt; 0;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Extensions ===&amp;quot;&lt;br /&gt;
if [ -d &amp;quot;extensions/PageNotice&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;PageNotice: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;PageNotice: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/YouTube&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;YouTube: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;YouTube: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/ContributionScores&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;ContributionScores: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;ContributionScores: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Update System ===&amp;quot;&lt;br /&gt;
if [ -f &amp;quot;.last_import&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Current version: $(grep &#039;Import:&#039; .last_import | cut -d&#039; &#039; -f2)&amp;quot;&lt;br /&gt;
    echo &amp;quot;Import date: $(grep &#039;Date:&#039; .last_import | cut -d&#039; &#039; -f2-)&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No version info available&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
STATUS_EOF&lt;br /&gt;
chmod +x /var/www/html/check_status.sh&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Setup completed!&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;Update scripts installed:&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/check_updates.sh (user-friendly)&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/update_xml.sh (direct)&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Final counts&lt;br /&gt;
PAGES=$(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;0&amp;quot;)&lt;br /&gt;
echo &amp;quot;Pages imported: $PAGES&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb stop&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.4: XML Update Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano update_xml.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki XML Updater ===&amp;quot;&lt;br /&gt;
echo &amp;quot;This will check for new and updated pages&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Function to check if running in container&lt;br /&gt;
check_environment() {&lt;br /&gt;
    if [ ! -f &amp;quot;/var/www/html/LocalSettings.php&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Error: This script must be run inside the wiki container&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to get current XML version&lt;br /&gt;
get_current_version() {&lt;br /&gt;
    if [ -f &amp;quot;/var/www/html/.last_import&amp;quot; ]; then&lt;br /&gt;
        grep &amp;quot;Import:&amp;quot; /var/www/html/.last_import | cut -d&#039; &#039; -f2&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;none&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to check for updates&lt;br /&gt;
check_for_updates() {&lt;br /&gt;
    python3 - &amp;lt;&amp;lt; &#039;PYTHON_EOF&#039;&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_latest_dump():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        if dumps:&lt;br /&gt;
            latest = sorted(dumps, key=parse_date_from_dump, reverse=True)[0]&lt;br /&gt;
            &lt;br /&gt;
            # Get XML files from latest dump&lt;br /&gt;
            response = requests.get(f&amp;quot;{BASE_URL}{latest}/&amp;quot;, timeout=30)&lt;br /&gt;
            response.raise_for_status()&lt;br /&gt;
            soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
            files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                    if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
            &lt;br /&gt;
            if files:&lt;br /&gt;
                newest_xml = sorted(files, reverse=True)[0]&lt;br /&gt;
                print(f&amp;quot;{latest}/{newest_xml}&amp;quot;)&lt;br /&gt;
                sys.exit(0)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;ERROR: {e}&amp;quot;, file=sys.stderr)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
    &lt;br /&gt;
    print(&amp;quot;ERROR: No dumps found&amp;quot;, file=sys.stderr)&lt;br /&gt;
    sys.exit(1)&lt;br /&gt;
&lt;br /&gt;
get_latest_dump()&lt;br /&gt;
PYTHON_EOF&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to download new XML&lt;br /&gt;
download_xml() {&lt;br /&gt;
    local dump_info=&amp;quot;$1&amp;quot;&lt;br /&gt;
    local dump_dir=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f1)&lt;br /&gt;
    local xml_file=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f2)&lt;br /&gt;
    local url=&amp;quot;https://xml.completenoobs.com/xmlDumps/${dump_info}&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Downloading: $xml_file&amp;quot;&lt;br /&gt;
    echo &amp;quot;From: $dump_dir&amp;quot;&lt;br /&gt;
    echo &amp;quot;URL: $url&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if wget -O /tmp/new_dump.xml &amp;quot;$url&amp;quot; --progress=bar:force 2&amp;gt;&amp;amp;1; then&lt;br /&gt;
        echo &amp;quot;Download successful!&amp;quot;&lt;br /&gt;
        return 0&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;Download failed!&amp;quot;&lt;br /&gt;
        return 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to backup current database&lt;br /&gt;
backup_database() {&lt;br /&gt;
    echo &amp;quot;Creating database backup...&amp;quot;&lt;br /&gt;
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)&lt;br /&gt;
    mysqldump --user=wikiuser --password=wikipass completenoobs_wiki &amp;gt; /tmp/wiki_backup_${TIMESTAMP}.sql&lt;br /&gt;
    echo &amp;quot;Backup created: /tmp/wiki_backup_${TIMESTAMP}.sql&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to analyze and import changes&lt;br /&gt;
analyze_and_import() {&lt;br /&gt;
    echo &amp;quot;Analyzing differences between XML and local wiki...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Create analysis and import script for MediaWiki 1.44+&lt;br /&gt;
    cat &amp;gt; /tmp/analyze_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class AnalyzeAndImport extends Maintenance {&lt;br /&gt;
    private $db;&lt;br /&gt;
    private $new_pages = [];&lt;br /&gt;
    private $changed_pages = [];&lt;br /&gt;
    private $unchanged_pages = [];&lt;br /&gt;
    &lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addDescription(&#039;Analyze and selectively import from XML dump&#039;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $this-&amp;gt;db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // MediaWiki 1.35+ uses slots and content tables&lt;br /&gt;
        // Get existing pages with their content&lt;br /&gt;
        $query = &amp;quot;&lt;br /&gt;
            SELECT p.page_title, p.page_id, c.content_address, c.content_sha1&lt;br /&gt;
            FROM page p&lt;br /&gt;
            JOIN revision r ON p.page_latest = r.rev_id&lt;br /&gt;
            JOIN slots s ON r.rev_id = s.slot_revision_id&lt;br /&gt;
            JOIN slot_roles sr ON s.slot_role_id = sr.role_id&lt;br /&gt;
            JOIN content c ON s.slot_content_id = c.content_id&lt;br /&gt;
            WHERE p.page_namespace = 0 AND sr.role_name = &#039;main&#039;&lt;br /&gt;
        &amp;quot;;&lt;br /&gt;
        &lt;br /&gt;
        $result = $this-&amp;gt;db-&amp;gt;query($query);&lt;br /&gt;
        if (!$result) {&lt;br /&gt;
            $this-&amp;gt;error(&amp;quot;Database query failed: &amp;quot; . $this-&amp;gt;db-&amp;gt;error);&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $existing = [];&lt;br /&gt;
        while ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
            // Get actual text content&lt;br /&gt;
            $text_content = $this-&amp;gt;getTextContent($row[&#039;content_address&#039;]);&lt;br /&gt;
            $existing[$row[&#039;page_title&#039;]] = [&lt;br /&gt;
                &#039;id&#039; =&amp;gt; $row[&#039;page_id&#039;],&lt;br /&gt;
                &#039;content&#039; =&amp;gt; $text_content,&lt;br /&gt;
                &#039;sha1&#039; =&amp;gt; $row[&#039;content_sha1&#039;]&lt;br /&gt;
            ];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Parse XML and compare&lt;br /&gt;
        $xml = simplexml_load_file(&#039;/tmp/new_dump.xml&#039;);&lt;br /&gt;
        &lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            $title = str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title);&lt;br /&gt;
            $xml_content = (string)$page-&amp;gt;revision-&amp;gt;text;&lt;br /&gt;
            &lt;br /&gt;
            if (!isset($existing[$title])) {&lt;br /&gt;
                // New page&lt;br /&gt;
                $this-&amp;gt;new_pages[$title] = $xml_content;&lt;br /&gt;
            } else {&lt;br /&gt;
                // Compare content using SHA1 for efficiency&lt;br /&gt;
                $xml_sha1 = sha1($xml_content);&lt;br /&gt;
                &lt;br /&gt;
                if ($existing[$title][&#039;sha1&#039;] !== $xml_sha1) {&lt;br /&gt;
                    // Content is different&lt;br /&gt;
                    $this-&amp;gt;changed_pages[$title] = [&lt;br /&gt;
                        &#039;local&#039; =&amp;gt; $existing[$title][&#039;content&#039;],&lt;br /&gt;
                        &#039;xml&#039; =&amp;gt; $xml_content,&lt;br /&gt;
                        &#039;page_id&#039; =&amp;gt; $existing[$title][&#039;id&#039;]&lt;br /&gt;
                    ];&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;unchanged_pages[] = $title;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Display summary&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Update Analysis ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages to import: &amp;quot; . count($this-&amp;gt;new_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Changed pages found: &amp;quot; . count($this-&amp;gt;changed_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Unchanged pages: &amp;quot; . count($this-&amp;gt;unchanged_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Save analysis for review&lt;br /&gt;
        file_put_contents(&#039;/tmp/update_analysis.json&#039;, json_encode([&lt;br /&gt;
            &#039;new&#039; =&amp;gt; array_keys($this-&amp;gt;new_pages),&lt;br /&gt;
            &#039;changed&#039; =&amp;gt; array_keys($this-&amp;gt;changed_pages),&lt;br /&gt;
            &#039;unchanged&#039; =&amp;gt; $this-&amp;gt;unchanged_pages&lt;br /&gt;
        ], JSON_PRETTY_PRINT));&lt;br /&gt;
        &lt;br /&gt;
        // Show changed pages with preview (limit to first 20 for readability)&lt;br /&gt;
        if (count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Changed Pages ===\n&amp;quot;);&lt;br /&gt;
            $count = 0;&lt;br /&gt;
            $total_changed = count($this-&amp;gt;changed_pages);&lt;br /&gt;
            &lt;br /&gt;
            foreach ($this-&amp;gt;changed_pages as $title =&amp;gt; $data) {&lt;br /&gt;
                $count++;&lt;br /&gt;
                if ($count &amp;lt;= 20) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;\n$count. $title\n&amp;quot;);&lt;br /&gt;
                    &lt;br /&gt;
                    // Create a simple diff preview (first 300 chars)&lt;br /&gt;
                    $xml_preview = substr($data[&#039;xml&#039;], 0, 100);&lt;br /&gt;
                    &lt;br /&gt;
                    // Save full diff to file&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;=== FULL DIFF FOR: $title ===\n\n&amp;quot;);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- LOCAL VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;local&#039;] . &amp;quot;\n\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- XML VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;xml&#039;], FILE_APPEND);&lt;br /&gt;
                    &lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;   Preview: &amp;quot; . $xml_preview . &amp;quot;...\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ($total_changed &amp;gt; 20) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\n... and &amp;quot; . ($total_changed - 20) . &amp;quot; more changed pages.\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;All diff files saved to /tmp/diff_*.txt\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Interactive selection&lt;br /&gt;
        if (count($this-&amp;gt;new_pages) &amp;gt; 0 || count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Import Options ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;1. Import new pages only (preserve all local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;2. Import new pages + update ALL changed pages (overwrites local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;3. Selective import (choose which updates to apply)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;4. Cancel (no changes)\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            // Save state for import script WITHOUT the XML object&lt;br /&gt;
            $import_data = [&lt;br /&gt;
                &#039;new_pages&#039; =&amp;gt; $this-&amp;gt;new_pages,&lt;br /&gt;
                &#039;changed_pages&#039; =&amp;gt; $this-&amp;gt;changed_pages,&lt;br /&gt;
                &#039;xml_file&#039; =&amp;gt; &#039;/tmp/new_dump.xml&#039;  // Save path instead of object&lt;br /&gt;
            ];&lt;br /&gt;
            &lt;br /&gt;
            file_put_contents(&#039;/tmp/import_data.ser&#039;, serialize($import_data));&lt;br /&gt;
        } else {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\nNo changes detected. Your wiki is up to date!\n&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function getTextContent($address) {&lt;br /&gt;
        // Handle different content storage formats in MW 1.35+&lt;br /&gt;
        if (strpos($address, &#039;tt:&#039;) === 0) {&lt;br /&gt;
            // Text table reference&lt;br /&gt;
            $text_id = substr($address, 3);&lt;br /&gt;
            $result = $this-&amp;gt;db-&amp;gt;query(&amp;quot;SELECT old_text FROM text WHERE old_id = $text_id&amp;quot;);&lt;br /&gt;
            if ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
                return $row[&#039;old_text&#039;];&lt;br /&gt;
            }&lt;br /&gt;
        } elseif (strpos($address, &#039;es:&#039;) === 0) {&lt;br /&gt;
            // External storage - would need special handling&lt;br /&gt;
            return &amp;quot;[External storage content]&amp;quot;;&lt;br /&gt;
        }&lt;br /&gt;
        // Direct content&lt;br /&gt;
        return $address;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = AnalyzeAndImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/analyze_import.php&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to perform selected import&lt;br /&gt;
perform_import() {&lt;br /&gt;
    local choice=$1&lt;br /&gt;
    &lt;br /&gt;
    cat &amp;gt; /tmp/do_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class DoImport extends Maintenance {&lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addOption(&#039;mode&#039;, &#039;Import mode&#039;, true, true);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $mode = $this-&amp;gt;getOption(&#039;mode&#039;);&lt;br /&gt;
        $data = unserialize(file_get_contents(&#039;/tmp/import_data.ser&#039;));&lt;br /&gt;
        &lt;br /&gt;
        // Load XML file&lt;br /&gt;
        $xml = simplexml_load_file($data[&#039;xml_file&#039;]);&lt;br /&gt;
        &lt;br /&gt;
        $new_imported = 0;&lt;br /&gt;
        $updated = 0;&lt;br /&gt;
        &lt;br /&gt;
        // Import new pages (always for modes 1-3)&lt;br /&gt;
        if ($mode != &#039;4&#039;) {&lt;br /&gt;
            foreach ($data[&#039;new_pages&#039;] as $title =&amp;gt; $content) {&lt;br /&gt;
                $this-&amp;gt;importPage($title, $content, $xml);&lt;br /&gt;
                $new_imported++;&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Imported new page: $title\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Handle changed pages based on mode&lt;br /&gt;
        if ($mode == &#039;2&#039;) {&lt;br /&gt;
            // Update all changed pages&lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Updating: $title\n&amp;quot;);&lt;br /&gt;
                if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                    $updated++;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Updated page: $title\n&amp;quot;);&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        } elseif ($mode == &#039;3&#039;) {&lt;br /&gt;
            // Selective update&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Selective Import Mode ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;For each changed page, choose:\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  y = yes, update this page\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  n = no, keep local version\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  d = show diff file\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  a = update all remaining pages\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  s = skip all remaining pages\n\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            $update_all = false;&lt;br /&gt;
            $skip_all = false;&lt;br /&gt;
            &lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                if ($skip_all) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($update_all) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\nPage: $title\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Action (y/n/d/a/s): &amp;quot;);&lt;br /&gt;
                $handle = fopen(&amp;quot;php://stdin&amp;quot;, &amp;quot;r&amp;quot;);&lt;br /&gt;
                $line = trim(fgets($handle));&lt;br /&gt;
                &lt;br /&gt;
                while ($line == &#039;d&#039;) {&lt;br /&gt;
                    // Show diff&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    if (file_exists($diff_file)) {&lt;br /&gt;
                        system(&amp;quot;head -50 $diff_file&amp;quot;);&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;\n[Showing first 50 lines - full file at $diff_file]\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Action (y/n/a/s): &amp;quot;);&lt;br /&gt;
                    $line = trim(fgets($handle));&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($line == &#039;a&#039;) {&lt;br /&gt;
                    $update_all = true;&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } elseif ($line == &#039;s&#039;) {&lt;br /&gt;
                    $skip_all = true;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                } elseif ($line == &#039;y&#039;) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Import Complete ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages imported: $new_imported\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Pages updated: $updated\n&amp;quot;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function importPage($title, $content, $xml) {&lt;br /&gt;
        // Create single page XML for import&lt;br /&gt;
        $tempFile = &#039;/tmp/single_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
        $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
        &lt;br /&gt;
        if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
            $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
            foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Find and add the page&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                break;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
        exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;);&lt;br /&gt;
        unlink($tempFile);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function reimportPage($title, $xml) {&lt;br /&gt;
        // For updating existing pages, delete then reimport&lt;br /&gt;
        $db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Delete the existing page&lt;br /&gt;
        $safe_title = $db-&amp;gt;real_escape_string(str_replace(&#039; &#039;, &#039;_&#039;, $title));&lt;br /&gt;
        $db-&amp;gt;query(&amp;quot;DELETE FROM page WHERE page_title = &#039;$safe_title&#039; AND page_namespace = 0&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        $db-&amp;gt;close();&lt;br /&gt;
        &lt;br /&gt;
        // Now import the new version&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $tempFile = &#039;/tmp/update_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
                $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
                &lt;br /&gt;
                if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
                    $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
                    foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                        $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
                $result = exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;, $output, $return);&lt;br /&gt;
                unlink($tempFile);&lt;br /&gt;
                &lt;br /&gt;
                return ($return === 0);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = DoImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/do_import.php --mode=&amp;quot;$choice&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Main execution&lt;br /&gt;
main() {&lt;br /&gt;
    check_environment&lt;br /&gt;
    &lt;br /&gt;
    # Start MariaDB if not running&lt;br /&gt;
    service mariadb status &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 || service mariadb start&lt;br /&gt;
    &lt;br /&gt;
    # Wait for MariaDB&lt;br /&gt;
    for i in {1..30}; do&lt;br /&gt;
        if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
            break&lt;br /&gt;
        fi&lt;br /&gt;
        sleep 1&lt;br /&gt;
    done&lt;br /&gt;
    &lt;br /&gt;
    CURRENT=$(get_current_version)&lt;br /&gt;
    echo &amp;quot;Current version: $CURRENT&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Checking for updates...&amp;quot;&lt;br /&gt;
    LATEST=$(check_for_updates 2&amp;gt;/dev/null)&lt;br /&gt;
    if [ $? -ne 0 ] || [ -z &amp;quot;$LATEST&amp;quot; ] || [[ &amp;quot;$LATEST&amp;quot; == *&amp;quot;ERROR&amp;quot;* ]]; then&lt;br /&gt;
        echo &amp;quot;Failed to check for updates&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Latest available: $LATEST&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Always proceed to analysis even if versions match&lt;br /&gt;
    # (there might be content updates in the same version)&lt;br /&gt;
    echo &amp;quot;Proceeding with content analysis...&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Backup database&lt;br /&gt;
    backup_database&lt;br /&gt;
    &lt;br /&gt;
    # Download new XML&lt;br /&gt;
    if ! download_xml &amp;quot;$LATEST&amp;quot;; then&lt;br /&gt;
        echo &amp;quot;Failed to download new XML&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Analyze differences&lt;br /&gt;
    analyze_and_import&lt;br /&gt;
    &lt;br /&gt;
    # Check if there are changes to import&lt;br /&gt;
    if [ -f &amp;quot;/tmp/import_data.ser&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;&amp;quot;&lt;br /&gt;
        read -p &amp;quot;Choose option (1-4): &amp;quot; -n 1 -r&lt;br /&gt;
        echo&lt;br /&gt;
        &lt;br /&gt;
        if [[ $REPLY =~ ^[1-4]$ ]]; then&lt;br /&gt;
            if [ &amp;quot;$REPLY&amp;quot; != &amp;quot;4&amp;quot; ]; then&lt;br /&gt;
                perform_import &amp;quot;$REPLY&amp;quot;&lt;br /&gt;
                &lt;br /&gt;
                # Update version info&lt;br /&gt;
                echo &amp;quot;Import: $LATEST&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
                echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
                &lt;br /&gt;
                # Rebuild indices&lt;br /&gt;
                echo &amp;quot;Rebuilding indices...&amp;quot;&lt;br /&gt;
                php maintenance/rebuildrecentchanges.php&lt;br /&gt;
                php maintenance/initSiteStats.php&lt;br /&gt;
            else&lt;br /&gt;
                echo &amp;quot;Update cancelled&amp;quot;&lt;br /&gt;
            fi&lt;br /&gt;
        else&lt;br /&gt;
            echo &amp;quot;Invalid option. Update cancelled&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Clean up temp files&lt;br /&gt;
    rm -f /tmp/import_data.ser /tmp/update_analysis.json /tmp/diff_*.txt /tmp/analyze_import.php /tmp/do_import.php 2&amp;gt;/dev/null&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    echo &amp;quot;Done!&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Run main function&lt;br /&gt;
main &amp;quot;$@&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.5: Entrypoint Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano entrypoint.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Starting CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 1&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;CompleteNoobs Wiki ready at: http://localhost:8080&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin login: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Features:&amp;quot;&lt;br /&gt;
echo &amp;quot;- Complete wiki content imported from XML&amp;quot;&lt;br /&gt;
echo &amp;quot;- License notices on all pages (via PageNotice)&amp;quot;&lt;br /&gt;
echo &amp;quot;- SyntaxHighlight for code blocks&amp;quot;&lt;br /&gt;
echo &amp;quot;- YouTube video embedding&amp;quot;&lt;br /&gt;
echo &amp;quot;- Contribution Scores special page&amp;quot;&lt;br /&gt;
echo &amp;quot;- XML update system (preserves local edits)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
apache2-foreground&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 3: Build and Run ==&lt;br /&gt;
&lt;br /&gt;
=== 3.1: Build the Image ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t completenoobs/wiki:latest .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will take several minutes.&lt;br /&gt;
&lt;br /&gt;
=== 3.2: Run the Container ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 4: Test Everything ==&lt;br /&gt;
&lt;br /&gt;
=== 4.1: Check the Wiki ===&lt;br /&gt;
* Visit: http://localhost:8080&lt;br /&gt;
* You should see the PageNotice at the top&lt;br /&gt;
* Login with: admin / AdminPass123!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Change Admin Password:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By default, the admin user&#039;s password is &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;. It&#039;s highly recommended to change this immediately. You can do this either through the wiki&#039;s web interface or directly in the Docker terminal.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 1: Change Password via Web Interface&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
This is the easiest method. You can change your password directly from the wiki itself.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Log in to your wiki with the default credentials: &amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
2. Once logged in, click your username (&amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt;) in the top-right corner of the page.&amp;lt;br&amp;gt;&lt;br /&gt;
3. From the drop-down menu, select &amp;lt;b&amp;gt;Preferences&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
4. On the Preferences page, go to the &amp;lt;b&amp;gt;Password&amp;lt;/b&amp;gt; tab.&amp;lt;br&amp;gt;&lt;br /&gt;
5. Enter the current password (&amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;), then enter your new password twice.&amp;lt;br&amp;gt;&lt;br /&gt;
6. Click &amp;lt;b&amp;gt;Change password&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Your password is now changed, and you will need to use the new one for future logins.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 2: Change Password via Terminal (No-Email Reset)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you have forgotten the password or prefer to use the command line, you can reset it directly inside the Docker container using a MediaWiki maintenance script.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;b&amp;gt;Access the container&#039;s shell&amp;lt;/b&amp;gt; with the following command from your host machine:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Once inside the container, use the &amp;lt;code&amp;gt;changePassword.php&amp;lt;/code&amp;gt; maintenance script to change the password. This is the modern, recommended way to run MediaWiki maintenance scripts.&amp;lt;br&amp;gt;&lt;br /&gt;
* Change &amp;lt;b&amp;gt;NEWPASSWORD&amp;lt;/b&amp;gt; to your new password&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt; to leave the container&#039;s shell.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The admin password has now been reset. You can log in to your wiki with the new password.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.2: Test Extensions ===&lt;br /&gt;
* &#039;&#039;&#039;YouTube&#039;&#039;&#039;: Edit any page, add &amp;lt;code&amp;gt;&amp;lt;youtube&amp;gt;N9qYF9DZPdw&amp;lt;/youtube&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;PageNotice&#039;&#039;&#039;: Should already be visible at the top&lt;br /&gt;
* &#039;&#039;&#039;SyntaxHighlight&#039;&#039;&#039;: Add code blocks with &amp;lt;nowiki&amp;gt;&amp;lt;code&amp;gt;&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;code here&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.3: Check Status ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/check_status.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 5: XML Update Operations ==&lt;br /&gt;
*NOTE: update_xml.sh still needs alot of work, this just idea placeholder for now.&lt;br /&gt;
=== 5.1: Check for Updates (Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will:&lt;br /&gt;
* Check the CompleteNoobs XML repository for new dumps&lt;br /&gt;
* Compare with your current version&lt;br /&gt;
* Ask for confirmation before updating&lt;br /&gt;
* Import ONLY new pages (preserves your edits)&lt;br /&gt;
* Create a backup before making changes&lt;br /&gt;
&lt;br /&gt;
=== 5.2: Force Update (Non-Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki bash -c &amp;quot;echo &#039;y&#039; | /var/www/html/update_xml.sh&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5.3: Manual Update Process ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# 1. Enter container&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&lt;br /&gt;
# 2. Check current version&lt;br /&gt;
cat /var/www/html/.last_import&lt;br /&gt;
&lt;br /&gt;
# 3. Run update&lt;br /&gt;
/var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# 4. Exit container&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 6: Troubleshooting Commands ==&lt;br /&gt;
=== Access container shell: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Edit configuration: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki nano /var/www/html/LocalSettings.php&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Check logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== View update logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki tail -f /tmp/mediawiki-debug.log&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Complete restart: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Expected Results ==&lt;br /&gt;
* Working wiki with imported CompleteNoobs content&lt;br /&gt;
* PageNotice visible at top of all pages&lt;br /&gt;
* All extensions functional&lt;br /&gt;
* Text editors (nano/vim) available in container&lt;br /&gt;
* Utility scripts for maintenance&lt;br /&gt;
* XML update system that preserves local edits&lt;br /&gt;
&lt;br /&gt;
== Update System Features ==&lt;br /&gt;
&lt;br /&gt;
=== How Updates Work ===&lt;br /&gt;
# Version Tracking: System tracks which XML dump version you have&lt;br /&gt;
# Smart Import: Only imports pages that don&#039;t exist locally&lt;br /&gt;
# Edit Preservation: Never overwrites pages you&#039;ve edited&lt;br /&gt;
# Automatic Backup: Creates SQL backup before any updates&lt;br /&gt;
# User Confirmation: Asks before making changes&lt;br /&gt;
# Progress Feedback: Shows what&#039;s being imported/skipped&lt;br /&gt;
&lt;br /&gt;
== Important Notes ==&lt;br /&gt;
* First Import: Initial build imports ALL content from XML&lt;br /&gt;
* Subsequent Updates: Only import NEW pages, preserving your edits&lt;br /&gt;
* Conflict Resolution: To force-update a specific page with XML version, delete it first through wiki interface&lt;br /&gt;
* Backups: Stored in /tmp/ (cleared on container restart)&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=651</id>
		<title>CompleteNoobs Docker Image Creation</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=CompleteNoobs_Docker_Image_Creation&amp;diff=651"/>
		<updated>2025-09-01T17:36:41Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: Created page with &amp;quot;= Complete Noobs Docker Wiki Tutorial =  ==errors== * This mainly works - just need to fix the extensions popular pages and contrubtion scores * The XML updater requires more work - currently idea placeholder  == Prerequisites == * Ubuntu 24.04 * Docker installed and running * Your user in docker group: &amp;lt;code&amp;gt;sudo usermod -aG docker $USER&amp;lt;/code&amp;gt; (then logout/login)  == Step 1: Clean Start == Remove any previous attempts:  &amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt; # Stop and remove e...&amp;quot;&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Complete Noobs Docker Wiki Tutorial =&lt;br /&gt;
&lt;br /&gt;
==errors==&lt;br /&gt;
* This mainly works - just need to fix the extensions popular pages and contrubtion scores&lt;br /&gt;
* The XML updater requires more work - currently idea placeholder&lt;br /&gt;
&lt;br /&gt;
== Prerequisites ==&lt;br /&gt;
* Ubuntu 24.04&lt;br /&gt;
* Docker installed and running&lt;br /&gt;
* Your user in docker group: &amp;lt;code&amp;gt;sudo usermod -aG docker $USER&amp;lt;/code&amp;gt; (then logout/login)&lt;br /&gt;
&lt;br /&gt;
== Step 1: Clean Start ==&lt;br /&gt;
Remove any previous attempts:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# Stop and remove existing containers/images&lt;br /&gt;
docker stop completenoobs_wiki 2&amp;gt;/dev/null || true&lt;br /&gt;
docker rm completenoobs_wiki 2&amp;gt;/dev/null || true&lt;br /&gt;
docker rmi completenoobs/wiki:latest 2&amp;gt;/dev/null || true&lt;br /&gt;
&lt;br /&gt;
# Remove old build directory&lt;br /&gt;
cd ~&lt;br /&gt;
rm -rf completenoobs-docker-image&lt;br /&gt;
&lt;br /&gt;
# Create fresh directory&lt;br /&gt;
mkdir completenoobs-docker-image&lt;br /&gt;
cd completenoobs-docker-image&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 2: Create All Files ==&lt;br /&gt;
&lt;br /&gt;
=== 2.1: Dockerfile ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano Dockerfile&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Copy this exactly:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;dockerfile&amp;quot;&amp;gt;&lt;br /&gt;
FROM mediawiki:1.44&lt;br /&gt;
# Mediawiki 1.44 used over latest because can confirm extensions youtube and pagenotice works&lt;br /&gt;
# Install dependencies&lt;br /&gt;
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \&lt;br /&gt;
    mariadb-server \&lt;br /&gt;
    python3 \&lt;br /&gt;
    python3-requests \&lt;br /&gt;
    python3-bs4 \&lt;br /&gt;
    python3-pygments \&lt;br /&gt;
    curl \&lt;br /&gt;
    wget \&lt;br /&gt;
    unzip \&lt;br /&gt;
    nano \&lt;br /&gt;
    git \&lt;br /&gt;
    &amp;amp;&amp;amp; apt-get clean&lt;br /&gt;
&lt;br /&gt;
# Copy scripts&lt;br /&gt;
COPY download_latest_xml.py /usr/src/download_latest_xml.py&lt;br /&gt;
COPY setup_wiki.sh /usr/src/setup_wiki.sh&lt;br /&gt;
COPY update_xml.sh /usr/src/update_xml.sh&lt;br /&gt;
COPY entrypoint.sh /entrypoint.sh&lt;br /&gt;
&lt;br /&gt;
# Make executable&lt;br /&gt;
RUN chmod +x /usr/src/setup_wiki.sh /entrypoint.sh /usr/src/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Download XML&lt;br /&gt;
RUN python3 /usr/src/download_latest_xml.py&lt;br /&gt;
&lt;br /&gt;
# Setup wiki&lt;br /&gt;
RUN /usr/src/setup_wiki.sh&lt;br /&gt;
&lt;br /&gt;
EXPOSE 80&lt;br /&gt;
VOLUME /var/lib/mysql&lt;br /&gt;
VOLUME /var/www/html/images&lt;br /&gt;
&lt;br /&gt;
ENTRYPOINT [&amp;quot;/entrypoint.sh&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.2: XML Download Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano download_latest_xml.py&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
#!/usr/bin/env python3&lt;br /&gt;
import os&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_available_dumps():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        return sorted(dumps, key=parse_date_from_dump, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dumps: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def get_dump_files(dump):&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(f&amp;quot;{BASE_URL}{dump}/&amp;quot;, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
        return sorted(files, reverse=True)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error fetching dump files: {e}&amp;quot;)&lt;br /&gt;
        return []&lt;br /&gt;
&lt;br /&gt;
def download_file(url, filename):&lt;br /&gt;
    try:&lt;br /&gt;
        print(f&amp;quot;Downloading {filename}...&amp;quot;)&lt;br /&gt;
        response = requests.get(url, stream=True, timeout=60)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        &lt;br /&gt;
        total_size = int(response.headers.get(&#039;content-length&#039;, 0))&lt;br /&gt;
        downloaded = 0&lt;br /&gt;
        &lt;br /&gt;
        with open(filename, &#039;wb&#039;) as f:&lt;br /&gt;
            for chunk in response.iter_content(chunk_size=8192):&lt;br /&gt;
                if chunk:&lt;br /&gt;
                    f.write(chunk)&lt;br /&gt;
                    downloaded += len(chunk)&lt;br /&gt;
                    if total_size &amp;gt; 0:&lt;br /&gt;
                        progress = (downloaded / total_size) * 100&lt;br /&gt;
                        print(f&amp;quot;\rProgress: {progress:.1f}%&amp;quot;, end=&#039;&#039;, flush=True)&lt;br /&gt;
        print()&lt;br /&gt;
        return True&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;Error downloading {filename}: {e}&amp;quot;)&lt;br /&gt;
        return False&lt;br /&gt;
&lt;br /&gt;
def main():&lt;br /&gt;
    print(&amp;quot;Fetching available XML dumps...&amp;quot;)&lt;br /&gt;
    dumps = get_available_dumps()&lt;br /&gt;
    &lt;br /&gt;
    if not dumps:&lt;br /&gt;
        print(&amp;quot;No dumps found!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_dump = dumps[0]&lt;br /&gt;
    print(f&amp;quot;Latest dump: {newest_dump}&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    files = get_dump_files(newest_dump)&lt;br /&gt;
    if not files:&lt;br /&gt;
        print(&amp;quot;No XML files found in latest dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
    &lt;br /&gt;
    newest_xml = files[0]&lt;br /&gt;
    xml_url = f&amp;quot;{BASE_URL}{newest_dump}/{newest_xml}&amp;quot;&lt;br /&gt;
    local_filename = &amp;quot;/tmp/completenoobs_dump.xml&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if download_file(xml_url, local_filename):&lt;br /&gt;
        print(f&amp;quot;Successfully downloaded {newest_xml}&amp;quot;)&lt;br /&gt;
        with open(&amp;quot;/tmp/dump_info.txt&amp;quot;, &amp;quot;w&amp;quot;) as f:&lt;br /&gt;
            f.write(f&amp;quot;{newest_dump}/{newest_xml}&amp;quot;)&lt;br /&gt;
    else:&lt;br /&gt;
        print(&amp;quot;Failed to download XML dump!&amp;quot;)&lt;br /&gt;
        exit(1)&lt;br /&gt;
&lt;br /&gt;
if __name__ == &amp;quot;__main__&amp;quot;:&lt;br /&gt;
    main()&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.3: Main Setup Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano setup_wiki.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Setting up CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Initialize MariaDB&lt;br /&gt;
if [ ! -d &amp;quot;/var/lib/mysql/mysql&amp;quot; ]; then&lt;br /&gt;
    mysql_install_db --user=mysql --datadir=/var/lib/mysql&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 2&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
# Setup database&lt;br /&gt;
mysql -e &amp;quot;CREATE DATABASE IF NOT EXISTS completenoobs_wiki CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;127.0.0.1&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;127.0.0.1&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;CREATE USER IF NOT EXISTS &#039;wikiuser&#039;@&#039;localhost&#039; IDENTIFIED BY &#039;wikipass&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;GRANT ALL PRIVILEGES ON completenoobs_wiki.* TO &#039;wikiuser&#039;@&#039;localhost&#039;;&amp;quot;&lt;br /&gt;
mysql -e &amp;quot;FLUSH PRIVILEGES;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Install MediaWiki&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
php maintenance/install.php \&lt;br /&gt;
    --dbtype=mysql \&lt;br /&gt;
    --dbserver=127.0.0.1 \&lt;br /&gt;
    --dbname=completenoobs_wiki \&lt;br /&gt;
    --dbuser=wikiuser \&lt;br /&gt;
    --dbpass=wikipass \&lt;br /&gt;
    --server=&amp;quot;http://localhost:8080&amp;quot; \&lt;br /&gt;
    --scriptpath=&amp;quot;&amp;quot; \&lt;br /&gt;
    --lang=en \&lt;br /&gt;
    --pass=AdminPass123! \&lt;br /&gt;
    &amp;quot;CompleteNoobs Wiki&amp;quot; \&lt;br /&gt;
    &amp;quot;admin&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Download and install extensions&lt;br /&gt;
cd extensions/&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/PageNotice --branch REL1_44 || echo &amp;quot;PageNotice download failed, continuing...&amp;quot;&lt;br /&gt;
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/YouTube --branch REL1_44 || echo &amp;quot;YouTube download failed, continuing...&amp;quot;&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
&lt;br /&gt;
# Configure LocalSettings.php&lt;br /&gt;
cat &amp;gt;&amp;gt; LocalSettings.php &amp;lt;&amp;lt; &#039;EOF&#039;&lt;br /&gt;
# Basic settings&lt;br /&gt;
$wgEnableUploads = true;&lt;br /&gt;
$wgUseImageMagick = true;&lt;br /&gt;
$wgImageMagickConvertCommand = &amp;quot;/usr/bin/convert&amp;quot;;&lt;br /&gt;
$wgDefaultSkin = &amp;quot;vector-2022&amp;quot;;&lt;br /&gt;
$wgAllowExternalImages = true;&lt;br /&gt;
&lt;br /&gt;
# Debug (can be removed later)&lt;br /&gt;
$wgShowExceptionDetails = true;&lt;br /&gt;
$wgDebugLogFile = &amp;quot;/tmp/mediawiki-debug.log&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
# PageNotice extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/PageNotice/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;PageNotice&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# YouTube extension (if available)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/YouTube/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;YouTube&#039; );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# SyntaxHighlight (usually bundled)&lt;br /&gt;
if ( file_exists( &amp;quot;$IP/extensions/SyntaxHighlight_GeSHi/extension.json&amp;quot; ) ) {&lt;br /&gt;
    wfLoadExtension( &#039;SyntaxHighlight_GeSHi&#039; );&lt;br /&gt;
    $wgPygmentizePath = &#039;/usr/bin/pygmentize&#039;;&lt;br /&gt;
}&lt;br /&gt;
EOF&lt;br /&gt;
&lt;br /&gt;
# Import XML dump if available&lt;br /&gt;
if [ -f &amp;quot;/tmp/completenoobs_dump.xml&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Importing XML dump...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if php maintenance/importDump.php --uploads &amp;lt; /tmp/completenoobs_dump.xml; then&lt;br /&gt;
        echo &amp;quot;XML import completed!&amp;quot;&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;XML import had warnings&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Basic maintenance&lt;br /&gt;
    php maintenance/update.php --quick || echo &amp;quot;Update completed with warnings&amp;quot;&lt;br /&gt;
    php maintenance/rebuildrecentchanges.php || echo &amp;quot;RecentChanges rebuilt with warnings&amp;quot;&lt;br /&gt;
    php maintenance/initSiteStats.php || echo &amp;quot;SiteStats initialized with warnings&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if [ -f &amp;quot;/tmp/dump_info.txt&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Import: $(cat /tmp/dump_info.txt)&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
        echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
    fi&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No XML dump found - starting with empty wiki&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
&lt;br /&gt;
# Copy update script to accessible location&lt;br /&gt;
cp /usr/src/update_xml.sh /var/www/html/update_xml.sh&lt;br /&gt;
chmod +x /var/www/html/update_xml.sh&lt;br /&gt;
&lt;br /&gt;
# Create user-friendly update wrapper&lt;br /&gt;
cat &amp;gt; /var/www/html/check_updates.sh &amp;lt;&amp;lt; &#039;UPDATE_WRAPPER_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki Update Checker ===&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;This tool checks for new content from the CompleteNoobs XML repository&amp;quot;&lt;br /&gt;
echo &amp;quot;and imports ONLY new pages, preserving all your local edits.&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
/var/www/html/update_xml.sh&lt;br /&gt;
UPDATE_WRAPPER_EOF&lt;br /&gt;
chmod +x /var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# Create simple status script&lt;br /&gt;
cat &amp;gt; /var/www/html/check_status.sh &amp;lt;&amp;lt; &#039;STATUS_EOF&#039;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
cd /var/www/html&lt;br /&gt;
echo &amp;quot;=== Wiki Status ===&amp;quot;&lt;br /&gt;
echo &amp;quot;Pages: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;Users: $(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM user WHERE user_id &amp;gt; 0;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;Error&amp;quot;)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Extensions ===&amp;quot;&lt;br /&gt;
if [ -d &amp;quot;extensions/PageNotice&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;PageNotice: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;PageNotice: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/YouTube&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;YouTube: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;YouTube: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
if [ -d &amp;quot;extensions/ContributionScores&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;ContributionScores: Installed&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;ContributionScores: Not installed&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;=== Update System ===&amp;quot;&lt;br /&gt;
if [ -f &amp;quot;.last_import&amp;quot; ]; then&lt;br /&gt;
    echo &amp;quot;Current version: $(grep &#039;Import:&#039; .last_import | cut -d&#039; &#039; -f2)&amp;quot;&lt;br /&gt;
    echo &amp;quot;Import date: $(grep &#039;Date:&#039; .last_import | cut -d&#039; &#039; -f2-)&amp;quot;&lt;br /&gt;
else&lt;br /&gt;
    echo &amp;quot;No version info available&amp;quot;&lt;br /&gt;
fi&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
STATUS_EOF&lt;br /&gt;
chmod +x /var/www/html/check_status.sh&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Setup completed!&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;Update scripts installed:&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/check_updates.sh (user-friendly)&amp;quot;&lt;br /&gt;
echo &amp;quot;- /var/www/html/update_xml.sh (direct)&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Final counts&lt;br /&gt;
PAGES=$(mysql --user=wikiuser --password=wikipass completenoobs_wiki -e &amp;quot;SELECT COUNT(*) FROM page;&amp;quot; -s -N 2&amp;gt;/dev/null || echo &amp;quot;0&amp;quot;)&lt;br /&gt;
echo &amp;quot;Pages imported: $PAGES&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb stop&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.4: XML Update Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano update_xml.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
set -e&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;=== CompleteNoobs Wiki XML Updater ===&amp;quot;&lt;br /&gt;
echo &amp;quot;This will check for new and updated pages&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Function to check if running in container&lt;br /&gt;
check_environment() {&lt;br /&gt;
    if [ ! -f &amp;quot;/var/www/html/LocalSettings.php&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;Error: This script must be run inside the wiki container&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to get current XML version&lt;br /&gt;
get_current_version() {&lt;br /&gt;
    if [ -f &amp;quot;/var/www/html/.last_import&amp;quot; ]; then&lt;br /&gt;
        grep &amp;quot;Import:&amp;quot; /var/www/html/.last_import | cut -d&#039; &#039; -f2&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;none&amp;quot;&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to check for updates&lt;br /&gt;
check_for_updates() {&lt;br /&gt;
    python3 - &amp;lt;&amp;lt; &#039;PYTHON_EOF&#039;&lt;br /&gt;
import requests&lt;br /&gt;
from bs4 import BeautifulSoup&lt;br /&gt;
import re&lt;br /&gt;
import sys&lt;br /&gt;
&lt;br /&gt;
BASE_URL = &amp;quot;https://xml.completenoobs.com/xmlDumps/&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def parse_date_from_dump(dump_name):&lt;br /&gt;
    match = re.match(r&#039;(\d{2})_(\d{2})_(\d{2})\.Noobs&#039;, dump_name)&lt;br /&gt;
    if match:&lt;br /&gt;
        day, month, year = match.groups()&lt;br /&gt;
        year_int = int(year)&lt;br /&gt;
        full_year = 2000 + year_int if year_int &amp;lt;= 49 else 1900 + year_int&lt;br /&gt;
        return (full_year, int(month), int(day))&lt;br /&gt;
    return (0, 0, 0)&lt;br /&gt;
&lt;br /&gt;
def get_latest_dump():&lt;br /&gt;
    try:&lt;br /&gt;
        response = requests.get(BASE_URL, timeout=30)&lt;br /&gt;
        response.raise_for_status()&lt;br /&gt;
        soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
        dumps = [link.get(&#039;href&#039;).rstrip(&#039;/&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                if re.match(r&#039;\d{2}_\d{2}_\d{2}\.Noobs/$&#039;, link.get(&#039;href&#039;, &#039;&#039;))]&lt;br /&gt;
        if dumps:&lt;br /&gt;
            latest = sorted(dumps, key=parse_date_from_dump, reverse=True)[0]&lt;br /&gt;
            &lt;br /&gt;
            # Get XML files from latest dump&lt;br /&gt;
            response = requests.get(f&amp;quot;{BASE_URL}{latest}/&amp;quot;, timeout=30)&lt;br /&gt;
            response.raise_for_status()&lt;br /&gt;
            soup = BeautifulSoup(response.text, &#039;html.parser&#039;)&lt;br /&gt;
            files = [link.get(&#039;href&#039;) for link in soup.find_all(&#039;a&#039;)&lt;br /&gt;
                    if link.get(&#039;href&#039;, &#039;&#039;).endswith(&#039;.xml&#039;)]&lt;br /&gt;
            &lt;br /&gt;
            if files:&lt;br /&gt;
                newest_xml = sorted(files, reverse=True)[0]&lt;br /&gt;
                print(f&amp;quot;{latest}/{newest_xml}&amp;quot;)&lt;br /&gt;
                sys.exit(0)&lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        print(f&amp;quot;ERROR: {e}&amp;quot;, file=sys.stderr)&lt;br /&gt;
        sys.exit(1)&lt;br /&gt;
    &lt;br /&gt;
    print(&amp;quot;ERROR: No dumps found&amp;quot;, file=sys.stderr)&lt;br /&gt;
    sys.exit(1)&lt;br /&gt;
&lt;br /&gt;
get_latest_dump()&lt;br /&gt;
PYTHON_EOF&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to download new XML&lt;br /&gt;
download_xml() {&lt;br /&gt;
    local dump_info=&amp;quot;$1&amp;quot;&lt;br /&gt;
    local dump_dir=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f1)&lt;br /&gt;
    local xml_file=$(echo &amp;quot;$dump_info&amp;quot; | cut -d&#039;/&#039; -f2)&lt;br /&gt;
    local url=&amp;quot;https://xml.completenoobs.com/xmlDumps/${dump_info}&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Downloading: $xml_file&amp;quot;&lt;br /&gt;
    echo &amp;quot;From: $dump_dir&amp;quot;&lt;br /&gt;
    echo &amp;quot;URL: $url&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    if wget -O /tmp/new_dump.xml &amp;quot;$url&amp;quot; --progress=bar:force 2&amp;gt;&amp;amp;1; then&lt;br /&gt;
        echo &amp;quot;Download successful!&amp;quot;&lt;br /&gt;
        return 0&lt;br /&gt;
    else&lt;br /&gt;
        echo &amp;quot;Download failed!&amp;quot;&lt;br /&gt;
        return 1&lt;br /&gt;
    fi&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to backup current database&lt;br /&gt;
backup_database() {&lt;br /&gt;
    echo &amp;quot;Creating database backup...&amp;quot;&lt;br /&gt;
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)&lt;br /&gt;
    mysqldump --user=wikiuser --password=wikipass completenoobs_wiki &amp;gt; /tmp/wiki_backup_${TIMESTAMP}.sql&lt;br /&gt;
    echo &amp;quot;Backup created: /tmp/wiki_backup_${TIMESTAMP}.sql&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to analyze and import changes&lt;br /&gt;
analyze_and_import() {&lt;br /&gt;
    echo &amp;quot;Analyzing differences between XML and local wiki...&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Create analysis and import script for MediaWiki 1.44+&lt;br /&gt;
    cat &amp;gt; /tmp/analyze_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class AnalyzeAndImport extends Maintenance {&lt;br /&gt;
    private $db;&lt;br /&gt;
    private $new_pages = [];&lt;br /&gt;
    private $changed_pages = [];&lt;br /&gt;
    private $unchanged_pages = [];&lt;br /&gt;
    &lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addDescription(&#039;Analyze and selectively import from XML dump&#039;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $this-&amp;gt;db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // MediaWiki 1.35+ uses slots and content tables&lt;br /&gt;
        // Get existing pages with their content&lt;br /&gt;
        $query = &amp;quot;&lt;br /&gt;
            SELECT p.page_title, p.page_id, c.content_address, c.content_sha1&lt;br /&gt;
            FROM page p&lt;br /&gt;
            JOIN revision r ON p.page_latest = r.rev_id&lt;br /&gt;
            JOIN slots s ON r.rev_id = s.slot_revision_id&lt;br /&gt;
            JOIN slot_roles sr ON s.slot_role_id = sr.role_id&lt;br /&gt;
            JOIN content c ON s.slot_content_id = c.content_id&lt;br /&gt;
            WHERE p.page_namespace = 0 AND sr.role_name = &#039;main&#039;&lt;br /&gt;
        &amp;quot;;&lt;br /&gt;
        &lt;br /&gt;
        $result = $this-&amp;gt;db-&amp;gt;query($query);&lt;br /&gt;
        if (!$result) {&lt;br /&gt;
            $this-&amp;gt;error(&amp;quot;Database query failed: &amp;quot; . $this-&amp;gt;db-&amp;gt;error);&lt;br /&gt;
            return;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $existing = [];&lt;br /&gt;
        while ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
            // Get actual text content&lt;br /&gt;
            $text_content = $this-&amp;gt;getTextContent($row[&#039;content_address&#039;]);&lt;br /&gt;
            $existing[$row[&#039;page_title&#039;]] = [&lt;br /&gt;
                &#039;id&#039; =&amp;gt; $row[&#039;page_id&#039;],&lt;br /&gt;
                &#039;content&#039; =&amp;gt; $text_content,&lt;br /&gt;
                &#039;sha1&#039; =&amp;gt; $row[&#039;content_sha1&#039;]&lt;br /&gt;
            ];&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Parse XML and compare&lt;br /&gt;
        $xml = simplexml_load_file(&#039;/tmp/new_dump.xml&#039;);&lt;br /&gt;
        &lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            $title = str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title);&lt;br /&gt;
            $xml_content = (string)$page-&amp;gt;revision-&amp;gt;text;&lt;br /&gt;
            &lt;br /&gt;
            if (!isset($existing[$title])) {&lt;br /&gt;
                // New page&lt;br /&gt;
                $this-&amp;gt;new_pages[$title] = $xml_content;&lt;br /&gt;
            } else {&lt;br /&gt;
                // Compare content using SHA1 for efficiency&lt;br /&gt;
                $xml_sha1 = sha1($xml_content);&lt;br /&gt;
                &lt;br /&gt;
                if ($existing[$title][&#039;sha1&#039;] !== $xml_sha1) {&lt;br /&gt;
                    // Content is different&lt;br /&gt;
                    $this-&amp;gt;changed_pages[$title] = [&lt;br /&gt;
                        &#039;local&#039; =&amp;gt; $existing[$title][&#039;content&#039;],&lt;br /&gt;
                        &#039;xml&#039; =&amp;gt; $xml_content,&lt;br /&gt;
                        &#039;page_id&#039; =&amp;gt; $existing[$title][&#039;id&#039;]&lt;br /&gt;
                    ];&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;unchanged_pages[] = $title;&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Display summary&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Update Analysis ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages to import: &amp;quot; . count($this-&amp;gt;new_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Changed pages found: &amp;quot; . count($this-&amp;gt;changed_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Unchanged pages: &amp;quot; . count($this-&amp;gt;unchanged_pages) . &amp;quot;\n&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        // Save analysis for review&lt;br /&gt;
        file_put_contents(&#039;/tmp/update_analysis.json&#039;, json_encode([&lt;br /&gt;
            &#039;new&#039; =&amp;gt; array_keys($this-&amp;gt;new_pages),&lt;br /&gt;
            &#039;changed&#039; =&amp;gt; array_keys($this-&amp;gt;changed_pages),&lt;br /&gt;
            &#039;unchanged&#039; =&amp;gt; $this-&amp;gt;unchanged_pages&lt;br /&gt;
        ], JSON_PRETTY_PRINT));&lt;br /&gt;
        &lt;br /&gt;
        // Show changed pages with preview (limit to first 20 for readability)&lt;br /&gt;
        if (count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Changed Pages ===\n&amp;quot;);&lt;br /&gt;
            $count = 0;&lt;br /&gt;
            $total_changed = count($this-&amp;gt;changed_pages);&lt;br /&gt;
            &lt;br /&gt;
            foreach ($this-&amp;gt;changed_pages as $title =&amp;gt; $data) {&lt;br /&gt;
                $count++;&lt;br /&gt;
                if ($count &amp;lt;= 20) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;\n$count. $title\n&amp;quot;);&lt;br /&gt;
                    &lt;br /&gt;
                    // Create a simple diff preview (first 300 chars)&lt;br /&gt;
                    $xml_preview = substr($data[&#039;xml&#039;], 0, 100);&lt;br /&gt;
                    &lt;br /&gt;
                    // Save full diff to file&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;=== FULL DIFF FOR: $title ===\n\n&amp;quot;);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- LOCAL VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;local&#039;] . &amp;quot;\n\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, &amp;quot;--- XML VERSION ---\n&amp;quot;, FILE_APPEND);&lt;br /&gt;
                    file_put_contents($diff_file, $data[&#039;xml&#039;], FILE_APPEND);&lt;br /&gt;
                    &lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;   Preview: &amp;quot; . $xml_preview . &amp;quot;...\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            if ($total_changed &amp;gt; 20) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\n... and &amp;quot; . ($total_changed - 20) . &amp;quot; more changed pages.\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;All diff files saved to /tmp/diff_*.txt\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Interactive selection&lt;br /&gt;
        if (count($this-&amp;gt;new_pages) &amp;gt; 0 || count($this-&amp;gt;changed_pages) &amp;gt; 0) {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Import Options ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;1. Import new pages only (preserve all local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;2. Import new pages + update ALL changed pages (overwrites local changes)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;3. Selective import (choose which updates to apply)\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;4. Cancel (no changes)\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            // Save state for import script WITHOUT the XML object&lt;br /&gt;
            $import_data = [&lt;br /&gt;
                &#039;new_pages&#039; =&amp;gt; $this-&amp;gt;new_pages,&lt;br /&gt;
                &#039;changed_pages&#039; =&amp;gt; $this-&amp;gt;changed_pages,&lt;br /&gt;
                &#039;xml_file&#039; =&amp;gt; &#039;/tmp/new_dump.xml&#039;  // Save path instead of object&lt;br /&gt;
            ];&lt;br /&gt;
            &lt;br /&gt;
            file_put_contents(&#039;/tmp/import_data.ser&#039;, serialize($import_data));&lt;br /&gt;
        } else {&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\nNo changes detected. Your wiki is up to date!\n&amp;quot;);&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function getTextContent($address) {&lt;br /&gt;
        // Handle different content storage formats in MW 1.35+&lt;br /&gt;
        if (strpos($address, &#039;tt:&#039;) === 0) {&lt;br /&gt;
            // Text table reference&lt;br /&gt;
            $text_id = substr($address, 3);&lt;br /&gt;
            $result = $this-&amp;gt;db-&amp;gt;query(&amp;quot;SELECT old_text FROM text WHERE old_id = $text_id&amp;quot;);&lt;br /&gt;
            if ($row = $result-&amp;gt;fetch_assoc()) {&lt;br /&gt;
                return $row[&#039;old_text&#039;];&lt;br /&gt;
            }&lt;br /&gt;
        } elseif (strpos($address, &#039;es:&#039;) === 0) {&lt;br /&gt;
            // External storage - would need special handling&lt;br /&gt;
            return &amp;quot;[External storage content]&amp;quot;;&lt;br /&gt;
        }&lt;br /&gt;
        // Direct content&lt;br /&gt;
        return $address;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = AnalyzeAndImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/analyze_import.php&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Function to perform selected import&lt;br /&gt;
perform_import() {&lt;br /&gt;
    local choice=$1&lt;br /&gt;
    &lt;br /&gt;
    cat &amp;gt; /tmp/do_import.php &amp;lt;&amp;lt; &#039;PHP_EOF&#039;&lt;br /&gt;
&amp;lt;?php&lt;br /&gt;
require_once &#039;/var/www/html/maintenance/Maintenance.php&#039;;&lt;br /&gt;
&lt;br /&gt;
class DoImport extends Maintenance {&lt;br /&gt;
    public function __construct() {&lt;br /&gt;
        parent::__construct();&lt;br /&gt;
        $this-&amp;gt;addOption(&#039;mode&#039;, &#039;Import mode&#039;, true, true);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    public function execute() {&lt;br /&gt;
        $mode = $this-&amp;gt;getOption(&#039;mode&#039;);&lt;br /&gt;
        $data = unserialize(file_get_contents(&#039;/tmp/import_data.ser&#039;));&lt;br /&gt;
        &lt;br /&gt;
        // Load XML file&lt;br /&gt;
        $xml = simplexml_load_file($data[&#039;xml_file&#039;]);&lt;br /&gt;
        &lt;br /&gt;
        $new_imported = 0;&lt;br /&gt;
        $updated = 0;&lt;br /&gt;
        &lt;br /&gt;
        // Import new pages (always for modes 1-3)&lt;br /&gt;
        if ($mode != &#039;4&#039;) {&lt;br /&gt;
            foreach ($data[&#039;new_pages&#039;] as $title =&amp;gt; $content) {&lt;br /&gt;
                $this-&amp;gt;importPage($title, $content, $xml);&lt;br /&gt;
                $new_imported++;&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Imported new page: $title\n&amp;quot;);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Handle changed pages based on mode&lt;br /&gt;
        if ($mode == &#039;2&#039;) {&lt;br /&gt;
            // Update all changed pages&lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Updating: $title\n&amp;quot;);&lt;br /&gt;
                if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                    $updated++;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Updated page: $title\n&amp;quot;);&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        } elseif ($mode == &#039;3&#039;) {&lt;br /&gt;
            // Selective update&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;\n=== Selective Import Mode ===\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;For each changed page, choose:\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  y = yes, update this page\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  n = no, keep local version\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  d = show diff file\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  a = update all remaining pages\n&amp;quot;);&lt;br /&gt;
            $this-&amp;gt;output(&amp;quot;  s = skip all remaining pages\n\n&amp;quot;);&lt;br /&gt;
            &lt;br /&gt;
            $update_all = false;&lt;br /&gt;
            $skip_all = false;&lt;br /&gt;
            &lt;br /&gt;
            foreach ($data[&#039;changed_pages&#039;] as $title =&amp;gt; $info) {&lt;br /&gt;
                if ($skip_all) {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($update_all) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    continue;&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;\nPage: $title\n&amp;quot;);&lt;br /&gt;
                $this-&amp;gt;output(&amp;quot;Action (y/n/d/a/s): &amp;quot;);&lt;br /&gt;
                $handle = fopen(&amp;quot;php://stdin&amp;quot;, &amp;quot;r&amp;quot;);&lt;br /&gt;
                $line = trim(fgets($handle));&lt;br /&gt;
                &lt;br /&gt;
                while ($line == &#039;d&#039;) {&lt;br /&gt;
                    // Show diff&lt;br /&gt;
                    $safe_title = preg_replace(&#039;/[^a-zA-Z0-9_-]/&#039;, &#039;_&#039;, $title);&lt;br /&gt;
                    $diff_file = &amp;quot;/tmp/diff_${safe_title}.txt&amp;quot;;&lt;br /&gt;
                    if (file_exists($diff_file)) {&lt;br /&gt;
                        system(&amp;quot;head -50 $diff_file&amp;quot;);&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;\n[Showing first 50 lines - full file at $diff_file]\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Action (y/n/a/s): &amp;quot;);&lt;br /&gt;
                    $line = trim(fgets($handle));&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if ($line == &#039;a&#039;) {&lt;br /&gt;
                    $update_all = true;&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } elseif ($line == &#039;s&#039;) {&lt;br /&gt;
                    $skip_all = true;&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                } elseif ($line == &#039;y&#039;) {&lt;br /&gt;
                    if ($this-&amp;gt;reimportPage($title, $xml)) {&lt;br /&gt;
                        $updated++;&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Updated: $title\n&amp;quot;);&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $this-&amp;gt;output(&amp;quot;Failed to update: $title\n&amp;quot;);&lt;br /&gt;
                    }&lt;br /&gt;
                } else {&lt;br /&gt;
                    $this-&amp;gt;output(&amp;quot;Skipped: $title\n&amp;quot;);&lt;br /&gt;
                }&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;\n=== Import Complete ===\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;New pages imported: $new_imported\n&amp;quot;);&lt;br /&gt;
        $this-&amp;gt;output(&amp;quot;Pages updated: $updated\n&amp;quot;);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function importPage($title, $content, $xml) {&lt;br /&gt;
        // Create single page XML for import&lt;br /&gt;
        $tempFile = &#039;/tmp/single_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
        $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
        &lt;br /&gt;
        if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
            $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
            foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        // Find and add the page&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                break;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
        exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;);&lt;br /&gt;
        unlink($tempFile);&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    private function reimportPage($title, $xml) {&lt;br /&gt;
        // For updating existing pages, delete then reimport&lt;br /&gt;
        $db = new mysqli(&#039;127.0.0.1&#039;, &#039;wikiuser&#039;, &#039;wikipass&#039;, &#039;completenoobs_wiki&#039;);&lt;br /&gt;
        &lt;br /&gt;
        // Delete the existing page&lt;br /&gt;
        $safe_title = $db-&amp;gt;real_escape_string(str_replace(&#039; &#039;, &#039;_&#039;, $title));&lt;br /&gt;
        $db-&amp;gt;query(&amp;quot;DELETE FROM page WHERE page_title = &#039;$safe_title&#039; AND page_namespace = 0&amp;quot;);&lt;br /&gt;
        &lt;br /&gt;
        $db-&amp;gt;close();&lt;br /&gt;
        &lt;br /&gt;
        // Now import the new version&lt;br /&gt;
        foreach ($xml-&amp;gt;page as $page) {&lt;br /&gt;
            if (str_replace(&#039; &#039;, &#039;_&#039;, (string)$page-&amp;gt;title) == $title) {&lt;br /&gt;
                $tempFile = &#039;/tmp/update_page_&#039; . md5($title) . &#039;.xml&#039;;&lt;br /&gt;
                $singlePage = new SimpleXMLElement(&#039;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;&amp;lt;mediawiki&amp;gt;&amp;lt;/mediawiki&amp;gt;&#039;);&lt;br /&gt;
                &lt;br /&gt;
                if ($xml-&amp;gt;siteinfo) {&lt;br /&gt;
                    $siteinfo = $singlePage-&amp;gt;addChild(&#039;siteinfo&#039;);&lt;br /&gt;
                    foreach ($xml-&amp;gt;siteinfo-&amp;gt;children() as $child) {&lt;br /&gt;
                        $siteinfo-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $newPage = $singlePage-&amp;gt;addChild(&#039;page&#039;);&lt;br /&gt;
                foreach ($page-&amp;gt;children() as $child) {&lt;br /&gt;
                    if ($child-&amp;gt;getName() == &#039;revision&#039;) {&lt;br /&gt;
                        $revision = $newPage-&amp;gt;addChild(&#039;revision&#039;);&lt;br /&gt;
                        foreach ($child-&amp;gt;children() as $revChild) {&lt;br /&gt;
                            $revision-&amp;gt;addChild($revChild-&amp;gt;getName(), (string)$revChild);&lt;br /&gt;
                        }&lt;br /&gt;
                    } else {&lt;br /&gt;
                        $newPage-&amp;gt;addChild($child-&amp;gt;getName(), (string)$child);&lt;br /&gt;
                    }&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                $singlePage-&amp;gt;asXML($tempFile);&lt;br /&gt;
                $result = exec(&amp;quot;php /var/www/html/maintenance/importDump.php &amp;lt; $tempFile 2&amp;gt;&amp;amp;1&amp;quot;, $output, $return);&lt;br /&gt;
                unlink($tempFile);&lt;br /&gt;
                &lt;br /&gt;
                return ($return === 0);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        return false;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
$maintClass = DoImport::class;&lt;br /&gt;
require_once RUN_MAINTENANCE_IF_MAIN;&lt;br /&gt;
PHP_EOF&lt;br /&gt;
&lt;br /&gt;
    cd /var/www/html&lt;br /&gt;
    php /tmp/do_import.php --mode=&amp;quot;$choice&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Main execution&lt;br /&gt;
main() {&lt;br /&gt;
    check_environment&lt;br /&gt;
    &lt;br /&gt;
    # Start MariaDB if not running&lt;br /&gt;
    service mariadb status &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 || service mariadb start&lt;br /&gt;
    &lt;br /&gt;
    # Wait for MariaDB&lt;br /&gt;
    for i in {1..30}; do&lt;br /&gt;
        if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
            break&lt;br /&gt;
        fi&lt;br /&gt;
        sleep 1&lt;br /&gt;
    done&lt;br /&gt;
    &lt;br /&gt;
    CURRENT=$(get_current_version)&lt;br /&gt;
    echo &amp;quot;Current version: $CURRENT&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Checking for updates...&amp;quot;&lt;br /&gt;
    LATEST=$(check_for_updates 2&amp;gt;/dev/null)&lt;br /&gt;
    if [ $? -ne 0 ] || [ -z &amp;quot;$LATEST&amp;quot; ] || [[ &amp;quot;$LATEST&amp;quot; == *&amp;quot;ERROR&amp;quot;* ]]; then&lt;br /&gt;
        echo &amp;quot;Failed to check for updates&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;Latest available: $LATEST&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Always proceed to analysis even if versions match&lt;br /&gt;
    # (there might be content updates in the same version)&lt;br /&gt;
    echo &amp;quot;Proceeding with content analysis...&amp;quot;&lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    &lt;br /&gt;
    # Backup database&lt;br /&gt;
    backup_database&lt;br /&gt;
    &lt;br /&gt;
    # Download new XML&lt;br /&gt;
    if ! download_xml &amp;quot;$LATEST&amp;quot;; then&lt;br /&gt;
        echo &amp;quot;Failed to download new XML&amp;quot;&lt;br /&gt;
        exit 1&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Analyze differences&lt;br /&gt;
    analyze_and_import&lt;br /&gt;
    &lt;br /&gt;
    # Check if there are changes to import&lt;br /&gt;
    if [ -f &amp;quot;/tmp/import_data.ser&amp;quot; ]; then&lt;br /&gt;
        echo &amp;quot;&amp;quot;&lt;br /&gt;
        read -p &amp;quot;Choose option (1-4): &amp;quot; -n 1 -r&lt;br /&gt;
        echo&lt;br /&gt;
        &lt;br /&gt;
        if [[ $REPLY =~ ^[1-4]$ ]]; then&lt;br /&gt;
            if [ &amp;quot;$REPLY&amp;quot; != &amp;quot;4&amp;quot; ]; then&lt;br /&gt;
                perform_import &amp;quot;$REPLY&amp;quot;&lt;br /&gt;
                &lt;br /&gt;
                # Update version info&lt;br /&gt;
                echo &amp;quot;Import: $LATEST&amp;quot; &amp;gt; /var/www/html/.last_import&lt;br /&gt;
                echo &amp;quot;Date: $(date)&amp;quot; &amp;gt;&amp;gt; /var/www/html/.last_import&lt;br /&gt;
                &lt;br /&gt;
                # Rebuild indices&lt;br /&gt;
                echo &amp;quot;Rebuilding indices...&amp;quot;&lt;br /&gt;
                php maintenance/rebuildrecentchanges.php&lt;br /&gt;
                php maintenance/initSiteStats.php&lt;br /&gt;
            else&lt;br /&gt;
                echo &amp;quot;Update cancelled&amp;quot;&lt;br /&gt;
            fi&lt;br /&gt;
        else&lt;br /&gt;
            echo &amp;quot;Invalid option. Update cancelled&amp;quot;&lt;br /&gt;
        fi&lt;br /&gt;
    fi&lt;br /&gt;
    &lt;br /&gt;
    # Clean up temp files&lt;br /&gt;
    rm -f /tmp/import_data.ser /tmp/update_analysis.json /tmp/diff_*.txt /tmp/analyze_import.php /tmp/do_import.php 2&amp;gt;/dev/null&lt;br /&gt;
    &lt;br /&gt;
    echo &amp;quot;&amp;quot;&lt;br /&gt;
    echo &amp;quot;Done!&amp;quot;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
# Run main function&lt;br /&gt;
main &amp;quot;$@&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 2.5: Entrypoint Script ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
nano entrypoint.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
#!/bin/bash&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;Starting CompleteNoobs Wiki...&amp;quot;&lt;br /&gt;
&lt;br /&gt;
service mariadb start&lt;br /&gt;
&lt;br /&gt;
# Wait for MariaDB&lt;br /&gt;
for i in {1..30}; do&lt;br /&gt;
    if mysql -e &amp;quot;SELECT 1;&amp;quot; &amp;amp;&amp;gt;/dev/null; then&lt;br /&gt;
        echo &amp;quot;MariaDB ready!&amp;quot;&lt;br /&gt;
        break&lt;br /&gt;
    fi&lt;br /&gt;
    sleep 1&lt;br /&gt;
done&lt;br /&gt;
&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;CompleteNoobs Wiki ready at: http://localhost:8080&amp;quot;&lt;br /&gt;
echo &amp;quot;Admin login: admin / AdminPass123!&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;Features:&amp;quot;&lt;br /&gt;
echo &amp;quot;- Complete wiki content imported from XML&amp;quot;&lt;br /&gt;
echo &amp;quot;- License notices on all pages (via PageNotice)&amp;quot;&lt;br /&gt;
echo &amp;quot;- SyntaxHighlight for code blocks&amp;quot;&lt;br /&gt;
echo &amp;quot;- YouTube video embedding&amp;quot;&lt;br /&gt;
echo &amp;quot;- Contribution Scores special page&amp;quot;&lt;br /&gt;
echo &amp;quot;- XML update system (preserves local edits)&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
echo &amp;quot;To check for updates: docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;quot;&lt;br /&gt;
echo &amp;quot;&amp;quot;&lt;br /&gt;
&lt;br /&gt;
apache2-foreground&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 3: Build and Run ==&lt;br /&gt;
&lt;br /&gt;
=== 3.1: Build the Image ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker build -t completenoobs/wiki:latest .&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will take several minutes.&lt;br /&gt;
&lt;br /&gt;
=== 3.2: Run the Container ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 4: Test Everything ==&lt;br /&gt;
&lt;br /&gt;
=== 4.1: Check the Wiki ===&lt;br /&gt;
* Visit: http://localhost:8080&lt;br /&gt;
* You should see the PageNotice at the top&lt;br /&gt;
* Login with: admin / AdminPass123!&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Change Admin Password:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By default, the admin user&#039;s password is &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;. It&#039;s highly recommended to change this immediately. You can do this either through the wiki&#039;s web interface or directly in the Docker terminal.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 1: Change Password via Web Interface&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
This is the easiest method. You can change your password directly from the wiki itself.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Log in to your wiki with the default credentials: &amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
2. Once logged in, click your username (&amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt;) in the top-right corner of the page.&amp;lt;br&amp;gt;&lt;br /&gt;
3. From the drop-down menu, select &amp;lt;b&amp;gt;Preferences&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
4. On the Preferences page, go to the &amp;lt;b&amp;gt;Password&amp;lt;/b&amp;gt; tab.&amp;lt;br&amp;gt;&lt;br /&gt;
5. Enter the current password (&amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;), then enter your new password twice.&amp;lt;br&amp;gt;&lt;br /&gt;
6. Click &amp;lt;b&amp;gt;Change password&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Your password is now changed, and you will need to use the new one for future logins.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 2: Change Password via Terminal (No-Email Reset)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you have forgotten the password or prefer to use the command line, you can reset it directly inside the Docker container using a MediaWiki maintenance script.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;b&amp;gt;Access the container&#039;s shell&amp;lt;/b&amp;gt; with the following command from your host machine:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Once inside the container, use the &amp;lt;code&amp;gt;changePassword.php&amp;lt;/code&amp;gt; maintenance script to change the password. This is the modern, recommended way to run MediaWiki maintenance scripts.&amp;lt;br&amp;gt;&lt;br /&gt;
* Change &amp;lt;b&amp;gt;NEWPASSWORD&amp;lt;/b&amp;gt; to your new password&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt; to leave the container&#039;s shell.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The admin password has now been reset. You can log in to your wiki with the new password.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.2: Test Extensions ===&lt;br /&gt;
* &#039;&#039;&#039;YouTube&#039;&#039;&#039;: Edit any page, add &amp;lt;code&amp;gt;&amp;lt;youtube&amp;gt;N9qYF9DZPdw&amp;lt;/youtube&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
* &#039;&#039;&#039;PageNotice&#039;&#039;&#039;: Should already be visible at the top&lt;br /&gt;
* &#039;&#039;&#039;SyntaxHighlight&#039;&#039;&#039;: Add code blocks with &amp;lt;code&amp;gt;&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;code here&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 4.3: Check Status ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki /var/www/html/check_status.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 5: XML Update Operations ==&lt;br /&gt;
*NOTE: update_xml.sh still needs alot of work, this just idea placeholder for now.&lt;br /&gt;
=== 5.1: Check for Updates (Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
This will:&lt;br /&gt;
* Check the CompleteNoobs XML repository for new dumps&lt;br /&gt;
* Compare with your current version&lt;br /&gt;
* Ask for confirmation before updating&lt;br /&gt;
* Import ONLY new pages (preserves your edits)&lt;br /&gt;
* Create a backup before making changes&lt;br /&gt;
&lt;br /&gt;
=== 5.2: Force Update (Non-Interactive) ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki bash -c &amp;quot;echo &#039;y&#039; | /var/www/html/update_xml.sh&amp;quot;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== 5.3: Manual Update Process ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
# 1. Enter container&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&lt;br /&gt;
# 2. Check current version&lt;br /&gt;
cat /var/www/html/.last_import&lt;br /&gt;
&lt;br /&gt;
# 3. Run update&lt;br /&gt;
/var/www/html/check_updates.sh&lt;br /&gt;
&lt;br /&gt;
# 4. Exit container&lt;br /&gt;
exit&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Step 6: Troubleshooting Commands ==&lt;br /&gt;
=== Access container shell: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Edit configuration: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki nano /var/www/html/LocalSettings.php&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Check logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker logs completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== View update logs: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec completenoobs_wiki tail -f /tmp/mediawiki-debug.log&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Complete restart: ===&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki completenoobs/wiki:latest&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Expected Results ==&lt;br /&gt;
* Working wiki with imported CompleteNoobs content&lt;br /&gt;
* PageNotice visible at top of all pages&lt;br /&gt;
* All extensions functional&lt;br /&gt;
* Text editors (nano/vim) available in container&lt;br /&gt;
* Utility scripts for maintenance&lt;br /&gt;
* XML update system that preserves local edits&lt;br /&gt;
&lt;br /&gt;
== Update System Features ==&lt;br /&gt;
&lt;br /&gt;
=== How Updates Work ===&lt;br /&gt;
# Version Tracking: System tracks which XML dump version you have&lt;br /&gt;
# Smart Import: Only imports pages that don&#039;t exist locally&lt;br /&gt;
# Edit Preservation: Never overwrites pages you&#039;ve edited&lt;br /&gt;
# Automatic Backup: Creates SQL backup before any updates&lt;br /&gt;
# User Confirmation: Asks before making changes&lt;br /&gt;
# Progress Feedback: Shows what&#039;s being imported/skipped&lt;br /&gt;
&lt;br /&gt;
== Important Notes ==&lt;br /&gt;
* First Import: Initial build imports ALL content from XML&lt;br /&gt;
* Subsequent Updates: Only import NEW pages, preserving your edits&lt;br /&gt;
* Conflict Resolution: To force-update a specific page with XML version, delete it first through wiki interface&lt;br /&gt;
* Backups: Stored in /tmp/ (cleared on container restart)&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Ubuntu2404_Install_Docker_and_Docker_Compose&amp;diff=650</id>
		<title>Ubuntu2404 Install Docker and Docker Compose</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Ubuntu2404_Install_Docker_and_Docker_Compose&amp;diff=650"/>
		<updated>2025-09-01T17:34:45Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Preparation ===&lt;br /&gt;
&lt;br /&gt;
Before we begin, make sure you&#039;re logged in with a user account that has sudo privileges.&lt;br /&gt;
&lt;br /&gt;
=== Update System Packages ===&lt;br /&gt;
&lt;br /&gt;
Update your package list to ensure you have the latest versions of packages:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Install Docker Prerequisites ===&lt;br /&gt;
&lt;br /&gt;
Install the necessary packages for Docker setup:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Setup Docker Repository ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Add Docker&#039;s Official GPG Key&#039;&#039;&#039;:&lt;br /&gt;
 &lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Add the Docker Repository&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  echo &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&amp;quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Install Docker and Docker Compose ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Update Package List Again&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo apt update&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Install Docker Engine, CLI, Containerd, and Additional Tools&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo apt install -y docker-ce docker-ce-cli containerd.io python3-bs4 python3-requests docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- &lt;br /&gt;
* &#039;&#039;&#039;Install Docker Compose&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
Here we&#039;re downloading the latest version of Docker Compose:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo curl -L &amp;quot;https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)&amp;quot; -o /usr/local/bin/docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Make the Docker Compose binary executable:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo chmod +x /usr/local/bin/docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Verify Installation ===&lt;br /&gt;
&lt;br /&gt;
Check if Docker and Docker Compose are installed correctly:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker --version&lt;br /&gt;
docker-compose --version&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Configure User Permissions ===&lt;br /&gt;
&lt;br /&gt;
To run Docker commands without &amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt;, add your user to the &amp;lt;code&amp;gt;docker&amp;lt;/code&amp;gt; group:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo usermod -aG docker $USER&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note&#039;&#039;&#039;: After adding your user to the docker group, you&#039;ll need to &#039;&#039;&#039;log out and log back in&#039;&#039;&#039; for the changes to take effect.&lt;br /&gt;
If you do not log out and back in, Or you do not add your $USER to the docker group, you will be required to use sudo in some cases. such as ..&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
a way to apply group changes without logging out and back in - tip:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
exec sudo su -l $USER&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will replace your current shell with a new login shell for your user, which will have the updated group memberships. Both of these methods will apply the group changes immediately, allowing you to use LXD commands without having to log out and back in. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Remember&amp;lt;/b&amp;gt;, these changes only apply to the current terminal session. If you open a new terminal window, you might need to run the command again or log out and back in for the changes to take effect system-wide.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===If Installing Docker messed up your LXC/LXD Networking===&lt;br /&gt;
To resolve networking conflicts between Docker and LXC containers on Ubuntu 24.04, enable IP forwarding on the host system:&lt;br /&gt;
* Open the sysctl configuration file in your preferred editor&lt;br /&gt;
&amp;lt;code&amp;gt;sudo $EDITOR /etc/sysctl.conf&amp;lt;/code&amp;gt;&lt;br /&gt;
* Uncomment or add the following line (around line 28):&lt;br /&gt;
&amp;lt;pre&amp;gt;net.ipv4.ip_forward=1&amp;lt;/pre&amp;gt;&lt;br /&gt;
* Apply the updated configuration to enable IP forwarding:&lt;br /&gt;
&amp;lt;code&amp;gt;sysctl -p&amp;lt;/code&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
* Restart the system to ensure all changes take effect.&lt;br /&gt;
&lt;br /&gt;
This should resolve the networking issue for LXC containers when Docker is installed.&lt;br /&gt;
&lt;br /&gt;
==Install First Container/Image==&lt;br /&gt;
&lt;br /&gt;
Download the completenoobs container image, mediawiki with the completenoobs xml installed.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;code&amp;gt;docker pull completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Run container===&lt;br /&gt;
&lt;br /&gt;
* Quick Start&lt;br /&gt;
&amp;lt;code&amp;gt;docker run -d -p 8080:80 completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Quick Start with Persistent Storage&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
docker run -d -p 8080:80 \&lt;br /&gt;
  -v completenoobs_mysql:/var/lib/mysql \&lt;br /&gt;
  -v completenoobs_images:/var/www/html/images \&lt;br /&gt;
  --name completenoobs_wiki \&lt;br /&gt;
  completenoobs/cnoobs-wiki:0.1&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* Now visit http://localhost:8080 on your browser&lt;br /&gt;
&lt;br /&gt;
* Due to (unknown) bug you might need to update the xml to download missing pages:&lt;br /&gt;
&amp;lt;code&amp;gt;docker exec -it completenoobs_wiki /var/www/html/check_updates.sh&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
Change Admin Password:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&lt;br /&gt;
By default, the admin user&#039;s password is &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;. It&#039;s highly recommended to change this immediately. You can do this either through the wiki&#039;s web interface or directly in the Docker terminal.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 1: Change Password via Web Interface&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
This is the easiest method. You can change your password directly from the wiki itself.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. Log in to your wiki with the default credentials: &amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt; / &amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
2. Once logged in, click your username (&amp;lt;code&amp;gt;admin&amp;lt;/code&amp;gt;) in the top-right corner of the page.&amp;lt;br&amp;gt;&lt;br /&gt;
3. From the drop-down menu, select &amp;lt;b&amp;gt;Preferences&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
4. On the Preferences page, go to the &amp;lt;b&amp;gt;Password&amp;lt;/b&amp;gt; tab.&amp;lt;br&amp;gt;&lt;br /&gt;
5. Enter the current password (&amp;lt;code&amp;gt;AdminPass123!&amp;lt;/code&amp;gt;), then enter your new password twice.&amp;lt;br&amp;gt;&lt;br /&gt;
6. Click &amp;lt;b&amp;gt;Change password&amp;lt;/b&amp;gt;.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Your password is now changed, and you will need to use the new one for future logins.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Method 2: Change Password via Terminal (No-Email Reset)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you have forgotten the password or prefer to use the command line, you can reset it directly inside the Docker container using a MediaWiki maintenance script.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
1. &amp;lt;b&amp;gt;Access the container&#039;s shell&amp;lt;/b&amp;gt; with the following command from your host machine:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker exec -it completenoobs_wiki bash&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
2. Once inside the container, use the &amp;lt;code&amp;gt;changePassword.php&amp;lt;/code&amp;gt; maintenance script to change the password. This is the modern, recommended way to run MediaWiki maintenance scripts.&amp;lt;br&amp;gt;&lt;br /&gt;
* Change &amp;lt;b&amp;gt;NEWPASSWORD&amp;lt;/b&amp;gt; to your new password&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
php /var/www/html/maintenance/run.php changePassword.php --user=admin --password=NEWPASSWORD&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
3. Type &amp;lt;code&amp;gt;exit&amp;lt;/code&amp;gt; to leave the container&#039;s shell.&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The admin password has now been reset. You can log in to your wiki with the new password.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
===Remove container===&lt;br /&gt;
&lt;br /&gt;
To completely remove the &amp;lt;code&amp;gt;completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; container and image from your Ubuntu 24.04 system, follow these steps. You can also remove associated persistent storage volumes if they were created.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 1: Stop and Remove the Container&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If the container is running, stop it and then remove it.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker stop completenoobs_wiki&lt;br /&gt;
docker rm completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Alternatively, stop and remove in one command:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rm -f completenoobs_wiki&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 2: Remove the Docker Image&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Remove the &amp;lt;code&amp;gt;completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; image.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker rmi completenoobs/cnoobs-wiki:0.1&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Note: If the image is in use by other containers, remove those containers first or use &amp;lt;code&amp;gt;docker rmi -f completenoobs/cnoobs-wiki:0.1&amp;lt;/code&amp;gt; to force removal (use with caution).&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 3: Remove Persistent Storage Volumes (Optional)&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
If you used persistent storage, remove the associated volumes to free up space.&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume rm completenoobs_mysql completenoobs_images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
Note: Ensure no other containers are using these volumes, as this will delete all stored data.&amp;lt;br&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Step 4: Verify Removal&amp;lt;/b&amp;gt;&amp;lt;br&amp;gt;&lt;br /&gt;
Check that the container, image, and volumes are removed.&amp;lt;br&amp;gt;&lt;br /&gt;
- List all containers (including stopped ones):&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker ps -a&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- List all images:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker images&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
- List all volumes:&amp;lt;br&amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker volume ls&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
If any items remain, repeat the relevant removal commands or check for dependencies.&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
	<entry>
		<id>https://www.completenoobs.com/noobs/index.php?title=Ubuntu2404_Install_Docker_and_Docker_Compose&amp;diff=649</id>
		<title>Ubuntu2404 Install Docker and Docker Compose</title>
		<link rel="alternate" type="text/html" href="https://www.completenoobs.com/noobs/index.php?title=Ubuntu2404_Install_Docker_and_Docker_Compose&amp;diff=649"/>
		<updated>2025-08-30T14:40:45Z</updated>

		<summary type="html">&lt;p&gt;AwesomO: /* Install Docker and Docker Compose */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;=== Preparation ===&lt;br /&gt;
&lt;br /&gt;
Before we begin, make sure you&#039;re logged in with a user account that has sudo privileges.&lt;br /&gt;
&lt;br /&gt;
=== Update System Packages ===&lt;br /&gt;
&lt;br /&gt;
Update your package list to ensure you have the latest versions of packages:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Install Docker Prerequisites ===&lt;br /&gt;
&lt;br /&gt;
Install the necessary packages for Docker setup:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Setup Docker Repository ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Add Docker&#039;s Official GPG Key&#039;&#039;&#039;:&lt;br /&gt;
 &lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Add the Docker Repository&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  echo &amp;quot;deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&amp;quot; | sudo tee /etc/apt/sources.list.d/docker.list &amp;gt; /dev/null&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Install Docker and Docker Compose ===&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Update Package List Again&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo apt update&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
* &#039;&#039;&#039;Install Docker Engine, CLI, Containerd, and Additional Tools&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo apt install -y docker-ce docker-ce-cli containerd.io python3-bs4 python3-requests docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- &lt;br /&gt;
* &#039;&#039;&#039;Install Docker Compose&#039;&#039;&#039;:&lt;br /&gt;
&lt;br /&gt;
Here we&#039;re downloading the latest version of Docker Compose:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo curl -L &amp;quot;https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)&amp;quot; -o /usr/local/bin/docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Make the Docker Compose binary executable:&lt;br /&gt;
&lt;br /&gt;
  &amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
  sudo chmod +x /usr/local/bin/docker-compose&lt;br /&gt;
  &amp;lt;/source&amp;gt;&lt;br /&gt;
--&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Verify Installation ===&lt;br /&gt;
&lt;br /&gt;
Check if Docker and Docker Compose are installed correctly:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
docker --version&lt;br /&gt;
docker-compose --version&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Configure User Permissions ===&lt;br /&gt;
&lt;br /&gt;
To run Docker commands without &amp;lt;code&amp;gt;sudo&amp;lt;/code&amp;gt;, add your user to the &amp;lt;code&amp;gt;docker&amp;lt;/code&amp;gt; group:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;source lang=&amp;quot;bash&amp;quot;&amp;gt;&lt;br /&gt;
sudo usermod -aG docker $USER&lt;br /&gt;
&amp;lt;/source&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;Note&#039;&#039;&#039;: After adding your user to the docker group, you&#039;ll need to &#039;&#039;&#039;log out and log back in&#039;&#039;&#039; for the changes to take effect.&lt;br /&gt;
If you do not log out and back in, Or you do not add your $USER to the docker group, you will be required to use sudo in some cases. such as ..&lt;br /&gt;
&lt;br /&gt;
&amp;lt;div class=&amp;quot;toccolours mw-collapsible mw-collapsed&amp;quot;&amp;gt;&lt;br /&gt;
a way to apply group changes without logging out and back in - tip:&lt;br /&gt;
&amp;lt;div class=&amp;quot;mw-collapsible-content&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
exec sudo su -l $USER&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This command will replace your current shell with a new login shell for your user, which will have the updated group memberships. Both of these methods will apply the group changes immediately, allowing you to use LXD commands without having to log out and back in. &lt;br /&gt;
&lt;br /&gt;
&amp;lt;b&amp;gt;Remember&amp;lt;/b&amp;gt;, these changes only apply to the current terminal session. If you open a new terminal window, you might need to run the command again or log out and back in for the changes to take effect system-wide.&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;br /&gt;
&amp;lt;/div&amp;gt;&lt;/div&gt;</summary>
		<author><name>AwesomO</name></author>
	</entry>
</feed>