Install with Docker
หมายเหตุ: บทความนี้ยังเป็นภาษาอังกฤษ เนื่องจากคำแปลภาษาไทยกำลังอยู่ระหว่างจัดทำ
Purpose
หัวข้อที่มีชื่อว่า “Purpose”Run immutable EmuFramework images with persistent database, attachment, and archive storage.
Audience
หัวข้อที่มีชื่อว่า “Audience”Docker operators and framework administrators.
Prerequisites
หัวข้อที่มีชื่อว่า “Prerequisites”Docker Engine with Compose and access to ghcr.io.
Procedure
หัวข้อที่มีชื่อว่า “Procedure”-
Obtain
docker-compose.ymlfrom the framework repository. Cloning the repository is not required. -
Create
.envbesidedocker-compose.yml:EMU_VERSION=1.4.0EMU_UPDATER_TOKEN=replace-with-a-random-secret-at-least-24-charactersPORT=3399EMU_SECURE_COOKIES=trueEMU_APP_TITLE=EmuFrameworkThe 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.EMU_VERSIONis optional because the Compose file defaults to the release it ships with (1.4.0), but pinning it makes the installed version explicit. Use a stableX.Y.Ztag, neverlatest.EMU_APP_TITLEsets the product name shown in the browser title and on the login and setup pages. -
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 three named volumes into both the app and the updater:
| Volume | Mount | Contents |
|---|---|---|
emu-data |
/data |
data.db, designer.db, the secret key, backups, fonts, and update and restore status files |
emu-files |
/files |
Live attachment files, selected with EMU_FILE_STORAGE_PATH=/files |
emu-archive |
/archive |
The archive catalog and archived payloads, selected with EMU_ARCHIVE_STORAGE_PATH=/archive |
If EMU_FILE_STORAGE_PATH or EMU_ARCHIVE_STORAGE_PATH is not set, the app falls back to /data/files and /data/archive and the Storage Overview shows a warning that shared fallback storage is in use. Separate volumes keep attachment and archive growth away from the databases and let you place them on larger disks. See Manage storage and archiving.
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
หัวข้อที่มีชื่อว่า “Overriding the volume and network names”Compose already gives the volumes and network explicit names (emuframework-data, emuframework-files, emuframework-archive, and emuframework-network), 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_FILES_VOLUME_NAME=mycompany-emu-filesEMU_ARCHIVE_VOLUME_NAME=mycompany-emu-archiveEMU_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
หัวข้อที่มีชื่อว่า “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>Pin the version with EMU_VERSION before running docker compose pull. Run the app and updater images at the same version. The web update replaces only the app container, so after it completes, set EMU_VERSION to the new version and run docker compose pull and docker compose up -d to bring the updater image to the same version.
Docker Desktop without cloning
หัวข้อที่มีชื่อว่า “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 volumes first:
docker network create emu-networkdocker volume create emu-datadocker volume create emu-filesdocker volume create emu-archivedocker network inspect emu-networkdocker volume inspect emu-data emu-files emu-archiveIf 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)Volume: emu-files -> /files (Read/Write)Volume: emu-archive -> /archive (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.keyEMU_FILE_STORAGE_PATH=/filesEMU_ARCHIVE_STORAGE_PATH=/archiveAfter 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)Volume: emu-files -> /files (Read/Write)Volume: emu-archive -> /archive (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.jsonEMU_FILE_STORAGE_PATH=/filesEMU_ARCHIVE_STORAGE_PATH=/archiveThe app and the updater must mount the same /files and /archive volumes. Before it stops the app, the updater compares the two containers’ mounts and refuses to update if they differ (see Update the framework).
Do 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
หัวข้อที่มีชื่อว่า “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. To check the updater’s own health endpoint, request /health instead; it returns HTTP 200 with {"ok":true,"busy":false} when the updater is idle:
docker exec emu-framework node -e "fetch('http://updater:3400/health').then(async r=>console.log(r.status, await r.text())).catch(e=>{console.error(e);process.exit(1)})"Check logs for either side if this fails:
docker logs --tail 100 emu-frameworkdocker logs --tail 100 emu-framework-updaterExisting installation and data migration
หัวข้อที่มีชื่อว่า “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. An installation that already stored attachments before separate volumes were added may also contain files/ and archive/ directories. 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>Add file and archive volumes to an existing installation
หัวข้อที่มีชื่อว่า “Add file and archive volumes to an existing installation”Installations created before v1.1.0 have only emu-data. They keep working after an update, with attachments and the archive stored in /data/files and /data/archive. To move to separate volumes:
-
Create a verified Full backup and a separate copy of the secret key.
-
Stop the stack with
docker compose stop. -
Replace your
docker-compose.ymlwith the current one from the framework repository, or add theemu-files:/filesandemu-archive:/archivevolumes and theEMU_FILE_STORAGE_PATHandEMU_ARCHIVE_STORAGE_PATHvariables to both services. -
Create the volumes and copy the existing content, if any, before the stack starts. The example copies into the default Compose volume names:
Terminal window docker volume create emuframework-filesdocker volume create emuframework-archivedocker run --rm -v emuframework-data:/from -v emuframework-files:/to alpine sh -c 'if [ -d /from/files ]; then cp -a /from/files/. /to/; fi'docker run --rm -v emuframework-data:/from -v emuframework-archive:/to alpine sh -c 'if [ -d /from/archive ]; then cp -a /from/archive/. /to/; fi' -
Start the stack with
docker compose up -d --force-recreate. -
Open Settings → System Maintenance → Storage Overview, confirm the storage paths show
/filesand/archive, and confirm the fallback warning is gone. Open an existing attachment to confirm it still downloads. -
After you have verified the result and kept a backup, remove the old
filesandarchivedirectories from/datato reclaim the space.
Database location
หัวข้อที่มีชื่อว่า “Database location”In production the app requires both EMU_DB_PATH and EMU_DESIGNER_DB_PATH to be under /data/, and it refuses to start otherwise. The updater snapshots data.db and designer.db from the /data volume before every update and restore, and backups read the same files. Keep both databases on the emu-data volume; placing designer.db on a separate volume is not supported. To give the databases more space, grow the disk that holds Docker volumes (see Database storage). Attachments and the archive, which usually grow fastest, already use their own volumes.
Do not run docker compose down -v; it deletes named volumes.
Expected result
หัวข้อที่มีชื่อว่า “Expected result”The app uses the emu-data, emu-files, and emu-archive volumes, and the updater mounts the same three. The updater has no public port, and both containers report healthy in docker compose ps.
Common errors
หัวข้อที่มีชื่อว่า “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. - A warning in Storage Overview that shared fallback storage is in use means
EMU_FILE_STORAGE_PATHorEMU_ARCHIVE_STORAGE_PATHis not set or the volume is not mounted. See Manage storage and archiving. - If port
3399is already allocated, stop the old app container or choose another host port, for example3401:3399.
Related topics
หัวข้อที่มีชื่อว่า “Related topics”Configuration · Storage and archive · Docker operations · Framework update