<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Ramblings from Roy</title>
    <link>https://blog.meghadeep.com/</link>
    <description>My personal take on anything that interests (or irritates) me</description>
    <pubDate>Mon, 06 Apr 2026 20:55:23 +0000</pubDate>
    <item>
      <title>How to integrate CESC&#39;s new &#34;smart&#34; meter (Iskraemeco ME260) to Home Assistant with behind the meter rooftop solar installation</title>
      <link>https://blog.meghadeep.com/how-to-integrate-cescs-new-smart-meter-iskraemeco-me260-to-home-assistant</link>
      <description>&lt;![CDATA[So you got a new rooftop solar installation. Your distributor installed a brand new smart meter which turned out to be Iskraemeco ME260, a consumer hostile smart meter with no effective means for you to programmatically gather data (well, almost), and your distributor also turned out to be too consumer hostile to provide you with any means of programmatically getting your consumption data from them either, or even be willing to explain certain functions of your smart meter to you. If this sounds like you, read on. !--more--&#xA;&#xA;Disclaimer: This is intended mainly for me to remember how to do this later, as such, there is no guarantee that this will work for you. I also ABSOLUTELY DO NOT condone work on your house&#39;s main lines unless you know what you are doing, and in some regions, it might even be illegal for you to work on your electrical main lines without being a licensed professional. You won&#39;t actually have to even touch your main lines for this guide, but the disclaimer applies nonetheless.&#xA;&#xA;------&#xA;&#xA;Acknowledgement&#xA;&#xA;Without the Open Home Foundation and the Home Assistant Community, especially the developer and the contributors of Home Assistant Glow, I would not have been able to do any of this. &#xA;&#xA;It would have been easier if CESC, my distributor, had helped me with any information about my meter, but they were too busy chastising me for attempting to integrate my energy data to Home Assistant since they think that without time-of-use rates, it&#39;s worthless to monitor my power usage, and that I should just take a reading manually every night if I am so interested. Thanks for fueling this project with spite, I got record turnaround! &#xA;&#xA;------&#xA;&#xA;Assumptions&#xA;&#xA;This guide makes several assumptions that reflect my personal choices. If you have different needs, or if you use something other than what I did, you will need to adapt this guide to your needs.&#xA;&#xA;You have a Iskraemeco ME260 meter.&#xA;  Or any energy meter that has two indicator lights to indicate the net flow of energy, one light indicating kWh, and one light indicating kVarh, and blinks 3200 times (trivial to change in code later) per unit of kWh or kVarh of net energy flow through the meter. This means that the blinking LED lights indicate your exported energy when your generation is greater than your consumption, and your imported energy when your consumption is greater than your generation. If your meter measures your consumption separately, then this blog post doesn&#39;t apply to you, since you already have all the info that we are trying to calculate.&#xA;You have Home Assistant up and running in your home.&#xA;You have accurate generation data from your inverter.&#xA;You are comfortable with scripting and tinkering, and have a multi-meter.&#xA;&#xA;Things you will need&#xA;&#xA;1x ESP32, or ESP8266 (either works, I use a ESP8266 since they are so darn cheap, this guide will assume that you are using a NodeMCU ESP8266)&#xA;2x LDR Modules with LDR (something like this)&#xA;1x ADS1115 (since ESP32 and ESP8266 have way too much noise in their ADCs)&#xA;1x CT Clamp with voltage output (something like this, please choose your required amperage, but remember that in this specific case, it is better to underestimate your amperage than overestimate it)&#xA;1x 3.5mm audio jack (3 pin, female)&#xA;2x 10kΩ resistors&#xA;1x 10µF electrolytic capacitor (10V to 25V is fine)&#xA;2x 22µF electrolytic capacitor (optional, but nice to have, 10V to 25V is fine)&#xA;&#xA;Wiring&#xA;&#xA;Fritzing breadboard diagram&#xA;&#xA;Please let me know if you find any improvements to be made! I&#39;m still learning this stuff!&#xA;&#xA;Installation&#xA;&#xA;This ESPHome YAML file largely follows Home Assistant Glow, but has some minor changes to include the kVarh pulse meter, and the CT clamp in the same device (you can very easily use vanilla Home Assistant Glow yourself with this project, but then you have to have some other controller measuring you kVarh usage, and you mains amperage). Use your ESPHome dashboard to install this YAML file in your NodeMCU ESP8266. Please pay attention to the keys and passwords, which should be auto-generated by Home Assistant&#39;s ESPHome addon. If you have any deviations from this blog&#39;s assumptions or wiring scheme, you&#39;ll need to make those changes here.&#xA;&#xA;esphome:&#xA;  name: grid-net-flow&#xA;  friendlyname: &#39;Grid Net Flow&#39;&#xA;&#xA;esp8266:&#xA;  board: esp011m&#xA;&#xA;Enable logging&#xA;logger:&#xA;&#xA;Enable Home Assistant API&#xA;api:&#xA;  encryption:&#xA;    key: &#34;7x2fgb3oK7RXgBmWYPjjEXq8vd1ePRXkUcPARYERWzU=&#34;&#xA;  services:&#xA;    service: resettotalrealenergy&#xA;      then:&#xA;        button.press:&#xA;            id: buttonresettotalreal&#xA;    service: resettotalreactiveenergy&#xA;      then:&#xA;        button.press:&#xA;            id: buttonresettotalreactive&#xA;&#xA;ota:&#xA;  platform: esphome&#xA;    password: &#34;a742d78ef1fa851e8d7d8e7c06495588&#34;&#xA;&#xA;wifi:&#xA;  ssid: !secret wifissid&#xA;  password: !secret wifipassword&#xA;&#xA;  # Enable fallback hotspot (captive portal) in case wifi connection fails&#xA;  ap:&#xA;    ssid: &#34;Grid Net Flow hotspot&#34;&#xA;    password: &#34;OHjwap17xhfV&#34;&#xA;&#xA;substitutions:&#xA;&#xA;  # Define the GPIO pins&#xA;  pulsepin: GPIO13&#xA;  pulsekarhpin: GPIO12&#xA;  ledpinred: GPIO2&#xA;&#xA;Sensors for ESP version and WIFI information&#xA;textsensor:&#xA;  # ESPHome version&#xA;  platform: version&#xA;    hidetimestamp: true&#xA;    name: &#39;Grid Net Flow - ESPHome Version&#39;&#xA;  # IP address and connected SSID&#xA;  platform: wifiinfo&#xA;    ipaddress:&#xA;      name: &#39;Grid Net Flow - IP Address&#39;&#xA;      icon: mdi:wifi&#xA;    ssid:&#xA;      name: &#39;Grid Net Flow - Connected SSID&#39;&#xA;      icon: mdi:wifi-strength-2&#xA;&#xA;Enable time component to reset energy at midnight&#xA;https://esphome.io/components/time/homeassistant.html&#xA;time:&#xA;  platform: homeassistant&#xA;    id: homeassistanttime&#xA;&#xA;Status LED configuration &#xA;output:&#xA;  # - platform: gpio&#xA;  #   pin: GPIO5&#xA;  #   id: outputblue&#xA;  platform: gpio&#xA;    pin: ${ledpinred}&#xA;    id: outputred&#xA;&#xA;light:&#xA;  platform: binary&#xA;    internal: true&#xA;    id: ledred&#xA;    name: Red&#xA;    output: outputred&#xA;&#xA;Pulse meter configuration &#xA;number:&#xA;  # Set the pulse rate of the LED on your meter&#xA;  platform: template&#xA;    id: selectpulserate&#xA;    name: &#39;Pulse rate - imp/kWh&#39;&#xA;    optimistic: true&#xA;    mode: box&#xA;    minvalue: 10&#xA;    maxvalue: 10000&#xA;    step: 10&#xA;    restorevalue: yes&#xA;    initialvalue: 3200&#xA;&#xA;  # Reset total energy to given value&#xA;  platform: template&#xA;    id: selectresettotalreal&#xA;    name: &#39;Reset Value - Transient Total Energy kWh&#39;&#xA;    entitycategory: config&#xA;    optimistic: true&#xA;    mode: box&#xA;    minvalue: 0&#xA;    maxvalue: 1000000&#xA;    step: 1&#xA;    initialvalue: 0&#xA;&#xA;  # Reset total energy to given value&#xA;  platform: template&#xA;    id: selectresettotalreactive&#xA;    name: &#39;Reset Value - Total Energy kVarh&#39;&#xA;    entitycategory: config&#xA;    optimistic: true&#xA;    mode: box&#xA;    minvalue: 0&#xA;    maxvalue: 1000000&#xA;    step: 1&#xA;    initialvalue: 0&#xA;&#xA;button:&#xA;  # Restart the ESP&#xA;  platform: restart&#xA;    name: &#34;Restart - Grid Net Flow Sensor&#34;&#xA;&#xA;  # Reset the total real energy entity&#xA;  platform: template&#xA;    id: buttonresettotalreal&#xA;    name: &#34;Reset - Transient Total Real Energy Flow&#34;&#xA;    onpress:&#xA;      pulsemeter.settotalpulses:&#xA;          id: sensorenergypulsemeter&#xA;          value: !lambda &#34;return id(selectresettotalreal).state  id(selectpulserate).state;&#34;&#xA;&#xA;  # Reset the total reactive energy entity&#xA;  platform: template&#xA;    id: buttonresettotalreactive&#xA;    name: &#34;Reset - Total Reactive Energy&#34;&#xA;    onpress:&#xA;      pulsemeter.settotalpulses:&#xA;          id: sensorreactenergypulsemeter&#xA;          value: !lambda &#34;return id(selectresettotalreactive).state  id(selectpulserate).state;&#34;&#xA;&#xA;i2c:&#xA;  sda: GPIO4&#xA;  scl: GPIO5&#xA;&#xA;ads1115:&#xA;  address: 0x48&#xA;    continuousmode: on&#xA;&#xA;sensor:&#xA;  # ADS1115&#xA;  platform: ads1115&#xA;    multiplexer: &#39;A0GND&#39;&#xA;    gain: 4.096&#xA;    name: &#34;CT Clamp Raw Value&#34;&#xA;    id: ads1115sensor&#xA;    updateinterval: 0.5s&#xA;    accuracydecimals: 2&#xA;&#xA;  # CT Clamp&#xA;  platform: ctclamp&#xA;    sensor: ads1115sensor&#xA;    name: &#34;Consumption Current&#34;&#xA;    sampleduration: 400ms&#xA;    updateinterval: 0.5s&#xA;    id: consumptioncurrent&#xA;    accuracydecimals: 1&#xA;    #filters:&#xA;      #- calibratelinear:&#xA;          ## Measured value of 0 maps to 0A&#xA;          #- 0.00 -  0.00&#xA;          ## Known load: 2.27A&#xA;          ## Value shown in logs: 0.08865A&#xA;          #- 0.08865 -  2.27&#xA;&#xA;  # WiFi signal&#xA;  platform: wifisignal&#xA;    name: &#34;Grid Net Flow - WiFi Signal&#34;&#xA;    updateinterval: 120s&#xA;&#xA;  # Pulse meter kWh&#xA;  platform: pulsemeter&#xA;    id: sensorenergypulsemeter&#xA;    name: &#39;Net Real Power Flow&#39;&#xA;    unitofmeasurement: W&#xA;    stateclass: measurement&#xA;    deviceclass: power&#xA;    icon: mdi:flash-outline&#xA;    accuracydecimals: 3&#xA;    pin: ${pulsepin}&#xA;    internalfilter: 2ms&#xA;    internalfiltermode: &#39;PULSE&#39;&#xA;    onrawvalue:&#xA;      then:&#xA;        light.turnon:&#xA;            id: ledred&#xA;        delay: 0.2s&#xA;        light.turnoff:&#xA;            id: ledred&#xA;    filters:&#xA;      # multiply value = (60 / imp value)  1000&#xA;      # - multiply: 60&#xA;      lambda: return x  ((60.0 / id(selectpulserate).state)  1000.0);&#xA;&#xA;    total:&#xA;      id: sensortotalrealenergy&#xA;      name: &#39;Transient Total Real Energy Flow&#39;&#xA;      unitofmeasurement: kWh&#xA;      icon: mdi:transmission-tower-export&#xA;      stateclass: totalincreasing&#xA;      deviceclass: energy&#xA;      accuracydecimals: 6&#xA;      filters:&#xA;        # multiply value = 1 / imp value&#xA;        # - multiply: 0.001&#xA;        lambda: return x  (1.0 / id(selectpulserate).state);&#xA;&#xA;  # Pulse meter kVarh&#xA;  platform: pulsemeter&#xA;    id: sensorreactenergypulsemeter&#xA;    name: &#39;Reactive Power Consumption&#39;&#xA;    unitofmeasurement: Var&#xA;    stateclass: measurement&#xA;    deviceclass: power&#xA;    icon: mdi:flash-outline&#xA;    accuracydecimals: 0&#xA;    pin: ${pulsekarhpin}&#xA;    internalfilter: 2ms&#xA;    internalfiltermode: &#39;PULSE&#39;&#xA;    onrawvalue:&#xA;      then:&#xA;        light.turnon:&#xA;            id: ledred&#xA;        delay: 0.2s&#xA;        light.turnoff:&#xA;            id: ledred&#xA;    filters:&#xA;      # multiply value = (60 / imp value)  1000&#xA;      # - multiply: 60&#xA;      lambda: return x  ((60.0 / id(selectpulserate).state)  1000.0);&#xA;&#xA;    total:&#xA;      id: sensortotalreactiveenergy&#xA;      name: &#39;Total Reactive Energy Consumption&#39;&#xA;      unitofmeasurement: kVarh&#xA;      icon: mdi:transmission-tower-export&#xA;      stateclass: totalincreasing&#xA;      deviceclass: energy&#xA;      accuracydecimals: 3&#xA;      filters:&#xA;        # multiply value = 1 / imp value&#xA;        # - multiply: 0.001&#xA;        lambda: return x  (1.0 / id(selectpulserate).state);&#xA;&#xA;  # Total day usage&#xA;  platform: totaldailyenergy&#xA;    id: sensortotaldailyreactiveenergy&#xA;    name: &#39;Daily Reactive Energy Consumption&#39;&#xA;    powerid: sensorreactenergypulsemeter&#xA;    unitofmeasurement: kVarh&#xA;    icon: mdi:transmission-tower-export&#xA;    stateclass: totalincreasing&#xA;    deviceclass: energy&#xA;    accuracydecimals: 3&#xA;    filters:&#xA;      # Multiplication factor from Var to kVar is 0.001&#xA;      multiply: 0.001&#xA;&#xA;captiveportal:&#xA;&#xA;Once your modified Home Assistant Glow boots up for the first time, it&#39;s time to calibrate your CT clamp sensor. Please be careful from this point onward, since you&#39;ll be working close to mains AC voltage; stop if you are unsure, and research about what you are doing from reputed sources (hint: not an LLM). Attach the CT clamp around one of the (insulated) power input wires of a purely resistive known load (such as fan-less radiant heater, hair straightener, resistive water heater, etc.), and calculate your amperage, I, from the formula: I=P/V, where P is the rated power of the purely resistive known load, and V is your mains voltage. You can find out your mains voltage using a multi-meter. Avoid using any load that has a motor, compressor, or large inductors (e.g. the ones in computer power supply units) in it, since that will introduce reactive energy in our wires, and mess with our calibration; if you absolutely have to, try to use something that has a weak (or, maybe small DC) motor as a secondary load, the primary load still needs to be purely resistive. Please go through this forum post to learn more about how to work with CT clamps in the context of Home Assistant and ESPHome: https://community.home-assistant.io/t/esphome-ct-calmp-sct-013-000-and-nodemcu/174608/12.&#xA;&#xA;Once you calibrate your CT clamp sensor, it&#39;s time to install the device in place. Simply place the LDR modules in line with the blinking LEDs on your meter (make sure to note what the LED measures, so that you attach the correct LDR module to the corresponding blinking LED), and attach the CT clamp around one of the wires of your main AC input. And voila, you&#39;re done with the hard part!&#xA;&#xA;Integrating with Home Assistant&#xA;&#xA;You may think that once you install the ESPHome device on your meter, and its data starts flowing back to Home Assistant, you&#39;ll be done, but not quite. Since your meter measures the net flow of energy, you still have a lot of calculations left to calculate your consumption, export revenue, return on investment from your solar, etc.&#xA;&#xA;This is where a custom integration I wrote comes in. Please follow the instructions to install it in Home Assistant here. This custom integration will allow us to measure our consumption power and energy, import power and energy, and export power and energy. Please note that this integration is in its MVP (Minimum Viable Product) state, so for now, it works fine for the absolute basic info that it has to calculate.&#xA;&#xA;Once we get the calculations from the custom integration, we&#39;ll find that the calculations can frequently reset, especially if you reboot your Home Assistant, or reset your modified Home Assistant Glow device (again, since the integration is still in its MVP state). While those are rare, they can still introduce large deviations from the real values. To mitigate that, we will create two utility meter helpers.&#xA;&#xA;Today&#39;s Imported Energy: We&#39;ll create this utility meter to aggregate the imported energy data from our custom integration. If we keep the reset interval to daily, we can accurately measure the amount of energy we imported from the grid each day.&#xA;&#xA;Today&#39;s Exported Energy: We&#39;ll create this utility meter to aggregate the exported energy data from our custom integration. If we keep the reset interval to daily, we can accurately measure the amount of energy we exported to the grid each day.&#xA;&#xA;This will also allow us to rapidly develop and test new code without losing too much real data.&#xA;&#xA;At this point, we can effectively set up our Home Assistant Energy Dashboard. We have our solar generation data from our inverter, and our grid import and export data from the two utility meter helpers we just set up. We won&#39;t be able to track costs yet, but at least we&#39;ll get the energy usage and distribution data right in our Home Assistant dashboard.&#xA;&#xA;Further Integration for Economic Data&#xA;&#xA;If you are anything like I am, you can never have too much data. And, something as important as the economics of your solar installation should be something that you&#39;d want to keep track of anyway.&#xA;&#xA;We&#39;ll start with building a couple more utility meter helpers in Home Assistant. We&#39;ll create these aligned to our distributor&#39;s billing cycle, to make all the rest of the calculations fairly simple.&#xA;&#xA;Monthly Solar Generation: We&#39;ll create this utility meter aligned to our billing cycle to measure how much solar power we have generated in a given billing cycle.&#xA;&#xA;Monthly Grid Import: This is similar to Today&#39;s Imported Energy, but we&#39;ll create this utility meter aligned to our billing cycle to measure how much energy we have imported from the grid in a given billing cycle. It might also be better if we choose Today&#39;s Imported Energy as the basis of this aggregation, instead of the Imported Energy sensor value from our custom integration.&#xA;&#xA;Monthly Grid Export: This is similar to Today&#39;s Exported Energy, but we&#39;ll create this utility meter aligned to our billing cycle to measure how much energy we have exported to the grid in a given billing cycle. It might also be better if we choose Today&#39;s Exported Energy as the basis of this aggregation, instead of the Exported Energy sensor value from our custom integration.&#xA;&#xA;Now that we have the important data aligned to our billing cycle, we can make a few template sensors in Home Assistant to take care of the rest!&#xA;&#xA;These are my template sensors in Home Assistant that tracks all the important metrics of my solar installation. Please note, that this part needs the largest number of changes from you, so that you track your costs, not mine:&#xA;&#xA;Templates&#xA;template:&#xA;  sensor:&#xA;      name: Monthly Net Energy Import&#xA;        unitofmeasurement: &#34;kWh&#34;&#xA;        icon: mdi:lightning-bolt-circle&#xA;        defaultentityid: sensor.netrealenergyimport&#xA;        uniqueid: netrealenergyimport&#xA;        deviceclass: energy&#xA;        stateclass: total&#xA;        state: &#34;{{ (states(&#39;sensor.monthlyenergyimport&#39;)|float) - (states(&#39;sensor.monthlyenergyexport&#39;)|float) }}&#34;&#xA;  sensor:&#xA;      name: Monthly CESC Fees and Govt. Tax&#xA;        unitofmeasurement: INR&#xA;        icon: mdi:cash-multiple&#xA;        defaultentityid: sensor.monthlyenergyfeestaxes&#xA;        uniqueid: monthlyenergyfeestaxes&#xA;        deviceclass: monetary&#xA;        stateclass: total&#xA;        state:   {% set grid = states(&#39;sensor.monthlyenergyimport&#39;)| float %}&#xA;            {% set fixedcost = 67.51.082 %}&#xA;            {% if grid|float &lt;= 25 %}&#xA;             {{ (fixedcost + 0.082(grid  5.18 ))  | float(0)|round(2) }}&#xA;            {% elif grid|float   25 and grid|float &lt;= 60 %}&#xA;             {{ (fixedcost + 0.082(25  5.18 + (grid - 25)  5.69 ))  | float(0)|round(2) }}&#xA;            {% elif grid|float   60 and grid|float &lt;= 100 %}&#xA;             {{ (fixedcost + 0.082(25  5.18 + 35  5.69 + (grid - 60)  6.70 ))  | float(0)|round(2) }}&#xA;            {% elif grid|float   100 and grid|float &lt;= 150 %}&#xA;             {{ (fixedcost + 0.082(25  5.18 + 35  5.69 + 40  6.70 + (grid - 100)  7.45 ))  | float(0)|round(2) }}&#xA;            {% elif grid|float   150 and grid|float &lt;= 200 %}&#xA;             {{ (fixedcost + 0.082(25  5.18 + 35  5.69 + 40  6.70 + 50  7.45 + (grid - 150)  7.62 ))  | float(0)|round(2) }}&#xA;            {% elif grid|float   200 and grid|float &lt;= 300 %}&#xA;             {{ (fixedcost + 0.082(25  5.18 + 35  5.69 + 40  6.70 + 50  7.45 + 50  7.62 + (grid - 200)  7.62 ))  | float(0)|round(2) }}&#xA;            {% elif grid|float   300 %}&#xA;             {{ (fixedcost + 0.082(25  5.18 + 35  5.69 + 40  6.70 + 50  7.45 + 50  7.62 + 100  7.62 + (grid - 300)  9.21 ))  | float(0)|round(2) }}&#xA;            {%else%}&#xA;            off&#xA;            {% endif %}&#xA;  sensor:&#xA;      name: Monthly Net Energy Cost&#xA;        unitofmeasurement: INR&#xA;        icon: mdi:cash-multiple&#xA;        defaultentityid: sensor.netenergycost&#xA;        uniqueid: netenergycost&#xA;        deviceclass: monetary&#xA;        stateclass: total&#xA;        state:   {% set grid = states(&#39;sensor.netrealenergyimport&#39;)| float %}&#xA;            {% set feestaxes = states(&#39;sensor.monthlyenergyfeestaxes&#39;)| float %}&#xA;            {% if grid|float &lt;= 0 %}&#xA;             {{ feestaxes  | float(0)|round(2) }}&#xA;            {% elif grid|float   0 and grid|float &lt;= 25 %}&#xA;             {{ (feestaxes + grid  5.18 )  | float(0)|round(2) }}&#xA;            {% elif grid|float   25 and grid|float &lt;= 60 %}&#xA;             {{ (feestaxes + 25  5.18 + (grid - 25)  5.69 )  | float(0)|round(2) }}&#xA;            {% elif grid|float   60 and grid|float &lt;= 100 %}&#xA;             {{ (feestaxes + 25  5.18 + 35  5.69 + (grid - 60)  6.70 )  | float(0)|round(2) }}&#xA;            {% elif grid|float   100 and grid|float &lt;= 150 %}&#xA;             {{ (feestaxes + 25  5.18 + 35  5.69 + 40  6.70 + (grid - 100)  7.45 )  | float(0)|round(2) }}&#xA;            {% elif grid|float   150 and grid|float &lt;= 200 %}&#xA;             {{ (feestaxes + 25  5.18 + 35  5.69 + 40  6.70 + 50  7.45 + (grid - 150)  7.62 )  | float(0)|round(2) }}&#xA;            {% elif grid|float   200 and grid|float &lt;= 300 %}&#xA;             {{ (feestaxes + 25  5.18 + 35  5.69 + 40  6.70 + 50  7.45 + 50  7.62 + (grid - 200)  7.62 )  | float(0)|round(2) }}&#xA;            {% elif grid|float   300 %}&#xA;             {{ (feestaxes + 25  5.18 + 35  5.69 + 40  6.70 + 50  7.45 + 50  7.62 + 100  7.62 + (grid - 300)  9.21 )  | float(0)|round(2) }}&#xA;            {%else%}&#xA;            off&#xA;            {% endif %}&#xA;  sensor:&#xA;      name: Monthly Gross Energy Cost&#xA;        unitofmeasurement: INR&#xA;        icon: mdi:cash-multiple&#xA;        defaultentityid: sensor.grossenergycost&#xA;        uniqueid: grossenergycost&#xA;        deviceclass: monetary&#xA;        stateclass: total&#xA;        state:   {% set grid = states(&#39;sensor.monthlyenergyimport&#39;)| float %}&#xA;            {% set feestaxes = states(&#39;sensor.monthlyenergyfeestaxes&#39;)| float %}&#xA;            {% if grid|float &lt;= 25 %}&#xA;             {{ (feestaxes + grid  5.18 )  | float(0)|round(2) }}&#xA;            {% elif grid|float   25 and grid|float &lt;= 60 %}&#xA;             {{ (feestaxes + 25  5.18 + (grid - 25)  5.69 )  | float(0)|round(2) }}&#xA;            {% elif grid|float   60 and grid|float &lt;= 100 %}&#xA;             {{ (feestaxes + 25  5.18 + 35  5.69 + (grid - 60)  6.70 )  | float(0)|round(2) }}&#xA;            {% elif grid|float   100 and grid|float &lt;= 150 %}&#xA;             {{ (feestaxes + 25  5.18 + 35  5.69 + 40  6.70 + (grid - 100)  7.45 )  | float(0)|round(2) }}&#xA;            {% elif grid|float   150 and grid|float &lt;= 200 %}&#xA;             {{ (feestaxes + 25  5.18 + 35  5.69 + 40  6.70 + 50  7.45 + (grid - 150)  7.62 )  | float(0)|round(2) }}&#xA;            {% elif grid|float   200 and grid|float &lt;= 300 %}&#xA;             {{ (feestaxes + 25  5.18 + 35  5.69 + 40  6.70 + 50  7.45 + 50  7.62 + (grid - 200)  7.62 )  | float(0)|round(2) }}&#xA;            {% elif grid|float   300 %}&#xA;             {{ (feestaxes + 25  5.18 + 35  5.69 + 40  6.70 + 50  7.45 + 50  7.62 + 100  7.62 + (grid - 300)  9.21 )  | float(0)|round(2) }}&#xA;            {%else%}&#xA;            off&#xA;            {% endif %}&#xA;  sensor:&#xA;      name: Monthly Solar Revenue&#xA;        unitofmeasurement: INR&#xA;        icon: mdi:cash-multiple&#xA;        defaultentityid: sensor.solarrevenue&#xA;        uniqueid: solarrevenue&#xA;        deviceclass: monetary&#xA;        stateclass: total&#xA;        state: &#34;{{ ((states(&#39;sensor.grossenergycost&#39;)|float) - (states(&#39;sensor.netenergycost&#39;)|float))|round(2) }}&#34;&#xA;  trigger:&#xA;      platform: state&#xA;        entityid: sensor.monthlyenergyexport&#xA;        notto:&#xA;          unavailable&#xA;          unknown&#xA;      platform: state&#xA;        entityid: sensor.monthlyenergyimport&#xA;        notto:&#xA;          unavailable&#xA;          unknown&#xA;    sensor:&#xA;      name: Monthly Estimated Consumption&#xA;        unitofmeasurement: &#34;kWh&#34;&#xA;        icon: mdi:home-lightning-bolt-outline&#xA;        defaultentityid: sensor.estimatedconsumption&#xA;        uniqueid: estimatedconsumption&#xA;        deviceclass: energy&#xA;        stateclass: total&#xA;        state: &#34;{{ states(&#39;sensor.monthlyenergyimport&#39;)| float + states(&#39;sensor.monthlysolargeneration&#39;)|float - states(&#39;sensor.monthlyenergyexport&#39;)|float }}&#34;&#xA;  sensor:&#xA;      name: Monthly Estimated Consumption Cost&#xA;        unitofmeasurement: INR&#xA;        icon: mdi:cash-multiple&#xA;        defaultentityid: sensor.estimatedconsumptioncost&#xA;        uniqueid: estimatedconsumptioncost&#xA;        deviceclass: monetary&#xA;        stateclass: total&#xA;        state:   {% set grid = states(&#39;sensor.estimatedconsumption&#39;)| float %}&#xA;            {% set fixedcost = 67.5 %}&#xA;            {% if grid|float &lt;= 25 %}&#xA;             {{ (1.082(fixedcost + grid  5.18 ))  | float(0)|round(2) }}&#xA;            {% elif grid|float   25 and grid|float &lt;= 60 %}&#xA;             {{ (1.082(fixedcost + 25  5.18 + (grid - 25)  5.69 ))  | float(0)|round(2) }}&#xA;            {% elif grid|float   60 and grid|float &lt;= 100 %}&#xA;             {{ (1.082(fixedcost + 25  5.18 + 35  5.69 + (grid - 60)  6.70 ))  | float(0)|round(2) }}&#xA;            {% elif grid|float   100 and grid|float &lt;= 150 %}&#xA;             {{ (1.082(fixedcost + 25  5.18 + 35  5.69 + 40  6.70 + (grid - 100)  7.45 ))  | float(0)|round(2) }}&#xA;            {% elif grid|float   150 and grid|float &lt;= 200 %}&#xA;             {{ (1.082(fixedcost + 25  5.18 + 35  5.69 + 40  6.70 + 50  7.45 + (grid - 150)  7.62 ))  | float(0)|round(2) }}&#xA;            {% elif grid|float   200 and grid|float &lt;= 300 %}&#xA;             {{ (1.082(fixedcost + 25  5.18 + 35  5.69 + 40  6.70 + 50  7.45 + 50  7.62 + (grid - 200)  7.62 ))  | float(0)|round(2) }}&#xA;            {% elif grid|float   300 %}&#xA;             {{ (1.082(fixedcost + 25  5.18 + 35  5.69 + 40  6.70 + 50  7.45 + 50  7.62 + 100  7.62 + (grid - 300)  9.21 ))  | float(0)|round(2) }}&#xA;            {%else%}&#xA;            off&#xA;            {% endif %}&#xA;  trigger:&#xA;      platform: state&#xA;        entityid: sensor.estimatedconsumptioncost&#xA;        notto:&#xA;          unavailable&#xA;          unknown&#xA;    sensor:&#xA;      name: Monthly Solar Return on Investment&#xA;        unitofmeasurement: INR&#xA;        icon: mdi:cash-multiple&#xA;        defaultentityid: sensor.solarreturn&#xA;        uniqueid: solarreturn&#xA;        deviceclass: monetary&#xA;        stateclass: total&#xA;        state: &#34;{{ ((states(&#39;sensor.estimatedconsumptioncost&#39;)|float) - (states(&#39;sensor.netenergycost&#39;)|float))|round(2) }}&#34;&#xA;  trigger:&#xA;      platform: sun&#xA;        event: sunset&#xA;        offset: 0&#xA;      platform: numericstate&#xA;        entityid: sensor.lifetimesolarreturnoninvestment&#xA;        above: 153700&#xA;    sensor:&#xA;      name: Solar Estimated Remaining Payback Period&#xA;        icon: mdi:calendar&#xA;        defaultentityid: sensor.solarestimatedremainingpaybackperiod&#xA;        uniqueid: solarestimatedremainingpaybackperiod&#xA;        state:   {% set installdate = strptime(&#39;2025-11-20 00:00:00+0530&#39;, &#39;%Y-%m-%d %H:%M:%S%z&#39;) %}&#xA;            {% set operatingdays = (now() - installdate).days %}&#xA;            {% set totalroi = states(&#39;sensor.lifetimesolarreturnoninvestment&#39;) | float %}&#xA;            {% if totalroi &lt;= 154700 %}&#xA;              {% set daysremaining = operatingdays/totalroi(154700 - totalroi) %}&#xA;              {% if daysremaining &lt; 1 %}&#xA;                {% set operatingseconds = (now().timestamp() - installdate.timestamp()) %}&#xA;                {% set secondsremaining = operatingseconds/totalroi*(150000 - totalroi) | round(0) %}&#xA;                {% set paybackcompletedatetime = now() + timedelta(seconds = secondsremaining) %}&#xA;                {{ timeuntil(paybackcompletedatetime, precision=3) }}&#xA;              {%else%}&#xA;                {% set paybackcompletedatetime = now() + timedelta(days = daysremaining) %}&#xA;                {{ timeuntil(paybackcompletedatetime, precision=3) }}&#xA;              {% endif %}&#xA;            {%else%}&#xA;              {{ timeuntil(now(), precision=0) }}&#xA;            {% endif %}&#xA;Please go through the template and change the necessary items. Pay special attention to the names of the entities, and the staggered tariff that is specific to my situation. Your monthly utility meters might be named different, and unless your distributor is CESC, and you are their LT Domestic (Urban) customer, your tariff will be different too. And of course, your payback period calculation needs to be changed to track your costs, not mine.&#xA;&#xA;But that&#39;s it! This will give you your revenue generated from your rooftop solar for each billing cycle, and will help you calculate how much return you are getting on your investment each billing cycle.&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>So you got a new rooftop solar installation. Your distributor installed a brand new <em>smart</em> meter which turned out to be Iskraemeco ME260, a consumer hostile <em>smart</em> meter with no effective means for you to programmatically gather data (well, almost), and your distributor also turned out to be too consumer hostile to provide you with any means of programmatically getting your consumption data from them either, or even be willing to explain certain functions of <strong>your</strong> <em>smart</em> meter to <strong>you</strong>. If this sounds like you, read on. </p>

<p><strong>Disclaimer:</strong> This is intended mainly for me to remember how to do this later, as such, there is no guarantee that this will work for you. I also <strong>ABSOLUTELY DO NOT</strong> condone work on your house&#39;s main lines unless you know what you are doing, and in some regions, it might even be illegal for you to work on your electrical main lines without being a licensed professional. You won&#39;t actually have to even touch your main lines for this guide, but the disclaimer applies nonetheless.</p>

<hr>

<p><strong>Acknowledgement</strong></p>

<p>Without the <a href="https://www.openhomefoundation.org/">Open Home Foundation</a> and the <a href="https://community.home-assistant.io/">Home Assistant Community</a>, especially the developer and the contributors of <a href="https://glow-energy.io/">Home Assistant Glow</a>, I would not have been able to do any of this.</p>

<p>It would have been easier if <a href="https://www.cesc.co.in/">CESC</a>, my distributor, had helped me with any information about my meter, but they were too busy chastising me for attempting to integrate my energy data to Home Assistant since they think that without time-of-use rates, it&#39;s worthless to monitor my power usage, and that I should just take a reading manually every night if I am so interested. Thanks for fueling this project with spite, I got record turnaround!</p>

<hr>

<h2 id="assumptions">Assumptions</h2>

<p>This guide makes several assumptions that reflect my personal choices. If you have different needs, or if you use something other than what I did, you will need to adapt this guide to your needs.</p>
<ul><li>You have a Iskraemeco ME260 meter.
<ul><li>Or any energy meter that has two indicator lights to indicate the net flow of energy, one light indicating kWh, and one light indicating kVarh, and blinks 3200 times (trivial to change in code later) per unit of kWh or kVarh of net energy flow through the meter. This means that the <strong>blinking LED lights indicate your exported energy when your generation is greater than your consumption, and your imported energy when your consumption is greater than your generation</strong>. If your meter measures your consumption separately, then this blog post doesn&#39;t apply to you, since you already have all the info that we are trying to calculate.</li></ul></li>
<li>You have Home Assistant up and running in your home.</li>
<li>You have accurate generation data from your inverter.</li>
<li>You are comfortable with scripting and tinkering, and have a multi-meter.</li></ul>

<h2 id="things-you-will-need">Things you will need</h2>
<ul><li>1x ESP32, or ESP8266 (either works, I use a ESP8266 since they are so darn cheap, this guide will assume that you are using a NodeMCU ESP8266)</li>
<li>2x LDR Modules with LDR (something like <a href="https://robu.in/product/lm393-photosensitive-light-dependent-control-sensor-module/">this</a>)</li>
<li>1x ADS1115 (since ESP32 and ESP8266 have way too much noise in their ADCs)</li>
<li>1x CT Clamp with voltage output (something like <a href="https://robu.in/product/sct-013-030-non-invasive-ac-current-sensor-clamp-sensor-30a/">this</a>, please choose your required amperage, but remember that in this specific case, it is better to underestimate your amperage than overestimate it)</li>
<li>1x 3.5mm audio jack (3 pin, female)</li>
<li>2x 10kΩ resistors</li>
<li>1x 10µF electrolytic capacitor (10V to 25V is fine)</li>
<li>2x 22µF electrolytic capacitor (optional, but nice to have, 10V to 25V is fine)</li></ul>

<h2 id="wiring">Wiring</h2>

<p><img src="https://media.blog.meghadeep.com/How%20to%20integrate%20CESC%27s%20new%20%22smart%22%20meter%20%28Iskraemeco%20ME260%29%20to%20Home%20Assistant%20with%20behind%20the%20meter%20rooftop%20solar%20installation/grid%20net%20flow%20circuit.png" alt="Fritzing breadboard diagram"></p>

<p>Please let me know if you find any improvements to be made! I&#39;m still learning this stuff!</p>

<h2 id="installation">Installation</h2>

<p>This ESPHome YAML file largely follows Home Assistant Glow, but has some minor changes to include the kVarh pulse meter, and the CT clamp in the same device (you can very easily use vanilla Home Assistant Glow yourself with this project, but then you have to have some other controller measuring you kVarh usage, and you mains amperage). Use your ESPHome dashboard to install this YAML file in your NodeMCU ESP8266. Please pay attention to the keys and passwords, which should be auto-generated by Home Assistant&#39;s ESPHome addon. If you have any deviations from this blog&#39;s assumptions or wiring scheme, you&#39;ll need to make those changes here.</p>

<pre><code>esphome:
  name: grid-net-flow
  friendly_name: &#39;Grid Net Flow&#39;

esp8266:
  board: esp01_1m

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: &#34;7x2fgb3oK7RXgBmWYPjjEXq8vd1ePRXkUcPARYERWzU=&#34;
  services:
    - service: reset_total_real_energy
      then:
        - button.press:
            id: button_reset_total_real
    - service: reset_total_reactive_energy
      then:
        - button.press:
            id: button_reset_total_reactive

ota:
  - platform: esphome
    password: &#34;a742d78ef1fa851e8d7d8e7c06495588&#34;

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: &#34;Grid Net Flow hotspot&#34;
    password: &#34;OHjwap17xhfV&#34;

substitutions:

  # Define the GPIO pins
  pulse_pin: GPIO13
  pulse_karh_pin: GPIO12
  led_pin_red: GPIO2

# Sensors for ESP version and WIFI information
text_sensor:
  # ESPHome version
  - platform: version
    hide_timestamp: true
    name: &#39;Grid Net Flow - ESPHome Version&#39;
  # IP address and connected SSID
  - platform: wifi_info
    ip_address:
      name: &#39;Grid Net Flow - IP Address&#39;
      icon: mdi:wifi
    ssid:
      name: &#39;Grid Net Flow - Connected SSID&#39;
      icon: mdi:wifi-strength-2

# Enable time component to reset energy at midnight
# https://esphome.io/components/time/homeassistant.html
time:
  - platform: homeassistant
    id: homeassistant_time

# Status LED configuration #
output:
  # - platform: gpio
  #   pin: GPIO5
  #   id: output_blue
  - platform: gpio
    pin: ${led_pin_red}
    id: output_red

light:
  - platform: binary
    internal: true
    id: led_red
    name: Red
    output: output_red

# Pulse meter configuration #
number:
  # Set the pulse rate of the LED on your meter
  - platform: template
    id: select_pulse_rate
    name: &#39;Pulse rate - imp/kWh&#39;
    optimistic: true
    mode: box
    min_value: 10
    max_value: 10000
    step: 10
    restore_value: yes
    initial_value: 3200

  # Reset total energy to given value
  - platform: template
    id: select_reset_total_real
    name: &#39;Reset Value - Transient Total Energy kWh&#39;
    entity_category: config
    optimistic: true
    mode: box
    min_value: 0
    max_value: 1000000
    step: 1
    initial_value: 0

  # Reset total energy to given value
  - platform: template
    id: select_reset_total_reactive
    name: &#39;Reset Value - Total Energy kVarh&#39;
    entity_category: config
    optimistic: true
    mode: box
    min_value: 0
    max_value: 1000000
    step: 1
    initial_value: 0

button:
  # Restart the ESP
  - platform: restart
    name: &#34;Restart - Grid Net Flow Sensor&#34;

  # Reset the total real energy entity
  - platform: template
    id: button_reset_total_real
    name: &#34;Reset - Transient Total Real Energy Flow&#34;
    on_press:
      - pulse_meter.set_total_pulses:
          id: sensor_energy_pulse_meter
          value: !lambda &#34;return id(select_reset_total_real).state * id(select_pulse_rate).state;&#34;

  # Reset the total reactive energy entity
  - platform: template
    id: button_reset_total_reactive
    name: &#34;Reset - Total Reactive Energy&#34;
    on_press:
      - pulse_meter.set_total_pulses:
          id: sensor_react_energy_pulse_meter
          value: !lambda &#34;return id(select_reset_total_reactive).state * id(select_pulse_rate).state;&#34;

i2c:
  sda: GPIO4
  scl: GPIO5

ads1115:
  - address: 0x48
    continuous_mode: on

sensor:
  # ADS1115
  - platform: ads1115
    multiplexer: &#39;A0_GND&#39;
    gain: 4.096
    name: &#34;CT Clamp Raw Value&#34;
    id: ads1115_sensor
    update_interval: 0.5s
    accuracy_decimals: 2

  # CT Clamp
  - platform: ct_clamp
    sensor: ads1115_sensor
    name: &#34;Consumption Current&#34;
    sample_duration: 400ms
    update_interval: 0.5s
    id: consumption_current
    accuracy_decimals: 1
    #filters:
      #- calibrate_linear:
          ## Measured value of 0 maps to 0A
          #- 0.00 -&gt; 0.00
          ## Known load: 2.27A
          ## Value shown in logs: 0.08865A
          #- 0.08865 -&gt; 2.27

  # WiFi signal
  - platform: wifi_signal
    name: &#34;Grid Net Flow - WiFi Signal&#34;
    update_interval: 120s

  # Pulse meter kWh
  - platform: pulse_meter
    id: sensor_energy_pulse_meter
    name: &#39;Net Real Power Flow&#39;
    unit_of_measurement: W
    state_class: measurement
    device_class: power
    icon: mdi:flash-outline
    accuracy_decimals: 3
    pin: ${pulse_pin}
    internal_filter: 2ms
    internal_filter_mode: &#39;PULSE&#39;
    on_raw_value:
      then:
        - light.turn_on:
            id: led_red
        - delay: 0.2s
        - light.turn_off:
            id: led_red
    filters:
      # multiply value = (60 / imp value) * 1000
      # - multiply: 60
      - lambda: return x * ((60.0 / id(select_pulse_rate).state) * 1000.0);

    total:
      id: sensor_total_real_energy
      name: &#39;Transient Total Real Energy Flow&#39;
      unit_of_measurement: kWh
      icon: mdi:transmission-tower-export
      state_class: total_increasing
      device_class: energy
      accuracy_decimals: 6
      filters:
        # multiply value = 1 / imp value
        # - multiply: 0.001
        - lambda: return x * (1.0 / id(select_pulse_rate).state);

  # Pulse meter kVarh
  - platform: pulse_meter
    id: sensor_react_energy_pulse_meter
    name: &#39;Reactive Power Consumption&#39;
    unit_of_measurement: Var
    state_class: measurement
    device_class: power
    icon: mdi:flash-outline
    accuracy_decimals: 0
    pin: ${pulse_karh_pin}
    internal_filter: 2ms
    internal_filter_mode: &#39;PULSE&#39;
    on_raw_value:
      then:
        - light.turn_on:
            id: led_red
        - delay: 0.2s
        - light.turn_off:
            id: led_red
    filters:
      # multiply value = (60 / imp value) * 1000
      # - multiply: 60
      - lambda: return x * ((60.0 / id(select_pulse_rate).state) * 1000.0);


    total:
      id: sensor_total_reactive_energy
      name: &#39;Total Reactive Energy Consumption&#39;
      unit_of_measurement: kVarh
      icon: mdi:transmission-tower-export
      state_class: total_increasing
      device_class: energy
      accuracy_decimals: 3
      filters:
        # multiply value = 1 / imp value
        # - multiply: 0.001
        - lambda: return x * (1.0 / id(select_pulse_rate).state);

  # Total day usage
  - platform: total_daily_energy
    id: sensor_total_daily_reactive_energy
    name: &#39;Daily Reactive Energy Consumption&#39;
    power_id: sensor_react_energy_pulse_meter
    unit_of_measurement: kVarh
    icon: mdi:transmission-tower-export
    state_class: total_increasing
    device_class: energy
    accuracy_decimals: 3
    filters:
      # Multiplication factor from Var to kVar is 0.001
      - multiply: 0.001

captive_portal:

</code></pre>

<p>Once your modified Home Assistant Glow boots up for the first time, it&#39;s time to calibrate your CT clamp sensor. Please be careful from this point onward, since you&#39;ll be working close to mains AC voltage; stop if you are unsure, and research about what you are doing from reputed sources (hint: not an LLM). Attach the CT clamp around <strong>one of the</strong> (insulated) power input wires of a purely resistive known load (such as fan-less radiant heater, hair straightener, resistive water heater, etc.), and calculate your amperage, <code>I</code>, from the formula: <code>I=P/V</code>, where <code>P</code> is the rated power of the purely resistive known load, and <code>V</code> is your mains voltage. You can find out your mains voltage using a multi-meter. Avoid using any load that has a motor, compressor, or large inductors (e.g. the ones in computer power supply units) in it, since that will introduce reactive energy in our wires, and mess with our calibration; if you absolutely have to, try to use something that has a weak (or, maybe small DC) motor as a secondary load, the primary load still needs to be purely resistive. Please go through this forum post to learn more about how to work with CT clamps in the context of Home Assistant and ESPHome: <a href="https://community.home-assistant.io/t/esphome-ct-calmp-sct-013-000-and-nodemcu/174608/12">https://community.home-assistant.io/t/esphome-ct-calmp-sct-013-000-and-nodemcu/174608/12</a>.</p>

<p>Once you calibrate your CT clamp sensor, it&#39;s time to install the device in place. Simply place the LDR modules in line with the blinking LEDs on your meter (make sure to note what the LED measures, so that you attach the correct LDR module to the corresponding blinking LED), and attach the CT clamp around one of the wires of your main AC input. And voila, you&#39;re done with the hard part!</p>

<h2 id="integrating-with-home-assistant">Integrating with Home Assistant</h2>

<p>You may think that once you install the ESPHome device on your meter, and its data starts flowing back to Home Assistant, you&#39;ll be done, but not quite. Since your meter measures the net flow of energy, you still have a lot of calculations left to calculate your consumption, export revenue, return on investment from your solar, etc.</p>

<p>This is where a <a href="https://github.com/meghadeep-com/hass-energy-net-meter">custom integration</a> I wrote comes in. Please follow the instructions to install it in Home Assistant <a href="https://github.com/meghadeep-com/hass-energy-net-meter?tab=readme-ov-file#how-to-set-it-up">here</a>. This custom integration will allow us to measure our consumption power and energy, import power and energy, and export power and energy. Please note that this integration is in its MVP (Minimum Viable Product) state, so for now, it works fine for the absolute basic info that it has to calculate.</p>

<p>Once we get the calculations from the custom integration, we&#39;ll find that the calculations can frequently reset, especially if you reboot your Home Assistant, or reset your modified Home Assistant Glow device (again, since the integration is still in its MVP state). While those are rare, they can still introduce large deviations from the real values. To mitigate that, we will create two utility meter helpers.</p>
<ul><li><p><strong>Today&#39;s Imported Energy</strong>: We&#39;ll create this utility meter to aggregate the imported energy data from our custom integration. If we keep the reset interval to daily, we can accurately measure the amount of energy we imported from the grid each day.</p></li>

<li><p><strong>Today&#39;s Exported Energy</strong>: We&#39;ll create this utility meter to aggregate the exported energy data from our custom integration. If we keep the reset interval to daily, we can accurately measure the amount of energy we exported to the grid each day.</p></li></ul>

<p>This will also allow us to rapidly develop and test new code without losing too much real data.</p>

<p>At this point, we can effectively set up our Home Assistant Energy Dashboard. We have our solar generation data from our inverter, and our grid import and export data from the two utility meter helpers we just set up. We won&#39;t be able to track costs yet, but at least we&#39;ll get the energy usage and distribution data right in our Home Assistant dashboard.</p>

<h3 id="further-integration-for-economic-data">Further Integration for Economic Data</h3>

<p>If you are anything like I am, you can never have too much data. And, something as important as the economics of your solar installation should be something that you&#39;d want to keep track of anyway.</p>

<p>We&#39;ll start with building a couple more utility meter helpers in Home Assistant. We&#39;ll create these aligned to our distributor&#39;s billing cycle, to make all the rest of the calculations fairly simple.</p>
<ul><li><p><strong>Monthly Solar Generation</strong>: We&#39;ll create this utility meter aligned to our billing cycle to measure how much solar power we have generated in a given billing cycle.</p></li>

<li><p><strong>Monthly Grid Import</strong>: This is similar to <code>Today&#39;s Imported Energy</code>, but we&#39;ll create this utility meter aligned to our billing cycle to measure how much energy we have imported from the grid in a given billing cycle. It might also be better if we choose <code>Today&#39;s Imported Energy</code> as the basis of this aggregation, instead of the Imported Energy sensor value from our custom integration.</p></li>

<li><p><strong>Monthly Grid Export</strong>: This is similar to <code>Today&#39;s Exported Energy</code>, but we&#39;ll create this utility meter aligned to our billing cycle to measure how much energy we have exported to the grid in a given billing cycle. It might also be better if we choose <code>Today&#39;s Exported Energy</code> as the basis of this aggregation, instead of the Exported Energy sensor value from our custom integration.</p></li></ul>

<p>Now that we have the important data aligned to our billing cycle, we can make a few template sensors in Home Assistant to take care of the rest!</p>

<p>These are my template sensors in Home Assistant that tracks all the important metrics of my solar installation. Please note, that this part needs the largest number of changes from you, so that you track your costs, not mine:</p>

<pre><code># Templates
template:
  - sensor:
      - name: Monthly Net Energy Import
        unit_of_measurement: &#34;kWh&#34;
        icon: mdi:lightning-bolt-circle
        default_entity_id: sensor.net_real_energy_import
        unique_id: net_real_energy_import
        device_class: energy
        state_class: total
        state: &#34;{{ (states(&#39;sensor.monthly_energy_import&#39;)|float) - (states(&#39;sensor.monthly_energy_export&#39;)|float) }}&#34;
  - sensor:
      - name: Monthly CESC Fees and Govt. Tax
        unit_of_measurement: INR
        icon: mdi:cash-multiple
        default_entity_id: sensor.monthly_energy_fees_taxes
        unique_id: monthly_energy_fees_taxes
        device_class: monetary
        state_class: total
        state: &gt;
            {% set grid = states(&#39;sensor.monthly_energy_import&#39;)| float %}
            {% set fixed_cost = 67.5*1.082 %}
            {% if grid|float &lt;= 25 %}
             {{ (fixed_cost + 0.082*(grid * 5.18 ))  | float(0)|round(2) }}
            {% elif grid|float &gt; 25 and grid|float &lt;= 60 %}
             {{ (fixed_cost + 0.082*(25 * 5.18 + (grid - 25) * 5.69 ))  | float(0)|round(2) }}
            {% elif grid|float &gt; 60 and grid|float &lt;= 100 %}
             {{ (fixed_cost + 0.082*(25 * 5.18 + 35 * 5.69 + (grid - 60) * 6.70 ))  | float(0)|round(2) }}
            {% elif grid|float &gt; 100 and grid|float &lt;= 150 %}
             {{ (fixed_cost + 0.082*(25 * 5.18 + 35 * 5.69 + 40 * 6.70 + (grid - 100) * 7.45 ))  | float(0)|round(2) }}
            {% elif grid|float &gt; 150 and grid|float &lt;= 200 %}
             {{ (fixed_cost + 0.082*(25 * 5.18 + 35 * 5.69 + 40 * 6.70 + 50 * 7.45 + (grid - 150) * 7.62 ))  | float(0)|round(2) }}
            {% elif grid|float &gt; 200 and grid|float &lt;= 300 %}
             {{ (fixed_cost + 0.082*(25 * 5.18 + 35 * 5.69 + 40 * 6.70 + 50 * 7.45 + 50 * 7.62 + (grid - 200) * 7.62 ))  | float(0)|round(2) }}
            {% elif grid|float &gt; 300 %}
             {{ (fixed_cost + 0.082*(25 * 5.18 + 35 * 5.69 + 40 * 6.70 + 50 * 7.45 + 50 * 7.62 + 100 * 7.62 + (grid - 300) * 9.21 ))  | float(0)|round(2) }}
            {%else%}
            off
            {% endif %}
  - sensor:
      - name: Monthly Net Energy Cost
        unit_of_measurement: INR
        icon: mdi:cash-multiple
        default_entity_id: sensor.net_energy_cost
        unique_id: net_energy_cost
        device_class: monetary
        state_class: total
        state: &gt;
            {% set grid = states(&#39;sensor.net_real_energy_import&#39;)| float %}
            {% set fees_taxes = states(&#39;sensor.monthly_energy_fees_taxes&#39;)| float %}
            {% if grid|float &lt;= 0 %}
             {{ fees_taxes  | float(0)|round(2) }}
            {% elif grid|float &gt; 0 and grid|float &lt;= 25 %}
             {{ (fees_taxes + grid * 5.18 )  | float(0)|round(2) }}
            {% elif grid|float &gt; 25 and grid|float &lt;= 60 %}
             {{ (fees_taxes + 25 * 5.18 + (grid - 25) * 5.69 )  | float(0)|round(2) }}
            {% elif grid|float &gt; 60 and grid|float &lt;= 100 %}
             {{ (fees_taxes + 25 * 5.18 + 35 * 5.69 + (grid - 60) * 6.70 )  | float(0)|round(2) }}
            {% elif grid|float &gt; 100 and grid|float &lt;= 150 %}
             {{ (fees_taxes + 25 * 5.18 + 35 * 5.69 + 40 * 6.70 + (grid - 100) * 7.45 )  | float(0)|round(2) }}
            {% elif grid|float &gt; 150 and grid|float &lt;= 200 %}
             {{ (fees_taxes + 25 * 5.18 + 35 * 5.69 + 40 * 6.70 + 50 * 7.45 + (grid - 150) * 7.62 )  | float(0)|round(2) }}
            {% elif grid|float &gt; 200 and grid|float &lt;= 300 %}
             {{ (fees_taxes + 25 * 5.18 + 35 * 5.69 + 40 * 6.70 + 50 * 7.45 + 50 * 7.62 + (grid - 200) * 7.62 )  | float(0)|round(2) }}
            {% elif grid|float &gt; 300 %}
             {{ (fees_taxes + 25 * 5.18 + 35 * 5.69 + 40 * 6.70 + 50 * 7.45 + 50 * 7.62 + 100 * 7.62 + (grid - 300) * 9.21 )  | float(0)|round(2) }}
            {%else%}
            off
            {% endif %}
  - sensor:
      - name: Monthly Gross Energy Cost
        unit_of_measurement: INR
        icon: mdi:cash-multiple
        default_entity_id: sensor.gross_energy_cost
        unique_id: gross_energy_cost
        device_class: monetary
        state_class: total
        state: &gt;
            {% set grid = states(&#39;sensor.monthly_energy_import&#39;)| float %}
            {% set fees_taxes = states(&#39;sensor.monthly_energy_fees_taxes&#39;)| float %}
            {% if grid|float &lt;= 25 %}
             {{ (fees_taxes + grid * 5.18 )  | float(0)|round(2) }}
            {% elif grid|float &gt; 25 and grid|float &lt;= 60 %}
             {{ (fees_taxes + 25 * 5.18 + (grid - 25) * 5.69 )  | float(0)|round(2) }}
            {% elif grid|float &gt; 60 and grid|float &lt;= 100 %}
             {{ (fees_taxes + 25 * 5.18 + 35 * 5.69 + (grid - 60) * 6.70 )  | float(0)|round(2) }}
            {% elif grid|float &gt; 100 and grid|float &lt;= 150 %}
             {{ (fees_taxes + 25 * 5.18 + 35 * 5.69 + 40 * 6.70 + (grid - 100) * 7.45 )  | float(0)|round(2) }}
            {% elif grid|float &gt; 150 and grid|float &lt;= 200 %}
             {{ (fees_taxes + 25 * 5.18 + 35 * 5.69 + 40 * 6.70 + 50 * 7.45 + (grid - 150) * 7.62 )  | float(0)|round(2) }}
            {% elif grid|float &gt; 200 and grid|float &lt;= 300 %}
             {{ (fees_taxes + 25 * 5.18 + 35 * 5.69 + 40 * 6.70 + 50 * 7.45 + 50 * 7.62 + (grid - 200) * 7.62 )  | float(0)|round(2) }}
            {% elif grid|float &gt; 300 %}
             {{ (fees_taxes + 25 * 5.18 + 35 * 5.69 + 40 * 6.70 + 50 * 7.45 + 50 * 7.62 + 100 * 7.62 + (grid - 300) * 9.21 )  | float(0)|round(2) }}
            {%else%}
            off
            {% endif %}
  - sensor:
      - name: Monthly Solar Revenue
        unit_of_measurement: INR
        icon: mdi:cash-multiple
        default_entity_id: sensor.solar_revenue
        unique_id: solar_revenue
        device_class: monetary
        state_class: total
        state: &#34;{{ ((states(&#39;sensor.gross_energy_cost&#39;)|float) - (states(&#39;sensor.net_energy_cost&#39;)|float))|round(2) }}&#34;
  - trigger:
      - platform: state
        entity_id: sensor.monthly_energy_export
        not_to:
          - unavailable
          - unknown
      - platform: state
        entity_id: sensor.monthly_energy_import
        not_to:
          - unavailable
          - unknown
    sensor:
      - name: Monthly Estimated Consumption
        unit_of_measurement: &#34;kWh&#34;
        icon: mdi:home-lightning-bolt-outline
        default_entity_id: sensor.estimated_consumption
        unique_id: estimated_consumption
        device_class: energy
        state_class: total
        state: &#34;{{ states(&#39;sensor.monthly_energy_import&#39;)| float + states(&#39;sensor.monthly_solar_generation&#39;)|float - states(&#39;sensor.monthly_energy_export&#39;)|float }}&#34;
  - sensor:
      - name: Monthly Estimated Consumption Cost
        unit_of_measurement: INR
        icon: mdi:cash-multiple
        default_entity_id: sensor.estimated_consumption_cost
        unique_id: estimated_consumption_cost
        device_class: monetary
        state_class: total
        state: &gt;
            {% set grid = states(&#39;sensor.estimated_consumption&#39;)| float %}
            {% set fixed_cost = 67.5 %}
            {% if grid|float &lt;= 25 %}
             {{ (1.082*(fixed_cost + grid * 5.18 ))  | float(0)|round(2) }}
            {% elif grid|float &gt; 25 and grid|float &lt;= 60 %}
             {{ (1.082*(fixed_cost + 25 * 5.18 + (grid - 25) * 5.69 ))  | float(0)|round(2) }}
            {% elif grid|float &gt; 60 and grid|float &lt;= 100 %}
             {{ (1.082*(fixed_cost + 25 * 5.18 + 35 * 5.69 + (grid - 60) * 6.70 ))  | float(0)|round(2) }}
            {% elif grid|float &gt; 100 and grid|float &lt;= 150 %}
             {{ (1.082*(fixed_cost + 25 * 5.18 + 35 * 5.69 + 40 * 6.70 + (grid - 100) * 7.45 ))  | float(0)|round(2) }}
            {% elif grid|float &gt; 150 and grid|float &lt;= 200 %}
             {{ (1.082*(fixed_cost + 25 * 5.18 + 35 * 5.69 + 40 * 6.70 + 50 * 7.45 + (grid - 150) * 7.62 ))  | float(0)|round(2) }}
            {% elif grid|float &gt; 200 and grid|float &lt;= 300 %}
             {{ (1.082*(fixed_cost + 25 * 5.18 + 35 * 5.69 + 40 * 6.70 + 50 * 7.45 + 50 * 7.62 + (grid - 200) * 7.62 ))  | float(0)|round(2) }}
            {% elif grid|float &gt; 300 %}
             {{ (1.082*(fixed_cost + 25 * 5.18 + 35 * 5.69 + 40 * 6.70 + 50 * 7.45 + 50 * 7.62 + 100 * 7.62 + (grid - 300) * 9.21 ))  | float(0)|round(2) }}
            {%else%}
            off
            {% endif %}
  - trigger:
      - platform: state
        entity_id: sensor.estimated_consumption_cost
        not_to:
          - unavailable
          - unknown
    sensor:
      - name: Monthly Solar Return on Investment
        unit_of_measurement: INR
        icon: mdi:cash-multiple
        default_entity_id: sensor.solar_return
        unique_id: solar_return
        device_class: monetary
        state_class: total
        state: &#34;{{ ((states(&#39;sensor.estimated_consumption_cost&#39;)|float) - (states(&#39;sensor.net_energy_cost&#39;)|float))|round(2) }}&#34;
  - trigger:
      - platform: sun
        event: sunset
        offset: 0
      - platform: numeric_state
        entity_id: sensor.lifetime_solar_return_on_investment
        above: 153700
    sensor:
      - name: Solar Estimated Remaining Payback Period
        icon: mdi:calendar
        default_entity_id: sensor.solar_estimated_remaining_payback_period
        unique_id: solar_estimated_remaining_payback_period
        state: &gt;
            {% set install_date = strptime(&#39;2025-11-20 00:00:00+0530&#39;, &#39;%Y-%m-%d %H:%M:%S%z&#39;) %}
            {% set operating_days = (now() - install_date).days %}
            {% set total_roi = states(&#39;sensor.lifetime_solar_return_on_investment&#39;) | float %}
            {% if total_roi &lt;= 154700 %}
              {% set days_remaining = operating_days/total_roi*(154700 - total_roi) %}
              {% if days_remaining &lt; 1 %}
                {% set operating_seconds = (now().timestamp() - install_date.timestamp()) %}
                {% set seconds_remaining = operating_seconds/total_roi*(150000 - total_roi) | round(0) %}
                {% set payback_complete_datetime = now() + timedelta(seconds = seconds_remaining) %}
                {{ time_until(payback_complete_datetime, precision=3) }}
              {%else%}
                {% set payback_complete_datetime = now() + timedelta(days = days_remaining) %}
                {{ time_until(payback_complete_datetime, precision=3) }}
              {% endif %}
            {%else%}
              {{ time_until(now(), precision=0) }}
            {% endif %}
</code></pre>

<p>Please go through the template and change the necessary items. Pay special attention to the names of the entities, and the staggered tariff that is specific to my situation. Your monthly utility meters might be named different, and unless your distributor is CESC, and you are their LT Domestic (Urban) customer, your tariff will be different too. And of course, your payback period calculation needs to be changed to track your costs, not mine.</p>

<p>But that&#39;s it! This will give you your revenue generated from your rooftop solar for each billing cycle, and will help you calculate how much return you are getting on your investment each billing cycle.</p>
]]></content:encoded>
      <guid>https://blog.meghadeep.com/how-to-integrate-cescs-new-smart-meter-iskraemeco-me260-to-home-assistant</guid>
      <pubDate>Sun, 30 Nov 2025 12:25:51 +0000</pubDate>
    </item>
    <item>
      <title>How to host your own Mastodon server using rootless Podman containers</title>
      <link>https://blog.meghadeep.com/how-to-host-your-own-mastodon-server-using-rootless-podman-containers</link>
      <description>&lt;![CDATA[So, you’ve decided to host Mastodon yourself. If you are reading this post, you obviously know what Mastodon is, so I will not waste your time in explaining the things you already know. What I would like to point out, is something that I wish I knew myself before starting on this self-hosted route.&#xA;!--more--&#xA;&#xA;Why should you NOT host Mastodon yourself?&#xA;&#xA;Don’t get me wrong, I love that people are joining Mastodon. I love that decentralized social network is now mainstream. But, there is a difference between joining Mastodon, and hosting a Mastodon instance. If you want to experience Mastodon like any other social networks, just join any well-reputed server that already exists, and you’ll be on your way. But, if you insist on having complete control over your social network presence, and do not want to leave anything up to somebody else’s opinions, including the length of your posts or display name, whether you federate with some specific server, emoji reactions, etc., only then you might want to host your own instance. Since you already considered the advantages of building your own Mastodon instance, let’s take a look at a few downsides to hosting your own.&#xA;&#xA;Moderation headaches:&#xA;&#xA;Even if you plan on running a single-user instance, which my own old personal instance was, you gotta clean up your public feed. This applies even if you don’t subscribe to any relays. And please don’t take moderation lightly. I found and reported (to the FBI) an instance solely used for sharing CSAM (although I was subscribed to two relays at that time, and this was well before Mastodon put in much stronger protection) when one of their members’ account showed up in my federated feed. &#xA;&#xA;Difficult broader outreach:&#xA;&#xA;Unless you plan on hosting a multi-user server serving hundreds of users, you might find broader outreach to be difficult. It is especially difficult for single-user instances, unless you have web notoriety and a lot of people follow you. Although, recently, Mastodon made a lot of changes to help you discover new people. Relays also exist to give your instance a broader outreach without anyone specifically following you.&#xA;&#xA;Storage concerns:&#xA;&#xA;Running Mastodon takes a significant amount of storage space. You either need to look for a cheap S3 compatible object storage service, or be willing to host it in a server you run at your home with a big RAID array. Just as an example, my one year old single-user Mastodon instance uses about 670 GB of storage in my RAID array as of the time of writing this post (keep in mind that I was subscribed to two relays at the time, one quite big, and the other medium sized). If you want to scale, you definitely need S3 compatible object storage in the cloud.&#xA;&#xA;Understood, but how do you host your own instance?&#xA;&#xA;Okay, so you’re adamant about hosting your own Mastodon instance. In that case, let’s look at Podman containers. Containers make everything easier. Whoever made the comic about &#34;it works on my computer&#34;, clearly underestimated the proliferation of containers in the server space. So, to make things easier, we’ll look at how to use Podman rootless containers to host our new Mastodon instance.&#xA;&#xA;Choosing your “version” of Mastodon:&#xA;&#xA;Since Mastodon is open-source, we can choose from multiple different forks of it. In this guide, I will talk about vanilla Mastodon (what mastodon.social runs), Mastodon glitch-soc version (what infosec.exchange, and a bunch of other big servers run), and TheEssem’s fork of glitch-soc (what wetdry.world, and a few other servers including my own run). Let’s run down the main differences of the three.&#xA;&#xA;Vanilla Mastodon&#xA;&#xA;This is the “official” version of Mastodon, and to no one’s surprise, this is also the most widely used version of Mastodon. Most major instances run vanilla Mastodon, including the original server: mastodon.social. With vanilla Mastodon, you will always have a 500 character limit for your toots, 30 character limit for your display name, be stuck to plain text toots, etc. These are the limitations of any other microblogging services that people are used to and are fine with. This also gets the quickest security patches than any other versions/forks, so it is the safest one to use. If you want to use this version, pull the official container image while running your Podman containers:&#xA;&#xA;ghcr.io/mastodon/mastodon:latest&#xA;&#xA;Mastodon glitch-soc&#xA;&#xA;This is a major fork of vanilla Mastodon maintained by an amazing group of volunteers. A lot of major instances like infosec.exchange run this fork of Mastodon. This comes with a lot more customizability, like changing character limits for your toots, display name, bio, publish toots in markdown format, etc. My old personal instance ran this fork of Mastodon. This gets the second quickest security patches, and so, is relatively safe to use. If you want to use this fork, pull this container image while running your Podman containers:&#xA;&#xA;ghcr.io/glitch-soc/mastodon:latest&#xA;&#xA;Chuckya fork of Mastodon (by The Essem)&#xA;&#xA;This is a fork of the glitch-soc fork of Mastodon. This is maintained by one dedicated volunteer, TheEssem on Github. This retains all the customizability of glitch-soc, and adds the ability to react with emojis. Very few instances run this fork, including TheEssem’s wetdry.world, and my new personal instance (meghadeep.social). As much fun as emoji reactions are, please keep in mind that this fork is maintained by one single person for free, so security patches entirely depend on their availability. And, this patch will be merged with glitch-soc soon, so if you can wait for a little while to get emoji reactions, you should stick to the glitch-soc version. If you want to use this fork, pull this container image while running your Podman containers (please note these images are built by me, and pushed manually, so this will take even more time to get an update):&#xA;&#xA;docker.io/meghadeep/mastodon:chuckya-latest&#xA;&#xA;For the purposes of this guide, I will assume that you are using the Chuckya fork (by The Essem on GitHub), since that’s what I personally use.&#xA;&#xA;Preparing your machine:&#xA;&#xA;First, we have to create a few directories where we want to store our data. If you want to use an S3 compatible object storage, you can create these directories anywhere you want, but if you simply wish to use the Linux filesystem to store your Mastodon data, I suggest you create these directories in a partition with plenty of storage space. My old instance had these directories in my RAID array, while my new instance with an S3 compatible object storage has these right in my home partition.&#xA;&#xA;mkdir /path/to/maindirectory&#xA;&#xA;Change the path to your liking.&#xA;&#xA;Now we will create the app directory and db directory inside the main directory.&#xA;&#xA;mkdir /path/to/maindirectory/db&#xA;mkdir /path/to/maindirectory/app&#xA;&#xA;Customizing environment:&#xA;&#xA;Now that the directories are set up, we’ll create the environment file that we want our Mastodon containers to use. This would depend on which “version” of Mastodon you want to use. Please read through the whole environment file to change the required few. I have already put in some defaults that I use in my new personal Mastodon instance, so, you should be fine with only adding the few environment variables that I have not added in this example environment file.&#xA;&#xA;This is a sample configuration file. You can generate your configuration&#xA;with the rake mastodon:setup interactive setup wizard, but to customize&#xA;your setup even further, you&#39;ll need to edit it manually. This sample does&#xA;not demonstrate all available configuration options. Please look at&#xA;https://docs.joinmastodon.org/admin/config/ for the full documentation.&#xA;&#xA;Note that this file accepts slightly different syntax depending on whether&#xA;you are using docker-compose or not. In particular, if you use&#xA;docker-compose, the value of each declared variable will be taken verbatim,&#xA;including surrounding quotes.&#xA;See: https://github.com/mastodon/mastodon/issues/16895&#xA;&#xA;Federation (REQUIRES CHANGES)&#xA;----------&#xA;This identifies your server and cannot be changed safely later&#xA;----------&#xA;LOCALDOMAIN=&#xA;&#xA;Use this only if you need to run mastodon on a different domain than the one used for federation.&#xA;# You can read more about this option on https://docs.joinmastodon.org/admin/config/#web-domain&#xA;DO NOT USE THIS UNLESS YOU KNOW EXACTLY WHAT YOU ARE DOING.&#xA;WEBDOMAIN=mastodon.example.com&#xA;&#xA;Use this if you want to have several aliases handler@example1.com&#xA;handler@example2.com etc. for the same user. LOCALDOMAIN should not&#xA;be added. Comma separated values&#xA;ALTERNATEDOMAINS=example1.com,example2.com&#xA;&#xA;Use HTTP proxy for outgoing request (optional)&#xA;httpproxy=http://gateway.local:8118&#xA;Access control for hidden service.&#xA;ALLOWACCESSTOHIDDENSERVICE=true&#xA;&#xA;Authorized fetch mode (optional)&#xA;Require remote servers to authentify when fetching toots, see&#xA;# https://docs.joinmastodon.org/admin/config/#authorizedfetch&#xA;AUTHORIZEDFETCH=true&#xA;&#xA;Limited federation mode (optional)&#xA;Only allow federation with specific domains, see&#xA;# https://docs.joinmastodon.org/admin/config/#whitelistmode&#xA;LIMITEDFEDERATIONMODE=true&#xA;&#xA;Redis (NO CHANGE)&#xA;-----&#xA;REDISHOST=localhost&#xA;REDISPORT=6379&#xA;REDISNAMESPACE=mastodon&#xA;&#xA;PostgreSQL (REQUIRES CHANGES)&#xA;----------&#xA;DBHOST=localhost&#xA;DBUSER=postgres&#xA;DBNAME=mastodonproduction&#xA;DBPASS=postgres&#xA;DBPORT=5432&#xA;Generate with podman run --rm meghadeep/mastodon:chuckya-latest bin/rails db:encryption:init&#xA;ACTIVERECORDENCRYPTIONPRIMARYKEY=&#xA;ACTIVERECORDENCRYPTIONDETERMINISTICKEY=&#xA;ACTIVERECORDENCRYPTIONKEYDERIVATIONSALT=&#xA;&#xA;Elasticsearch (optional)&#xA;------------------------&#xA;ESENABLED=true&#xA;ESHOST=&#xA;ESPORT=9200&#xA;ESPRESET=&#xA;&#xA;Secrets (REQUIRES CHANGES)&#xA;-------&#xA;Generate each with the RAILSENV=production podman run --rm meghadeep/mastodon:chuckya-latest bundle exec rails secret&#xA;-------&#xA;SECRETKEYBASE= &#xA;OTPSECRET=&#xA;&#xA;Web Push (REQUIRES CHANGES)&#xA;--------&#xA;Generate with podman run --rm meghadeep/mastodon:chuckya-latest rails mastodon:webpush:generatevapidkey (first is the private key, second is the public one)&#xA;You should only generate this once per instance. If you later decide to change it, all push subscription will&#xA;be invalidated, requiring the users to access the website again to resubscribe.&#xA;--------&#xA;VAPIDPRIVATEKEY=&#xA;VAPIDPUBLICKEY=&#xA;&#xA;Registrations (OPTIONAL CHANGES)&#xA;-------------&#xA;&#xA;Single user mode will disable registrations and redirect frontpage to the first profile&#xA;SINGLEUSERMODE=true&#xA;&#xA;Prevent registrations with following e-mail domains&#xA;EMAILDOMAINDENYLIST=example1.com|example2.de|etc&#xA;&#xA;Only allow registrations with the following e-mail domains&#xA;EMAILDOMAINALLOWLIST=&#xA;&#xA;TODO move this&#xA;Optionally change default language&#xA;DEFAULTLOCALE=de&#xA;&#xA;Sending mail (REQUIRES CHANGES)&#xA;------------&#xA;SMTPSERVER=&#xA;SMTPPORT=&#xA;SMTPLOGIN=&#xA;SMTPPASSWORD=&#xA;SMTPFROMADDRESS=&#xA;&#xA;File storage (OPTIONAL CHANGES)&#xA;-----------------------&#xA;The attachment host must allow cross origin request from WEBDOMAIN or&#xA;LOCALDOMAIN if WEBDOMAIN is not set. For example, the server may have the&#xA;following header field:&#xA;Access-Control-Allow-Origin: https://192.168.1.123:9000/&#xA;-----------------------&#xA;S3ENABLED=true&#xA;S3ENDPOINT=&#xA;S3HOSTNAME=&#xA;S3BUCKET=&#xA;AWSACCESSKEYID=&#xA;AWSSECRETACCESSKEY=&#xA;S3PROTOCOL=https&#xA;S3SIGNATUREVERSION=v4&#xA;&#xA;IP and session retention&#xA;-----------------------&#xA;Make sure to modify the scheduling of ipcleanupscheduler in config/sidekiq.yml&#xA;to be less than daily if you lower IPRETENTIONPERIOD below two days (172800).&#xA;-----------------------&#xA;IPRETENTIONPERIOD=31556952&#xA;SESSIONRETENTIONPERIOD=31556952&#xA;&#xA;------------------------------------&#xA;--------  GLITCH-SOC ONLY ----------&#xA;------------------------------------&#xA;&#xA;Swift (optional)&#xA;The attachment host must allow cross origin request - see the description&#xA;above.&#xA;SWIFTENABLED=true&#xA;SWIFTUSERNAME=&#xA;For Keystone V3, the value for SWIFTTENANT should be the project name&#xA;SWIFTTENANT=&#xA;SWIFTPASSWORD=&#xA;Some OpenStack V3 providers require PROJECTID (optional)&#xA;SWIFTPROJECTID=&#xA;Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid&#xA;issues with token rate-limiting during high load.&#xA;SWIFTAUTHURL=&#xA;SWIFTCONTAINER=&#xA;SWIFTOBJECTURL=&#xA;SWIFTREGION=&#xA;Defaults to &#39;default&#39;&#xA;SWIFTDOMAINNAME=&#xA;Defaults to 60 seconds. Set to 0 to disable&#xA;SWIFTCACHETTL=&#xA;&#xA;Optional asset host for multi-server setups&#xA;The asset host must allow cross origin request from WEBDOMAIN or LOCALDOMAIN&#xA;if WEBDOMAIN is not set. For example, the server may have the&#xA;following header field:&#xA;Access-Control-Allow-Origin: https://example.com/&#xA;CDNHOST=https://assets.example.com&#xA;&#xA;Optional list of hosts that are allowed to serve media for your instance&#xA;This is useful if you include external media in your custom CSS or about page,&#xA;or if your data storage provider makes use of redirects to other domains.&#xA;EXTRADATAHOSTS=https://data.example1.com|https://data.example2.com&#xA;&#xA;Optional alias for S3 (e.g. to serve files on a custom domain, possibly using Cloudfront or Cloudflare)&#xA;S3ALIASHOST=&#xA;&#xA;Streaming API integration&#xA;STREAMINGAPIBASEURL=&#xA;&#xA;External authentication (optional)&#xA;----------------------------------&#xA;LDAP authentication (optional)&#xA;LDAPENABLED=true&#xA;LDAPHOST=localhost&#xA;LDAPPORT=389&#xA;LDAPMETHOD=simpletls&#xA;LDAPBASE=&#xA;LDAPBINDDN=&#xA;LDAPPASSWORD=&#xA;LDAPUID=cn&#xA;LDAPMAIL=mail&#xA;LDAPSEARCHFILTER=(|(%{uid}=%{email})(%{mail}=%{email}))&#xA;LDAPUIDCONVERSIONENABLED=true&#xA;LDAPUIDCONVERSIONSEARCH=., -&#xA;LDAPUIDCONVERSIONREPLACE=&#xA;&#xA;PAM authentication (optional)&#xA;PAM authentication uses for the email generation the &#34;email&#34; pam variable&#xA;and optional as fallback PAMDEFAULTSUFFIX&#xA;The pam environment variable &#34;email&#34; is provided by:&#xA;https://github.com/devkral/pamemailextractor&#xA;PAMENABLED=true&#xA;Fallback email domain for email address generation (LOCALDOMAIN by default)&#xA;PAMEMAILDOMAIN=example.com&#xA;Name of the pam service (pam &#34;auth&#34; section is evaluated)&#xA;PAMDEFAULTSERVICE=rpam&#xA;Name of the pam service used for checking if an user can register (pam &#34;account&#34; section is evaluated) (nil (disabled) by default)&#xA;PAMCONTROLLEDSERVICE=rpam&#xA;&#xA;Global OAuth settings (optional) :&#xA;If you have only one strategy, you may want to enable this&#xA;OAUTHREDIRECTATSIGNIN=true&#xA;&#xA;Optional CAS authentication (cf. omniauth-cas) :&#xA;CASENABLED=true&#xA;CASURL=https://sso.myserver.com/&#xA;CASHOST=sso.myserver.com/&#xA;CASPORT=443&#xA;CASSSL=true&#xA;CASVALIDATEURL=&#xA;CASCALLBACKURL=&#xA;CASLOGOUTURL=&#xA;CASLOGINURL=&#xA;CASUIDFIELD=&#39;user&#39;&#xA;CASCAPATH=&#xA;CASDISABLESSLVERIFICATION=false&#xA;CASUIDKEY=&#39;user&#39;&#xA;CASNAMEKEY=&#39;name&#39;&#xA;CASEMAILKEY=&#39;email&#39;&#xA;CASNICKNAMEKEY=&#39;nickname&#39;&#xA;CASFIRSTNAMEKEY=&#39;firstname&#39;&#xA;CASLASTNAMEKEY=&#39;lastname&#39;&#xA;CASLOCATIONKEY=&#39;location&#39;&#xA;CASIMAGEKEY=&#39;image&#39;&#xA;CASPHONEKEY=&#39;phone&#39;&#xA;&#xA;Optional SAML authentication (cf. omniauth-saml)&#xA;SAMLENABLED=true&#xA;SAMLACSURL=http://localhost:3000/auth/auth/saml/callback&#xA;SAMLISSUER=https://example.com&#xA;SAMLIDPSSOTARGETURL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO&#xA;SAMLIDPCERT=&#xA;SAMLIDPCERTFINGERPRINT=&#xA;SAMLNAMEIDENTIFIERFORMAT=RAILSSERVESTATICFILES: true&#xA;SAMLCERT=&#xA;SAMLPRIVATEKEY=&#xA;SAMLSECURITYWANTASSERTIONSIGNED=true&#xA;SAMLSECURITYWANTASSERTIONENCRYPTED=true&#xA;SAMLSECURITYASSUMEEMAILISVERIFIED=true&#xA;SAMLATTRIBUTESSTATEMENTSUID=&#34;urn:oid:0.9.2342.19200300.100.1.1&#34;&#xA;SAMLATTRIBUTESSTATEMENTSEMAIL=&#34;urn:oid:1.3.6.1.4.1.5923.1.1.1.6&#34;&#xA;SAMLATTRIBUTESSTATEMENTSFULLNAME=&#34;urn:oid:2.16.840.1.113730.3.1.241&#34;&#xA;SAMLATTRIBUTESSTATEMENTSFIRSTNAME=&#34;urn:oid:2.5.4.42&#34;&#xA;SAMLATTRIBUTESSTATEMENTSLASTNAME=&#34;urn:oid:2.5.4.4&#34;&#xA;SAMLUIDATTRIBUTE=&#34;urn:oid:0.9.2342.19200300.100.1.1&#34;&#xA;SAMLATTRIBUTESSTATEMENTSVERIFIED=&#xA;SAMLATTRIBUTESSTATEMENTSVERIFIEDEMAIL=&#xA;&#xA;Custom settings&#xA;---------------&#xA;Various ways to customize Mastodon&#39;s behavior&#xA;---------------&#xA;&#xA;Maximum allowed character count (OPTIONAL CHANGES)&#xA;MAXTOOTCHARS=2500&#xA;&#xA;Maximum allowed hashtags to follow in a feed column&#xA;Note that setting this value higher may cause significant&#xA;database load (OPTIONAL CHANGES)&#xA;MAXFEEDHASHTAGS=4&#xA;&#xA;Maximum number of pinned posts (OPTIONAL CHANGES)&#xA;MAXPINNEDTOOTS=5&#xA;&#xA;Maximum allowed bio characters (OPTIONAL CHANGES)&#xA;MAXBIOCHARS=2500&#xA;&#xA;Maximim number of profile fields allowed (OPTIONAL CHANGES)&#xA;MAXPROFILEFIELDS=10&#xA;&#xA;Maximum allowed display name characters (OPTIONAL CHANGES)&#xA;MAXDISPLAYNAMECHARS=200&#xA;&#xA;Maximum allowed poll options (OPTIONAL CHANGES)&#xA;MAXPOLLOPTIONS=10&#xA;&#xA;Maximum allowed poll option characters (OPTIONAL CHANGES)&#xA;MAXPOLLOPTIONCHARS=300&#xA;&#xA;Rails option&#xA;RAILSSERVESTATICFILES=true&#xA;RAILSENV=production&#xA;NODEENV=production&#xA;&#xA;LibreTranslate&#xA;LIBRETRANSLATEENDPOINT=&#xA;&#xA;Maximum image and video/audio upload sizes&#xA;Units are in bytes&#xA;1048576 bytes equals 1 megabyte&#xA;MAXIMAGESIZE=8388608&#xA;MAXVIDEOSIZE=41943040&#xA;&#xA;Maximum search results to display&#xA;Only relevant when elasticsearch is installed&#xA;MAXSEARCHRESULTS=20&#xA;&#xA;Maximum hashtags to display&#xA;Customize the number of hashtags shown in &#39;Explore&#39;&#xA;MAXTRENDINGTAGS=10&#xA;&#xA;Maximum custom emoji file sizes&#xA;If undefined or smaller than MAXEMOJISIZE, the value&#xA;of MAXEMOJISIZE will be used for MAXREMOTEEMOJISIZE&#xA;Units are in bytes&#xA;MAXEMOJISIZE=262144&#xA;MAXREMOTEEMOJISIZE=262144&#xA;&#xA;Optional hCaptcha support&#xA;HCAPTCHASECRETKEY=&#xA;HCAPTCHASITEKEY=&#xA;&#xA;---------------------------------&#xA;------ ESSEM&#39;S FORK ONLY --------&#xA;---------------------------------&#xA;&#xA;Maximum number of emoji reactions per toot and user (minimum 1) (OPTIONAL CHANGES)&#xA;MAXREACTIONS=5&#xA;&#xA;Change the environment variables that need to be changed, and save it as .env.production in the maindirectory.&#xA;&#xA;nano /path/to/maindirectory/.env.production&#xA;&#xA;And paste the entire environment file with your changes. CTRL+X to save and exit.&#xA;&#xA;After we change all the required fields in the environment file, it is time to start initializing our Mastodon instance.&#xA;&#xA;Initializing your Mastodon instance:&#xA;&#xA;As long as you correctly modify the environment file with your own configurations, the hard part for you is done! &#xA;&#xA;Create a new file in the maindirectory to initialize the mastodon instance.&#xA;&#xA;nano /path/to/maindirectory/initialize.sh&#xA;&#xA;Copy-paste the following code snippet, make sure to change your directories for the volume mounts and the environment file. If you are using TheEssem’s fork of Mastodon, you won’t need to change the container images, otherwise change it to any version of Mastodon mentioned at the very beginning:&#xA;&#xA;!/bin/bash&#xA;&#xA;podman pod create \&#xA;--replace \&#xA;--name mastodon-emoji-pod&#xA;&#xA;podman run \&#xA;--replace \&#xA;-d \&#xA;--name mast-emoji-db \&#xA;--pod mastodon-emoji-pod \&#xA;--shm-size=256mb \&#xA;-e &#39;POSTGRESHOSTAUTHMETHOD=trust&#39; \&#xA;-v /path/to/maindirectory/db:/var/lib/postgresql/data:Z \&#xA;--health-cmd=&#39;[&#34;pgisready&#34;, &#34;-U&#34;, &#34;postgres&#34;]&#39; \&#xA;--restart=unless-stopped \&#xA;postgres:14-alpine&#xA;&#xA;podman run \&#xA;--replace \&#xA;-d \&#xA;--name mast-emoji-valkey \&#xA;--pod mastodon-emoji-pod \&#xA;valkey/valkey:latest&#xA;&#xA;echo &#34;waiting for postgres to be ready&#34;&#xA;sleep 10&#xA;&#xA;podman run \&#xA;--replace \&#xA;--rm \&#xA;--name mast-emoji-web \&#xA;--pod mastodon-emoji-pod \&#xA;--env-file=/path/to/maindirectory/.env.production \&#xA;--health-cmd=&#34;wget -q --spider --proxy=off localhost:3000/health || exit 1&#34; \&#xA;-v /path/to/maindirectory/app:/mastodon/public/system:Z \&#xA;--requires=mast-emoji-db,mast-emoji-valkey \&#xA;docker.io/meghadeep/mastodon:chuckya-latest \&#xA;bin/rails db:create&#xA;&#xA;sleep 2&#xA;&#xA;podman run \&#xA;--replace \&#xA;--rm \&#xA;--name mast-emoji-web \&#xA;--pod mastodon-emoji-pod \&#xA;--env-file=/path/to/maindirectory/.env.production \&#xA;--health-cmd=&#34;wget -q --spider --proxy=off localhost:3000/health || exit 1&#34; \&#xA;-v /path/to/maindirectory/app:/mastodon/public/system:Z \&#xA;--requires=mast-emoji-db,mast-emoji-valkey \&#xA;docker.io/meghadeep/mastodon:chuckya-latest \&#xA;bin/rails db:migrate&#xA;&#xA;Make sure you change the paths in the env file and volume mapping parts of the script before saving it!&#xA;&#xA;Make the file executable, and run it:&#xA;&#xA;chmod +x /path/to/maindirectory/initialize.sh&#xA;/path/to/maindirectory/initialize.sh&#xA;&#xA;At this point, our database will have been initialized for production use. The only thing left is to run all the containers necessary.&#xA;&#xA;Running your Mastodon instance:&#xA;&#xA;We will create our last shell script to pull up all the necessary Podman containers. &#xA;&#xA;Create a new file in the maindirectory to help start the mastodon instance.&#xA;&#xA;nano /path/to/maindirectory/compose.sh&#xA;&#xA;Copy-paste the following code snippet, make sure to change your directories for the volume mounts and the environment file. If you are using TheEssem’s fork of Mastodon, you won’t need to change the container images, otherwise change it to any version of Mastodon mentioned at the very beginning:&#xA;&#xA;!/bin/bash&#xA;&#xA;podman pod create \&#xA;--replace \&#xA;--name mastodon-emoji-pod \&#xA;--net=slirp4netns:allowhostloopback=true,porthandler=slirp4netns \&#xA;-p 14000:4000 -p 13000:3000&#xA;&#xA;podman run \&#xA;--replace \&#xA;-d \&#xA;--name mast-emoji-db \&#xA;--pod mastodon-emoji-pod \&#xA;--shm-size=256mb \&#xA;-e &#39;POSTGRESHOSTAUTHMETHOD=trust&#39; \&#xA;-v /path/to/maindirectory/db:/var/lib/postgresql/data:Z \&#xA;--health-cmd=&#39;[&#34;pgisready&#34;, &#34;-U&#34;, &#34;postgres&#34;]&#39; \&#xA;--restart=unless-stopped \&#xA;postgres:14-alpine&#xA;&#xA;podman run \&#xA;--replace \&#xA;-d \&#xA;--name mast-emoji-valkey \&#xA;--pod mastodon-emoji-pod \&#xA;valkey/valkey:latest&#xA;&#xA;podman run \&#xA;--replace \&#xA;-d \&#xA;--name mast-emoji-streaming \&#xA;--pod mastodon-emoji-pod \&#xA;--restart=unless-stopped \&#xA;--env-file=/path/to/maindirectory/.env.production \&#xA;--health-cmd=&#34;wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1&#34; \&#xA;--requires=mast-emoji-db,mast-emoji-valkey \&#xA;docker.io/meghadeep/mastodon:chuckya-streaming-latest \&#xA;node ./streaming&#xA;&#xA;podman run \&#xA;--replace \&#xA;-d \&#xA;--name mast-emoji-web \&#xA;--pod mastodon-emoji-pod \&#xA;--restart=unless-stopped \&#xA;--env-file=/path/to/maindirectory/.env.production \&#xA;--health-cmd=&#34;wget -q --spider --proxy=off localhost:3000/health || exit 1&#34; \&#xA;-v /path/to/maindirectory/app:/mastodon/public/system:Z \&#xA;--requires=mast-emoji-db,mast-emoji-valkey \&#xA;docker.io/meghadeep/mastodon:chuckya-latest \&#xA;bundle exec puma -C config/puma.rb&#xA;&#xA;podman run \&#xA;--replace \&#xA;-d \&#xA;--name mast-emoji-sidekiq \&#xA;--pod mastodon-emoji-pod \&#xA;--restart=unless-stopped \&#xA;--env-file=/path/to/maindirectory/.env.production \&#xA;--health-cmd=&#34;ps aux | grep &#39;[s]idekiq\ 6&#39; || false&#34; \&#xA;-v /path/to/maindirectory/app:/mastodon/public/system:Z \&#xA;--requires=mast-emoji-db,mast-emoji-valkey \&#xA;docker.io/meghadeep/mastodon:chuckya-latest \&#xA;bundle exec sidekiq&#xA;&#xA;Make the file executable, and run it:&#xA;&#xA;chmod +x /path/to/maindirectory/compose.sh&#xA;/path/to/maindirectory/compose.sh&#xA;&#xA;And that’s it! You now have all the containers pulled up for running your own Mastodon instance. Keep in mind that you’ll need a reverse proxy with proper FQDN that you provisioned in the environment file.&#xA;&#xA;I currently use NGINX, and the following config works for me (replace back.end.ip.addr with the IP address of your server running the containers, and please note that I&#39;m only including the reverse proxy location configs within the server directive):&#xA;&#xA;&#x9;&#x9;location /api/v1/streaming {&#xA;&#x9;&#x9;&#x9;proxypass http://back.end.ip.addr:14000/;&#xA;&#x9;&#x9;&#x9;proxyhttpversion 1.1;&#xA;&#x9;&#x9;&#x9;proxyredirect off;&#xA;&#x9;&#x9;&#x9;proxybuffering off;&#xA;&#x9;&#x9;&#x9;proxysetheader Upgrade $httpupgrade;&#xA;&#x9;&#x9;&#x9;proxysetheader Connection Upgrade;&#xA;&#x9;&#x9;&#x9;proxysetheader Host $host;&#xA;&#x9;&#x9;}&#xA;&#x9;&#x9;location ^~ /.well-known/acme-challenge/ {&#xA;&#x9;&#x9;&#x9;tryfiles $uri /;&#xA;&#x9;&#x9;}&#xA;&#x9;&#x9;location / {&#xA;&#x9;&#x9;&#x9;proxypass http://back.end.ip.addr:13000/;&#xA;&#x9;&#x9;&#x9;proxyhttpversion 1.1;&#xA;&#x9;&#x9;&#x9;proxysetheader Upgrade $httpupgrade;&#xA;&#x9;&#x9;&#x9;proxysetheader Connection Upgrade;&#xA;&#x9;&#x9;&#x9;proxysetheader Host $host;&#xA;&#x9;&#x9;&#x9;proxysetheader X-Real-IP $remoteaddr;&#xA;&#x9;&#x9;&#x9;proxysetheader X-Forwarded-For $proxyaddxforwardedfor;&#xA;&#x9;&#x9;&#x9;proxyset_header X-Forwarded-Proto https;&#xA;&#x9;&#x9;}&#xA;&#xA;I have used Apache in the past, and the following config worked for me (replace back.end.ip.addr with the IP address of your server running the containers, and please note that I&#39;m only including the reverse proxy location configs within the virtual host directive):&#xA;&#xA;ProxyPass /.well-known/acme-challenge !&#xA;ProxyPreserveHost On&#xA;RequestHeader set X-Forwarded-Proto &#34;https&#34;&#xA;ProxyPass /api/v1/streaming http://back.end.ip.addr:14000/&#xA;ProxyPass / http://back.end.ip.addr:13000/&#xA;ProxyPassReverse / http://back.end.ip.addr:13000/&#xA;&#xA;Once you have your reverse proxy set up, you’ll need to create and authorize your new Owner account.&#xA;&#xA;podman exec -i mast-emoji-web tootctl accounts create your-local-username --email your-email --confirmed --role Owner&#xA;podman exec -i mast-emoji-web tootctl accounts approve your-local-username&#xA;&#xA;You should now be able to login using your newly created account. Make sure to change the password afterwards.&#xA;&#xA;Welcome to the Fediverse!]]&gt;</description>
      <content:encoded><![CDATA[<p>So, you’ve decided to host Mastodon yourself. If you are reading this post, you obviously know what Mastodon is, so I will not waste your time in explaining the things you already know. What I would like to point out, is something that I wish I knew myself before starting on this self-hosted route.
</p>

<h2 id="why-should-you-not-host-mastodon-yourself">Why should you NOT host Mastodon yourself?</h2>

<p>Don’t get me wrong, I love that people are joining Mastodon. I love that decentralized social network is now mainstream. But, there is a difference between joining Mastodon, and hosting a Mastodon instance. If you want to experience Mastodon like any other social networks, just <a href="https://joinmastodon.org/servers">join any well-reputed server</a> that already exists, and you’ll be on your way. But, if you insist on having complete control over your social network presence, and do not want to leave anything up to somebody else’s opinions, including the length of your posts or display name, whether you federate with some specific server, emoji reactions, etc., only then you might want to host your own instance. Since you already considered the advantages of building your own Mastodon instance, let’s take a look at a few downsides to hosting your own.</p>

<h3 id="moderation-headaches">Moderation headaches:</h3>

<p>Even if you plan on running a single-user instance, which my own old personal instance was, you gotta clean up your public feed. This applies even if you don’t subscribe to any <a href="https://relaylist.com/">relays</a>. And please don’t take moderation lightly. I found and reported (to the FBI) an instance solely used for sharing CSAM (although I was subscribed to two relays at that time, and this was well before Mastodon put in much stronger protection) when one of their members’ account showed up in my federated feed.</p>

<h3 id="difficult-broader-outreach">Difficult broader outreach:</h3>

<p>Unless you plan on hosting a multi-user server serving hundreds of users, you might find broader outreach to be difficult. It is especially difficult for single-user instances, unless you have web notoriety and a lot of people follow you. Although, recently, Mastodon made a lot of changes to help you discover new people. Relays also exist to give your instance a broader outreach without anyone specifically following you.</p>

<h3 id="storage-concerns">Storage concerns:</h3>

<p>Running Mastodon takes a significant amount of storage space. You either need to look for a cheap S3 compatible object storage service, or be willing to host it in a server you run at your home with a big RAID array. Just as an example, my one year old single-user Mastodon instance uses about 670 GB of storage in my RAID array as of the time of writing this post (keep in mind that I was subscribed to two relays at the time, one quite big, and the other medium sized). If you want to scale, you definitely need S3 compatible object storage in the cloud.</p>

<h2 id="understood-but-how-do-you-host-your-own-instance">Understood, but how do you host your own instance?</h2>

<p>Okay, so you’re adamant about hosting your own Mastodon instance. In that case, let’s look at Podman containers. Containers make everything easier. Whoever made the comic about <a href="https://donthitsave.com/comic/2016/07/15/it-works-on-my-computer">“it works on my computer”</a>, clearly underestimated the proliferation of containers in the server space. So, to make things easier, we’ll look at how to use Podman rootless containers to host our new Mastodon instance.</p>

<h3 id="choosing-your-version-of-mastodon">Choosing your “version” of Mastodon:</h3>

<p>Since Mastodon is open-source, we can choose from multiple different forks of it. In this guide, I will talk about vanilla Mastodon (what mastodon.social runs), Mastodon glitch-soc version (what infosec.exchange, and a bunch of other big servers run), and TheEssem’s fork of glitch-soc (what wetdry.world, and a few other servers including my own run). Let’s run down the main differences of the three.</p>

<h4 id="vanilla-mastodon-https-github-com-mastodon-mastodon"><a href="https://github.com/mastodon/mastodon">Vanilla Mastodon</a></h4>

<p>This is the “official” version of Mastodon, and to no one’s surprise, this is also the most widely used version of Mastodon. Most major instances run vanilla Mastodon, including the original server: <a href="https://mastodon.social/">mastodon.social</a>. With vanilla Mastodon, you will always have a 500 character limit for your toots, 30 character limit for your display name, be stuck to plain text toots, etc. These are the limitations of any other microblogging services that people are used to and are fine with. This also gets the quickest security patches than any other versions/forks, so it is the safest one to use. If you want to use this version, pull the official container image while running your Podman containers:</p>

<p><code>ghcr.io/mastodon/mastodon:latest</code></p>

<h4 id="mastodon-glitch-soc-https-github-com-glitch-soc-mastodon"><a href="https://github.com/glitch-soc/mastodon">Mastodon glitch-soc</a></h4>

<p>This is a major fork of vanilla Mastodon maintained by an amazing group of volunteers. A lot of major instances like <a href="https://infosec.exchange/">infosec.exchange</a> run this fork of Mastodon. This comes with a lot more customizability, like changing character limits for your toots, display name, bio, publish toots in markdown format, etc. My old personal instance ran this fork of Mastodon. This gets the second quickest security patches, and so, is relatively safe to use. If you want to use this fork, pull this container image while running your Podman containers:</p>

<p><code>ghcr.io/glitch-soc/mastodon:latest</code></p>

<h4 id="chuckya-fork-of-mastodon-by-the-essem-https-github-com-theessem-mastodon-tree-main"><a href="https://github.com/TheEssem/mastodon/tree/main">Chuckya fork of Mastodon (by The Essem)</a></h4>

<p>This is a fork of the glitch-soc fork of Mastodon. This is maintained by one dedicated volunteer, TheEssem on Github. This retains all the customizability of glitch-soc, and adds the ability to react with emojis. Very few instances run this fork, including TheEssem’s <a href="https://wetdry.world/">wetdry.world</a>, and my new personal instance (<a href="https://meghadeep.social/">meghadeep.social</a>). As much fun as emoji reactions are, please keep in mind that this fork is maintained by one single person for free, so security patches entirely depend on their availability. And, this patch will be merged with glitch-soc soon, so if you can wait for a little while to get emoji reactions, you should stick to the glitch-soc version. If you want to use this fork, pull this container image while running your Podman containers (please note these images are built by me, and pushed manually, so this will take even more time to get an update):</p>

<p><code>docker.io/meghadeep/mastodon:chuckya-latest</code></p>

<p>For the purposes of this guide, I will assume that you are using the Chuckya fork (by The Essem on GitHub), since that’s what I personally use.</p>

<h3 id="preparing-your-machine">Preparing your machine:</h3>

<p>First, we have to create a few directories where we want to store our data. If you want to use an S3 compatible object storage, you can create these directories anywhere you want, but if you simply wish to use the Linux filesystem to store your Mastodon data, I suggest you create these directories in a partition with plenty of storage space. My old instance had these directories in my RAID array, while my new instance with an S3 compatible object storage has these right in my home partition.</p>

<p><code>mkdir /path/to/main_directory</code></p>

<p>Change the path to your liking.</p>

<p>Now we will create the app directory and db directory inside the main directory.</p>

<p><code>mkdir /path/to/main_directory/db</code>
<code>mkdir /path/to/main_directory/app</code></p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20host%20your%20own%20Mastodon%20server%20using%20rootless%20Podman%20containers/directories.png" alt=""></p>

<h3 id="customizing-environment">Customizing environment:</h3>

<p>Now that the directories are set up, we’ll create the environment file that we want our Mastodon containers to use. This would depend on which “version” of Mastodon you want to use. Please read through the whole environment file to change the required few. I have already put in some defaults that I use in my new personal Mastodon instance, so, you should be fine with only adding the few environment variables that I have not added in this example environment file.</p>

<pre><code># This is a sample configuration file. You can generate your configuration
# with the `rake mastodon:setup` interactive setup wizard, but to customize
# your setup even further, you&#39;ll need to edit it manually. This sample does
# not demonstrate all available configuration options. Please look at
# https://docs.joinmastodon.org/admin/config/ for the full documentation.

# Note that this file accepts slightly different syntax depending on whether
# you are using `docker-compose` or not. In particular, if you use
# `docker-compose`, the value of each declared variable will be taken verbatim,
# including surrounding quotes.
# See: https://github.com/mastodon/mastodon/issues/16895

# Federation (REQUIRES CHANGES)
# ----------
# This identifies your server and cannot be changed safely later
# ----------
LOCAL_DOMAIN=

# Use this only if you need to run mastodon on a different domain than the one used for federation.
# You can read more about this option on https://docs.joinmastodon.org/admin/config/#web-domain
# DO *NOT* USE THIS UNLESS YOU KNOW *EXACTLY* WHAT YOU ARE DOING.
# WEB_DOMAIN=mastodon.example.com

# Use this if you want to have several aliases handler@example1.com
# handler@example2.com etc. for the same user. LOCAL_DOMAIN should not
# be added. Comma separated values
# ALTERNATE_DOMAINS=example1.com,example2.com

# Use HTTP proxy for outgoing request (optional)
# http_proxy=http://gateway.local:8118
# Access control for hidden service.
# ALLOW_ACCESS_TO_HIDDEN_SERVICE=true

# Authorized fetch mode (optional)
# Require remote servers to authentify when fetching toots, see
# https://docs.joinmastodon.org/admin/config/#authorized_fetch
AUTHORIZED_FETCH=true

# Limited federation mode (optional)
# Only allow federation with specific domains, see
# https://docs.joinmastodon.org/admin/config/#whitelist_mode
# LIMITED_FEDERATION_MODE=true

# Redis (NO CHANGE)
# -----
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_NAMESPACE=mastodon

# PostgreSQL (REQUIRES CHANGES)
# ----------
DB_HOST=localhost
DB_USER=postgres
DB_NAME=mastodon_production
DB_PASS=postgres
DB_PORT=5432
# Generate with `podman run --rm meghadeep/mastodon:chuckya-latest bin/rails db:encryption:init`
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=

# Elasticsearch (optional)
# ------------------------
# ES_ENABLED=true
# ES_HOST=
# ES_PORT=9200
# ES_PRESET=


# Secrets (REQUIRES CHANGES)
# -------
# Generate each with the `RAILS_ENV=production podman run --rm meghadeep/mastodon:chuckya-latest bundle exec rails secret`
# -------
SECRET_KEY_BASE= 
OTP_SECRET=


# Web Push (REQUIRES CHANGES)
# --------
# Generate with `podman run --rm meghadeep/mastodon:chuckya-latest rails mastodon:webpush:generate_vapid_key` (first is the private key, second is the public one)
# You should only generate this once per instance. If you later decide to change it, all push subscription will
# be invalidated, requiring the users to access the website again to resubscribe.
# --------
VAPID_PRIVATE_KEY=
VAPID_PUBLIC_KEY=


# Registrations (OPTIONAL CHANGES)
# -------------

# Single user mode will disable registrations and redirect frontpage to the first profile
# SINGLE_USER_MODE=true

# Prevent registrations with following e-mail domains
# EMAIL_DOMAIN_DENYLIST=example1.com|example2.de|etc

# Only allow registrations with the following e-mail domains
# EMAIL_DOMAIN_ALLOWLIST=

#TODO move this
# Optionally change default language
# DEFAULT_LOCALE=de


# Sending mail (REQUIRES CHANGES)
# ------------
SMTP_SERVER=
SMTP_PORT=
SMTP_LOGIN=
SMTP_PASSWORD=
SMTP_FROM_ADDRESS=


# File storage (OPTIONAL CHANGES)
# -----------------------
# The attachment host must allow cross origin request from WEB_DOMAIN or
# LOCAL_DOMAIN if WEB_DOMAIN is not set. For example, the server may have the
# following header field:
# Access-Control-Allow-Origin: https://192.168.1.123:9000/
# -----------------------
# S3_ENABLED=true
# S3_ENDPOINT=
# S3_HOSTNAME=
# S3_BUCKET=
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# S3_PROTOCOL=https
# S3_SIGNATURE_VERSION=v4


# IP and session retention
# -----------------------
# Make sure to modify the scheduling of ip_cleanup_scheduler in config/sidekiq.yml
# to be less than daily if you lower IP_RETENTION_PERIOD below two days (172800).
# -----------------------
IP_RETENTION_PERIOD=31556952
SESSION_RETENTION_PERIOD=31556952


# ------------------------------------
# --------  GLITCH-SOC ONLY ----------
# ------------------------------------


# Swift (optional)
# The attachment host must allow cross origin request - see the description
# above.
# SWIFT_ENABLED=true
# SWIFT_USERNAME=
# For Keystone V3, the value for SWIFT_TENANT should be the project name
# SWIFT_TENANT=
# SWIFT_PASSWORD=
# Some OpenStack V3 providers require PROJECT_ID (optional)
# SWIFT_PROJECT_ID=
# Keystone V2 and V3 URLs are supported. Use a V3 URL if possible to avoid
# issues with token rate-limiting during high load.
# SWIFT_AUTH_URL=
# SWIFT_CONTAINER=
# SWIFT_OBJECT_URL=
# SWIFT_REGION=
# Defaults to &#39;default&#39;
# SWIFT_DOMAIN_NAME=
# Defaults to 60 seconds. Set to 0 to disable
# SWIFT_CACHE_TTL=

# Optional asset host for multi-server setups
# The asset host must allow cross origin request from WEB_DOMAIN or LOCAL_DOMAIN
# if WEB_DOMAIN is not set. For example, the server may have the
# following header field:
# Access-Control-Allow-Origin: https://example.com/
# CDN_HOST=https://assets.example.com

# Optional list of hosts that are allowed to serve media for your instance
# This is useful if you include external media in your custom CSS or about page,
# or if your data storage provider makes use of redirects to other domains.
# EXTRA_DATA_HOSTS=https://data.example1.com|https://data.example2.com

# Optional alias for S3 (e.g. to serve files on a custom domain, possibly using Cloudfront or Cloudflare)
# S3_ALIAS_HOST=

# Streaming API integration
# STREAMING_API_BASE_URL=


# External authentication (optional)
# ----------------------------------
# LDAP authentication (optional)
# LDAP_ENABLED=true
# LDAP_HOST=localhost
# LDAP_PORT=389
# LDAP_METHOD=simple_tls
# LDAP_BASE=
# LDAP_BIND_DN=
# LDAP_PASSWORD=
# LDAP_UID=cn
# LDAP_MAIL=mail
# LDAP_SEARCH_FILTER=(|(%{uid}=%{email})(%{mail}=%{email}))
# LDAP_UID_CONVERSION_ENABLED=true
# LDAP_UID_CONVERSION_SEARCH=., -
# LDAP_UID_CONVERSION_REPLACE=_

# PAM authentication (optional)
# PAM authentication uses for the email generation the &#34;email&#34; pam variable
# and optional as fallback PAM_DEFAULT_SUFFIX
# The pam environment variable &#34;email&#34; is provided by:
# https://github.com/devkral/pam_email_extractor
# PAM_ENABLED=true
# Fallback email domain for email address generation (LOCAL_DOMAIN by default)
# PAM_EMAIL_DOMAIN=example.com
# Name of the pam service (pam &#34;auth&#34; section is evaluated)
# PAM_DEFAULT_SERVICE=rpam
# Name of the pam service used for checking if an user can register (pam &#34;account&#34; section is evaluated) (nil (disabled) by default)
# PAM_CONTROLLED_SERVICE=rpam

# Global OAuth settings (optional) :
# If you have only one strategy, you may want to enable this
# OAUTH_REDIRECT_AT_SIGN_IN=true

# Optional CAS authentication (cf. omniauth-cas) :
# CAS_ENABLED=true
# CAS_URL=https://sso.myserver.com/
# CAS_HOST=sso.myserver.com/
# CAS_PORT=443
# CAS_SSL=true
# CAS_VALIDATE_URL=
# CAS_CALLBACK_URL=
# CAS_LOGOUT_URL=
# CAS_LOGIN_URL=
# CAS_UID_FIELD=&#39;user&#39;
# CAS_CA_PATH=
# CAS_DISABLE_SSL_VERIFICATION=false
# CAS_UID_KEY=&#39;user&#39;
# CAS_NAME_KEY=&#39;name&#39;
# CAS_EMAIL_KEY=&#39;email&#39;
# CAS_NICKNAME_KEY=&#39;nickname&#39;
# CAS_FIRST_NAME_KEY=&#39;firstname&#39;
# CAS_LAST_NAME_KEY=&#39;lastname&#39;
# CAS_LOCATION_KEY=&#39;location&#39;
# CAS_IMAGE_KEY=&#39;image&#39;
# CAS_PHONE_KEY=&#39;phone&#39;

# Optional SAML authentication (cf. omniauth-saml)
# SAML_ENABLED=true
# SAML_ACS_URL=http://localhost:3000/auth/auth/saml/callback
# SAML_ISSUER=https://example.com
# SAML_IDP_SSO_TARGET_URL=https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO
# SAML_IDP_CERT=
# SAML_IDP_CERT_FINGERPRINT=
# SAML_NAME_IDENTIFIER_FORMAT=RAILS_SERVE_STATIC_FILES: true
# SAML_CERT=
# SAML_PRIVATE_KEY=
# SAML_SECURITY_WANT_ASSERTION_SIGNED=true
# SAML_SECURITY_WANT_ASSERTION_ENCRYPTED=true
# SAML_SECURITY_ASSUME_EMAIL_IS_VERIFIED=true
# SAML_ATTRIBUTES_STATEMENTS_UID=&#34;urn:oid:0.9.2342.19200300.100.1.1&#34;
# SAML_ATTRIBUTES_STATEMENTS_EMAIL=&#34;urn:oid:1.3.6.1.4.1.5923.1.1.1.6&#34;
# SAML_ATTRIBUTES_STATEMENTS_FULL_NAME=&#34;urn:oid:2.16.840.1.113730.3.1.241&#34;
# SAML_ATTRIBUTES_STATEMENTS_FIRST_NAME=&#34;urn:oid:2.5.4.42&#34;
# SAML_ATTRIBUTES_STATEMENTS_LAST_NAME=&#34;urn:oid:2.5.4.4&#34;
# SAML_UID_ATTRIBUTE=&#34;urn:oid:0.9.2342.19200300.100.1.1&#34;
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED=
# SAML_ATTRIBUTES_STATEMENTS_VERIFIED_EMAIL=


# Custom settings
# ---------------
# Various ways to customize Mastodon&#39;s behavior
# ---------------

# Maximum allowed character count (OPTIONAL CHANGES)
MAX_TOOT_CHARS=2500

# Maximum allowed hashtags to follow in a feed column
# Note that setting this value higher may cause significant
# database load (OPTIONAL CHANGES)
MAX_FEED_HASHTAGS=4

# Maximum number of pinned posts (OPTIONAL CHANGES)
MAX_PINNED_TOOTS=5

# Maximum allowed bio characters (OPTIONAL CHANGES)
MAX_BIO_CHARS=2500

# Maximim number of profile fields allowed (OPTIONAL CHANGES)
MAX_PROFILE_FIELDS=10

# Maximum allowed display name characters (OPTIONAL CHANGES)
MAX_DISPLAY_NAME_CHARS=200

# Maximum allowed poll options (OPTIONAL CHANGES)
MAX_POLL_OPTIONS=10

# Maximum allowed poll option characters (OPTIONAL CHANGES)
MAX_POLL_OPTION_CHARS=300

# Rails option
RAILS_SERVE_STATIC_FILES=true
RAILS_ENV=production
NODE_ENV=production

# LibreTranslate
# LIBRE_TRANSLATE_ENDPOINT=

# Maximum image and video/audio upload sizes
# Units are in bytes
# 1048576 bytes equals 1 megabyte
# MAX_IMAGE_SIZE=8388608
# MAX_VIDEO_SIZE=41943040

# Maximum search results to display
# Only relevant when elasticsearch is installed
# MAX_SEARCH_RESULTS=20

# Maximum hashtags to display
# Customize the number of hashtags shown in &#39;Explore&#39;
# MAX_TRENDING_TAGS=10

# Maximum custom emoji file sizes
# If undefined or smaller than MAX_EMOJI_SIZE, the value
# of MAX_EMOJI_SIZE will be used for MAX_REMOTE_EMOJI_SIZE
# Units are in bytes
# MAX_EMOJI_SIZE=262144
# MAX_REMOTE_EMOJI_SIZE=262144

# Optional hCaptcha support
# HCAPTCHA_SECRET_KEY=
# HCAPTCHA_SITE_KEY=


# ---------------------------------
# ------ ESSEM&#39;S FORK ONLY --------
# ---------------------------------

# Maximum number of emoji reactions per toot and user (minimum 1) (OPTIONAL CHANGES)
MAX_REACTIONS=5
</code></pre>

<p>Change the environment variables that need to be changed, and save it as .env.production in the <code>main_directory</code>.</p>

<p><code>nano /path/to/main_directory/.env.production</code></p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20host%20your%20own%20Mastodon%20server%20using%20rootless%20Podman%20containers/env_file.png" alt=""></p>

<p>And paste the entire environment file with your changes. CTRL+X to save and exit.</p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20host%20your%20own%20Mastodon%20server%20using%20rootless%20Podman%20containers/env_file_2.png" alt=""></p>

<p>After we change all the required fields in the environment file, it is time to start initializing our Mastodon instance.</p>

<h3 id="initializing-your-mastodon-instance">Initializing your Mastodon instance:</h3>

<p>As long as you correctly modify the environment file with your own configurations, the hard part for you is done!</p>

<p>Create a new file in the <code>main_directory</code> to initialize the mastodon instance.</p>

<p><code>nano /path/to/main_directory/initialize.sh</code></p>

<p>Copy-paste the following code snippet, make sure to change your directories for the volume mounts and the environment file. If you are using TheEssem’s fork of Mastodon, you won’t need to change the container images, otherwise change it to any version of Mastodon mentioned at the very beginning:</p>

<pre><code>#!/bin/bash

podman pod create \
--replace \
--name mastodon-emoji-pod

podman run \
--replace \
-d \
--name mast-emoji-db \
--pod mastodon-emoji-pod \
--shm-size=256mb \
-e &#39;POSTGRES_HOST_AUTH_METHOD=trust&#39; \
-v /path/to/main_directory/db:/var/lib/postgresql/data:Z \
--health-cmd=&#39;[&#34;pg_isready&#34;, &#34;-U&#34;, &#34;postgres&#34;]&#39; \
--restart=unless-stopped \
postgres:14-alpine

podman run \
--replace \
-d \
--name mast-emoji-valkey \
--pod mastodon-emoji-pod \
valkey/valkey:latest

echo &#34;waiting for postgres to be ready&#34;
sleep 10

podman run \
--replace \
--rm \
--name mast-emoji-web \
--pod mastodon-emoji-pod \
--env-file=/path/to/main_directory/.env.production \
--health-cmd=&#34;wget -q --spider --proxy=off localhost:3000/health || exit 1&#34; \
-v /path/to/main_directory/app:/mastodon/public/system:Z \
--requires=mast-emoji-db,mast-emoji-valkey \
docker.io/meghadeep/mastodon:chuckya-latest \
bin/rails db:create

sleep 2

podman run \
--replace \
--rm \
--name mast-emoji-web \
--pod mastodon-emoji-pod \
--env-file=/path/to/main_directory/.env.production \
--health-cmd=&#34;wget -q --spider --proxy=off localhost:3000/health || exit 1&#34; \
-v /path/to/main_directory/app:/mastodon/public/system:Z \
--requires=mast-emoji-db,mast-emoji-valkey \
docker.io/meghadeep/mastodon:chuckya-latest \
bin/rails db:migrate
</code></pre>

<p>Make sure you change the paths in the env file and volume mapping parts of the script before saving it!</p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20host%20your%20own%20Mastodon%20server%20using%20rootless%20Podman%20containers/init.png" alt=""></p>

<p>Make the file executable, and run it:</p>

<p><code>chmod +x /path/to/main_directory/initialize.sh</code>
<code>/path/to/main_directory/initialize.sh</code></p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20host%20your%20own%20Mastodon%20server%20using%20rootless%20Podman%20containers/init_2.png" alt=""></p>

<p>At this point, our database will have been initialized for production use. The only thing left is to run all the containers necessary.</p>

<h3 id="running-your-mastodon-instance">Running your Mastodon instance:</h3>

<p>We will create our last shell script to pull up all the necessary Podman containers.</p>

<p>Create a new file in the <code>main_directory</code> to help start the mastodon instance.</p>

<p><code>nano /path/to/main_directory/compose.sh</code></p>

<p>Copy-paste the following code snippet, make sure to change your directories for the volume mounts and the environment file. If you are using TheEssem’s fork of Mastodon, you won’t need to change the container images, otherwise change it to any version of Mastodon mentioned at the very beginning:</p>

<pre><code>#!/bin/bash

podman pod create \
--replace \
--name mastodon-emoji-pod \
--net=slirp4netns:allow_host_loopback=true,port_handler=slirp4netns \
-p 14000:4000 -p 13000:3000

podman run \
--replace \
-d \
--name mast-emoji-db \
--pod mastodon-emoji-pod \
--shm-size=256mb \
-e &#39;POSTGRES_HOST_AUTH_METHOD=trust&#39; \
-v /path/to/main_directory/db:/var/lib/postgresql/data:Z \
--health-cmd=&#39;[&#34;pg_isready&#34;, &#34;-U&#34;, &#34;postgres&#34;]&#39; \
--restart=unless-stopped \
postgres:14-alpine

podman run \
--replace \
-d \
--name mast-emoji-valkey \
--pod mastodon-emoji-pod \
valkey/valkey:latest

podman run \
--replace \
-d \
--name mast-emoji-streaming \
--pod mastodon-emoji-pod \
--restart=unless-stopped \
--env-file=/path/to/main_directory/.env.production \
--health-cmd=&#34;wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1&#34; \
--requires=mast-emoji-db,mast-emoji-valkey \
docker.io/meghadeep/mastodon:chuckya-streaming-latest \
node ./streaming

podman run \
--replace \
-d \
--name mast-emoji-web \
--pod mastodon-emoji-pod \
--restart=unless-stopped \
--env-file=/path/to/main_directory/.env.production \
--health-cmd=&#34;wget -q --spider --proxy=off localhost:3000/health || exit 1&#34; \
-v /path/to/main_directory/app:/mastodon/public/system:Z \
--requires=mast-emoji-db,mast-emoji-valkey \
docker.io/meghadeep/mastodon:chuckya-latest \
bundle exec puma -C config/puma.rb

podman run \
--replace \
-d \
--name mast-emoji-sidekiq \
--pod mastodon-emoji-pod \
--restart=unless-stopped \
--env-file=/path/to/main_directory/.env.production \
--health-cmd=&#34;ps aux | grep &#39;[s]idekiq\ 6&#39; || false&#34; \
-v /path/to/main_directory/app:/mastodon/public/system:Z \
--requires=mast-emoji-db,mast-emoji-valkey \
docker.io/meghadeep/mastodon:chuckya-latest \
bundle exec sidekiq
</code></pre>

<p><img src="https://media.blog.meghadeep.com/How%20to%20host%20your%20own%20Mastodon%20server%20using%20rootless%20Podman%20containers/compose.png" alt=""></p>

<p>Make the file executable, and run it:</p>

<p><code>chmod +x /path/to/main_directory/compose.sh</code>
<code>/path/to/main_directory/compose.sh</code></p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20host%20your%20own%20Mastodon%20server%20using%20rootless%20Podman%20containers/compose_2.png" alt=""></p>

<p>And that’s it! You now have all the containers pulled up for running your own Mastodon instance. Keep in mind that you’ll need a reverse proxy with proper FQDN that you provisioned in the environment file.</p>

<p>I currently use NGINX, and the following config works for me (replace back.end.ip.addr with the IP address of your server running the containers, and please note that I&#39;m only including the reverse proxy location configs within the server directive):</p>

<pre><code>		location /api/v1/streaming {
			proxy_pass http://back.end.ip.addr:14000/;
			proxy_http_version 1.1;
			proxy_redirect off;
			proxy_buffering off;
			proxy_set_header Upgrade $http_upgrade;
			proxy_set_header Connection Upgrade;
			proxy_set_header Host $host;
		}
		location ^~ /.well-known/acme-challenge/ {
			try_files $uri /;
		}
		location / {
			proxy_pass http://back.end.ip.addr:13000/;
			proxy_http_version 1.1;
			proxy_set_header Upgrade $http_upgrade;
			proxy_set_header Connection Upgrade;
			proxy_set_header Host $host;
			proxy_set_header X-Real-IP $remote_addr;
			proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
			proxy_set_header X-Forwarded-Proto https;
		}
</code></pre>

<p>I have used Apache in the past, and the following config worked for me (replace back.end.ip.addr with the IP address of your server running the containers, and please note that I&#39;m only including the reverse proxy location configs within the virtual host directive):</p>

<pre><code>ProxyPass /.well-known/acme-challenge !
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto &#34;https&#34;
ProxyPass /api/v1/streaming http://back.end.ip.addr:14000/
ProxyPass / http://back.end.ip.addr:13000/
ProxyPassReverse / http://back.end.ip.addr:13000/
</code></pre>

<p>Once you have your reverse proxy set up, you’ll need to create and authorize your new Owner account.</p>

<p><code>podman exec -i mast-emoji-web tootctl accounts create &lt;your-local-username&gt; --email &lt;your-email&gt; --confirmed --role Owner</code>
<code>podman exec -i mast-emoji-web tootctl accounts approve &lt;your-local-username&gt;</code></p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20host%20your%20own%20Mastodon%20server%20using%20rootless%20Podman%20containers/account.png" alt=""></p>

<p>You should now be able to login using your newly created account. Make sure to change the password afterwards.</p>

<p>Welcome to the Fediverse!</p>
]]></content:encoded>
      <guid>https://blog.meghadeep.com/how-to-host-your-own-mastodon-server-using-rootless-podman-containers</guid>
      <pubDate>Sun, 30 Mar 2025 10:19:05 +0000</pubDate>
    </item>
    <item>
      <title>Is Nuclear Power Making a Come-Back?</title>
      <link>https://blog.meghadeep.com/is-nuclear-power-making-a-come-back</link>
      <description>&lt;![CDATA[If you are working in the energy sector in the US, you probably already know that Palisades Nuclear Plant, a nuclear power plant that was decommissioned in 2022, is in line to come online as early as next year. And, it’s not the only shuttered plant that can come online this decade; the infamous Three Mile Island nuclear power plant is slated to come online as early as 2028. Even if you are not intimately familiar with the energy sector, you probably have heard rumors that Amazon is going nuclear to power its ginormous datacenters; and that even Microsoft has promised to look into nuclear power to power its next generation of AIs. With all these rumors and excitements in the nuclear power sector, I thought it was time to revisit nuclear power plants, why it might be making a come-back, and where it might be falling short.&#xA;!--more--&#xA;&#xA;Is there a renewed interest in nuclear power around the world?&#xA;&#xA;US&#xA;&#xA;In the US, there are at least two nuclear power plants that are supposed to start commercially producing power within the next five years.&#xA;&#xA;Palisades already signed a power purchase agreement, and plans to start producing power for the grid in late 2025. Holtec International is responsible for bringing the plant online, and they are starting with the existing retired 800 MW unit, with plans to build their SMRs on site after 2030.&#xA;&#xA;Alt text &#xA;Photo credit: Holtec International (https://holtecinternational.com/products-and-services/holtec-palisades/)&#xA;&#xA;Three Mile Island, the infamous site of the worst nuclear power disaster in the US, is also slated to be restarted. Constellation Energy, which bought the reactor that was unaffected by the partial meltdown, plans to start producing power by 2028. Microsoft already has a power purchase agreement with TMI to power its next generation of AIs with 800 MW of nuclear power.&#xA;&#xA;Alt text &#xA;Photo credit: Britannica (https://www.britannica.com/technology/nuclear-reactor/Three-Mile-Island-and-Chernobyl)&#xA;&#xA;India&#xA;&#xA;The Indian Department of Atomic Energy plans to add seven new reactors to its aging fleet by 2029 using public-private joint venture. At present, nuclear power provides a measly 2% of India’s total energy needs; with these seven new reactors, that is supposed to almost double, from 7.48 GWe to 13.08 GWe. DAE is also experimenting with indigenous SMRs, termed Bharat Small Modular Reactor, a 220MW reactor that uses light water instead of the traditional pressurized heavy water, and breeder reactors that can produce more nuclear fuel than it consumes to account for the relatively low nuclear fuel availability in the country. The first indigenous breeder reactor (550 MWe) at Kalpakkam Plant has already loaded the radioactive core, and is working towards achieving criticality. The biggest project is arguably the Kudankulam Nuclear Power Plant, with two already functioning reactors, and four more reactors approved at the site. It is projected to generate a total of 6000 MW of power when completed.&#xA;&#xA;Alt text&#xA;Kudankulam Nuclear Power Plant, Photo credit: IndiaTV (https://www.indiatvnews.com/news/india/kudankulam-nuclear-power-plant-russia-ships-equipment-627451)&#xA;&#xA;China&#xA;&#xA;China has always been a huge proponent of nuclear power, and already has a massive fleet of over fifty reactors. With twenty more reactors under construction as of 2022, China has the fastest growing nuclear power generation in the world. China is also heavily investing in SMRs, and has quite a few of those operating as a part of an extended test program.&#xA;&#xA;Alt text&#xA;Photo credit: CGTN News (https://news.cgtn.com/news/3d3d514f3541544e31457a6333566d54/share.html)&#xA;&#xA;Africa&#xA;&#xA;The sole operating nuclear power plant in all of Africa is in South Africa, and so far, other African countries had been quiet on nuclear power. But, that has started to change.&#xA;&#xA;Ghana is looking to build their own nuclear power plant. The Ministry of Energy (of Ghana) issued a Request for Information to gather technical and financial insights from vendors and countries regarding the deployment of SMRs in Ghana.&#xA;&#xA;Nigeria and Kenya are finalizing their nuclear preparedness applications with the International Atomic Energy Agency (IAEA). While Kenya is in Phase 2, where they are looking for a suitable site to build their first nuclear power plant, Nigeria is still in the initial phase, where they are bringing their infrastructure up to code for a nuclear power plant in the country.&#xA;&#xA;Uganda is on the verge of finalizing their partnership to start their nuclear initiative. 2024 will be a pivotal time in Uganda’s economy as they plan to secure financing required for a successful build of their first nuclear power plant.&#xA;&#xA;Rwanda has already entered into a collaboration with Dual Fluid, a Canadian-German nuclear technology company, to build a new generation demonstration reactor along with overseeing the necessary technology transfer. The demonstration reactor is scheduled to be operational by the end of 2026, and comprehensive technical tests and technology transfer are supposed to be completed by 2028.&#xA;&#xA;Egypt is by far in the most advanced stage of building their own nuclear power plant in Africa outside of South Africa. ROSATOM, the Russian state corporation, is overseeing the construction, and will provide the plant with Russian nuclear fuel after successful completion. The plant is being built in El-Dabaa (320 kilometers northwest of Cairo), and comprises of four reactors, each with a nameplate capacity of 1200 MW. GE has won the contract to provide the plant with their turbines once built. Egypt hopes to put the plant on the grid by 2028.&#xA;&#xA;Alt text&#xA;Photo credit: NPAA (https://web.archive.org/web/20220720201047/https://nppa.gov.eg/en/main-construction-phase-for-the-el-dabaa-nuclear-power-plant-project-and-pouring-of-the-first-concrete-for-unit-1-commenced-in-egypt/)&#xA;&#xA;South America&#xA;&#xA;South America is no stranger to nuclear power. Even though hydroelectric power is readily available in the region, nuclear power has been gaining traction among the South American countries since the 2000s.&#xA;&#xA;Argentina is leading nuclear power adoption in South America, with three already operating nuclear reactors, generating about 5% of the country’s electricity demand, and with a domestically designed SMR under construction. This new SMR, CAREM 25, will be a part of the new generation of reactors in Argentina, since the older three operating plants use traditional heavy water and enriched fuel systems.&#xA;&#xA;Brazil has two nuclear reactors in operation already, with the third reactor having been suspended under construction. Work on the third reactor, Angra 3, was started in 1984, but was suspended in 1986. The project resumed in 2006, but was suspended again in 2015, after the construction was 65% complete. Recently, Eletronuclear, the company in charge of the reactor, got the Brazilian court to give it permission to resume work. The reactor is expected to commercially start producing power in 2031.&#xA;&#xA;Oceania&#xA;&#xA;In Australia, Nuclear power is heavily politicized. The country does not have any energy producing reactors, even though it is rich in Uranium. The sole nuclear reactor in Australia at Lucas Heights is used to produce radioactive materials used in nuclear medicine.&#xA;&#xA;New Zealand goes even further in banning nuclear reactors by enforcing New Zealand Nuclear-Free Zone. Under the New Zealand Nuclear Free Zone, Disarmament, and Arms Control Act of 1987, territorial sea, land and airspace of New Zealand became nuclear-free zones. This has since remained a part of New Zealand’s foreign policy.&#xA;&#xA;European Union&#xA;&#xA;EU is highly divided on nuclear power. And although about 25% of the bloc’s total energy comes from nuclear power, not all countries in EU are in agreement about the future of nuclear power in the region.&#xA;&#xA;Germany is one of the strongest oppositions of nuclear power in the EU. Once a strong proponent of nuclear power, so much so that in 1990, a quarter of the country’s total energy needs came from nuclear plants, Germany has already phased out nuclear power in April 2023.&#xA;&#xA;Portugal, Denmark, and Austria are also heavily against nuclear power. In 2022, these three countries actually filed a legal challenge against the EU, claiming its categorization of nuclear energy as green investment was ‘greenwashing’.&#xA;&#xA;Opinions on nuclear power in Italy are mixed. Italy had previously shuttered all of its nuclear power plants, but in recent years there have been a number of proposals to revive nuclear power.&#xA;&#xA;Earlier this year, the EU held its first ever Nuclear Energy Summit in Brussels. 32 EU member countries attended the summit (Germany was notably absent). The summit highlighted the importance of nuclear power for meeting the region’s decarbonization goals, and had discussions about creating financing pipelines for future projects in the EU.&#xA;&#xA;Opinion&#xA;&#xA;I am very supportive of nuclear power. I think it is a fantastic way to decarbonize our grid, and provides a very safe form of reliable energy. Don’t get me wrong, renewables are great; but at the end of the day, it is dependent on the weather. And while batteries mitigate a lot of the intermittency issues associated with renewables, at its current state, it’s still not the silver bullet that humankind had been waiting for. And although accurate weather forecasts, and high performance grid scale batteries do help, we’d need to massively scale up our installed renewable capacity, and would need to ramp up battery research even more, if we want to completely decarbonize our grid without nuclear power.&#xA;&#xA;Full decarbonization is possible without nuclear power, but it’s just not practical (I may have to eat my words in the future). Lithium mining is highly problematic, with severe geopolitical implications, and other battery technologies (like sodium batteries) are not as energy dense as lithium batteries. And at its present state, batteries themselves are limited with their cycle/lifetime numbers. It is possible to offset the restrictions due to finite cycle/lifetime numbers using more installed capacity, but then we are making the grid purposefully inefficient. Grid scale batteries also store a finite amount of charge (usually 4 hours at full power), and any weather events lasting more than four hours will need to be met with even more installed capacity of renewables+batteries.&#xA;&#xA;Nuclear power, on the other hand, is a mature technology that produces minimal waste products with the highest capacity factor. Nuclear power is stable, and can provide the baseload power even on calm nights. Even at its present state, we have the technology to make nuclear power as safe as humanly possible that can withstand literal Tsunamis (hint: Fukushima Disaster), with safer smaller versions of big nuclear plants right around the corner (hint: SMRs).&#xA;&#xA;SMRs are poised to revolutionize the electric grid with smaller reactors taking up much less space, and with somewhat flexible power output that can ramp up or down according to demand. While they are not as flexible as gas units or batteries, they are also not limited by their available charge nor do they produce any greenhouse gases.&#xA;&#xA;In developing countries like India that are still heavily dependent on coal power, and where residential solar is still very much limited to a minuscule fraction of the already privileged part of the population, nuclear power can have a drastic effect on per capita carbon footprint. Renewables are obviously necessary, but if we use those as those are designed (intermittent resources), we make a much better use of our installed capacity.&#xA;&#xA;Even from a technical standpoint, nuclear power provides the grid with much needed system inertia (MVAR support) that renewables cannot. If we have too much renewable power in our generation mix, we’d need a new type of grid connected technology called synchronous condensers to supply the grid with system inertia. Nuclear power does not come with such requirement.&#xA;&#xA;Nuclear power does come with other restrictions though, such as radioactive waste disposal, and the need for a foolproof disaster management system. Any oversight in either of these invites disaster that can potentially kill tens of thousands of people. As it stands right now, we have effectively solved the disaster management system of nuclear plants; there are Thorium based reactors being tested that can be operated by high schoolers, and has such strong passive protection systems that it’s almost impossible for it to go into a meltdown. And if we look at older nuclear disaster management technologies, it might be controversial, but in my opinion, the Fukushima Disaster actually proved how safe nuclear power was even two decades ago; since even when everything went wrong in the decades old reactor, even when the plant got hit by a literal tsunami, the plant operators could direct the contaminated materials into safe storage tanks, and there had been no recorded deaths directly resulting from the disaster. However, disposal of radioactive waste is still a hot button issue that does not have an easy solution. Even though the amount of radioactive waste a nuclear plant produces is relatively small, the half-lives of the most common radioactive elements produced as waste products are in thousands of years which forces us to think in geological timescale. We need proper storage of radioactive materials in a place that’s geologically stable, and we need effective deterrents that can stand the test of time, hundreds of generations worth of time. So far, the most common method of storing radioactive waste is inside the plant area itself, in large concrete chambers, which isn’t ideal, and desperately needs to be changed.&#xA;&#xA;At the end of the day, we need a good mix of both nuclear power, and renewable power in our grid. It’s not a zero sum game, nuclear power and renewable power both have their own niche that they can target. Right now most of the world’s “peaking plants” are diesel units or gas turbine units. Both very expensive, and diesel units have the added problem of being environmentally disastrous. If we have enough nuclear power that can provide us with the baseload requirement, we can better direct renewables to provide us with power intermittently, without relying too much on batteries. I long to see the day when the whole world is powered by safe and affordable energy without any GHG emissions.]]&gt;</description>
      <content:encoded><![CDATA[<p>If you are working in the energy sector in the US, you probably already know that Palisades Nuclear Plant, a nuclear power plant that was decommissioned in 2022, is in line to come online as early as next year. And, it’s not the only shuttered plant that can come online this decade; the infamous Three Mile Island nuclear power plant is slated to come online as early as 2028. Even if you are not intimately familiar with the energy sector, you probably have heard rumors that Amazon is going nuclear to power its ginormous datacenters; and that even Microsoft has promised to look into nuclear power to power its next generation of AIs. With all these rumors and excitements in the nuclear power sector, I thought it was time to revisit nuclear power plants, why it might be making a come-back, and where it might be falling short.
</p>

<h2 id="is-there-a-renewed-interest-in-nuclear-power-around-the-world">Is there a renewed interest in nuclear power around the world?</h2>

<h3 id="us">US</h3>

<p>In the US, there are at least two nuclear power plants that are supposed to start commercially producing power within the next five years.</p>

<p>Palisades already signed a power purchase agreement, and plans to start producing power for the grid in late 2025. Holtec International is responsible for bringing the plant online, and they are starting with the existing retired 800 MW unit, with plans to build their SMRs on site after 2030.</p>

<p><img src="https://i0.wp.com/holtecinternational.com/wp-content/uploads/2022/06/MicrosoftTeams-image-6.jpg?resize=1024%2C672&amp;ssl=1" alt="Alt text">
Photo credit: Holtec International (<a href="https://holtecinternational.com/products-and-services/holtec-palisades/">https://holtecinternational.com/products-and-services/holtec-palisades/</a>)</p>

<p>Three Mile Island, the infamous site of the worst nuclear power disaster in the US, is also slated to be restarted. Constellation Energy, which bought the reactor that was unaffected by the partial meltdown, plans to start producing power by 2028. Microsoft already has a power purchase agreement with TMI to power its next generation of AIs with 800 MW of nuclear power.</p>

<p><img src="https://cdn.britannica.com/06/117706-050-F15B3FAC/view-Three-Mile-Island-Pennsylvania-Harrisburg.jpg" alt="Alt text">
Photo credit: Britannica (<a href="https://www.britannica.com/technology/nuclear-reactor/Three-Mile-Island-and-Chernobyl">https://www.britannica.com/technology/nuclear-reactor/Three-Mile-Island-and-Chernobyl</a>)</p>

<h3 id="india">India</h3>

<p>The Indian Department of Atomic Energy plans to add seven new reactors to its aging fleet by 2029 using public-private joint venture. At present, nuclear power provides a measly 2% of India’s total energy needs; with these seven new reactors, that is supposed to almost double, from 7.48 GWe to 13.08 GWe. DAE is also experimenting with indigenous SMRs, termed Bharat Small Modular Reactor, a 220MW reactor that uses light water instead of the traditional pressurized heavy water, and breeder reactors that can produce more nuclear fuel than it consumes to account for the relatively low nuclear fuel availability in the country. The first indigenous breeder reactor (550 MWe) at Kalpakkam Plant has already loaded the radioactive core, and is working towards achieving criticality. The biggest project is arguably the Kudankulam Nuclear Power Plant, with two already functioning reactors, and four more reactors approved at the site. It is projected to generate a total of 6000 MW of power when completed.</p>

<p><img src="https://resize.indiatvnews.com/en/resize/newbucket/1200_-/2020/06/kudankulam-nuclear-power-plant-1592552488.jpg" alt="Alt text">
Kudankulam Nuclear Power Plant, Photo credit: IndiaTV (<a href="https://www.indiatvnews.com/news/india/kudankulam-nuclear-power-plant-russia-ships-equipment-627451">https://www.indiatvnews.com/news/india/kudankulam-nuclear-power-plant-russia-ships-equipment-627451</a>)</p>

<h3 id="china">China</h3>

<p>China has always been a huge proponent of nuclear power, and already has a massive fleet of over fifty reactors. With twenty more reactors under construction as of 2022, China has the fastest growing nuclear power generation in the world. China is also heavily investing in SMRs, and has quite a few of those operating as a part of an extended test program.</p>

<p><img src="https://video.cgtn.com/news/3d3d514f3541544e31457a6333566d54/video/24b6ee98b97f493182d3e3df29c3f0d6/24b6ee98b97f493182d3e3df29c3f0d6.jpg" alt="Alt text">
Photo credit: CGTN News (<a href="https://news.cgtn.com/news/3d3d514f3541544e31457a6333566d54/share.html">https://news.cgtn.com/news/3d3d514f3541544e31457a6333566d54/share.html</a>)</p>

<h3 id="africa">Africa</h3>

<p>The sole operating nuclear power plant in all of Africa is in South Africa, and so far, other African countries had been quiet on nuclear power. But, that has started to change.</p>

<p><strong>Ghana</strong> is looking to build their own nuclear power plant. The Ministry of Energy (of Ghana) issued a Request for Information to gather technical and financial insights from vendors and countries regarding the deployment of SMRs in Ghana.</p>

<p><strong>Nigeria</strong> and <strong>Kenya</strong> are finalizing their nuclear preparedness applications with the International Atomic Energy Agency (IAEA). While Kenya is in Phase 2, where they are looking for a suitable site to build their first nuclear power plant, Nigeria is still in the initial phase, where they are bringing their infrastructure up to code for a nuclear power plant in the country.</p>

<p><strong>Uganda</strong> is on the verge of finalizing their partnership to start their nuclear initiative. 2024 will be a pivotal time in Uganda’s economy as they plan to secure financing required for a successful build of their first nuclear power plant.</p>

<p><strong>Rwanda</strong> has already entered into a collaboration with Dual Fluid, a Canadian-German nuclear technology company, to build a new generation demonstration reactor along with overseeing the necessary technology transfer. The demonstration reactor is scheduled to be operational by the end of 2026, and comprehensive technical tests and technology transfer are supposed to be completed by 2028.</p>

<p><strong>Egypt</strong> is by far in the most advanced stage of building their own nuclear power plant in Africa outside of South Africa. ROSATOM, the Russian state corporation, is overseeing the construction, and will provide the plant with Russian nuclear fuel after successful completion. The plant is being built in El-Dabaa (320 kilometers northwest of Cairo), and comprises of four reactors, each with a nameplate capacity of 1200 MW. GE has won the contract to provide the plant with their turbines once built. Egypt hopes to put the plant on the grid by 2028.</p>

<p><img src="https://web.archive.org/web/20220720201047im_/https://nppa.gov.eg/wp-content/uploads/2022/07/nppa.png" alt="Alt text">
Photo credit: NPAA (<a href="https://web.archive.org/web/20220720201047/https://nppa.gov.eg/en/main-construction-phase-for-the-el-dabaa-nuclear-power-plant-project-and-pouring-of-the-first-concrete-for-unit-1-commenced-in-egypt/">https://web.archive.org/web/20220720201047/https://nppa.gov.eg/en/main-construction-phase-for-the-el-dabaa-nuclear-power-plant-project-and-pouring-of-the-first-concrete-for-unit-1-commenced-in-egypt/</a>)</p>

<h3 id="south-america">South America</h3>

<p>South America is no stranger to nuclear power. Even though hydroelectric power is readily available in the region, nuclear power has been gaining traction among the South American countries since the 2000s.</p>

<p><strong>Argentina</strong> is leading nuclear power adoption in South America, with three already operating nuclear reactors, generating about 5% of the country’s electricity demand, and with a domestically designed SMR under construction. This new SMR, CAREM 25, will be a part of the new generation of reactors in Argentina, since the older three operating plants use traditional heavy water and enriched fuel systems.</p>

<p><strong>Brazil</strong> has two nuclear reactors in operation already, with the third reactor having been suspended under construction. Work on the third reactor, Angra 3, was started in 1984, but was suspended in 1986. The project resumed in 2006, but was suspended again in 2015, after the construction was 65% complete. Recently, Eletronuclear, the company in charge of the reactor, got the Brazilian court to give it permission to resume work. The reactor is expected to commercially start producing power in 2031.</p>

<h3 id="oceania">Oceania</h3>

<p>In <strong>Australia</strong>, Nuclear power is heavily politicized. The country does not have any energy producing reactors, even though it is rich in Uranium. The sole nuclear reactor in Australia at Lucas Heights is used to produce radioactive materials used in nuclear medicine.</p>

<p><strong>New Zealand</strong> goes even further in banning nuclear reactors by enforcing New Zealand Nuclear-Free Zone. Under the New Zealand Nuclear Free Zone, Disarmament, and Arms Control Act of 1987, territorial sea, land and airspace of New Zealand became nuclear-free zones. This has since remained a part of New Zealand’s foreign policy.</p>

<h3 id="european-union">European Union</h3>

<p>EU is highly divided on nuclear power. And although about 25% of the bloc’s total energy comes from nuclear power, not all countries in EU are in agreement about the future of nuclear power in the region.</p>

<p><strong>Germany</strong> is one of the strongest oppositions of nuclear power in the EU. Once a strong proponent of nuclear power, so much so that in 1990, a quarter of the country’s total energy needs came from nuclear plants, Germany has already phased out nuclear power in April 2023.</p>

<p><strong>Portugal</strong>, <strong>Denmark</strong>, and <strong>Austria</strong> are also heavily against nuclear power. In 2022, these three countries actually filed a legal challenge against the EU, claiming its categorization of nuclear energy as green investment was ‘greenwashing’.</p>

<p>Opinions on nuclear power in <strong>Italy</strong> are mixed. Italy had previously shuttered all of its nuclear power plants, but in recent years there have been a number of proposals to revive nuclear power.</p>

<p>Earlier this year, the EU held its first ever Nuclear Energy Summit in Brussels. 32 EU member countries attended the summit (Germany was notably absent). The summit highlighted the importance of nuclear power for meeting the region’s decarbonization goals, and had discussions about creating financing pipelines for future projects in the EU.</p>

<h2 id="opinion">Opinion</h2>

<p>I am very supportive of nuclear power. I think it is a fantastic way to decarbonize our grid, and provides a very safe form of reliable energy. Don’t get me wrong, renewables are great; but at the end of the day, it is dependent on the weather. And while batteries mitigate a lot of the intermittency issues associated with renewables, at its current state, it’s still not the silver bullet that humankind had been waiting for. And although accurate weather forecasts, and high performance grid scale batteries do help, we’d need to massively scale up our installed renewable capacity, and would need to ramp up battery research even more, if we want to completely decarbonize our grid without nuclear power.</p>

<p>Full decarbonization is possible without nuclear power, but it’s just not practical (I may have to eat my words in the future). Lithium mining is highly problematic, with severe geopolitical implications, and other battery technologies (like sodium batteries) are not as energy dense as lithium batteries. And at its present state, batteries themselves are limited with their cycle/lifetime numbers. It is possible to offset the restrictions due to finite cycle/lifetime numbers using more installed capacity, but then we are making the grid purposefully inefficient. Grid scale batteries also store a finite amount of charge (usually 4 hours at full power), and any weather events lasting more than four hours will need to be met with even more installed capacity of renewables+batteries.</p>

<p>Nuclear power, on the other hand, is a mature technology that produces minimal waste products with the highest capacity factor. Nuclear power is stable, and can provide the baseload power even on calm nights. Even at its present state, we have the technology to make nuclear power as safe as humanly possible that can withstand literal Tsunamis (hint: Fukushima Disaster), with safer smaller versions of big nuclear plants right around the corner (hint: SMRs).</p>

<p>SMRs are poised to revolutionize the electric grid with smaller reactors taking up much less space, and with somewhat flexible power output that can ramp up or down according to demand. While they are not as flexible as gas units or batteries, they are also not limited by their available charge nor do they produce any greenhouse gases.</p>

<p>In developing countries like India that are still heavily dependent on coal power, and where residential solar is still very much limited to a minuscule fraction of the already privileged part of the population, nuclear power can have a drastic effect on per capita carbon footprint. Renewables are obviously necessary, but if we use those as those are designed (intermittent resources), we make a much better use of our installed capacity.</p>

<p>Even from a technical standpoint, nuclear power provides the grid with much needed system inertia (MVAR support) that renewables cannot. If we have too much renewable power in our generation mix, we’d need a new type of grid connected technology called synchronous condensers to supply the grid with system inertia. Nuclear power does not come with such requirement.</p>

<p>Nuclear power does come with other restrictions though, such as radioactive waste disposal, and the need for a foolproof disaster management system. Any oversight in either of these invites disaster that can potentially kill tens of thousands of people. As it stands right now, we have effectively solved the disaster management system of nuclear plants; there are Thorium based reactors being tested that can be operated by high schoolers, and has such strong passive protection systems that it’s almost impossible for it to go into a meltdown. And if we look at older nuclear disaster management technologies, it might be controversial, but in my opinion, the Fukushima Disaster actually proved how safe nuclear power was even two decades ago; since even when everything went wrong in the decades old reactor, even when the plant got hit by a literal tsunami, the plant operators could direct the contaminated materials into safe storage tanks, and there had been no recorded deaths directly resulting from the disaster. However, disposal of radioactive waste is still a hot button issue that does not have an easy solution. Even though the amount of radioactive waste a nuclear plant produces is relatively small, the half-lives of the most common radioactive elements produced as waste products are in thousands of years which forces us to think in geological timescale. We need proper storage of radioactive materials in a place that’s geologically stable, and we need effective deterrents that can stand the test of time, hundreds of generations worth of time. So far, the most common method of storing radioactive waste is inside the plant area itself, in large concrete chambers, which isn’t ideal, and desperately needs to be changed.</p>

<p>At the end of the day, we need a good mix of both nuclear power, and renewable power in our grid. It’s not a zero sum game, nuclear power and renewable power both have their own niche that they can target. Right now most of the world’s “peaking plants” are diesel units or gas turbine units. Both very expensive, and diesel units have the added problem of being environmentally disastrous. If we have enough nuclear power that can provide us with the baseload requirement, we can better direct renewables to provide us with power intermittently, without relying too much on batteries. I long to see the day when the whole world is powered by safe and affordable energy without any GHG emissions.</p>
]]></content:encoded>
      <guid>https://blog.meghadeep.com/is-nuclear-power-making-a-come-back</guid>
      <pubDate>Tue, 12 Nov 2024 05:24:24 +0000</pubDate>
    </item>
    <item>
      <title>How to create a Wi-Fi hotspot in Fedora Linux Atomic Desktops (Kinoite)</title>
      <link>https://blog.meghadeep.com/how-to-create-a-wi-fi-hotspot-in-fedora-linux-atomic-desktops-kinoite</link>
      <description>&lt;![CDATA[When I went from being an occasional Linux user to an exclusive Linux user about five years ago, I knew there would be times I’d miss the simplicity of having a one-click solution to a lot of things that came with Windows (albeit, at a steep price of being spied on by your own machine). Over the years, I have tinkered with a few Linux machines, and have been missing those solutions less and less, but in these five years, I have not had to deal with creating a hotspot on my laptop. !--more--Recently, I went to a hotel that had a smart TV, but had casting turned off at the router level (the mDNS port was blocked). The Wi-Fi signal itself was very weak too at one part of the room. I thought I’d set up a hotspot using my laptop, that way I would get stable Wi-Fi signal for my phone, and would be able to cast videos on the TV. As luck would have it, it turned out to be quite complicated in the distro I use as my daily driver, and had to give up trying to figure it out during my stay.&#xA;&#xA;I use Fedora Kinoite as my daily driver; it’s one of the new style of “immutable” distros (although we’ve graduated from that term these days, and prefer to use the term “atomic” distros). Other than all the good things that came with immutable, or atomic distros, it meant that I could not use the fantastic tool called linux-wifi-hotspot. Seriously, if you have any regular distros like Ubuntu, Debian, regular Fedora Workstation, etc. use that fantastic tool; you can still use this guide, but it’ll be more complicated for you than it needs to.&#xA;&#xA;Disclaimer: Being a Mechanical Engineer by degree and a Market Analyst by profession, everything I know about computers is a weird mixture of trial-and-error, common sense, Reddit, and StackExchange. So, needless to say, proceed at your own risk.&#xA;&#xA;The first question we need to ask is whether we want a bridged hotspot, or a NATed hotspot. Bridged hotspot is easier to set up, but we lack finer controls over our network (like enabling mDNS for Google Cast, or AirPlay). NATed hotspot may be slightly more complicated to set up, but it comes with a whole lot of creature comforts like improved security, finer network controls, etc. For this blog post, let’s assume we want to set up a NATed hotspot.&#xA;&#xA;First thing we need to do is confirm that our Wi-Fi card (the physical device that allows us to use our router’s Wi-Fi) allows AP mode, and simultaneous operation as a client and an AP. If you are using Fedora Atomic Desktops in a previously Windows laptop, chances are your Wi-Fi card supports both. If it doesn’t support simultaneous operation, we won’t be able to use an existing Wi-Fi connection as our internet connection, and run a hotspot along with it; however, we’d still be able to use some other mode of internet connection (e.g. ethernet, another USB Wi-Fi card, etc.), and run a Wi-Fi hotspot. If our Wi-Fi card does not support AP mode, then we’d need to buy a USB Wi-Fi card that does. To confirm these, run the following command on your terminal:&#xA;&#xA;iw list&#xA;&#xA;Alt text&#xA;&#xA;As long as we see “AP” under the Supported interface modes, we can be sure that our Wi-Fi card supports AP mode.&#xA;&#xA;Alt text&#xA;&#xA;Scroll down, and search for valid interface combinations. As long as we have the total more than 1, we can be sure that our Wi-Fi card supports simultaneous operation. Bonus if we see the number of channels to be more than 1 as well (which would allow us to choose different Wi-Fi channels during simultaneous operation)!&#xA;&#xA;We’d then need to confirm if we have IP forwarding enabled. To do that, run the following command on your terminal:&#xA;&#xA;sudo sysctl -a | grep forward&#xA;&#xA;net.ipv4.ipforward = 1&#xA;net.ipv4.conf.all.forwarding = 1&#xA;net.ipv6.conf.all.forwarding = 1&#xA;&#xA;If you don’t see these in your result, you don’t have IP forwarding enabled. But, don’t worry, it’s quite easy to set up. Create a new file in /etc/sysctl.d/ directory, and write those there:&#xA;&#xA;sudo nano /etc/sysctl.d/50-ipforward.conf&#xA;&#xA;Alt text&#xA;&#xA;Hit CTRL+X to save and exit. Then run the following command for it to take effect immediately.&#xA;&#xA;sudo sysctl -p&#xA;&#xA;Now that you have IP forwarding set up, it’s time to configure the firewall. &#xA;&#xA;You can check if firewalld (the default firewall in Fedora) is active by running the following command in terminal. By default, it should be running.&#xA;&#xA;sudo firewall-cmd --state&#xA;&#xA;In case it is not running, or you get an error, run the following command in terminal:&#xA;&#xA;sudo systemctl restart firewalld&#xA;&#xA;Once you confirm your firewall is active, get the default zone by running the following command in terminal:&#xA;&#xA;sudo firewall-cmd --list-all-zones | grep &#34;(default, active)&#34;&#xA;&#xA;Alt text&#xA;&#xA;Since I changed my default zone to “external”, that’s what it shows me. You may get “public”, or “FedoraWorkstation” as your default zone. Either of those are fine, you’d just need to add “masquerading”. This allows your laptop to be used a NAT (Network Address Translator), which is how we are setting up our hotspot.&#xA;&#xA;Run the following command on your terminal:&#xA;&#xA;sudo firewall-cmd --zone=YOUR-DEFAULT-ZONE --add-masquerade&#xA;&#xA;To make this change permanent, you’d also need to run:&#xA;&#xA;sudo firewall-cmd --zone=YOUR-DEFAULT-ZONE --add-masquerade  --permanent&#xA;&#xA;Alt text&#xA;&#xA;Now, if our Wi-Fi card supports simultaneous operation of AP mode and client mode, we’d need to set up a virtual interface so that we can use an existing Wi-Fi connection as our internet connection while simultaneously providing a Wi-Fi hotspot. Let’s call that virtual interface hotspot to simplify things. We’d need to add this virtual interface to the internal zone of our firewall (if your Wi-Fi card does not support simultaneous operation, replace hotspot with the interface you want to use, run ip link show in terminal to get the name of your interface). To do that, run the following command in the terminal:&#xA;&#xA;sudo firewall-cmd --zone=internal --change-interface=hotspot --permanent&#xA;&#xA;Alt text&#xA;&#xA;Now, we’d need to make a new firewall policy to allow data from the internal zone to the outside internet, and valid existing connections from the internet to our internal zone. We’d also need to make sure we allow DNS queries, and DHCP queries in the internal zone. Run the following commands in the terminal in order:&#xA;&#xA;sudo firewall-cmd --permanent --new-policy int2ext&#xA;sudo firewall-cmd --permanent --policy int2ext --add-ingress-zone internal&#xA;sudo firewall-cmd --permanent --policy int2ext --add-egress-zone YOUR-DEFAULT-ZONE&#xA;sudo firewall-cmd --permanent --policy int2ext --set-target ACCEPT&#xA;sudo firewall-cmd --permanent --zone=internal --add-service=dns&#xA;sudo firewall-cmd --permanent --zone=internal --add-service=mdns&#xA;sudo firewall-cmd --permanent --zone=internal --add-service=dhcp&#xA;sudo firewall-cmd --permanent --zone=internal --add-service=dhcpv6-client&#xA;sudo firewall-cmd --reload&#xA;&#xA;At this point, we want to make sure we have all the dependencies installed. We’ll use a program called “hostapd” to manage the hotspot itself, and “dnsmasq” to manage the clients that connect to your hotspot. In Fedora Atomic Desktops, we can do that by running:&#xA;&#xA;sudo rpm-ostree install hostapd dnsmasq --apply-live&#xA;&#xA;After those are installed, you’d need to configure them. Run the following commands on your terminal to edit the hostapd.conf file:&#xA;&#xA;sudo mv /etc/hostapd/hostapd.conf /etc/hostapd/hostapd.conf.bak&#xA;sudo nano /etc/hostapd/hostapd.conf&#xA;&#xA;Following is my hostapd.conf file. For the most part, you can copy-paste this file, just make sure you edit the required parts:&#xA;&#xA;#&#xA;This will give you a minimal, secure wireless network.&#xA; &#xA;DO NOT BE SATISFIED WITH THAT!!!&#xA;&#xA;A complete, well commented example configuration file is&#xA;available here:&#xA;&#xA;/usr/share/doc/hostapd/hostapd.conf&#xA;&#xA;For more information, look here:&#xA;&#xA;http://wireless.kernel.org/en/users/Documentation/hostapd&#xA;&#xA;ctrlinterface=/var/run/hostapd&#xA;ctrlinterfacegroup=wheel&#xA;&#xA;Some usable default settings...&#xA;macaddracl=0&#xA;authalgs=1&#xA;ignorebroadcastssid=0&#xA;&#xA;Uncomment these for base WPA2 support with a pre-shared key&#xA;wpa=2&#xA;wpakeymgmt=WPA-PSK&#xA;wpapairwise=TKIP&#xA;rsnpairwise=CCMP&#xA;&#xA;DO NOT FORGET TO SET A WPA PASSPHRASE!!&#xA;wpapassphrase=MY-SUPER-SECURE-HOTSPOT-PASSWORD&#xA;&#xA;Most modern wireless drivers in the kernel need driver=nl80211&#xA;driver=nl80211&#xA;&#xA;Customize these for your local configuration...&#xA;Change the interface if you are enabling your hotspot in some other&#xA;network interface (like a USB card)&#xA;interface=hotspot&#xA;This will start the hotspot in 2.4GHz mode &#xA;hwmode=g&#xA;channel=7&#xA;ieee80211n=1&#xA;Change your hotspot name&#xA;ssid=MY-HOTSPOT-NAME&#xA;Enables QoS (Quality of Service)&#xA;wmmenabled=1&#xA;&#xA;Hit CTRL+X to save and exit. Please note the channel number in this file. If your Wi-Fi card does not support operation on multiple channels simultaneously, you will have to change the channel number to what your Wi-Fi card is already using to connect to the internet.&#xA;&#xA;We’d now configure dnsmasq. Dnsmasq is responsible for managing the clients that connect to your hotspot by providing them with dynamic local IPs, and a lightweight DNS server.&#xA;&#xA;sudo mv /etc/dnsmasq.conf /etc/dnsmasq.conf.bak&#xA;sudo nano /etc/dnsmasq.conf&#xA;&#xA;Following is my dnsmasq config file. You can copy-paste this for a quick easy configuration. You shouldn’t need to change anything, but do go through the comments to understand what’s happening behind the scenes.&#xA;&#xA;If you want dnsmasq to change uid and gid to something other&#xA;than the default, edit the following lines.&#xA;user=dnsmasq&#xA;group=dnsmasq&#xA;&#xA;If you want dnsmasq to listen for DHCP and DNS requests only on&#xA;specified interfaces (and the loopback) give the name of the&#xA;interface (eg eth0) here.&#xA;Repeat the line for more than one interface.&#xA;interface=hotspot&#xA;&#xA;Serve DNS and DHCP only to networks directly connected to this machine.&#xA;Any interface= line will override it.&#xA;local-service&#xA;Accept queries in default configuration only from localhost&#xA;Comment out following option or explicitly configure interfaces or&#xA;listen-address&#xA;local-service=host&#xA;&#xA;To listen only on localhost and do not receive packets on other&#xA;interfaces, bind only to lo device. Comment out to bind on single&#xA;wildcard socket.&#xA;bind-interfaces&#xA;Comment out above line and uncoment following 2 lines.&#xA;Update interface name, use ip link to get its name.&#xA;bind-dynamic&#xA;interface=hotspot&#xA;&#xA;Uncomment this to enable the integrated DHCP server, you need&#xA;to supply the range of addresses available for lease and optionally&#xA;a lease time. If you have more than one network, you will need to&#xA;repeat this for each network on which you want to supply DHCP&#xA;service.&#xA;dhcp-range=192.168.253.50,192.168.253.150,12h&#xA;&#xA;Set the DHCP server to authoritative mode. In this mode it will barge in&#xA;and take over the lease for any client which broadcasts on the network,&#xA;whether it has a record of the lease or not. This avoids long timeouts&#xA;when a machine wakes up on a new network. DO NOT enable this if there&#39;s&#xA;the slightest chance that you might end up accidentally configuring a DHCP&#xA;server for your campus/company accidentally. The ISC server uses&#xA;the same option, and this URL provides more information:&#xA;http://www.isc.org/files/auth.html&#xA;dhcp-authoritative&#xA;&#xA;Include all files in /etc/dnsmasq.d except RPM backup files&#xA;conf-dir=/etc/dnsmasq.d,.rpmnew,.rpmsave,.rpmorig&#xA;&#xA;Hit CTRL+X to save and exit. The hard part is almost over! We’ll just need few more files, and we’ll be on our way to start the hotspot! We’ll need to find the device name of our existing Wi-Fi card where we will add the virtual interface. To do that, run the following command in the terminal:&#xA;&#xA;ip link show&#xA;&#xA;Your Wi-Fi card should have a name that starts with w. For me, it is wlp2s0:&#xA;&#xA;Alt text&#xA;&#xA;At this point, we’re getting ready to make the final changes. Create a new directory in your /opt directory that’ll host the hotspot start and stop scripts.&#xA;&#xA;sudo mkdir /opt/hotspot&#xA;&#xA;We’ll now add the hotspot startup script here.&#xA;&#xA;sudo nano /opt/hotspot/start-hotspot.sh&#xA;&#xA;Copy-paste the following in the text editor. Make sure to change the required fields.&#xA;&#xA;!/bin/bash&#xA;[ &#34;$UID&#34; -eq 0 ] || exec sudo bash &#34;$0&#34; &#34;$@&#34;&#xA;CHANGE YOUR WI-FI CARD NAME or delete if you are using a USB WiFI card&#xA;iw dev YOUR-WIFI-CARD-NAME interface add hotspot type managed addr 90:61:ae:b8:39:c1&#xA;echo Created virtual interface&#xA;sleep 2&#xA;If you are using a USB card, change hotspot to your USB device name&#xA;ip addr add 192.168.253.1/24 dev hotspot&#xA;echo Added IP to the virtual interface&#xA;sleep 2&#xA;systemctl start dnsmasq&#xA;echo Started dnsmasq&#xA;sleep 1&#xA;echo Starting hostapd&#xA;hostapd /etc/hostapd/hostapd.conf&#xA;&#xA;Alt text&#xA;&#xA;Hit CTRL+X to save and exit. We’ll then add the hotspot stop script:&#xA;&#xA;sudo nano /opt/hotspot/stop-hotspot.sh&#xA;&#xA;Copy paste the following:&#xA;&#xA;!/bin/bash&#xA;[ &#34;$UID&#34; -eq 0 ] || exec sudo bash &#34;$0&#34; &#34;$@&#34;&#xA;pkill hostapd&#xA;echo Stopped hostapd service&#xA;sleep 1&#xA;pkill dnsmasq&#xA;echo Stopped dnsmasq&#xA;sleep 1&#xA;If you are using a USB card, change hotspot to your USB device name&#xA;ip address del 192.168.253.1/24 dev hotspot&#xA;Delete the line if you are using a USB card&#xA;iw dev hotspot del&#xA;echo Deleted virtual interface&#xA;&#xA;Hit CTRL+X to save and exit. We’ll now make these two files executable by changing the file permissions. Run the following commands in terminal:&#xA;&#xA;sudo chmod +x /opt/hotspot/start-hotspot.sh&#xA;sudo chmod +x /opt/hotspot/stop-hotspot.sh&#xA;&#xA;Alt text&#xA;&#xA;We’ll now need to tell our NetworkManager that we do not want it to manage our hotspot interface. We can do that by editing the NetworkManager.conf file. We can do that by running the following command in terminal:&#xA;&#xA;sudo nano /etc/NetworkManager/NetworkManager.conf&#xA;&#xA;Make sure that file contains the following:&#xA;&#xA;[main]&#xA;plugins=keyfile,ifcfg-rh&#xA;&#xA;[keyfile]&#xA;If you are using a USB card, change hotspot to your USB device name&#xA;unmanaged-devices=interface-name:hotspot&#xA;&#xA;Alt text&#xA;&#xA;Now run the following command in terminal to reload the NetworkManager configuration:&#xA;&#xA;sudo systemctl restart NetworkManager&#xA;&#xA;Alt text&#xA;&#xA;We’ll now make a systemctl service to make things super easy to start and stop. Run the following command in terminal:&#xA;&#xA;sudo nano /etc/systemd/system/hotspot.service&#xA;&#xA;Copy-paste the following in the text editor:&#xA;&#xA;[Unit]&#xA;Description=NATed Hotspot using hostapd and dnsmasq&#xA;&#xA;[Service]&#xA;User=root&#xA;Group=root&#xA;ExecStart=/bin/bash /var/opt/hotspot/start-hotspot.sh&#xA;ExecStop=/bin/bash /var/opt/hotspot/stop-hotspot.sh&#xA;&#xA;[Install]&#xA;WantedBy=multi-user.target&#xA;&#xA;Alt text&#xA;&#xA;Run the following command for systemctl to refresh existing services:&#xA;&#xA;sudo systemctl daemon-reload&#xA;&#xA;Alt text&#xA;&#xA;And you are done! You can easily start the hotspot by running:&#xA;&#xA;sudo systemctl start hotspot&#xA;&#xA;You can check the status by running:&#xA;&#xA;sudo systemctl status hotspot&#xA;&#xA;To stop the hotspot, simply run:&#xA;&#xA;sudo systemctl stop hotspot&#xA;&#xA;Please note that if your Wi-Fi card does not support operation on multiple channels simultaneously, you will have to change the channel number defined in /etc/hostapd/hostapd.conf to what your Wi-Fi card is already using to connect to the internet, before you start your hotspot service everytime.&#xA;&#xA;We are done, but it still isn’t quite as good as a one-click solution. Who wants to write a terminal command every time we need to start or stop the hotspot? Well, if you are running Fedora Kinoite, there’s a solution for that too!&#xA;&#xA;In KDE, we can install a Plasma applet called configurable button. You can install this by right clicking on the desktop, clicking on Add Widget, and then following Get New Widget -  Download New Plasma Widgets. Search for Configurable Button.&#xA;&#xA;Alt text&#xA;&#xA;Add this new widget anywhere you want to keep the Hotspot button, and then configure it like following:&#xA;&#xA;Alt text&#xA;&#xA;And now you have a one-click solution to start and stop your hotspot in your Fedora Atomic Distro!&#xA;&#xA;Huge thanks to the following contributors without whom I wouldn’t have been able to do this:&#xA;&#xA;ArchWiki -  Software Access Point&#xA;&#xA;ArchWiki -  Internet Sharing&#xA;&#xA;StackExchange&#xA;&#xA;Plasmoid Button&#xA;&#xA;Linux-wifi-hotspot&#xA;&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>When I went from being an occasional Linux user to an exclusive Linux user about five years ago, I knew there would be times I’d miss the simplicity of having a one-click solution to a lot of things that came with Windows (albeit, at a steep price of being spied on by your own machine). Over the years, I have tinkered with a few Linux machines, and have been missing those solutions less and less, but in these five years, I have not had to deal with creating a hotspot on my laptop. Recently, I went to a hotel that had a smart TV, but had casting turned off at the router level (the mDNS port was blocked). The Wi-Fi signal itself was very weak too at one part of the room. I thought I’d set up a hotspot using my laptop, that way I would get stable Wi-Fi signal for my phone, and would be able to cast videos on the TV. As luck would have it, it turned out to be quite complicated in the distro I use as my daily driver, and had to give up trying to figure it out during my stay.</p>

<p>I use <a href="https://fedoraproject.org/atomic-desktops/kinoite/">Fedora Kinoite</a> as my daily driver; it’s one of the new style of “immutable” distros (although we’ve graduated from that term these days, and prefer to use the term “atomic” distros). Other than all the good things that came with immutable, or atomic distros, it meant that I could not use the fantastic tool called <a href="https://github.com/lakinduakash/linux-wifi-hotspot">linux-wifi-hotspot</a>. Seriously, if you have any regular distros like Ubuntu, Debian, regular Fedora Workstation, etc. use that fantastic tool; you can still use this guide, but it’ll be more complicated for you than it needs to.</p>

<p>Disclaimer: Being a Mechanical Engineer by degree and a Market Analyst by profession, everything I know about computers is a weird mixture of trial-and-error, common sense, Reddit, and StackExchange. So, needless to say, proceed at your own risk.</p>

<p>The first question we need to ask is whether we want a bridged hotspot, or a NATed hotspot. Bridged hotspot is easier to set up, but we lack finer controls over our network (like enabling mDNS for Google Cast, or AirPlay). NATed hotspot may be slightly more complicated to set up, but it comes with a whole lot of creature comforts like improved security, finer network controls, etc. For this blog post, let’s assume we want to set up a NATed hotspot.</p>

<p>First thing we need to do is confirm that our Wi-Fi card (the physical device that allows us to use our router’s Wi-Fi) allows AP mode, and simultaneous operation as a client and an AP. If you are using Fedora Atomic Desktops in a previously Windows laptop, chances are your Wi-Fi card supports both. If it doesn’t support simultaneous operation, we won’t be able to use an existing Wi-Fi connection as our internet connection, and run a hotspot along with it; however, we’d still be able to use some other mode of internet connection (e.g. ethernet, another USB Wi-Fi card, etc.), and run a Wi-Fi hotspot. If our Wi-Fi card does not support AP mode, then we’d need to buy a USB Wi-Fi card that does. To confirm these, run the following command on your terminal:</p>

<p><code>iw list</code></p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20create%20a%20Wi-Fi%20hotspot%20in%20Fedora%20Linux%20Atomic%20Desktops/iw%20list.png" alt="Alt text"></p>

<p>As long as we see “AP” under the Supported interface modes, we can be sure that our Wi-Fi card supports AP mode.</p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20create%20a%20Wi-Fi%20hotspot%20in%20Fedora%20Linux%20Atomic%20Desktops/iw%20list%202.png" alt="Alt text"></p>

<p>Scroll down, and search for valid interface combinations. As long as we have the total more than 1, we can be sure that our Wi-Fi card supports simultaneous operation. Bonus if we see the number of channels to be more than 1 as well (which would allow us to choose different Wi-Fi channels during simultaneous operation)!</p>

<p>We’d then need to confirm if we have IP forwarding enabled. To do that, run the following command on your terminal:</p>

<p><code>sudo sysctl -a | grep forward</code></p>

<pre><code>net.ipv4.ip_forward = 1
net.ipv4.conf.all.forwarding = 1
net.ipv6.conf.all.forwarding = 1
</code></pre>

<p>If you don’t see these in your result, you don’t have IP forwarding enabled. But, don’t worry, it’s quite easy to set up. Create a new file in <code>/etc/sysctl.d/</code> directory, and write those there:</p>

<p><code>sudo nano /etc/sysctl.d/50-ipforward.conf</code></p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20create%20a%20Wi-Fi%20hotspot%20in%20Fedora%20Linux%20Atomic%20Desktops/ip%20forward.png" alt="Alt text"></p>

<p>Hit CTRL+X to save and exit. Then run the following command for it to take effect immediately.</p>

<p><code>sudo sysctl -p</code></p>

<p>Now that you have IP forwarding set up, it’s time to configure the firewall.</p>

<p>You can check if firewalld (the default firewall in Fedora) is active by running the following command in terminal. By default, it should be running.</p>

<p><code>sudo firewall-cmd --state</code></p>

<p>In case it is not running, or you get an error, run the following command in terminal:</p>

<p><code>sudo systemctl restart firewalld</code></p>

<p>Once you confirm your firewall is active, get the default zone by running the following command in terminal:</p>

<p><code>sudo firewall-cmd --list-all-zones | grep &#34;(default, active)&#34;</code></p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20create%20a%20Wi-Fi%20hotspot%20in%20Fedora%20Linux%20Atomic%20Desktops/default%20firewall%20zone.png" alt="Alt text"></p>

<p>Since I changed my default zone to “external”, that’s what it shows me. You may get “public”, or “FedoraWorkstation” as your default zone. Either of those are fine, you’d just need to add “masquerading”. This allows your laptop to be used a NAT (Network Address Translator), which is how we are setting up our hotspot.</p>

<p>Run the following command on your terminal:</p>

<p><code>sudo firewall-cmd --zone=&lt;YOUR-DEFAULT-ZONE&gt; --add-masquerade</code></p>

<p>To make this change permanent, you’d also need to run:</p>

<p><code>sudo firewall-cmd --zone=&lt;YOUR-DEFAULT-ZONE&gt; --add-masquerade  --permanent</code></p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20create%20a%20Wi-Fi%20hotspot%20in%20Fedora%20Linux%20Atomic%20Desktops/firewall%20masquerade.png" alt="Alt text"></p>

<p>Now, if our Wi-Fi card supports simultaneous operation of AP mode and client mode, we’d need to set up a virtual interface so that we can use an existing Wi-Fi connection as our internet connection while simultaneously providing a Wi-Fi hotspot. Let’s call that virtual interface <code>hotspot</code> to simplify things. We’d need to add this virtual interface to the internal zone of our firewall (if your Wi-Fi card does not support simultaneous operation, replace <code>hotspot</code> with the interface you want to use, run <code>ip link show</code> in terminal to get the name of your interface). To do that, run the following command in the terminal:</p>

<p><code>sudo firewall-cmd --zone=internal --change-interface=hotspot --permanent</code></p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20create%20a%20Wi-Fi%20hotspot%20in%20Fedora%20Linux%20Atomic%20Desktops/firewall%20hotspot.png" alt="Alt text"></p>

<p>Now, we’d need to make a new firewall policy to allow data from the internal zone to the outside internet, and valid existing connections from the internet to our internal zone. We’d also need to make sure we allow DNS queries, and DHCP queries in the internal zone. Run the following commands in the terminal in order:</p>

<p><code>sudo firewall-cmd --permanent --new-policy int2ext</code>
<code>sudo firewall-cmd --permanent --policy int2ext --add-ingress-zone internal</code>
<code>sudo firewall-cmd --permanent --policy int2ext --add-egress-zone &lt;YOUR-DEFAULT-ZONE&gt;</code>
<code>sudo firewall-cmd --permanent --policy int2ext --set-target ACCEPT</code>
<code>sudo firewall-cmd --permanent --zone=internal --add-service=dns</code>
<code>sudo firewall-cmd --permanent --zone=internal --add-service=mdns</code>
<code>sudo firewall-cmd --permanent --zone=internal --add-service=dhcp</code>
<code>sudo firewall-cmd --permanent --zone=internal --add-service=dhcpv6-client</code>
<code>sudo firewall-cmd --reload</code></p>

<p>At this point, we want to make sure we have all the dependencies installed. We’ll use a program called “hostapd” to manage the hotspot itself, and “dnsmasq” to manage the clients that connect to your hotspot. In Fedora Atomic Desktops, we can do that by running:</p>

<p><code>sudo rpm-ostree install hostapd dnsmasq --apply-live</code></p>

<p>After those are installed, you’d need to configure them. Run the following commands on your terminal to edit the hostapd.conf file:</p>

<p><code>sudo mv /etc/hostapd/hostapd.conf /etc/hostapd/hostapd.conf.bak</code>
<code>sudo nano /etc/hostapd/hostapd.conf</code></p>

<p>Following is my <code>hostapd.conf</code> file. For the most part, you can copy-paste this file, just make sure you edit the required parts:</p>

<pre><code>#
# This will give you a minimal, secure wireless network.
# 
# DO NOT BE SATISFIED WITH THAT!!!
#
# A complete, well commented example configuration file is
# available here:
#
#       /usr/share/doc/hostapd/hostapd.conf
#
# For more information, look here:
#
#       http://wireless.kernel.org/en/users/Documentation/hostapd
#

ctrl_interface=/var/run/hostapd
ctrl_interface_group=wheel

# Some usable default settings...
macaddr_acl=0
auth_algs=1
ignore_broadcast_ssid=0

# Uncomment these for base WPA2 support with a pre-shared key
wpa=2
wpa_key_mgmt=WPA-PSK
wpa_pairwise=TKIP
rsn_pairwise=CCMP

# DO NOT FORGET TO SET A WPA PASSPHRASE!!
wpa_passphrase=&lt;MY-SUPER-SECURE-HOTSPOT-PASSWORD&gt;

# Most modern wireless drivers in the kernel need driver=nl80211
driver=nl80211

# Customize these for your local configuration...
# Change the interface if you are enabling your hotspot in some other
# network interface (like a USB card)
interface=hotspot
# This will start the hotspot in 2.4GHz mode 
hw_mode=g
channel=7
ieee80211n=1
# Change your hotspot name
ssid=&lt;MY-HOTSPOT-NAME&gt;
# Enables QoS (Quality of Service)
wmm_enabled=1
</code></pre>

<p>Hit CTRL+X to save and exit. <strong>Please note the channel number in this file. If your Wi-Fi card does not support operation on multiple channels simultaneously, you will have to change the channel number to what your Wi-Fi card is already using to connect to the internet.</strong></p>

<p>We’d now configure dnsmasq. Dnsmasq is responsible for managing the clients that connect to your hotspot by providing them with dynamic local IPs, and a lightweight DNS server.</p>

<p><code>sudo mv /etc/dnsmasq.conf /etc/dnsmasq.conf.bak</code>
<code>sudo nano /etc/dnsmasq.conf</code></p>

<p>Following is my dnsmasq config file. You can copy-paste this for a quick easy configuration. You shouldn’t need to change anything, but do go through the comments to understand what’s happening behind the scenes.</p>

<pre><code># If you want dnsmasq to change uid and gid to something other
# than the default, edit the following lines.
user=dnsmasq
group=dnsmasq

# If you want dnsmasq to listen for DHCP and DNS requests only on
# specified interfaces (and the loopback) give the name of the
# interface (eg eth0) here.
# Repeat the line for more than one interface.
interface=hotspot

# Serve DNS and DHCP only to networks directly connected to this machine.
# Any interface= line will override it.
#local-service
# Accept queries in default configuration only from localhost
# Comment out following option or explicitly configure interfaces or
# listen-address
local-service=host

# To listen only on localhost and do not receive packets on other
# interfaces, bind only to lo device. Comment out to bind on single
# wildcard socket.
#bind-interfaces
# Comment out above line and uncoment following 2 lines.
# Update interface name, use ip link to get its name.
bind-dynamic
interface=hotspot

# Uncomment this to enable the integrated DHCP server, you need
# to supply the range of addresses available for lease and optionally
# a lease time. If you have more than one network, you will need to
# repeat this for each network on which you want to supply DHCP
# service.
dhcp-range=192.168.253.50,192.168.253.150,12h

# Set the DHCP server to authoritative mode. In this mode it will barge in
# and take over the lease for any client which broadcasts on the network,
# whether it has a record of the lease or not. This avoids long timeouts
# when a machine wakes up on a new network. DO NOT enable this if there&#39;s
# the slightest chance that you might end up accidentally configuring a DHCP
# server for your campus/company accidentally. The ISC server uses
# the same option, and this URL provides more information:
# http://www.isc.org/files/auth.html
dhcp-authoritative

# Include all files in /etc/dnsmasq.d except RPM backup files
conf-dir=/etc/dnsmasq.d,.rpmnew,.rpmsave,.rpmorig
</code></pre>

<p>Hit CTRL+X to save and exit. The hard part is almost over! We’ll just need few more files, and we’ll be on our way to start the hotspot! We’ll need to find the device name of our existing Wi-Fi card where we will add the virtual interface. To do that, run the following command in the terminal:</p>

<p><code>ip link show</code></p>

<p>Your Wi-Fi card should have a name that starts with w. For me, it is <code>wlp2s0</code>:</p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20create%20a%20Wi-Fi%20hotspot%20in%20Fedora%20Linux%20Atomic%20Desktops/ip%20link%20show.png" alt="Alt text"></p>

<p>At this point, we’re getting ready to make the final changes. Create a new directory in your /opt directory that’ll host the hotspot start and stop scripts.</p>

<p><code>sudo mkdir /opt/hotspot</code></p>

<p>We’ll now add the hotspot startup script here.</p>

<p><code>sudo nano /opt/hotspot/start-hotspot.sh</code></p>

<p>Copy-paste the following in the text editor. Make sure to change the required fields.</p>

<pre><code>#!/bin/bash
[ &#34;$UID&#34; -eq 0 ] || exec sudo bash &#34;$0&#34; &#34;$@&#34;
# CHANGE YOUR WI-FI CARD NAME or delete if you are using a USB WiFI card
iw dev &lt;YOUR-WIFI-CARD-NAME&gt; interface add hotspot type managed addr 90:61:ae:b8:39:c1
echo Created virtual interface
sleep 2
# If you are using a USB card, change hotspot to your USB device name
ip addr add 192.168.253.1/24 dev hotspot
echo Added IP to the virtual interface
sleep 2
systemctl start dnsmasq
echo Started dnsmasq
sleep 1
echo Starting hostapd
hostapd /etc/hostapd/hostapd.conf
</code></pre>

<p><img src="https://media.blog.meghadeep.com/How%20to%20create%20a%20Wi-Fi%20hotspot%20in%20Fedora%20Linux%20Atomic%20Desktops/bash%20start.png" alt="Alt text"></p>

<p>Hit CTRL+X to save and exit. We’ll then add the hotspot stop script:</p>

<p><code>sudo nano /opt/hotspot/stop-hotspot.sh</code></p>

<p>Copy paste the following:</p>

<pre><code>#!/bin/bash
[ &#34;$UID&#34; -eq 0 ] || exec sudo bash &#34;$0&#34; &#34;$@&#34;
pkill hostapd
echo Stopped hostapd service
sleep 1
pkill dnsmasq
echo Stopped dnsmasq
sleep 1
# If you are using a USB card, change hotspot to your USB device name
ip address del 192.168.253.1/24 dev hotspot
# Delete the line if you are using a USB card
iw dev hotspot del
echo Deleted virtual interface
</code></pre>

<p>Hit CTRL+X to save and exit. We’ll now make these two files executable by changing the file permissions. Run the following commands in terminal:</p>

<p><code>sudo chmod +x /opt/hotspot/start-hotspot.sh</code>
<code>sudo chmod +x /opt/hotspot/stop-hotspot.sh</code></p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20create%20a%20Wi-Fi%20hotspot%20in%20Fedora%20Linux%20Atomic%20Desktops/bash%20stop.png" alt="Alt text"></p>

<p>We’ll now need to tell our NetworkManager that we do not want it to manage our hotspot interface. We can do that by editing the NetworkManager.conf file. We can do that by running the following command in terminal:</p>

<p><code>sudo nano /etc/NetworkManager/NetworkManager.conf</code></p>

<p>Make sure that file contains the following:</p>

<pre><code>[main]
plugins=keyfile,ifcfg-rh

[keyfile]
# If you are using a USB card, change hotspot to your USB device name
unmanaged-devices=interface-name:hotspot
</code></pre>

<p><img src="https://media.blog.meghadeep.com/How%20to%20create%20a%20Wi-Fi%20hotspot%20in%20Fedora%20Linux%20Atomic%20Desktops/NetworkManager.png" alt="Alt text"></p>

<p>Now run the following command in terminal to reload the NetworkManager configuration:</p>

<p><code>sudo systemctl restart NetworkManager</code></p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20create%20a%20Wi-Fi%20hotspot%20in%20Fedora%20Linux%20Atomic%20Desktops/NetworkManager2.png" alt="Alt text"></p>

<p>We’ll now make a systemctl service to make things super easy to start and stop. Run the following command in terminal:</p>

<p><code>sudo nano /etc/systemd/system/hotspot.service</code></p>

<p>Copy-paste the following in the text editor:</p>

<pre><code>[Unit]
Description=NATed Hotspot using hostapd and dnsmasq

[Service]
User=root
Group=root
ExecStart=/bin/bash /var/opt/hotspot/start-hotspot.sh
ExecStop=/bin/bash /var/opt/hotspot/stop-hotspot.sh

[Install]
WantedBy=multi-user.target
</code></pre>

<p><img src="https://media.blog.meghadeep.com/How%20to%20create%20a%20Wi-Fi%20hotspot%20in%20Fedora%20Linux%20Atomic%20Desktops/hotspot%20service.png" alt="Alt text"></p>

<p>Run the following command for systemctl to refresh existing services:</p>

<p><code>sudo systemctl daemon-reload</code></p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20create%20a%20Wi-Fi%20hotspot%20in%20Fedora%20Linux%20Atomic%20Desktops/hotspot%20service2.png" alt="Alt text"></p>

<p>And you are done! You can easily start the hotspot by running:</p>

<p><code>sudo systemctl start hotspot</code></p>

<p>You can check the status by running:</p>

<p><code>sudo systemctl status hotspot</code></p>

<p>To stop the hotspot, simply run:</p>

<p><code>sudo systemctl stop hotspot</code></p>

<p><strong>Please note that if your Wi-Fi card does not support operation on multiple channels simultaneously, you will have to change the channel number defined in <code>/etc/hostapd/hostapd.conf</code> to what your Wi-Fi card is already using to connect to the internet, before you start your <code>hotspot</code> service everytime.</strong></p>

<p>We are done, but it still isn’t quite as good as a one-click solution. Who wants to write a terminal command every time we need to start or stop the hotspot? Well, if you are running Fedora Kinoite, there’s a solution for that too!</p>

<p>In KDE, we can install a Plasma applet called configurable button. You can install this by right clicking on the desktop, clicking on Add Widget, and then following Get New Widget –&gt; Download New Plasma Widgets. Search for Configurable Button.</p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20create%20a%20Wi-Fi%20hotspot%20in%20Fedora%20Linux%20Atomic%20Desktops/widget.png" alt="Alt text"></p>

<p>Add this new widget anywhere you want to keep the Hotspot button, and then configure it like following:</p>

<p><img src="https://media.blog.meghadeep.com/How%20to%20create%20a%20Wi-Fi%20hotspot%20in%20Fedora%20Linux%20Atomic%20Desktops/widget2.png" alt="Alt text"></p>

<p>And now you have a one-click solution to start and stop your hotspot in your Fedora Atomic Distro!</p>

<p>Huge thanks to the following contributors without whom I wouldn’t have been able to do this:</p>

<p><a href="https://wiki.archlinux.org/title/Software_access_point">ArchWiki –&gt; Software Access Point</a></p>

<p><a href="https://wiki.archlinux.org/title/Internet_sharing#Configuration">ArchWiki –&gt; Internet Sharing</a></p>

<p><a href="https://stackoverflow.com/questions/5321380/disable-network-manager-for-a-particular-interface">StackExchange</a></p>

<p><a href="https://github.com/pmarki/plasmoid-button">Plasmoid Button</a></p>

<p><a href="https://github.com/lakinduakash/linux-wifi-hotspot">Linux-wifi-hotspot</a></p>
]]></content:encoded>
      <guid>https://blog.meghadeep.com/how-to-create-a-wi-fi-hotspot-in-fedora-linux-atomic-desktops-kinoite</guid>
      <pubDate>Sat, 13 Apr 2024 09:15:18 +0000</pubDate>
    </item>
  </channel>
</rss>