Site ↗
Documentation sections
Operations · 0.8.1

Moving dev data to prod-local or another computer

Two separate step-by-step guides: import into local Docker or move development to a new machine, including dump, images, and startup.

On this page

This covers two local scenarios: moving dev data to prod-local and continuing development on another computer. Server transfer has a separate complete dev → VPS guide.

You need a PostgreSQL dump and image archive from the same data version. This guide assumes Local media → Local media. Users and passwords move with the database; existing and transferred data are not merged.

Run all blocks of one stage in the same terminal: path and container-name variables persist only there. On the destination computer, set variables again for your chosen scenario.

First: create a dev copy

Run this section on the source computer, from the generated project root. Stop make dev with Ctrl+C and your own tasks writing to the database or changing images. Keep PostgreSQL running:

make up
make wait-db
docker ps --format '{{.Names}}'

Take the dev PostgreSQL container name from the list, rather than prod-local. The example below uses example_project-postgres-1. Take POSTGRES_DB and POSTGRES_USER from your .env, and DEV_MEDIA from MEDIA_ROOT if overridden. The default image directory is shown in the example:

DEV_POSTGRES=example_project-postgres-1
DEV_DB=example_project
DEV_USER=postgres
DEV_MEDIA="$HOME/.local/share/example_project/media"
umask 077
TRANSFER_DIR="$HOME/example-transfer-$(date +%Y%m%d-%H%M%S)"
mkdir -m 700 "$TRANSFER_DIR"

Save the database, then all images and prepared sizes:

docker exec "$DEV_POSTGRES" \
  pg_dump -U "$DEV_USER" -d "$DEV_DB" -Fc \
  > "$TRANSFER_DIR/database.dump"
COPYFILE_DISABLE=1 tar -czf "$TRANSFER_DIR/media.tar.gz" -C "$DEV_MEDIA" .
printf 'Копия сохранена: %s\n' "$TRANSFER_DIR"

Proceed only after both commands succeed. If the database never contained images, skip the archive and all subsequent media steps. If images existed but the folder disappeared, the dump is insufficient: locate a file backup.

The copy folder is outside the project and must not enter Git. Do not start dev until both files are ready. Then choose one of the two scenarios.

Option A. Import into prod-local

A1. Start empty prod-local first

The destination must have the same code and migrations as the dump. Create a local profile with prod-local preset in Generator UI, save, and run Generate. If prod-local has not run yet:

bash ./deploy/control provision --env local
bash ./deploy/control up --env local
bash ./deploy/control status --env local --json

Wait for successful startup: status must show current and no active operation. Import happens afterward, rather than between provision and first up. Otherwise the database would be nonempty without a registered initial release.

Existing prod-local does not need another provision. If it contains valuable data, create a standard backup first:

bash ./deploy/control backup --env local --confirm local

Preserve the backup ID. The next import replaces target tables, records, and accounts. Skip this backup for a fresh empty environment.

A2. Select containers and stop the application

Run all following commands on the prod-local computer, in one terminal through A6. If this is another machine, first transfer the copy folder and project using your usual file-transfer method. No scp is needed when the copy is already on the same computer.

docker ps --format '{{.Names}}'

Substitute local container names, rather than dev. TRANSFER_DIR is the absolute directory containing database.dump and media.tar.gz:

TRANSFER_DIR=/absolute/path/to/example-transfer
TARGET_BACKEND=example_project-local-backend-1
TARGET_FRONTEND=example_project-local-frontend-1
TARGET_POSTGRES=example_project-local-postgres-1
TARGET_DB=app

docker stop "$TARGET_FRONTEND" "$TARGET_BACKEND"

Do not stop PostgreSQL. app is the database name of new prod-local; if restore was used before, use the active database name. Stop external writers too.

A3. Restore the database

docker exec -i "$TARGET_POSTGRES" \
  pg_restore -U postgres -d "$TARGET_DB" \
  --clean --if-exists --no-owner --no-privileges --single-transaction \
  < "$TRANSFER_DIR/database.dump"

--clean replaces contents included in the dump; --single-transaction cancels partial restoration on SQL error. If the command fails, do not proceed to application startup.

A4. Restore images

This step is needed if the dump contains images. Find the backend's internal path and corresponding computer directory:

export BACKEND_MEDIA=$(docker inspect "$TARGET_BACKEND" \
  --format '{{range .Config.Env}}{{println .}}{{end}}' \
  | sed -n 's/^MEDIA_ROOT=//p')
TARGET_MEDIA=$(docker inspect "$TARGET_BACKEND" \
  | jq -er --arg path "$BACKEND_MEDIA" \
    '.[0].Mounts[] | select(.Destination == $path and .Type == "bind") | .Source')
TARGET_IMAGE=$(docker inspect "$TARGET_BACKEND" --format '{{.Config.Image}}')
test -n "$BACKEND_MEDIA" && test -d "$TARGET_MEDIA"

Extract the archive and assign backend ownership. The temporary container uses the installed image without starting the application. This works with Docker Desktop and Linux, without changing ownership of the entire project folder manually:

