Box IO

Self-hosted IoT for Arduino and ESP32.

Install the Box IO Docker server on Linux

These steps install the Box IO station on Ubuntu 24.04 from the prebuilt image someone275/box_io-docker-server on Docker Hub. Docker runs two containers: boxio (the hub and the dashboard API) and nginx (HTTP and HTTPS). This station’s dashboard is http://localhost. Arduino boards use http://localhost:5923. The same image on other systems is listed under Docker Install: CentOS, Red Hat, Raspberry Pi, and BlueOnyx. BlueOnyx already uses ports 80 and 443, so that install does not start this nginx container.

The Ubuntu machine on the LAN is x.x.x.x. The SSH user in the commands below is user. Use the address and account on your own computer. Do not put the SSH password, the JWT secret, SMTP passwords, or Twilio tokens in git.

1. Point DNS and forward the ports

Do this before the first boot so the public name can reach the machine.

  1. In DNS for your domain, add an A record for localhost. The value is the router’s public WAN address, not x.x.x.x. From a PC on that network, a “what is my IP” page shows the WAN address.
  2. On a phone with Wi-Fi off, ping localhost must answer from that WAN address.
  3. Forward these TCP ports from the router to x.x.x.x. UDP is not required.
    • 80 to port 80 (HTTP, and the Let’s Encrypt check)
    • 443 to port 443 (the dashboard)
    • 5923 to port 5923 (Arduino and ESP32)

2. Sign in and update Ubuntu

From a PC on the same LAN:

ssh user@x.x.x.x

Type the account password when SSH asks. Then update the OS:

sudo apt update
sudo apt upgrade -y

3. Install Docker

Still signed in as user. This installs Docker Engine and the Compose plugin from Docker’s own Ubuntu repository.

sudo apt install -y ca-certificates curl git ufw
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a644 /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo ${UBUNTU_CODENAME:-$VERSION_CODENAME}) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker user

The docker group applies on the next login. Sign out and back in:

exit
ssh user@x.x.x.x
docker version
docker compose version

Both version commands should print a version. If docker says permission denied, the new login did not pick up the group yet.

4. Open the firewall

Allow SSH before you enable the firewall, or the next login can be locked out.

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 5923/tcp
sudo ufw enable
sudo ufw status

5. Download Box IO onto the server

Steps 1 through 4 install Docker. They do not create a Box IO folder. This step downloads it. Run it in the SSH session from step 3. git is already installed.

cd ~
git clone https://github.com/Someone275/Box_IO-Docker-server.git
cd ~/Box_IO-Docker-server
curl -fsSL -o docker-compose.yml https://box-io.com/station/docker-compose.yml
curl -fsSL -o keep-up.sh https://box-io.com/station/keep-up.sh
curl -fsSL -o nginx/nginx.conf https://box-io.com/station/nginx.conf
chmod +x keep-up.sh
ls docker-compose.yml .env.example keep-up.sh nginx/nginx.conf

The clone creates ~/Box_IO-Docker-server. If git says that folder already exists, skip the clone and run the curl lines from inside it. ls must print those four names. The three curl lines are the current compose file and HTTPS proxy from this website. The Box IO program is the Docker Hub image someone275/box_io-docker-server, so this machine does not compile it. If docker compose pull still asks for ghcr.io, add this line to .env:

BOXIO_IMAGE=someone275/box_io-docker-server:latest

6. Create the secret file

cd ~/Box_IO-Docker-server
cp .env.example .env
openssl rand -hex 48

Open .env and set JWT_SECRET to that hex string. One line, no quotes:

JWT_SECRET=paste_the_hex_here

Save the file. Never commit .env. The compose file reads this secret when the container starts. Projects, users, and device keys are stored in the Docker volume boxio-data, not in the git folder.

7. Start the containers

cd ~/Box_IO-Docker-server
docker compose pull
docker compose up -d --no-build
docker compose ps
docker compose logs -f --tail=50

Wait until the log shows the device hub on port 5923 and the web API on port 3847. Ctrl+C stops following the log. It does not stop the containers. boxio and nginx should both say Up. They restart with the machine because the compose file uses unless-stopped. docker compose pull downloads someone275/box_io-docker-server from Docker Hub. It does not build Node on this machine.

From a PC on the same LAN, check the hub and the dashboard. The first certificate is self-signed, so the browser warning is expected until the next step.

http://x.x.x.x:5923/health
http://localhost

The health URL returns JSON with ok set to true. Port 80 redirects to HTTPS. Port 443 is the dashboard. Arduino sketches keep using HTTP on port 5923.

8. Issue the HTTPS certificate

Let’s Encrypt has to see the public name. On the server, both of these must print boxio-http-ok:

curl -sS http://127.0.0.1/http-ok
curl -sS http://x.x.x.x/http-ok

Then, from a phone with Wi-Fi off, open http://localhost/http-ok. It must show the same text. A test from the LAN is not enough.

When that public check works, request the certificate:

cd ~/Box_IO-Docker-server
chmod +x nginx/init-letsencrypt.sh nginx/ensure-certs.sh nginx/issue-cert-dns.sh
DOMAIN=localhost EMAIL=you@example.com ./nginx/init-letsencrypt.sh

