IPFS handles media storage for Ogmara — images, videos, avatars, and file attachments. Without IPFS, your node can still relay text messages, but users connected to it will not be able to upload or view media content.
Prerequisite: you've completed Install — Docker or Install — Source, plus the Configuration page. IPFS lives alongside the L2 node and the L2 config's [ipfs] block must point at the daemon you're about to set up.
Kubo is the reference implementation of IPFS. There are two ways to run it — pick whichever matches how you installed the L2 node. Option A (Docker) needs nothing installed on the host and is the natural fit if you ran the node from the Docker install page. Option B (native binary) installs Kubo directly and manages it with systemd, matching the source install. Do one of them, not both.
The official ipfs/kubo image initializes its repository automatically on first start. The IPFS_PROFILE=server environment variable applies the same server profile as the native install (disables local-network peer discovery, tunes the connection manager). Data persists in a host directory you bind-mount, so the container stays disposable.
# Create the data directory. The kubo image runs as uid 1000 inside the
# container, so hand the directory to that uid or the daemon can't write.
sudo mkdir -p /var/lib/ogmara/ipfs
sudo chown -R 1000:1000 /var/lib/ogmara/ipfs
# A shared Docker network lets a dockerized L2 node reach IPFS by name
# (http://ogmara-ipfs:5001). Harmless if your L2 node runs from source.
docker network create ogmara-net 2>/dev/null || true
# Pull and run Kubo. The API (5001) and gateway (8081) are bound to
# 127.0.0.1 only — never expose 5001 publicly. We use gateway port 8081,
# NOT Kubo's default 8080: 8080 is the most contested alt-HTTP port (web
# apps, proxies, and the Klever validator all grab it). The next step
# moves the container's gateway to 8081 so the host port, the internal
# port, and the same-network address all agree. The swarm port (4001)
# stays open so IPFS can reach peers.
docker run -d \
--name ogmara-ipfs \
--restart unless-stopped \
--network ogmara-net \
-e IPFS_PROFILE=server \
-v /var/lib/ogmara/ipfs:/data/ipfs \
-p 4001:4001 \
-p 4001:4001/udp \
-p 127.0.0.1:5001:5001 \
-p 127.0.0.1:8081:8081 \
ipfs/kubo:latest
# Verify it came up
docker exec ogmara-ipfs ipfs version
docker logs --tail 20 ogmara-ipfs
Because the container carries --restart unless-stopped, Docker keeps it running across reboots — you do not need the systemd unit in the section below. Skip ahead to Configure IPFS, then Connect the L2 node.
Download and install the latest Kubo release directly on the host. Check dist.ipfs.tech/kubo for the current version and adjust the URL accordingly.
If you installed the L2 node via Docker: the ogmara system user probably doesn't exist yet — Server Prep marks it optional for Docker operators (the node container runs as your host UID, not ogmara). Running native IPFS still needs that user, so sudo -u ogmara ... ipfs init would fail with sudo: unknown user: ogmara. The first block below creates it — it's idempotent, so source-build operators who already have the user can run it harmlessly too.
# Download the latest Kubo release. We resolve the current version tag
# from the dist site so this stays correct over time; for a different
# CPU pick the matching asset (e.g. linux-arm64) from dist.ipfs.tech/kubo.
KUBO_VERSION=$(curl -s https://dist.ipfs.tech/kubo/versions | tail -n 1)
wget "https://dist.ipfs.tech/kubo/${KUBO_VERSION}/kubo_${KUBO_VERSION}_linux-amd64.tar.gz"
tar -xzf "kubo_${KUBO_VERSION}_linux-amd64.tar.gz"
cd kubo
sudo bash install.sh
# Ensure the dedicated ogmara user and IPFS data dir exist. Idempotent:
# source-build operators created the user in Server Prep; Docker
# operators usually skipped it. This makes Option B self-contained.
id ogmara &>/dev/null || sudo useradd -r -m -d /var/lib/ogmara -s /usr/sbin/nologin ogmara
sudo install -d -o ogmara -g ogmara /var/lib/ogmara/.ipfs
# Initialize IPFS as the ogmara user
sudo -u ogmara IPFS_PATH=/var/lib/ogmara/.ipfs ipfs init --profile server
# Verify installation
sudo -u ogmara IPFS_PATH=/var/lib/ogmara/.ipfs ipfs version
Set a storage limit appropriate for your available disk space. Use the command block that matches your install method.
Docker (Option A):
# Set the storage limit and move the gateway off Kubo's default 8080
# to 8081 (matching the -p mapping above), then restart so the daemon
# picks both up. Binding 0.0.0.0 inside the container lets BOTH the
# host-published port and same-ogmara-net containers reach it; the
# host -p is pinned to 127.0.0.1, so it stays off the public internet.
docker exec ogmara-ipfs ipfs config Datastore.StorageMax "10GB"
docker exec ogmara-ipfs ipfs config Addresses.Gateway "/ip4/0.0.0.0/tcp/8081"
docker restart ogmara-ipfs
Native binary (Option B):
# Set storage limit (adjust based on your available disk space)
sudo -u ogmara IPFS_PATH=/var/lib/ogmara/.ipfs \
ipfs config Datastore.StorageMax "10GB"
# Listen on localhost only for the API (security)
sudo -u ogmara IPFS_PATH=/var/lib/ogmara/.ipfs \
ipfs config Addresses.API "/ip4/127.0.0.1/tcp/5001"
# Configure the gateway to listen on localhost. Use port 8081, NOT
# Kubo's default 8080 — 8080 is the most commonly contested alt-HTTP
# port (web apps, proxies, and the Klever validator all bind it), so
# the daemon will exit with "bind: address already in use" if you leave
# it at 8080. 8081 keeps the gateway clear of that collision.
sudo -u ogmara IPFS_PATH=/var/lib/ogmara/.ipfs \
ipfs config Addresses.Gateway "/ip4/127.0.0.1/tcp/8081"
Security: NEVER expose port 5001 (the IPFS API) to the public internet. This port provides full read/write access to your IPFS node, including the ability to add, pin, and delete content. It must only be accessible from localhost or through a secured tunnel. (The Docker -p 127.0.0.1:5001:5001 mapping above already enforces this; for a native install confirm Addresses.API stays bound to 127.0.0.1.)
Docker users: skip this section. The --restart unless-stopped flag on your ogmara-ipfs container already keeps IPFS running across crashes and reboots. This systemd unit is only for the native binary install.
# /etc/systemd/system/ipfs.service
[Unit]
Description=IPFS Daemon
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=ogmara
Group=ogmara
Environment=IPFS_PATH=/var/lib/ogmara/.ipfs
ExecStart=/usr/local/bin/ipfs daemon
Restart=on-failure
RestartSec=5
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
# Enable and start IPFS
sudo systemctl daemon-reload
sudo systemctl enable ipfs
sudo systemctl start ipfs
# Verify it is running
sudo systemctl status ipfs
curl -s http://127.0.0.1:5001/api/v0/id | head -c 200
Add the IPFS section to your ogmara.toml configuration file.
# In /etc/ogmara/ogmara.toml (auto-generated default already has the
# [ipfs] block; just confirm api_url and gateway_url point at your
# running IPFS daemon, and adjust the limits if needed)
[ipfs]
api_url = "http://127.0.0.1:5001"
gateway_url = "http://127.0.0.1:8081" # matches the 8081 gateway set above
# Maximum upload size in MB (default: 50)
max_upload_size_mb = 50
# Optional media handler tuning — defaults are safe for a ~4 GiB-RAM
# VPS. Lower for resource-constrained nodes; raise for high-readership
# deployments. See the annotated example on github for the full list:
# https://github.com/Ogmara/l2-node/blob/main/ogmara.example.toml
# media_cache_total_mb = 256 # Total LRU weight cap
# media_handler_permits = 32 # Concurrent /api/v1/media/:cid handlers
# media_per_ip_permits = 4 # Per-client-IP sub-cap
If your L2 node runs in Docker, 127.0.0.1 above is wrong. Inside the container 127.0.0.1 is the container's own loopback, not the host — so a dockerized node points at an IPFS that isn't there and the dashboard shows IPFS Offline even though the daemon is healthy. Which address to use depends on where IPFS runs:
Attach the node container to the same ogmara-net network (add --network ogmara-net to its docker run, or recreate it) and address IPFS by container name:
# ogmara.toml — when BOTH containers share the ogmara-net network.
# Same ports as the host-bound case (5001 / 8081), just addressed by
# container name instead of 127.0.0.1.
[ipfs]
api_url = "http://ogmara-ipfs:5001"
gateway_url = "http://ogmara-ipfs:8081"
This is the trickiest combo. The native daemon is bound to the host's 127.0.0.1, which the container can't reach across the Docker bridge. The cleanest fix is to give the node container the host's network namespace — then 127.0.0.1 inside the container is the host, the config above works unchanged, and IPFS stays safely bound to localhost (never exposed). Recreate the node with --network host (drop the -p flags — with host networking the node binds the host ports directly):
# Recreate the L2 node sharing the host network namespace.
docker rm -f ogmara-l2
docker run -d \
--name ogmara-l2 \
--restart unless-stopped \
--network host \
--user $(id -u):$(id -g) \
-v /var/lib/ogmara/data:/data \
-v /etc/ogmara:/etc/ogmara \
ogmara/ogmara:l2-node-latest
# ogmara.toml stays on localhost — now it resolves to the host's daemon:
# api_url = "http://127.0.0.1:5001"
# gateway_url = "http://127.0.0.1:8081"
If you can't use host networking (e.g. you need the container's isolated network), keep bridge networking and reach the host over the Docker bridge gateway instead. Bind the native daemon to that gateway as well as localhost, then point the config at it. Find the gateway IP with ip -4 addr show docker0 (usually 172.17.0.1):
# On the HOST — also bind the IPFS API + gateway to the bridge gateway
# so the container can reach them (keep 127.0.0.1 too for host-local use):
sudo -u ogmara IPFS_PATH=/var/lib/ogmara/.ipfs ipfs config --json \
Addresses.API '["/ip4/127.0.0.1/tcp/5001","/ip4/172.17.0.1/tcp/5001"]'
sudo -u ogmara IPFS_PATH=/var/lib/ogmara/.ipfs ipfs config --json \
Addresses.Gateway '["/ip4/127.0.0.1/tcp/8081","/ip4/172.17.0.1/tcp/8081"]'
sudo systemctl restart ipfs
# Make sure the API never leaks to the public internet. 172.17.0.0/16 is
# not internet-routable, but block 5001 at the firewall as defense-in-depth:
sudo ufw deny 5001/tcp
# ogmara.toml — point at the bridge gateway instead of 127.0.0.1:
# api_url = "http://172.17.0.1:5001"
# gateway_url = "http://172.17.0.1:8081"
Security: binding the IPFS API (5001) to the bridge gateway exposes its full read/write control to every container on that bridge and to the host. Only do this on a host you control, keep 5001 firewalled off the public interfaces, and prefer the host-networking option above when you can — it keeps the API on 127.0.0.1 only.
No Docker involved — the 127.0.0.1 config at the top of this section is correct as written. Nothing to change.
# Restart the L2 node to pick up the new config
sudo systemctl restart ogmara-node
# (or for docker) docker restart ogmara-l2
If you want to run IPFS on a separate server (for example, a machine with more storage), you can connect the L2 node to it securely using a WireGuard VPN tunnel or an SSH tunnel.
# SSH tunnel example: forward local port 5001 to remote IPFS
ssh -N -L 5001:127.0.0.1:5001 user@ipfs-server.example.com
# Then configure ogmara.toml to use 127.0.0.1:5001 as usual
For a permanent remote IPFS setup, use WireGuard instead of SSH tunnels. WireGuard is faster, more reliable, and reconnects automatically. See the deployment documentation for detailed WireGuard configuration.