docker run --rm -i --platform linux/amd64 --user 0 \
  --entrypoint tar \
  --mount "type=bind,src=$TARGET_MEDIA,dst=/media" \
  "$TARGET_IMAGE" -xzf - -C /media < "$TRANSFER_DIR/media.tar.gz"

docker run --rm --platform linux/amd64 --user 0 \
  --entrypoint chown \
  --mount "type=bind,src=$TARGET_MEDIA,dst=/media" \
  "$TARGET_IMAGE" -R 10001:10001 /media

A5. Update the image directory binding

The database came from dev and remembers the old path. After files are copied, this block calculates the new value from the backend path; manual identifier calculation or comparison is unnecessary. Python 3 is required:

MEDIA_ID=$(python3 - <<'PYTHON'
import hashlib, os, posixpath
root = os.environ['BACKEND_MEDIA']
assert root.startswith('/') and root != '/', 'Проверьте BACKEND_MEDIA'
print(hashlib.sha256(b'local\0' + posixpath.normpath(root).encode() + b'\0').hexdigest())
PYTHON
)

docker exec -i "$TARGET_POSTGRES" \
  psql -U postgres -d "$TARGET_DB" \
  -v ON_ERROR_STOP=1 -v media_id="$MEDIA_ID" <<'SQL'
BEGIN;
SELECT pg_advisory_xact_lock(731947120);
INSERT INTO admingen_media.store_identity (singleton, identity)
VALUES (true, :'media_id')
ON CONFLICT (singleton) DO UPDATE SET identity = EXCLUDED.identity;
COMMIT;
SQL

This does not restore images — apply it only after A4 to the matching file copy.

A6. Start prod-local

docker start "$TARGET_BACKEND"
docker logs --tail 50 "$TARGET_BACKEND"
docker exec "$TARGET_BACKEND" migrate --verify

If backend started without error and migrations are verified:

docker start "$TARGET_FRONTEND"
bash ./deploy/control status --env local --json

Sign in to local admin with the dev account. Open records and images. Sign in again if necessary: server signing keys were not copied with the database. Keep dump and archive until the result is confirmed.

Option B. Continue dev on another computer

B1. Transfer the project and copy

Transfer source at the same commit and TRANSFER_DIR to the new computer. The project needs .admingen/project.yaml, custom code, and all of backend/migrations, including the manifest. Use Git for files and copy the archive separately. Do not commit the dump.

On the new computer, open the project root. Run B1–B4 blocks in one terminal. Configure its .env; if missing:

make local-environment

Setup creates local parameters for this machine. It does not replace data import. Start PostgreSQL only, without make dev yet:

make up
make wait-db
docker ps --format '{{.Names}}'

Select the dev container and fill values from the new .env:

TRANSFER_DIR=/absolute/path/to/example-transfer
TARGET_POSTGRES=example_project-postgres-1
TARGET_DB=example_project
TARGET_USER=postgres

B2. Restore the empty database

This option assumes an empty target dev database:

docker exec -i "$TARGET_POSTGRES" \
  pg_restore -U "$TARGET_USER" -d "$TARGET_DB" \
  --no-owner --no-privileges --single-transaction \
  < "$TRANSFER_DIR/database.dump"

If tables already exist, the command reports an error. Do not add --clean until you decide to replace existing data: that is another scenario rather than a command-format fix.

B3. Extract images and set the new path

If no images existed, go to B4. Otherwise, on the new computer:

export MEDIA_ROOT="$HOME/.local/share/example_project/media"
mkdir -p "$MEDIA_ROOT"
tar -xzf "$TRANSFER_DIR/media.tar.gz" -C "$MEDIA_ROOT"

The archive created earlier contains files without an outer media folder. Extract it exactly this way. Then update the database binding:

MEDIA_ID=$(python3 - <<'PYTHON'
import hashlib, os
from pathlib import Path
root = Path(os.environ['MEDIA_ROOT']).resolve(strict=True)
assert root.is_dir(), 'MEDIA_ROOT должен быть папкой'
print(hashlib.sha256(b'local\0' + str(root).encode() + b'\0').hexdigest())
PYTHON
)

docker exec -i "$TARGET_POSTGRES" \
  psql -U "$TARGET_USER" -d "$TARGET_DB" \
  -v ON_ERROR_STOP=1 -v media_id="$MEDIA_ID" <<'SQL'
BEGIN;
SELECT pg_advisory_xact_lock(731947120);
INSERT INTO admingen_media.store_identity (singleton, identity)
VALUES (true, :'media_id')
ON CONFLICT (singleton) DO UPDATE SET identity = EXCLUDED.identity;
COMMIT;
SQL

B4. Start development

From the same terminal where MEDIA_ROOT is set:

make dev

Sign in with the transferred account and check records and images. Use the same MEDIA_ROOT on subsequent starts; the standard path needs no separate setting. For a custom path, preserve it in the backend startup environment.

If the backend reports media storage cannot change while assets exist, first check the path it receives and whether B3 was performed after extraction. Do not delete images or the binding table to bypass the error.