Self Hosting

Run your own OpenShock server stack with Docker Compose, Postgres and a Redis-compatible cache.

Requirements

Hardware:

  1. A server / computer to run linux containers on. Docker Desktop with WSL also works, but isn't really recommended.

Software:

  1. docker and docker compose installed on the server. You can use the Docker install script for linux, or Docker Desktop on Windows.
    Alternatively podman or podman desktop with compose addon.

Other:

  1. A domain or subdomain is recommended.
  2. HTTPS - required for cookies to work securely. You can use cloudflare for lets encrypt for example.

Preparing the server

Install software from the Requirements on the server.

Docker compose setup

Make a new folder in a known location.

Add two files with the names docker-compose.yml and .env. Paste their contents from below.

These two can also be found in the API repository

Reverse proxy

By default the reverse proxy that comes with this example is traefik. Everything should be setup and should be available under https on port 443 on your domain if done correctly.

Done

Congratulations, the backend and website should be working now. 🥳

You can now set the backend domain for the firmware to your api url via the domain serial command.

Migrating from the old docker compose setup

If you are running the previous subdomain-based stack (api.example.com / gateway.example.com plus the webui container), read this before you pull.

Back up first

Back up your postgres-data folder before starting. The Postgres upgrade and the volume path change below both touch persistent data.

What changed

OldNew
Subdomains: api., gateway., UI on the rootOne host, one port, split by path: /api, /gateway, UI on /
webui container (ghcr.io/openshock/webui, port 80)frontend container (ghcr.io/openshock/frontend, port 3000)
redis/redis-stack-server, keyspace events KEADragonflyDB, keyspace events Ex
postgres:17, volume at /var/lib/postgresql/datapostgres:18, volume at /var/lib/postgresql
.env holds raw OPENSHOCK__* variables.env holds short knobs, the compose file maps them to OPENSHOCK__*
Images pinned to latestOPENSHOCK_TAG / OPENSHOCK_FRONTEND_TAG
Traefik redirectregex rules for /s/, /c/, /t/Handled by the frontend itself (/c/ is now /usc/)

1. Postgres 17 → 18

Postgres 18 will not start on a 17 data directory, and the mount point moved (PG18 keeps its data in postgres-data/18/docker). Dump on the old stack first:

docker compose exec db pg_dump -U openshock -d openshock -Fc > openshock.dump
docker compose down
mv postgres-data postgres-data-17-backup

Then start the new stack's database and restore into it:

docker compose up -d db
docker compose exec -T db pg_restore -U openshock -d openshock < openshock.dump

2. Redis → Dragonfly

Nothing to migrate — only cache and ephemeral state lived there. Delete ./redis-data once the new stack is running. If you keep your own Redis instead of Dragonfly, set its keyspace events to at least Ex.

3. Check your database name

The old compose file built the connection string with Database=${PG_USER} while it created POSTGRES_DB=${PG_DB}, so the database in use was actually named after the user. This is fixed now. If you had set PG_USER and PG_DB to different values, your data lives in the database named after PG_USER — set PG_DB to that name, or restore your dump into PG_DB.

4. Rewrite your .env

Copy the new .env from above and port your values across:

Old (.env)New (.env)
OPENSHOCK_DOMAINOPENSHOCK_HOST
OPENSHOCK_API_SUBDOMAINOPENSHOCK_API_PATH (default /api)
OPENSHOCK_GATEWAY_SUBDOMAINOPENSHOCK_GATEWAY_PATH (default /gateway)
OPENSHOCK__MAIL__TYPE=SMTPMAIL_TYPE=Smtp — or Mailjet, or None for no outbound mail
OPENSHOCK__MAIL__SENDER__NAME / __EMAILMAIL_SENDER_NAME / MAIL_SENDER_EMAIL
OPENSHOCK__MAIL__SMTP__*SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_ENABLESSL, SMTP_VERIFYCERTIFICATE
OPENSHOCK__MAIL__MAILJET__KEY / __SECRETMAILJET_KEY / MAILJET_SECRET
OPENSHOCK__MAIL__MAILJET__TEMPLATE__PASSWORDRESETGone — mail templates are generated now
OPENSHOCK__TURNSTILE__ENABLEOPENSHOCK_TURNSTILE_ENABLE
OPENSHOCK__ACCOUNT__REGISTRATIONENABLEDOPENSHOCK_REGISTRATION_ENABLED
OPENSHOCK__LCG__COUNTRYCODEOPENSHOCK_LCG_COUNTRYCODE

PG_PASS, PG_USER and PG_DB keep their names.

Extra variables no longer pass through

The old api service had env_file: .env, so any extra OPENSHOCK__* variable you dropped into .env reached the container automatically. It does not anymore. Anything not in the table above has to be added explicitly to the x-openshock-env block (shared by api, cron and lcg) or to the individual service.

New knobs worth knowing:

  • OPENSHOCK_PORT — serve the stack on a non-443 port. Leave blank for 443.
  • OPENSHOCK_TAG / OPENSHOCK_FRONTEND_TAG — pin images instead of tracking latest. The frontend is a separate repository and versions independently of the backend.
  • OPENSHOCK_FRONTEND_TLS_INSECURE — skip backend certificate verification from the frontend. For testing with self-signed certs only, never in production.

5. DNS and certificates

Only one hostname is needed now, so you can drop the api. and gateway. DNS records and use a single-name certificate instead of a wildcard. Keep the old records pointed at the server until every client has moved over.

6. Clients and firmware

The gateway now advertises host, port and path prefix (OPENSHOCK__LCG__PUBLICPORT, OPENSHOCK__LCG__PUBLICPATH) rather than a bare FQDN, and devices pick that up from the API when they reconnect. Check that your firmware and client versions understand a gateway path prefix before cutting over — in this single-host layout you cannot avoid it by blanking OPENSHOCK_GATEWAY_PATH, because the frontend owns the root and the gateway needs a prefix of its own to be routable.

7. Bring it up

docker compose pull
docker compose up -d

The UI is on https://<host>/, the API docs on https://<host>/api/scalar/viewer and Hangfire on https://<host>/hangfire.

Using your own reverse proxy? Remove the traefik service and route by path: /frontend:3000, OPENSHOCK_API_PATHapi:80, OPENSHOCK_GATEWAY_PATHlcg:80, /hangfirecron:780. Do not strip the prefixes — each app serves under its own UsePathBase.

Last updated on

On this page