Use an email address you can read. Let’s Encrypt sends expiry notices there. If the script times out during connect, port 80 is not reachable from the internet. Use the DNS method instead. It prints a TXT name and value. Create that record, wait until dig shows it, then press Enter in the script:

cd ~/Box_IO-Docker-server
EMAIL=you@example.com ./nginx/issue-cert-dns.sh
dig +short TXT _acme-challenge.localhost

From the phone on cellular, http://localhost should open with a normal padlock.

Renew the certificate automatically

The certificate from init-letsencrypt.sh lasts 90 days. Let’s Encrypt issues a replacement when fewer than 30 days remain. Nginx keeps serving the copied certificate until it is reloaded, so a nightly crontab runs nginx/renew-cert.sh. That script renews the certificate, copies it into place, and reloads nginx. A certificate from issue-cert-dns.sh is not renewed by this job. Run that script again before 90 days.

cd ~/Box_IO-Docker-server
curl -fsSL -o nginx/renew-cert.sh https://box-io.com/station/renew-cert.sh
chmod +x nginx/renew-cert.sh
sh nginx/renew-cert.sh
(crontab -l 2>/dev/null | grep -v renew-cert.sh; echo '15 3 * * * cd /home/user/Box_IO-Docker-server && /home/user/Box_IO-Docker-server/nginx/renew-cert.sh >>/home/user/boxio-cert-renew.log 2>&1') | crontab -
crontab -l

15 3 * * * is 03:15 every night. Cron does not expand ~, so the command uses /home/user. Change user if the SSH account has another name. crontab -l lists the line. The log is /home/user/boxio-cert-renew.log. The account that owns the crontab must be allowed to run Docker.

9. Create the admin account

  1. Open http://localhost.
  2. Create the admin username and password. This is the first user, so the page asks for it.
  3. Sign in. Under device keys, generate a key. It starts with bx_. Copy it into the sketch as BOXIO_AUTH. Do not commit that key.
  4. Create a project and assign that device key.
  5. Turn on Edit, add widgets, set each virtual pin, and Save layout.
  6. Switch to Live before using buttons, sliders, and the input box.
  7. Optional: in Settings, save SMTP for email and Twilio for SMS. The email and SMS pages list those fields.

10. Install a license

Pin values, graphs, and /public/... stay empty until this Docker server has a license. Open License in the dashboard.

A year is $30. A trial is 30 days, once per account. Auto-renew charges $30 again each year.

11. Read a virtual pin from a web page

Use the device key from the dashboard. The web pin page shows V0, a lowercase v, the bare pin number, .txt, ?format=text, and the Accept: text/plain header. HTTP on port 80 redirects to HTTPS.

12. Update an existing station

Run this on the Ubuntu machine. docker compose pull updates the image from Docker Hub. It does not wipe boxio-data, so users, device keys, and layouts stay.

ssh user@x.x.x.x
cd ~/Box_IO-Docker-server
curl -fsSL -o docker-compose.yml https://box-io.com/station/docker-compose.yml
curl -fsSL -o keep-up.sh https://box-io.com/station/keep-up.sh
curl -fsSL -o nginx/nginx.conf https://box-io.com/station/nginx.conf
chmod +x keep-up.sh
docker compose pull
docker compose up -d --no-build
docker compose ps

Hard-refresh the dashboard after both containers are Up. Sketches keep the same host, port 5923, and device key. From ~/Box_IO-Docker-server, docker compose restart restarts the containers, and docker compose down stops them without deleting the volume. docker compose up -d --no-build starts them again. The proxy listens on 80 and 443 and calls the app by its container name. If the browser says 502 Bad Gateway or 504 Gateway Time-out after one page, download the current compose file and proxy again, then recreate the containers:

cd ~/Box_IO-Docker-server
curl -fsSL -o docker-compose.yml https://box-io.com/station/docker-compose.yml
curl -fsSL -o keep-up.sh https://box-io.com/station/keep-up.sh
curl -fsSL -o nginx/nginx.conf https://box-io.com/station/nginx.conf
chmod +x keep-up.sh
docker compose up -d --force-recreate

The app log stays empty while the proxy is stuck. Do not use docker compose down -v. That deletes the saved dashboards.

If the dashboard does not come back:

cd ~/Box_IO-Docker-server
docker compose logs -f --tail=80 boxio

13. Export and import this station

Use this to move the station you are running now onto another machine. Export writes one archive and starts the station again. On the next machine, import replaces the database, the license, the JWT secret, and the HTTPS certificate. Keep the archive private and delete it after the import.

cd ~/Box_IO-Docker-server
sh scripts/export-data.sh -o ~/boxio-export.tar.gz

Copy boxio-export.tar.gz to the next server, then from ~/Box_IO-Docker-server there:

sh scripts/import-data.sh ~/boxio-export.tar.gz
curl -sk https://127.0.0.1/api/health
rm -f ~/boxio-export.tar.gz

On BlueOnyx, add -f docker-compose.blueonyx.yml to both commands. Sign in with the same admin account. Device keys and layouts are in the database.