Install with Docker
Purpose
Section titled “Purpose”Run immutable EmuFramework images with persistent database storage.
Audience
Section titled “Audience”Docker operators and framework administrators.
Prerequisites
Section titled “Prerequisites”Docker Engine with Compose and access to ghcr.io.
Procedure
Section titled “Procedure”-
Obtain
docker-compose.ymlfrom the framework repository. Cloning the repository is not required. -
Create
.envbesidedocker-compose.yml:EMU_VERSION=0.1.1.0EMU_UPDATER_TOKEN=replace-with-a-random-secret-at-least-24-charactersPORT=3399EMU_SECURE_COOKIES=trueThe token is created by the operator; it is not downloaded from GitHub. Use the same token for the app and updater, and never commit
.env. -
Run:
Terminal window docker compose pulldocker compose up -ddocker compose ps -
Read the one-time administrator setup code:
Terminal window docker compose logs app -
Open
http://localhost:3399, enter the code, choose the administrator username, and set a password of at least 12 characters.
The code expires after 15 minutes or ten failed attempts. Restart the app container to generate a new code:
docker compose restart appdocker compose logs --tail=100 appThe Compose file supplies the app with EMU_DEPLOYMENT_MODE=docker, connects it to the internal updater at http://updater:3400, and mounts the named emu-data volume at /data.
The default integration secret key is /data/.emu-secret.key, so it persists in the same named volume. It is not included in .emubackup exports; keep a separate secure copy after configuring SMTP. Set EMU_SECRET_KEY_PATH only when the replacement path is also mounted persistently.
Overriding the volume and network names
Section titled “Overriding the volume and network names”Compose already gives the volume and network explicit names, so backup, monitoring, migration, and external service connections can reference them reliably. To use different names — for example to avoid a collision with another deployment on the same host — set these before the first docker compose up:
EMU_VOLUME_NAME=mycompany-emu-dataEMU_NETWORK_NAME=mycompany-emu-networkChanging these after the volume and network already exist does not rename them; set them before first creating the stack, or migrate data to the new volume as described below.
Image source
Section titled “Image source”Both images are published to the GitHub Container Registry (ghcr.io) under the emu479p01 org, listed at Your Packages:
ghcr.io/emu479p01/emu-framework:<version> # appghcr.io/emu479p01/emu-framework-updater:<version> # updaterdocker-compose.yml resolves <version> from EMU_VERSION in .env, so docker compose pull fetches both images at that tag. No account or authentication is required to pull, since the packages are public. Pulling manually without Compose looks like:
docker pull ghcr.io/emu479p01/emu-framework:<version>docker pull ghcr.io/emu479p01/emu-framework-updater:<version>To use a specific version instead of the latest, set EMU_VERSION accordingly before running docker compose pull.
Docker Desktop without cloning
Section titled “Docker Desktop without cloning”Pulling an image alone is not a complete installation. docker pull downloads an image but does not create the app, updater, network, environment variables, or persistent volume.
Create the shared network and volume first:
docker network create emu-networkdocker volume create emu-datadocker network inspect emu-networkdocker volume inspect emu-dataIf Docker reports that the network or volume already exists, it can be reused, but verify it belongs to this EmuFramework installation before continuing.
The app container must have:
Image: ghcr.io/emu479p01/emu-framework:<version>Container name: emu-frameworkRestart policy: Unless stoppedPort: 3399 -> 3399Network: emu-networkVolume: emu-data -> /data (Read/Write)NODE_ENV=productionEMU_APP_TITLE=EmuFrameworkEMU_DEPLOYMENT_MODE=dockerEMU_UPDATER_URL=http://updater:3400EMU_UPDATER_TOKEN=<same token as updater>EMU_DB_PATH=/data/data.dbEMU_DESIGNER_DB_PATH=/data/designer.dbEMU_SECRET_KEY_PATH=/data/.emu-secret.keyAfter starting a manually created app container, run docker logs --tail 100 emu-framework, copy the one-time setup code, and complete Administrator setup at http://localhost:3399. Restart the container if the code expires.
The updater container must use the matching updater image, the same token, the same emu-network network, the emu-data volume, and a Docker socket mount:
Image: ghcr.io/emu479p01/emu-framework-updater:<version>Container name: emu-framework-updaterRestart policy: Unless stoppedNetwork: emu-networkNetwork alias: updaterVolume: emu-data -> /data (Read/Write)Docker socket: /var/run/docker.sock -> /var/run/docker.sock (Read/Write)EMU_UPDATER_TOKEN=<same token as app>EMU_APP_CONTAINER=emu-frameworkEMU_IMAGE_REPOSITORY=ghcr.io/emu479p01/emu-frameworkEMU_UPDATE_STATE_PATH=/data/update-status.jsonDo not publish updater port 3400 to the host; it is an internal service. The updater’s container name can be anything, but it must carry the network alias updater, because the app reaches it at http://updater:3400. If the containers were created without a network, connect them afterward:
docker network connect emu-network emu-frameworkdocker network connect --alias updater emu-network emu-framework-updaterAfter changing environment variables, recreate the app container. Restarting an existing container does not add new environment variables.
Verify app-to-updater connectivity
Section titled “Verify app-to-updater connectivity”docker exec emu-framework node -e "fetch('http://updater:3400/').then(r=>console.log(r.status)).catch(e=>{console.error(e);process.exit(1)})"An HTTP 404 response confirms the network and DNS alias work; the updater has no GET / route, but the app reached the service successfully. Check logs for either side if this fails:
docker logs --tail 100 emu-frameworkdocker logs --tail 100 emu-framework-updaterExisting installation and data migration
Section titled “Existing installation and data migration”Before removing an existing container, inspect its mounts:
docker inspect <app-container> --format '{{json .Mounts}}'The database files should be mounted at /data. A named volume such as emu-data can be reused safely when recreating the container. Do not run docker compose down -v unless deleting all application data is intentional. Compose names its containers emuframework-app and emuframework-updater; manually created containers are commonly named emu-framework and emu-framework-updater. List actual names with:
docker ps -a --format "table {{.Names}}\t{{.Image}}"Find the volume mounted at /data for the app container:
docker inspect emu-framework --format '{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}'A long hexadecimal result is normally an anonymous volume. Verify it before continuing:
docker volume inspect <SOURCE_VOLUME>Stop both the app and the updater before copying, so the SQLite database and its WAL files are in a consistent state:
docker stop emu-framework emu-framework-updaterdocker volume create emu-dataCopy the data. This command refuses to run if emu-data already contains files, preventing an accidental merge of two installations:
docker run --rm --mount type=volume,src=<SOURCE_VOLUME>,dst=/source,readonly --mount type=volume,src=emu-data,dst=/target alpine:3.20 sh -c 'if [ -n "$(find /target -mindepth 1 -maxdepth 1 -print -quit)" ]; then echo "ERROR: emu-data is not empty"; exit 1; fi; cp -a /source/. /target/'Inspect the copied files before changing or removing anything:
docker run --rm --mount type=volume,src=emu-data,dst=/data,readonly alpine:3.20 sh -c "find /data -maxdepth 2 -type f -exec ls -ln {} ';'"Expect to find data.db, designer.db, backups/, and update-status.json; the exact list depends on features previously used. Recreate both containers with emu-data mounted at /data — a running container’s volume mount cannot be changed in place:
docker compose up -d --force-recreateFor manually managed containers, remove only the stopped containers (never the source volume yet) and create them again with emu-data -> /data for both app and updater. Confirm the mount on each:
docker inspect emu-framework --format '{{range .Mounts}}{{println .Name "->" .Destination}}{{end}}'docker inspect emu-framework-updater --format '{{range .Mounts}}{{println .Name "->" .Destination}}{{end}}'Before removing the old volume, verify: the app opens at http://localhost:3399; existing tables and transactions are intact; reports and installed fonts still work; the backup list still appears; the app can reach the updater; and Check for updates succeeds without fetch failed. Keep the old volume until all of this is confirmed and a separate backup exists:
docker volume rm <SOURCE_VOLUME>Separate database volumes
Section titled “Separate database volumes”By default data.db and designer.db share the single emu-data volume mounted at /data. To isolate them on separate volumes:
-
Add a second named volume, for example
emu-designer-data, todocker-compose.ymland mount it on the app container alongside the existing one:emu-data:/dataemu-designer-data:/designer-data -
Set:
EMU_DB_PATH=/data/data.dbEMU_DESIGNER_DB_PATH=/designer-data/designer.db -
Before recreating the container, create a verified backup and inspect the existing mount:
Terminal window docker inspect <app-container> --format '{{json .Mounts}}' -
Create the new volume and copy
designer.db(and its-wal/-shmfiles if present) into it:Terminal window docker volume create emu-designer-datadocker run --rm -v emu-data:/from -v emu-designer-data:/to alpine sh -c 'cp -a /from/designer.db* /to/' -
Recreate the app container so it picks up the new environment variables:
Terminal window docker compose up -d --force-recreate app -
Verify
data.dbremains inemu-dataanddesigner.dbnow lives inemu-designer-data, then confirm the app starts and reads both correctly before removingdesigner.dbfrom the original volume.
Do not run docker compose down -v during this process; it deletes named volumes.
Expected result
Section titled “Expected result”The app uses the emu-data volume. The updater has no public port.
Common errors
Section titled “Common errors”- Do not commit
.env. - The updater mounts
/var/run/docker.sock, which grants host-level Docker control. Disable the updater and use the manual procedure if that risk is unacceptable. EMU_UPDATER_URLis normallyhttp://updater:3400; it is a Docker service name, not a public URL and notlocalhost.Deployment: unsupportedmeans the app container did not receiveEMU_DEPLOYMENT_MODE=docker; setting the variable only on the updater is insufficient.- If the app cannot resolve
updater, the two containers are not on the same user-defined network. - If port
3399is already allocated, stop the old app container or choose another host port, for example3401:3399.